diff --git a/x509-cert/src/builder.rs b/x509-cert/src/builder.rs index ba83db0d0..bd0341de8 100644 --- a/x509-cert/src/builder.rs +++ b/x509-cert/src/builder.rs @@ -7,23 +7,22 @@ use der::{asn1::BitString, referenced::OwnedToRef, Encode}; use signature::{rand_core::CryptoRngCore, Keypair, RandomizedSigner, Signer}; use spki::{ AlgorithmIdentifier, DynSignatureAlgorithmIdentifier, EncodePublicKey, ObjectIdentifier, - SignatureBitStringEncoding, SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef, + SignatureBitStringEncoding, SubjectPublicKeyInfoOwned, }; use crate::{ certificate::{Certificate, TbsCertificate, Version}, - ext::{ - pkix::{ - AuthorityKeyIdentifier, BasicConstraints, KeyUsage, KeyUsages, SubjectKeyIdentifier, - }, - AsExtension, Extension, Extensions, - }, + ext::{AsExtension, Extensions}, name::Name, request::{attributes::AsAttribute, CertReq, CertReqInfo, ExtensionReq}, serial_number::SerialNumber, time::Validity, }; +pub mod profile; + +use self::profile::Profile; + const NULL_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("0.0.0"); /// Error type @@ -38,6 +37,23 @@ pub enum Error { /// Signing error propagated for the [`signature::Error`] type. Signature(signature::Error), + + /// Each RelativeDistinguishedName MUST contain exactly one AttributeTypeAndValue. + NonUniqueRdn, + + /// Each Name MUST NOT contain more than one instance of a given + /// AttributeTypeAndValue across all RelativeDistinguishedNames unless explicitly + /// allowed in these Requirements + NonUniqueATV, + + /// Non-ordered attribute or invalid attribute + InvalidAttribute { + /// Offending [`ObjectIdentifier`] + oid: ObjectIdentifier, + }, + + /// Not all required elements were specified + MissingAttributes, } #[cfg(feature = "std")] @@ -49,6 +65,13 @@ impl fmt::Display for Error { Error::Asn1(err) => write!(f, "ASN.1 error: {}", err), Error::PublicKey(err) => write!(f, "public key error: {}", err), Error::Signature(err) => write!(f, "signature error: {}", err), + Error::NonUniqueRdn => write!( + f, + "Each RelativeDistinguishedName MUST contain exactly one AttributeTypeAndValue." + ), + Error::NonUniqueATV => write!(f, "Each Name MUST NOT contain more than one instance of a given AttributeTypeAndValue"), + Error::InvalidAttribute{oid} => write!(f, "Non-ordered attribute or invalid attribute found (oid={oid})"), + Error::MissingAttributes => write!(f, "Not all required elements were specified"), } } } @@ -74,156 +97,12 @@ impl From for Error { /// Result type pub type Result = core::result::Result; -/// The type of certificate to build -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Profile { - /// Build a root CA certificate - Root, - /// Build an intermediate sub CA certificate - SubCA { - /// issuer Name, - /// represents the name signing the certificate - issuer: Name, - /// pathLenConstraint INTEGER (0..MAX) OPTIONAL - /// BasicConstraints as defined in [RFC 5280 Section 4.2.1.9]. - path_len_constraint: Option, - }, - /// Build an end certificate - Leaf { - /// issuer Name, - /// represents the name signing the certificate - issuer: Name, - /// should the key agreement flag of KeyUsage be enabled - enable_key_agreement: bool, - /// should the key encipherment flag of KeyUsage be enabled - enable_key_encipherment: bool, - /// should the subject key identifier extension be included - /// - /// From [RFC 5280 Section 4.2.1.2]: - /// For end entity certificates, subject key identifiers SHOULD be - /// derived from the public key. Two common methods for generating key - /// identifiers from the public key are identified above. - #[cfg(feature = "hazmat")] - include_subject_key_identifier: bool, - }, - #[cfg(feature = "hazmat")] - /// Opt-out of the default extensions - Manual { - /// issuer Name, - /// represents the name signing the certificate - /// A `None` will make it a self-signed certificate - issuer: Option, - }, -} - -impl Profile { - fn get_issuer(&self, subject: &Name) -> Name { - match self { - Profile::Root => subject.clone(), - Profile::SubCA { issuer, .. } => issuer.clone(), - Profile::Leaf { issuer, .. } => issuer.clone(), - #[cfg(feature = "hazmat")] - Profile::Manual { issuer, .. } => issuer.as_ref().unwrap_or(subject).clone(), - } - } - - fn build_extensions( - &self, - spk: SubjectPublicKeyInfoRef<'_>, - issuer_spk: SubjectPublicKeyInfoRef<'_>, - tbs: &TbsCertificate, - ) -> Result> { - #[cfg(feature = "hazmat")] - // User opted out of default extensions set. - if let Profile::Manual { .. } = self { - return Ok(vec::Vec::default()); - } - - let mut extensions: vec::Vec = vec::Vec::new(); - - match self { - #[cfg(feature = "hazmat")] - Profile::Leaf { - include_subject_key_identifier: false, - .. - } => {} - _ => extensions.push( - SubjectKeyIdentifier::try_from(spk)?.to_extension(&tbs.subject, &extensions)?, - ), - } - - // Build Authority Key Identifier - match self { - Profile::Root => {} - _ => { - extensions.push( - AuthorityKeyIdentifier::try_from(issuer_spk.clone())? - .to_extension(&tbs.subject, &extensions)?, - ); - } - } - - // Build Basic Contraints extensions - extensions.push(match self { - Profile::Root => BasicConstraints { - ca: true, - path_len_constraint: None, - } - .to_extension(&tbs.subject, &extensions)?, - Profile::SubCA { - path_len_constraint, - .. - } => BasicConstraints { - ca: true, - path_len_constraint: *path_len_constraint, - } - .to_extension(&tbs.subject, &extensions)?, - Profile::Leaf { .. } => BasicConstraints { - ca: false, - path_len_constraint: None, - } - .to_extension(&tbs.subject, &extensions)?, - #[cfg(feature = "hazmat")] - Profile::Manual { .. } => unreachable!(), - }); - - // Build Key Usage extension - match self { - Profile::Root | Profile::SubCA { .. } => { - extensions.push( - KeyUsage(KeyUsages::KeyCertSign | KeyUsages::CRLSign) - .to_extension(&tbs.subject, &extensions)?, - ); - } - Profile::Leaf { - enable_key_agreement, - enable_key_encipherment, - .. - } => { - let mut key_usage = KeyUsages::DigitalSignature | KeyUsages::NonRepudiation; - if *enable_key_encipherment { - key_usage |= KeyUsages::KeyEncipherment; - } - if *enable_key_agreement { - key_usage |= KeyUsages::KeyAgreement; - } - - extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?); - } - #[cfg(feature = "hazmat")] - Profile::Manual { .. } => unreachable!(), - } - - Ok(extensions) - } -} - /// X509 Certificate builder /// /// ``` /// use der::Decode; /// use x509_cert::spki::SubjectPublicKeyInfoOwned; -/// use x509_cert::builder::{CertificateBuilder, Profile, Builder}; +/// use x509_cert::builder::{CertificateBuilder, Builder, profile}; /// use x509_cert::name::Name; /// use x509_cert::serial_number::SerialNumber; /// use x509_cert::time::Validity; @@ -237,14 +116,14 @@ impl Profile { /// # use der::referenced::RefToOwned; /// # fn rsa_signer() -> SigningKey { /// # let private_key = rsa::RsaPrivateKey::from_pkcs1_der(RSA_2048_PRIV_DER).unwrap(); -/// # let signing_key = SigningKey::::new_with_prefix(private_key); +/// # let signing_key = SigningKey::::new(private_key); /// # signing_key /// # } /// /// let serial_number = SerialNumber::from(42u32); /// let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); -/// let profile = Profile::Root; /// let subject = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); +/// let profile = profile::cabf::Root::new(false,subject).expect("Create root profile"); /// /// let pub_key = SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER).expect("get rsa pub key"); /// @@ -253,26 +132,27 @@ impl Profile { /// profile, /// serial_number, /// validity, -/// subject, /// pub_key, /// ) /// .expect("Create certificate builder"); /// /// let cert = builder.build(&signer).expect("Create certificate"); /// ``` -pub struct CertificateBuilder { +pub struct CertificateBuilder

