diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b5a00..b5047e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Some subject attributes can now be specified. + + ## [0.11.0] - 2020-09-19 ### Added diff --git a/acme_common/src/crypto.rs b/acme_common/src/crypto.rs index 6ddbf66..6697c32 100644 --- a/acme_common/src/crypto.rs +++ b/acme_common/src/crypto.rs @@ -7,10 +7,29 @@ mod key_type; mod openssl_certificate; mod openssl_hash; mod openssl_keys; +mod openssl_subject_attribute; mod openssl_version; pub const CRT_NB_DAYS_VALIDITY: u32 = 7; +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum BaseSubjectAttribute { + CountryName, + LocalityName, + StateOrProvinceName, + StreetAddress, + OrganizationName, + OrganizationalUnitName, + Name, + GivenName, + Initials, + Title, + Surname, + Pseudonym, + GenerationQualifier, + FriendlyName, +} + #[derive(Clone, Copy, Debug, PartialEq)] pub enum BaseHashFunction { Sha256, @@ -54,4 +73,5 @@ pub use key_type::KeyType; pub use openssl_certificate::{Csr, X509Certificate}; pub use openssl_hash::HashFunction; pub use openssl_keys::{gen_keypair, KeyPair}; +pub use openssl_subject_attribute::SubjectAttribute; pub use openssl_version::{get_lib_name, get_lib_version}; diff --git a/acme_common/src/crypto/openssl_certificate.rs b/acme_common/src/crypto/openssl_certificate.rs index 6e331bb..22f6d81 100644 --- a/acme_common/src/crypto/openssl_certificate.rs +++ b/acme_common/src/crypto/openssl_certificate.rs @@ -1,4 +1,4 @@ -use super::{gen_keypair, KeyPair, KeyType}; +use super::{gen_keypair, KeyPair, KeyType, SubjectAttribute}; use crate::b64_encode; use crate::crypto::HashFunction; use crate::error::Error; @@ -8,7 +8,7 @@ use openssl::hash::MessageDigest; use openssl::stack::Stack; use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName}; use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder, X509}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::net::IpAddr; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -43,9 +43,18 @@ impl Csr { digest: HashFunction, domains: &[String], ips: &[String], + subject_attributes: &HashMap, ) -> Result { let mut builder = X509ReqBuilder::new()?; builder.set_pubkey(&key_pair.inner_key)?; + if !subject_attributes.is_empty() { + let mut snb = X509NameBuilder::new()?; + for (sattr, val) in subject_attributes.iter() { + snb.append_entry_by_nid(sattr.get_nid(), &val)?; + } + let name = snb.build(); + builder.set_subject_name(&name)?; + } let ctx = builder.x509v3_context(None); let mut san = SubjectAlternativeName::new(); for dns in domains.iter() { diff --git a/acme_common/src/crypto/openssl_subject_attribute.rs b/acme_common/src/crypto/openssl_subject_attribute.rs new file mode 100644 index 0000000..7ea4d5e --- /dev/null +++ b/acme_common/src/crypto/openssl_subject_attribute.rs @@ -0,0 +1,24 @@ +use openssl::nid::Nid; + +pub type SubjectAttribute = super::BaseSubjectAttribute; + +impl SubjectAttribute { + pub fn get_nid(&self) -> Nid { + match self { + SubjectAttribute::CountryName => Nid::COUNTRYNAME, + SubjectAttribute::LocalityName => Nid::LOCALITYNAME, + SubjectAttribute::StateOrProvinceName => Nid::STATEORPROVINCENAME, + SubjectAttribute::StreetAddress => Nid::STREETADDRESS, + SubjectAttribute::OrganizationName => Nid::ORGANIZATIONNAME, + SubjectAttribute::OrganizationalUnitName => Nid::ORGANIZATIONALUNITNAME, + SubjectAttribute::Name => Nid::NAME, + SubjectAttribute::GivenName => Nid::GIVENNAME, + SubjectAttribute::Initials => Nid::INITIALS, + SubjectAttribute::Title => Nid::TITLE, + SubjectAttribute::Surname => Nid::SURNAME, + SubjectAttribute::Pseudonym => Nid::PSEUDONYM, + SubjectAttribute::GenerationQualifier => Nid::GENERATIONQUALIFIER, + SubjectAttribute::FriendlyName => Nid::FRIENDLYNAME, + } + } +} diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index aca6299..ed37780 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -187,6 +187,7 @@ pub fn request_certificate( cert.csr_digest, domains.as_slice(), ips.as_slice(), + &cert.subject_attributes, )?; cert.trace(&format!("new CSR:\n{}", csr.to_pem()?)); let csr = json!({ diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index c570fea..845ac2e 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -3,7 +3,7 @@ use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, HookType, PostOpe use crate::identifier::{Identifier, IdentifierType}; use crate::logs::HasLogger; use crate::storage::{certificate_files_exists, get_certificate, FileManager}; -use acme_common::crypto::{HashFunction, KeyType, X509Certificate}; +use acme_common::crypto::{HashFunction, KeyType, SubjectAttribute, X509Certificate}; use acme_common::error::Error; use log::{debug, info, trace, warn}; use std::collections::{HashMap, HashSet}; @@ -14,6 +14,7 @@ use std::time::Duration; pub struct Certificate { pub account_name: String, pub identifiers: Vec, + pub subject_attributes: HashMap, pub key_type: KeyType, pub csr_digest: HashFunction, pub kp_reuse: bool, diff --git a/acmed/src/config.rs b/acmed/src/config.rs index 75c228d..48281ae 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -3,7 +3,7 @@ use crate::hooks; use crate::identifier::IdentifierType; use crate::storage::FileManager; use acme_common::b64_decode; -use acme_common::crypto::{HashFunction, JwsSignatureAlgorithm, KeyType}; +use acme_common::crypto::{HashFunction, JwsSignatureAlgorithm, KeyType, SubjectAttribute}; use acme_common::error::Error; use glob::glob; use log::info; @@ -24,6 +24,14 @@ macro_rules! set_cfg_attr { }; } +macro_rules! push_subject_attr { + ($hm: expr, $attr: expr, $attr_type: ident) => { + if let Some(v) = &$attr { + $hm.insert(SubjectAttribute::$attr_type, v.to_owned()); + } + }; +} + fn get_stdin(hook: &Hook) -> Result { match &hook.stdin { Some(file) => match &hook.stdin_str { @@ -379,6 +387,8 @@ pub struct Certificate { pub account: String, pub endpoint: String, pub identifiers: Vec, + #[serde(default)] + pub subject_attributes: SubjectAttributes, pub key_type: Option, pub csr_digest: Option, pub kp_reuse: Option, @@ -546,6 +556,46 @@ impl Identifier { } } +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SubjectAttributes { + pub country_name: Option, + pub locality_name: Option, + pub state_or_province_name: Option, + pub street_address: Option, + pub organization_name: Option, + pub organizational_unit_name: Option, + pub name: Option, + pub given_name: Option, + pub initials: Option, + pub title: Option, + pub surname: Option, + pub pseudonym: Option, + pub generation_qualifier: Option, + pub friendly_name: Option, +} + +impl SubjectAttributes { + pub fn to_generic(&self) -> HashMap { + let mut ret = HashMap::new(); + push_subject_attr!(ret, self.country_name, CountryName); + push_subject_attr!(ret, self.locality_name, LocalityName); + push_subject_attr!(ret, self.state_or_province_name, StateOrProvinceName); + push_subject_attr!(ret, self.street_address, StreetAddress); + push_subject_attr!(ret, self.organization_name, OrganizationName); + push_subject_attr!(ret, self.organizational_unit_name, OrganizationalUnitName); + push_subject_attr!(ret, self.name, Name); + push_subject_attr!(ret, self.given_name, GivenName); + push_subject_attr!(ret, self.initials, Initials); + push_subject_attr!(ret, self.title, Title); + push_subject_attr!(ret, self.surname, Surname); + push_subject_attr!(ret, self.pseudonym, Pseudonym); + push_subject_attr!(ret, self.generation_qualifier, GenerationQualifier); + push_subject_attr!(ret, self.friendly_name, FriendlyName); + ret + } +} + fn create_dir(path: &str) -> Result<(), Error> { if Path::new(path).is_dir() { Ok(()) diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index 646ceb6..082acc5 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -126,6 +126,7 @@ impl MainEventLoop { let cert = Certificate { account_name: crt.account.clone(), identifiers: crt.get_identifiers()?, + subject_attributes: crt.subject_attributes.to_generic(), key_type, csr_digest: crt.get_csr_digest()?, kp_reuse: crt.get_kp_reuse(), diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 92a507b..b137357 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -172,7 +172,7 @@ A mailto URI as defined by This URI cannot contains neither "hfields" nor more than one "addr-spec" in the "to" component. .El .It Cm key_type Ar string -Name of the asymmetric cryptography algorithm used to generate the key pair. Possible values are : +Name of the asymmetric cryptography algorithm used to generate the key pair. Possible values are: .Bl -dash -compact .It rsa2048 @@ -257,8 +257,40 @@ The IP address. .It Ic env Ar table Table of environment variables that will be accessible from hooks. .El +.It subject_attributes Ar table +Table where the certificate's subject attributes are specified. Possible keys are: +.Bl -dash -compact +.It +country_name +.It +locality_name +.It +state_or_province_name +.It +street_address +.It +organization_name +.It +organizational_unit_name +.It +name +.It +given_name +.It +initials +.It +title +.It +surname +.It +pseudonym +.It +generation_qualifier +.It +friendly_name +.El .It Ic key_type Ar string -Name of the asymmetric cryptography algorithm used to generate the certificate's key pair. Possible values are : +Name of the asymmetric cryptography algorithm used to generate the certificate's key pair. Possible values are: .Bl -dash -compact .It rsa2048 @@ -271,7 +303,7 @@ ecdsa_p256 ecdsa_p384 .El .It Ic csr_digest Ar string -Name of the certificate's signing request digest algorithm. Possible values are : +Name of the certificate's signing request digest algorithm. Possible values are: .Bl -dash -compact .It sha256