{ tbs: TbsCertificate, extensions: Extensions, - profile: Profile, + profile: P, } -impl CertificateBuilder { +impl

CertificateBuilder

+where + P: Profile, +{ /// Creates a new certificate builder pub fn new( - profile: Profile, + profile: P, serial_number: SerialNumber, mut validity: Validity, - subject: Name, subject_public_key_info: SubjectPublicKeyInfoOwned, ) -> Result { let signature_alg = AlgorithmIdentifier { @@ -280,6 +160,7 @@ impl CertificateBuilder { parameters: None, }; + let subject = profile.get_subject(); let issuer = profile.get_issuer(&subject); validity.not_before.rfc5280_adjust_utc_time()?; @@ -455,7 +336,10 @@ pub trait Builder: Sized { } } -impl Builder for CertificateBuilder { +impl

Builder for CertificateBuilder

+where + P: Profile, +{ type Output = Certificate; fn finalize(&mut self, cert_signer: &S) -> Result> diff --git a/x509-cert/src/builder/profile.rs b/x509-cert/src/builder/profile.rs new file mode 100644 index 000000000..6f2e88f8d --- /dev/null +++ b/x509-cert/src/builder/profile.rs @@ -0,0 +1,41 @@ +//! Certificate profiles +//! +//! Profiles need implement by the [`Profile`] trait. +//! They may then be consumed by a [`builder::CertificateBuilder`]. +//! +//! +//! Multiple profiles are provided and you may select one depending on your use-case: +//! - [`cabf`] implements the Baseline Requirement from the CA Browser Forum as close as it can be +//! done. +//! - [`devid`] implements the specification for IEEE 802.1 AR. Certificates for Secure +//! Device Identity. +//! +//! Please follow each sub-module documentation and select a profile that may suit your needs, or +//! you may implement your own profile, if need be. + +#[cfg(doc)] +use crate::builder; + +use crate::{builder::Result, certificate::TbsCertificate, ext::Extension, name::Name}; +use alloc::vec; +use spki::SubjectPublicKeyInfoRef; + +pub mod cabf; +pub mod devid; + +/// Profile for certificates +pub trait Profile { + /// Issuer to be used for issued certificates + fn get_issuer(&self, subject: &Name) -> Name; + + /// Subject for the certificate to be used. + fn get_subject(&self) -> Name; + + /// X509v3 extensions to be added in the certificates. + fn build_extensions( + &self, + spk: SubjectPublicKeyInfoRef<'_>, + issuer_spk: SubjectPublicKeyInfoRef<'_>, + tbs: &TbsCertificate, + ) -> Result>; +} diff --git a/x509-cert/src/builder/profile/cabf.rs b/x509-cert/src/builder/profile/cabf.rs new file mode 100644 index 000000000..6655d4b47 --- /dev/null +++ b/x509-cert/src/builder/profile/cabf.rs @@ -0,0 +1,213 @@ +//! CA/Browser forum specific profiles +//! +//! +use alloc::vec; +use std::collections::HashSet; + +use crate::{ + builder::{Error, Profile, Result}, + certificate::TbsCertificate, + ext::{ + pkix::{ + AuthorityKeyIdentifier, BasicConstraints, KeyUsage, KeyUsages, SubjectKeyIdentifier, + }, + AsExtension, Extension, + }, + name::Name, +}; +use const_oid::db::{rfc2256, rfc4519}; +use spki::SubjectPublicKeyInfoRef; + +/// Check Name encoding +/// +/// BR 7.1.4.1 Name Encoding +pub fn check_names_encoding(name: &Name, multiple_allowed: bool) -> Result<()> { + // NOTE: RDNSequence may be empty (at least with tls Subscribers). + + let ordering = vec![ + rfc4519::DOMAIN_COMPONENT, + rfc4519::COUNTRY_NAME, + rfc2256::STATE_OR_PROVINCE_NAME, + rfc4519::LOCALITY_NAME, + rfc4519::POSTAL_CODE, + rfc2256::STREET_ADDRESS, + rfc4519::ORGANIZATION_NAME, + rfc4519::SURNAME, + rfc4519::GIVEN_NAME, + rfc4519::ORGANIZATIONAL_UNIT_NAME, + rfc4519::COMMON_NAME, + ]; + let mut ordering = ordering.iter(); + + let mut seen = HashSet::new(); + + for rdn in name.0.iter() { + if rdn.0.len() != 1 { + return Err(Error::NonUniqueRdn); + } + + for atv in rdn.0.iter() { + if !multiple_allowed && !seen.insert(atv.oid) { + return Err(Error::NonUniqueATV); + } + + if !ordering.any(|attr| attr == &atv.oid) { + return Err(Error::InvalidAttribute { oid: atv.oid }); + } + } + } + + Ok(()) +} + +/// Check CA subject naming +/// +/// BR 7.1.2.10.2 CA Certificate Naming +pub fn ca_certificate_naming(subject: &Name) -> Result<()> { + let mut required = HashSet::from([ + rfc4519::COUNTRY_NAME, + rfc4519::ORGANIZATION_NAME, + rfc4519::COMMON_NAME, + ]); + let mut allowed = HashSet::from([ + rfc4519::COUNTRY_NAME, + rfc2256::STATE_OR_PROVINCE_NAME, + rfc4519::LOCALITY_NAME, + rfc4519::POSTAL_CODE, + rfc2256::STREET_ADDRESS, + rfc4519::ORGANIZATION_NAME, + rfc4519::COMMON_NAME, + ]); + + check_names_encoding(subject, false)?; + + for rdn in subject.0.iter() { + for atv in rdn.0.iter() { + if !allowed.remove(&atv.oid) { + return Err(Error::InvalidAttribute { oid: atv.oid }); + } + required.remove(&atv.oid); + } + } + + if !required.is_empty() { + return Err(Error::MissingAttributes); + } + + Ok(()) +} + +/// Root CA certificate profile +/// +/// Certificate profile conforming - to the extent possible - to the CABF BR for Root CAs. +pub struct Root { + /// Whether the root CA will emit OCSP responses. + /// This adds the [`KeyUsages::DigitalSignature`] bit to the [`KeyUsage`] extension. + pub emits_ocsp_response: bool, + subject: Name, +} + +impl Root { + /// Create a new root profile. + pub fn new(emits_ocsp_response: bool, subject: Name) -> Result { + ca_certificate_naming(&subject)?; + + Ok(Self { + emits_ocsp_response, + subject, + }) + } +} + +impl Profile for Root { + fn get_issuer(&self, subject: &Name) -> Name { + subject.clone() + } + + fn get_subject(&self) -> Name { + self.subject.clone() + } + + fn build_extensions( + &self, + spk: SubjectPublicKeyInfoRef<'_>, + _issuer_spk: SubjectPublicKeyInfoRef<'_>, + tbs: &TbsCertificate, + ) -> Result> { + let mut extensions: vec::Vec = vec::Vec::new(); + + // 7.1.2.1.2 Root CA Extensions + + let ski = SubjectKeyIdentifier::try_from(spk)?; + + // ## authorityKeyIdentifier RECOMMENDED + // 7.1.2.1.3 Root CA Authority Key Identifier + extensions.push( + AuthorityKeyIdentifier { + // KeyIdentifier must be the same as subjectKeyIdentifier + key_identifier: Some(ski.0.clone()), + // other fields must not be present. + ..Default::default() + } + .to_extension(&tbs.subject, &extensions)?, + ); + + // ## basicConstraints MUST + // Spec: 7.1.2.1.4 Root CA Basic Constraints + extensions.push( + BasicConstraints { + ca: true, + path_len_constraint: None, + } + .to_extension(&tbs.subject, &extensions)?, + ); + + // ## keyUsage MUST + // Spec: 7.1.2.10.7 CA Certificate Key Usage + let mut key_usage = KeyUsages::KeyCertSign | KeyUsages::CRLSign; + if self.emits_ocsp_response { + key_usage |= KeyUsages::DigitalSignature; + } + extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?); + + // ## subjectKeyIdentifier MUST + // + // TODO: from 7.1.2.11.4 Subject Key Identifier + // The CA MUST generate a subjectKeyIdentifier that is unique within the scope of all + // Certificates it has issued for each unique public key (the subjectPublicKeyInfo field of the + // tbsCertificate). For example, CAs may generate the subject key identifier using an algorithm + // derived from the public key, or may generate a sufficiently‐large unique number, such by using a + // CSPRNG. + extensions.push(ski.to_extension(&tbs.subject, &extensions)?); + + // ## extKeyUsage MUST NOT + + // ## certificatePolicies NOT RECOMMENDED + + // ## Signed Certificate Timestamp List MAY + + // ## Any other extension NOT RECOMMENDED + + Ok(extensions) + } + + // 7.1.2.1 Root CA Certificate Profile + // TODO: + // - issuerUniqueID MUST NOT be present + // - subjectUniqueID MUST NOT be present + // NOTE(baloo): we never build those? + // + // 7.1.2.1.1 Root CA Validity + // TODO: + // - Minimum 2922 days (approx. 8 years) + // - Max 9132 days (approx. 25 years) + // + // +} + +pub mod tls; + +pub mod codesigning { + //! + // TODO +} diff --git a/x509-cert/src/builder/profile/cabf/tls.rs b/x509-cert/src/builder/profile/cabf/tls.rs new file mode 100644 index 000000000..a157295e5 --- /dev/null +++ b/x509-cert/src/builder/profile/cabf/tls.rs @@ -0,0 +1,339 @@ +//! +//! 7.1.2.6 TLS Subordinate CA Certificate Profile +use alloc::vec; + +use const_oid::db::{ + rfc4519, + rfc5280::{ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH}, +}; +use der::asn1::SetOfVec; + +#[cfg(feature = "hazmat")] +use const_oid::db::rfc5912; + +use crate::{ + attr::AttributeTypeAndValue, + builder::{Profile, Result}, + certificate::TbsCertificate, + ext::{ + pkix::{ + name::GeneralNames, AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, + KeyUsage, KeyUsages, SubjectKeyIdentifier, + }, + AsExtension, Extension, + }, + name::{Name, RelativeDistinguishedName}, +}; +use spki::SubjectPublicKeyInfoRef; + +/// TLS Subordinate CA Certificate Profile +/// +/// BR 7.1.2.6 TLS Subordinate CA Certificate Profile +pub struct Subordinate { + /// issuer Name, + /// represents the name signing the certificate + pub issuer: Name, + + /// subject Name, + /// represents the name of the newly issued certificated + pub subject: Name, + + /// pathLenConstraint INTEGER (0..MAX) OPTIONAL + /// BasicConstraints as defined in [RFC 5280 Section 4.2.1.9]. + pub path_len_constraint: Option, + + /// `emits_ocsp_response` will append the [`KeyUsages::DigitalSignature`]. This is meant for + /// CAs that will reply to OCSP requests. + pub emits_ocsp_response: bool, + + /// Allows this subordinate CA to issue certificates capable of doing client authentication + pub client_auth: bool, +} + +impl Profile for Subordinate { + fn get_issuer(&self, _subject: &Name) -> Name { + self.issuer.clone() + } + + fn get_subject(&self) -> Name { + self.subject.clone() + } + + fn build_extensions( + &self, + spk: SubjectPublicKeyInfoRef<'_>, + issuer_spk: SubjectPublicKeyInfoRef<'_>, + tbs: &TbsCertificate, + ) -> Result> { + let mut extensions: vec::Vec = vec::Vec::new(); + + // # 7.1.2.6.1 TLS Subordinate CA Extensions + + // ## authorityKeyIdentifier MUST + // 7.1.2.11.1 Authority Key Identifier + extensions.push( + AuthorityKeyIdentifier::try_from(issuer_spk.clone())? + .to_extension(&tbs.subject, &extensions)?, + ); + + // ## basicConstraints MUST + // Spec: 7.1.2.10.4 CA Certificate Basic Constraints + extensions.push( + BasicConstraints { + // MUST be set TRUE + ca: true, + // May be present + path_len_constraint: self.path_len_constraint, + } + .to_extension(&tbs.subject, &extensions)?, + ); + + // ## keyUsage MUST + // Spec: 7.1.2.10.7 CA Certificate Key Usage + let mut key_usage = KeyUsages::KeyCertSign | KeyUsages::CRLSign; + if self.emits_ocsp_response { + key_usage |= KeyUsages::DigitalSignature; + } + extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?); + + // ## subjectKeyIdentifier MUST + let ski = SubjectKeyIdentifier::try_from(spk)?; + extensions.push(ski.to_extension(&tbs.subject, &extensions)?); + + // ## extKeyUsage MUST + // Spec 7.1.2.10.6 CA Certificate Extended Key Usage + let mut eku = ExtendedKeyUsage(vec![ID_KP_SERVER_AUTH]); + if self.client_auth { + eku.0.push(ID_KP_CLIENT_AUTH); + } + + // ## authorityInformationAccess SHOULD + // Spec 7.1.2.10.3 CA Certificate Authority Information Access + // NOTE(baloo): this is a should and we can't put a generic value here, it's mostly up to + // the consumer of the API. + + Ok(extensions) + } + + // 7.1.2.6.1 TLS Subordinate CA Extensions + // Check certificatePolicies MUST + // check crlDistributionPoints MUST +} + +/// Type of Subscriber Certificates that may be issued. +#[derive(Debug, Clone, PartialEq)] +pub enum CertificateType { + /// Subscriber Certificate to be Domain Validated + DomainValidated(DomainValidated), + /// Subscriber Certificate to be Individual Validated + IndividualValidated, + /// Subscriber Certificate to be Organization Validated + OrganizationValidated, + /// Subscriber Certificate to be Extended Validation + ExtendedValidation, +} + +impl CertificateType { + /// Creates a new [`CertificateType`] that has been domain validated + pub fn domain_validated(subject: Name, names: GeneralNames) -> Result { + // # 7.1.2.7.2 Domain Validated + // CountryName MAY + // CommonName NOT RECOMMENDED + // Any other attribute MUST NOT + + // TODO(baloo): not very happy with all that, might as well throw that in a helper + // or something. + let rdns: vec::Vec = subject + .0 + .iter() + .filter_map(|rdn| { + let out = SetOfVec::::from_iter( + rdn.0 + .iter() + .filter(|attr_value| attr_value.oid == rfc4519::COUNTRY_NAME) + .cloned(), + ) + .ok()?; + + Some(RelativeDistinguishedName(out)) + }) + .filter(|rdn| !rdn.0.is_empty()) + .collect(); + + let subject: Name = rdns.into(); + + Ok(Self::DomainValidated(DomainValidated { subject, names })) + } +} + +/// Subscriber Certificate to be Domain Validated +#[derive(Debug, Clone, PartialEq)] +pub struct DomainValidated { + subject: Name, + names: GeneralNames, +} + +/// 7.1.2.7 Subscriber (Server) Certificate Profile +pub struct Subscriber { + /// Subtype of the Subscriber Certificate Profile + pub certificate_type: CertificateType, + + /// issuer Name, + /// represents the name signing the certificate + pub issuer: Name, + + /// Enable client authentication with the newly issued certificate + pub client_auth: bool, + + /// TLS1.2 specific flags + /// + /// It is only available under the `hazmat` feature flag. + #[cfg(feature = "hazmat")] + pub tls12_options: Tls12Options, + + /// Enable `dataEncipherment` bit on `KeyUsage`. + /// This bit is not recommended and is [`Pending Prohibition`]. + /// + /// It is only available under the `hazmat` feature flag. + /// + /// [`Pending Prohibition`]: https://github.com/cabforum/servercert/issues/384 + #[cfg(feature = "hazmat")] + pub enable_data_encipherment: bool, +} + +/// [`Tls12Options`] stores the KeyUsage bits that are required by specific uses of TLS1.2. +/// +/// This specifically refers to the [section 7.4.2 of RFC 5246]: +/// ``` text +/// RSA RSA public key; the certificate MUST allow the +/// RSA_PSK key to be used for encryption (the +/// keyEncipherment bit MUST be set if the key +/// usage extension is present). +/// Note: RSA_PSK is defined in [TLSPSK]. +/// [...] +/// DH_DSS Diffie-Hellman public key; the keyAgreement bit +/// DH_RSA MUST be set if the key usage extension is +/// present. +/// ``` +/// +/// Those are meant for consumers relying on non-DH schemes with RSA keys and non-ECDH schemes +/// with ECC keys. +/// +/// This behavior is no longer provided by TLS 1.3 and is NOT RECOMMENDED by CABF as it is +/// [`Pending Prohibition`]. +/// +/// [section 7.4.2 of RFC 5246]: https://www.rfc-editor.org/rfc/rfc5246#section-7.4.2 +/// [`Pending Prohibition`]: https://github.com/cabforum/servercert/issues/384 +#[derive(Default)] +pub struct Tls12Options { + /// Enable `keyEncipherment` on RSA keys. + pub enable_key_encipherment: bool, + /// Enable `keyAgreement` on ECC keys. + pub enable_key_agreement: bool, +} + +impl Profile for Subscriber { + fn get_issuer(&self, _subject: &Name) -> Name { + self.issuer.clone() + } + + fn get_subject(&self) -> Name { + match &self.certificate_type { + CertificateType::DomainValidated(DomainValidated { subject, .. }) => subject.clone(), + _ => todo!(), + } + } + + #[cfg_attr(not(feature = "hazmat"), allow(unused_variables))] + fn build_extensions( + &self, + spk: SubjectPublicKeyInfoRef<'_>, + issuer_spk: SubjectPublicKeyInfoRef<'_>, + tbs: &TbsCertificate, + ) -> Result> { + let mut extensions: vec::Vec = vec::Vec::new(); + + // # 7.1.2.7.6 Subscriber Certificate Extensions + + // ## authorityInformationAccess MUST + // 7.1.2.7.7 Subscriber Certificate Authority Information Access + // TODO + + // ## authorityKeyIdentifier MUST + // 7.1.2.11.1 Authority Key Identifier + extensions.push( + AuthorityKeyIdentifier::try_from(issuer_spk.clone())? + .to_extension(&tbs.subject, &extensions)?, + ); + + // ## extKeyUsage MUST + // 7.1.2.7.10 Subscriber Certificate Extended Key Usage + let mut eku = ExtendedKeyUsage(vec![ID_KP_SERVER_AUTH]); + if self.client_auth { + eku.0.push(ID_KP_CLIENT_AUTH); + } + + // ## basicConstraints MUST + // Spec: 7.1.2.7.8 Subscriber Certificate Basic Constraints + extensions.push( + BasicConstraints { + // MUST be set FALSE + ca: false, + // MUST NOT be preset + path_len_constraint: None, + } + .to_extension(&tbs.subject, &extensions)?, + ); + + // ## subjectAltName MUST + // TODO: move that to validation? + + // ## keyUsage SHOULD + // 7.1.2.7.11 Subscriber Certificate Key Usage + #[cfg_attr(not(feature = "hazmat"), allow(unused_mut))] + let mut key_usage = KeyUsages::DigitalSignature.into(); + #[cfg(feature = "hazmat")] + { + if spk.is_rsa() { + if self.enable_data_encipherment { + key_usage |= KeyUsages::DataEncipherment; + } + if self.tls12_options.enable_key_encipherment { + key_usage |= KeyUsages::KeyEncipherment; + } + } + if self.tls12_options.enable_key_agreement && spk.is_ecc() { + key_usage |= KeyUsages::KeyAgreement; + } + } + extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?); + + // ## subjectKeyIdentifier NOT RECOMMENDED + // TODO(baloo): there is a conflict between BRG and RFC 5280 4.2.1.2 + // RFC marks it as SHOULD, BRG marks it as NOT RECOMMENDED. + // + // Zlint (our linter) also emits an error if not applied. + // upstream PR: https://github.com/zmap/zlint/pull/788 + //let ski = SubjectKeyIdentifier::try_from(spk)?; + //extensions.push(ski.to_extension(&tbs.subject, &extensions)?); + + Ok(extensions) + } +} + +#[cfg(feature = "hazmat")] +trait KeyType { + fn is_rsa(&self) -> bool; + fn is_ecc(&self) -> bool; +} + +#[cfg(feature = "hazmat")] +impl<'a> KeyType for SubjectPublicKeyInfoRef<'a> { + fn is_rsa(&self) -> bool { + self.algorithm.oid == rfc5912::RSA_ENCRYPTION + } + + fn is_ecc(&self) -> bool { + self.algorithm.oid == rfc5912::ID_EC_PUBLIC_KEY + } +} diff --git a/x509-cert/src/builder/profile/devid.rs b/x509-cert/src/builder/profile/devid.rs new file mode 100644 index 000000000..f1b70847a --- /dev/null +++ b/x509-cert/src/builder/profile/devid.rs @@ -0,0 +1,196 @@ +//! Profile for 802.1AR // Secure Device Identity certificates +//! +//! Specification can be found here: +//! + +// NOTE(baloo): due to copyright issues, I am not going to +// copy paste parts of spec relevant to the implementation. +// Unlike other organizations, IEEE does not appear to grant a license for +// reproduction in implementations. +// There is a fair use exclusion to copyright, but I am not willing to +// go to court to test waters. +// +// You, as a reader/reviewer, are expected to download a copy of the spec +// yourself. + +use alloc::vec; + +use crate::{ + builder::{Profile, Result}, + certificate::TbsCertificate, + ext::{ + pkix::{ + name::{GeneralName, GeneralNames, HardwareModuleName, OtherName}, + AuthorityKeyIdentifier, KeyUsage, KeyUsages, SubjectAltName, + }, + AsExtension, Extension, + }, + name::Name, +}; +use const_oid::db::tcgtpm; +use der::{asn1::OctetString, ErrorKind}; +use spki::{ObjectIdentifier, SubjectPublicKeyInfoRef}; + +/// DevID Certificate +/// +/// See: section 8 DevID certificate fields and extensions +pub struct DevId { + /// issuer Name, + /// represents the name signing the certificate + pub issuer: Name, + + subject: Name, + + subject_alt_name: Option, +} + +impl DevId { + /// Create a new DevID + /// + /// Spec: 802.1AR Section 8.10.4 subjectAltName + /// Also documented in + /// + pub fn new(issuer: Name, subject: Name, alt_names: Option) -> Result { + // If alt_name is present it is required to include `HardwareModuleName` + // HardwareModuleName is der-encoded in an OtherName field of GeneralNames. + if let Some(ref alt_names) = alt_names { + // TODO: do we need to validate the SAN more than that? check for duplicates? + let mut found = false; + for gn in alt_names { + match gn { + GeneralName::OtherName(on) + if HardwareModuleName::from_other_name(on)?.is_some() => + { + found = true; + break; + } + _ => {} + } + } + + if !found { + return Err(der::Error::from(ErrorKind::Failed).into()); + } + } + + Ok(Self { + issuer, + subject, + subject_alt_name: alt_names, + }) + } + + /// Create a new IDevID for a TPM-based key. + pub fn idevid_tpm( + issuer: Name, + subject: Name, + hw_type: TpmVersion, + serial_number: OctetString, + ) -> Result { + let hardware_module_name = HardwareModuleName { + hw_type: hw_type.to_oid(), + hw_serial_num: serial_number, + }; + + let alt_names = vec![GeneralName::OtherName(OtherName::try_from( + &hardware_module_name, + )?)]; + + Ok(Self { + issuer, + subject, + subject_alt_name: Some(alt_names), + }) + } +} + +impl Profile for DevId { + fn get_issuer(&self, _subject: &Name) -> Name { + self.issuer.clone() + } + + fn get_subject(&self) -> Name { + self.subject.clone() + } + + fn build_extensions( + &self, + _spk: SubjectPublicKeyInfoRef<'_>, + issuer_spk: SubjectPublicKeyInfoRef<'_>, + tbs: &TbsCertificate, + ) -> Result> { + let mut extensions: vec::Vec = vec::Vec::new(); + + // # Table 8-2 - DevID certificate and intermediate certificate extensions + + // ## authorityKeyIdentifier MUST + // Section 8.10.1 + extensions.push( + AuthorityKeyIdentifier::try_from(issuer_spk.clone())? + .to_extension(&tbs.subject, &extensions)?, + ); + + // ## subjectKeyIdentifier NOT RECOMMENDED + + // ## keyUsage SHOULD + // Section 8.10.3 + // + // NOTE(baloo): + // IEEE spec allows for keyEncipherment but that would be used for TLS1.2 RSA and RSA_PSK + // (IE: non-DH) session scheme. + // In the mean time, when used with TPMs, the [TCG] will only allow for `digitalSignature`: + // Use of digitalSignature (only) is RECOMMENDED. Refer to section 3.8. + // + // [TCG]: https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf#page=57 + let key_usage = KeyUsages::DigitalSignature.into(); + extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?); + + // ## subjectAltName SHOULD + // 8.10.4 + if let Some(san) = &self.subject_alt_name { + extensions.push(SubjectAltName(san.clone()).to_extension(&tbs.subject, &extensions)?); + } + + Ok(extensions) + } +} + +/// Version of the TPM used for DevID +#[derive(Debug, Clone, PartialEq)] +pub enum TpmVersion { + /// TPM version 1.2 + Tpm12, + /// TPM version 2.0 + Tpm20, + /// Other TPM version + #[cfg(feature = "hazmat")] + Other(ObjectIdentifier), +} + +impl TpmVersion { + fn to_oid(&self) -> ObjectIdentifier { + match self { + Self::Tpm12 => tcgtpm::TCG_SV_TPM_12, + Self::Tpm20 => tcgtpm::TCG_SV_TPM_20, + #[cfg(feature = "hazmat")] + Self::Other(o) => *o, + } + } +} + +// Notes: +// Example of a certificate can be found in A.2 +// https://trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf#page=37 +// +// OID 2.23.133.1.0 for TPM version 1.2 +// OID 2.23.133.1.2 for TPM version 2.0 +// +// +// https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf#page=60 +// An IDevID/IAK complying with this specification SHOULD include tcg-cap-verifiedTPMResidency to indicate +// compliance with section 4 and also one of tcg-cap-verifiedTPMFixed (IDevID) or tcg-cap-verifiedTPMRestricted +// (IAK). +// +// tcg-cap-verifiedTPMResidency 2.23.133.11.1.1 +// tcg-cap-verifiedTPMFixed 2.23.133.11.1.2 +// tcg-cap-verifiedTPMRestricted 2.23.133.11.1.3 diff --git a/x509-cert/src/ext/pkix/name.rs b/x509-cert/src/ext/pkix/name.rs index 4e0112be4..8033ed11f 100644 --- a/x509-cert/src/ext/pkix/name.rs +++ b/x509-cert/src/ext/pkix/name.rs @@ -4,10 +4,12 @@ mod dirstr; mod dp; mod ediparty; mod general; +mod hardware; mod other; pub use dirstr::DirectoryString; pub use dp::DistributionPointName; pub use ediparty::EdiPartyName; pub use general::{GeneralName, GeneralNames}; +pub use hardware::HardwareModuleName; pub use other::OtherName; diff --git a/x509-cert/src/ext/pkix/name/hardware.rs b/x509-cert/src/ext/pkix/name/hardware.rs new file mode 100644 index 000000000..b65bcb356 --- /dev/null +++ b/x509-cert/src/ext/pkix/name/hardware.rs @@ -0,0 +1,48 @@ +use crate::ext::pkix::name::OtherName; +use const_oid::db::rfc5911::ID_ON_HARDWARE_MODULE_NAME; +use der::{ + asn1::{ObjectIdentifier, OctetString}, + Any, Sequence, ValueOrd, +}; + +/// HardwareModuleName as defined in [RFC 4108 Section 5]. +/// +/// ```text +/// HardwareModuleName ::= SEQUENCE { +/// hwType OBJECT IDENTIFIER, +/// hwSerialNum OCTET STRING +/// } +/// ``` +/// +/// [RFC 4108 Section 5]: https://www.rfc-editor.org/rfc/rfc4108#section-5 +#[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)] +#[allow(missing_docs)] +pub struct HardwareModuleName { + pub hw_type: ObjectIdentifier, + + pub hw_serial_num: OctetString, +} + +impl HardwareModuleName { + /// Convert from an [`OtherName`] if the other name is of the correct type. + /// + /// It will return `Ok(None)` if the [`OtherName`] contains another type. + pub fn from_other_name(other_name: &OtherName) -> der::Result> { + if other_name.type_id == ID_ON_HARDWARE_MODULE_NAME { + other_name.value.decode_as().map(Some) + } else { + Ok(None) + } + } +} + +impl TryFrom<&HardwareModuleName> for OtherName { + type Error = der::Error; + + fn try_from(hmn: &HardwareModuleName) -> der::Result { + Ok(Self { + type_id: ID_ON_HARDWARE_MODULE_NAME, + value: Any::encode_from(hmn)?, + }) + } +} diff --git a/x509-cert/tests/builder.rs b/x509-cert/tests/builder.rs index f1adfb682..101007b2b 100644 --- a/x509-cert/tests/builder.rs +++ b/x509-cert/tests/builder.rs @@ -1,6 +1,10 @@ #![cfg(all(feature = "builder", feature = "pem"))] -use der::{asn1::PrintableString, pem::LineEnding, Decode, Encode, EncodePem}; +use der::{ + asn1::{Ia5String, PrintableString}, + pem::LineEnding, + EncodePem, +}; use p256::{ecdsa::DerSignature, pkcs8::DecodePrivateKey, NistP256}; use rand::rngs::OsRng; use rsa::pkcs1::DecodeRsaPrivateKey; @@ -9,7 +13,10 @@ use sha2::Sha256; use spki::SubjectPublicKeyInfoOwned; use std::{str::FromStr, time::Duration}; use x509_cert::{ - builder::{AsyncBuilder, Builder, CertificateBuilder, Profile, RequestBuilder}, + builder::{ + profile::{self, cabf::tls::Tls12Options}, + AsyncBuilder, Builder, CertificateBuilder, RequestBuilder, + }, ext::pkix::{ name::{DirectoryString, GeneralName}, SubjectAltName, @@ -28,17 +35,15 @@ const PKCS8_PUBLIC_KEY_DER: &[u8] = include_bytes!("examples/p256-pub.der"); fn root_ca_certificate() { let serial_number = SerialNumber::from(42u32); let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); - let profile = Profile::Root; - let subject = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US") - .unwrap() - .to_der() - .unwrap(); - let subject = Name::from_der(&subject).unwrap(); + let subject = + Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); + let profile = profile::cabf::Root::new(false, subject).expect("create root profile"); + let pub_key = SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key"); let signer = rsa_signer(); - let builder = CertificateBuilder::new(profile, serial_number, validity, subject, pub_key) + let builder = CertificateBuilder::new(profile, serial_number, validity, pub_key) .expect("Create certificate"); let certificate = builder.build(&signer).unwrap(); @@ -54,17 +59,15 @@ fn root_ca_certificate() { fn root_ca_certificate_ecdsa() { let serial_number = SerialNumber::from(42u32); let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); - let profile = Profile::Root; - let subject = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US") - .unwrap() - .to_der() - .unwrap(); - let subject = Name::from_der(&subject).unwrap(); + + let subject = + Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); + let profile = profile::cabf::Root::new(false, subject).expect("create root profile"); let pub_key = SubjectPublicKeyInfoOwned::try_from(PKCS8_PUBLIC_KEY_DER).expect("get ecdsa pub key"); let signer = ecdsa_signer(); - let builder = CertificateBuilder::new(profile, serial_number, validity, subject, pub_key) + let builder = CertificateBuilder::new(profile, serial_number, validity, pub_key) .expect("Create certificate"); let certificate = builder.build::<_, DerSignature>(&signer).unwrap(); @@ -83,18 +86,21 @@ fn sub_ca_certificate() { let issuer = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); - let profile = Profile::SubCA { + let subject = + Name::from_str("CN=World domination task force,O=World domination Inc,C=US").unwrap(); + let profile = profile::cabf::tls::Subordinate { + subject, issuer, path_len_constraint: Some(0), + client_auth: false, + emits_ocsp_response: true, }; - let subject = - Name::from_str("CN=World domination task force,O=World domination Inc,C=US").unwrap(); let pub_key = SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key"); let signer = ecdsa_signer(); - let builder = CertificateBuilder::new(profile, serial_number, validity, subject, pub_key) + let builder = CertificateBuilder::new(profile, serial_number, validity, pub_key) .expect("Create certificate"); let certificate = builder.build::<_, DerSignature>(&signer).unwrap(); @@ -120,27 +126,31 @@ fn leaf_certificate() { let issuer = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); - let profile = Profile::Leaf { + let subject = Name::from_str("C=US").unwrap(); + let profile = profile::cabf::tls::Subscriber { + certificate_type: profile::cabf::tls::CertificateType::domain_validated( + subject.clone(), + vec![GeneralName::DnsName( + Ia5String::new(b"example.com").unwrap(), + )], + ) + .expect("create DomainValidated profile"), issuer: issuer.clone(), - enable_key_agreement: false, - enable_key_encipherment: false, + client_auth: false, + #[cfg(feature = "hazmat")] - include_subject_key_identifier: true, + tls12_options: Tls12Options::default(), + #[cfg(feature = "hazmat")] + enable_data_encipherment: false, }; - let subject = Name::from_str("CN=service.domination.world").unwrap(); let pub_key = SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key"); let signer = ecdsa_signer(); - let builder = CertificateBuilder::new( - profile, - serial_number.clone(), - validity, - subject.clone(), - pub_key.clone(), - ) - .expect("Create certificate"); + let builder = + CertificateBuilder::new(profile, serial_number.clone(), validity, pub_key.clone()) + .expect("Create certificate"); let certificate = builder.build::<_, DerSignature>(&signer).unwrap(); @@ -162,35 +172,13 @@ fn leaf_certificate() { "e_subject_common_name_not_exactly_from_san", // Extended key usage needs to be added by end-user and is use-case dependent "e_sub_cert_eku_missing", - // TODO(baloo): drop this in https://github.com/RustCrypto/formats/pull/1306 - // CABF SC-62 marked commoName (CN) as not recommended - "w_subject_common_name_included", + // CABF BRs v2 now marks ski as NOT RECOMMEND. Users of zlint are intended to + // select either RFC5280 lint or CABF lint. We want the CABF lint here and + // should ignore the RFC5280 one. + "w_ext_subject_key_identifier_missing_sub_cert", ]; zlint::check_certificate(pem.as_bytes(), &ignored); - - #[cfg(feature = "hazmat")] - { - let profile = Profile::Leaf { - issuer, - enable_key_agreement: false, - enable_key_encipherment: false, - include_subject_key_identifier: false, - }; - let builder = CertificateBuilder::new(profile, serial_number, validity, subject, pub_key) - .expect("Create certificate"); - - let certificate = builder.build::<_, DerSignature>(&signer).unwrap(); - - let pem = certificate.to_pem(LineEnding::LF).expect("generate pem"); - println!("{}", openssl::check_certificate(pem.as_bytes())); - - // Ignore warning about leaf not having SKI extension (this is a warning not a fail, as - // denoted by the `w_` prefix. - let mut ignored = ignored; - ignored.push("w_ext_subject_key_identifier_missing_sub_cert"); - zlint::check_certificate(pem.as_bytes(), &ignored); - } } #[test] @@ -200,20 +188,31 @@ fn pss_certificate() { let issuer = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); - let profile = Profile::Leaf { + + let subject = Name::from_str("C=US").unwrap(); + let profile = profile::cabf::tls::Subscriber { + certificate_type: profile::cabf::tls::CertificateType::domain_validated( + subject.clone(), + vec![GeneralName::DnsName( + Ia5String::new(b"example.com").unwrap(), + )], + ) + .expect("create DomainValidated profile"), + issuer, - enable_key_agreement: false, - enable_key_encipherment: false, + client_auth: false, + + #[cfg(feature = "hazmat")] + tls12_options: Tls12Options::default(), #[cfg(feature = "hazmat")] - include_subject_key_identifier: true, + enable_data_encipherment: false, }; - let subject = Name::from_str("CN=service.domination.world").unwrap(); let pub_key = SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key"); let signer = rsa_pss_signer(); - let builder = CertificateBuilder::new(profile, serial_number, validity, subject, pub_key) + let builder = CertificateBuilder::new(profile, serial_number, validity, pub_key) .expect("Create certificate"); let certificate = builder @@ -240,9 +239,10 @@ fn pss_certificate() { "e_sub_cert_eku_missing", // zlint warns on RSAPSS signature algorithms "e_signature_algorithm_not_supported", - // TODO(baloo): drop this in https://github.com/RustCrypto/formats/pull/1306 - // CABF SC-62 marked commoName (CN) as not recommended - "w_subject_common_name_included", + // CABF BRs v2 now marks ski as NOT RECOMMEND. Users of zlint are intended to + // select either RFC5280 lint or CABF lint. We want the CABF lint here and + // should ignore the RFC5280 one. + "w_ext_subject_key_identifier_missing_sub_cert", ]; zlint::check_certificate(pem.as_bytes(), ignored); @@ -338,17 +338,16 @@ fn dynamic_signer() { async fn async_builder() { let serial_number = SerialNumber::from(42u32); let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); - let profile = Profile::Root; - let subject = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US") - .unwrap() - .to_der() - .unwrap(); - let subject = Name::from_der(&subject).unwrap(); + + let subject = + Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); + let profile = profile::cabf::Root::new(false, subject).expect("create root profile"); + let pub_key = SubjectPublicKeyInfoOwned::try_from(PKCS8_PUBLIC_KEY_DER).expect("get ecdsa pub key"); let signer = ecdsa_signer(); - let builder = CertificateBuilder::new(profile, serial_number, validity, subject, pub_key) + let builder = CertificateBuilder::new(profile, serial_number, validity, pub_key) .expect("Create certificate"); let certificate = builder