diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index e34abd3f..ef05e73a 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::net::IpAddr; use std::str::FromStr; @@ -61,6 +62,8 @@ pub struct CertificateParams { pub distinguished_name: DistinguishedName, pub is_ca: IsCa, pub key_usages: Vec, + pub certificate_policies: Option, + pub inhibit_any_policy: Option, pub extended_key_usages: Vec, pub name_constraints: Option, /// An optional list of certificate revocation list (CRL) distribution points as described @@ -93,6 +96,8 @@ impl Default for CertificateParams { distinguished_name, is_ca: IsCa::NoCa, key_usages: Vec::new(), + certificate_policies: None, + inhibit_any_policy: None, extended_key_usages: Vec::new(), name_constraints: None, crl_distribution_points: Vec::new(), @@ -181,6 +186,8 @@ impl CertificateParams { distinguished_name: DistinguishedName::from_name(&x509.tbs_certificate.subject)?, not_before: x509.validity().not_before.to_datetime(), not_after: x509.validity().not_after.to_datetime(), + certificate_policies: CertificatePolicies::from_x509(&x509)?, + inhibit_any_policy: InhibitAnyPolicy::from_x509(&x509)?, ..Default::default() }) } @@ -351,6 +358,8 @@ impl CertificateParams { distinguished_name, is_ca, key_usages, + certificate_policies, + inhibit_any_policy, extended_key_usages, name_constraints, crl_distribution_points, @@ -373,6 +382,8 @@ impl CertificateParams { ); if serial_number.is_some() || name_constraints.is_some() + || certificate_policies.is_some() + || inhibit_any_policy.is_some() || !crl_distribution_points.is_empty() || *use_authority_key_identifier_extension { @@ -473,6 +484,8 @@ impl CertificateParams { || self.name_constraints.iter().any(|c| !c.is_empty()) || matches!(self.is_ca, IsCa::ExplicitNoCa) || matches!(self.is_ca, IsCa::Ca(_)) + || self.certificate_policies.is_some() + || self.inhibit_any_policy.is_some() || !self.custom_extensions.is_empty(); if !should_write_exts { return Ok(()); @@ -605,6 +618,18 @@ impl CertificateParams { IsCa::NoCa => {}, } + if let Some(certificate_policies) = &self.certificate_policies { + writer.next().write_der(&yasna::construct_der(|writer| { + certificate_policies.encode_der(writer); + })) + } + + if let Some(inhibit_any_policy) = &self.inhibit_any_policy { + writer.next().write_der(&yasna::construct_der(|writer| { + inhibit_any_policy.encode_der(writer) + })) + } + // Write the custom extensions for ext in &self.custom_extensions { write_x509_extension(writer.next(), &ext.oid, ext.critical, |writer| { @@ -656,6 +681,283 @@ fn write_general_subtrees(writer: DERWriter, tag: u64, general_subtrees: &[Gener }); } +/// The [Certificate Policies extension](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.4) +/// +/// This qualifier SHOULD only be present in end entity certificates and CA certificates issued to other organizations +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct CertificatePolicies { + /// Applications with specific policy requirements are expected to have a list of those policies + /// that they will accept and to compare the policy OIDs in the certificate to that list. If this + /// extension is critical, the path validation software MUST be able to interpret this extension + /// (including the optional qualifier), or MUST reject the certificate. + pub critical: bool, + /// A sequence of one or more policy information terms + /// + /// A certificate policy OID MUST NOT appear more than once in a + /// certificate policies extension. + policy_information: Vec, +} + +impl CertificatePolicies { + #[cfg(all(test, feature = "x509-parser"))] + fn from_x509( + x509: &x509_parser::certificate::X509Certificate<'_>, + ) -> Result, Error> { + use x509_parser::extensions::ParsedExtension; + use x509_parser::oid_registry::OID_X509_EXT_CERTIFICATE_POLICIES; + + let ext = x509 + .get_extension_unique(&OID_X509_EXT_CERTIFICATE_POLICIES) + .map_err(|_| Error::CouldNotParseCertificate)?; + + let Some(ext) = ext else { + return Ok(None); + }; + + let ParsedExtension::CertificatePolicies(policies) = ext.parsed_extension() else { + return Err(Error::X509("A CertificatePolicies extension was found by OID but not parsed into the expected type.".to_string())); + }; + + let mut policy_information: Vec = Vec::with_capacity(policies.len()); + for policy in policies.iter().cloned() { + policy_information.push(PolicyInformation::from_x509(policy)?); + } + + Ok(Some(Self { + critical: ext.critical, + policy_information, + })) + } +} + +// impl yasna::DEREncodable for CertificatePolicies { +impl CertificatePolicies { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + write_x509_extension(writer, oid::CERTIFICATE_POLICIES, self.critical, |writer| { + writer.write_sequence_of(|writer| { + for policy in &self.policy_information { + writer + .next() + .write_der(&yasna::construct_der(|writer| policy.encode_der(writer))) + } + }) + }); + } +} + +impl CertificatePolicies { + /// Create a new [`CertificatePolicies`] extension. + /// Returns [`None`] when `policies` is empty and [`Error::DuplicatePolicyInformation`] when policies are not unique + pub fn new(criticality: bool, policies: Vec) -> Result, Error> { + if policies.is_empty() { + return Ok(None); + } + + let mut unique_ids: HashSet<&[u64]> = HashSet::with_capacity(policies.len()); + for policy in &policies { + if !unique_ids.insert(&policy.policy_identifier) { + return Err(Error::DuplicatePolicyInformation( + policy.policy_identifier.clone(), + )); + } + } + + Ok(Some(Self { + critical: criticality, + policy_information: policies, + })) + } + + /// Returns the contained sequence of one or more policy information terms + pub fn policy_information(&self) -> &[PolicyInformation] { + &self.policy_information + } +} + +/// > A certificate policy OID MUST NOT appear more than once in a +/// > certificate policies extension. +/// > +/// > In an end entity certificate, these policy information terms indicate +/// > the policy under which the certificate has been issued and the +/// > purposes for which the certificate may be used. In a CA certificate, +/// > these policy information terms limit the set of policies for +/// > certification paths that include this certificate. When a CA does +/// > not wish to limit the set of policies for certification paths that +/// > include this certificate, it MAY assert the special policy anyPolicy, +/// > with a value of { 2 5 29 32 0 }. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PolicyInformation { + /// > To promote interoperability, this profile RECOMMENDS that policy information terms consist of only an OID. + pub policy_identifier: Vec, + /// Consider only populating [`Self::policy_identifier`] if possible. + /// + /// > To promote interoperability, this profile RECOMMENDS that policy + /// > information terms consist of only an OID. Where an OID alone is + /// > insufficient, this profile strongly recommends that the use of + /// > qualifiers be limited to those identified in this section. When + /// > qualifiers are used with the special policy anyPolicy, they MUST be + /// > limited to the qualifiers identified in this section. Only those + /// > qualifiers returned as a result of path validation are considered. + pub policy_qualifiers: Vec, +} + +// impl yasna::DEREncodable for PolicyInformation { +impl PolicyInformation { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + writer.write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(&self.policy_identifier)); + + if self.policy_qualifiers.is_empty() { + return; + }; + + writer.next().write_sequence_of(|writer| { + for policy_qualifier in &self.policy_qualifiers { + writer.next().write_der(&yasna::construct_der(|writer| { + policy_qualifier.encode_der(writer) + })) + } + }) + }) + } +} + +#[cfg(all(test, feature = "x509-parser"))] +impl PolicyInformation { + fn from_x509(value: x509_parser::extensions::PolicyInformation) -> Result { + let mut policy_identifier = Vec::new(); + + for v in value.policy_id.iter().ok_or(Error::X509(String::from( + "PolicyInformation without a policy_identifier is invalid", + )))? { + policy_identifier.push(v); + } + + let Some(qualifiers) = value.policy_qualifiers else { + return Ok(Self { + policy_identifier, + policy_qualifiers: Vec::new(), + }); + }; + let policy_qualifiers = qualifiers + .into_iter() + .map(PolicyQualifierInfo::from_x509) + .collect::, Error>>()?; + + Ok(Self { + policy_identifier, + policy_qualifiers, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +/// RFC5280 strongly recommends that no custom qualifiers are used. +/// +/// ```ASN.1 +/// PolicyQualifierInfo ::= SEQUENCE { +/// policyQualifierId PolicyQualifierId, +/// qualifier ANY DEFINED BY policyQualifierId } +/// ``` +pub struct PolicyQualifierInfo { + /// The OID of your [`PolicyQualifierInfo`] + pub policy_qualifier_id: Vec, + /// The DER encoded qualifier + /// + /// > ANY DEFINED BY policyQualifierId + pub qualifier: Vec, +} + +#[cfg(all(test, feature = "x509-parser"))] +impl PolicyQualifierInfo { + fn from_x509(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Result { + let mut oid = Vec::new(); + for arc in value + .policy_qualifier_id + .iter() + .ok_or(Error::X509(String::from( + "PolicyInformation without a policy_identifier is invalid", + )))? { + oid.push(arc); + } + + Ok(Self { + policy_qualifier_id: oid, + qualifier: value.qualifier.to_owned(), + }) + } +} + +// impl yasna::DEREncodable for PolicyQualifierInfo { +impl PolicyQualifierInfo { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + writer.write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(&self.policy_qualifier_id)); + writer.next().write_der(&self.qualifier); + }); + } +} + +/// Excerpt from [RFC5280 Section 4.2.1.14](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.14) +/// +/// > The inhibit anyPolicy extension can be used in certificates issued to +/// > CAs. The inhibit anyPolicy extension indicates that the special +/// > anyPolicy OID, with the value { 2 5 29 32 0 }, is not considered an +/// > explicit match for other certificate policies except when it appears +/// > in an intermediate self-issued CA certificate. The value indicates +/// > the number of additional non-self-issued certificates that may appear +/// > in the path before anyPolicy is no longer permitted. For example, a +/// > value of one indicates that anyPolicy may be processed in +/// > certificates issued by the subject of this certificate, but not in +/// > additional certificates in the path. +/// > +/// > > Conforming CAs MUST mark this extension as critical. +/// > > +/// > > id-ce-inhibitAnyPolicy OBJECT IDENTIFIER ::= { id-ce 54 } +/// > > +/// > > InhibitAnyPolicy ::= SkipCerts +/// > > +/// > > SkipCerts ::= INTEGER (0..MAX) +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct InhibitAnyPolicy { + /// The number of additional non-self-issued certificates that may appear + /// in the path before anyPolicy is no longer permitted. + pub skip_certs: u32, +} + +#[cfg(all(test, feature = "x509-parser"))] +impl InhibitAnyPolicy { + fn from_x509( + x509: &x509_parser::certificate::X509Certificate<'_>, + ) -> Result, Error> { + let inhibit_any_policy = x509 + .inhibit_anypolicy() + .map_err(|_| Error::CouldNotParseCertificate)?; + + let Some(inhibit_any_policy) = inhibit_any_policy else { + return Ok(None); + }; + + Ok(Some(Self { + skip_certs: inhibit_any_policy.value.skip_certs, + })) + } +} + +// impl yasna::DEREncodable for InhibitAnyPolicy { +impl InhibitAnyPolicy { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + // Conforming CAs MUST mark this extension as critical. + write_x509_extension(writer, oid::INHIBIT_ANY_POLICY, true, |writer| { + writer.write_u32(self.skip_certs) + }); + } +} + /// A PKCS #10 CSR attribute, as defined in [RFC 5280] and constrained /// by [RFC 2986]. /// @@ -1152,6 +1454,168 @@ mod tests { #[cfg(feature = "crypto")] use crate::KeyPair; + #[cfg(feature = "crypto")] + #[cfg(feature = "x509-parser")] + #[test] + fn test_certificate_policies_cert_encode_decode() { + use crate::{ + CertificateParams, CertificatePolicies, PolicyInformation, PolicyQualifierInfo, + }; + + const OID_CPS_URI: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 2, 1]; + + let params = CertificateParams { + certificate_policies: CertificatePolicies::new( + false, + vec![ + // domainValidated + PolicyInformation { + policy_identifier: vec![2, 23, 140, 1, 2, 1], + policy_qualifiers: Vec::new(), + }, + // CpsUri + PolicyInformation { + policy_identifier: OID_CPS_URI.to_vec(), + policy_qualifiers: vec![ + PolicyQualifierInfo { + policy_qualifier_id: OID_CPS_URI.to_vec(), + qualifier: yasna::construct_der(|writer| { + writer.write_ia5_string("https://cps.example.org") + }), + }, + PolicyQualifierInfo { + policy_qualifier_id: OID_CPS_URI.to_vec(), + qualifier: yasna::construct_der(|writer| { + writer.write_ia5_string("https://cps.example.com") + }), + }, + ], + }, + ], + ) + .expect("Constructing a well-formed extension shouldn't fail"), + ..Default::default() + }; + + let key_pair = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key_pair).unwrap(); + + let parsed = CertificateParams::from_ca_cert_der(cert.der()) + .expect("We should be able to parse the certificate we just created"); + assert_eq!(params.certificate_policies, parsed.certificate_policies); + assert_eq!( + params.certificate_policies.unwrap().policy_information(), + parsed.certificate_policies.unwrap().policy_information(), + ); + } + + #[test] + fn test_policy_information_new_oid_only_der() { + use crate::PolicyInformation; + const EXPECTED_DER: &[u8] = &[0x30, 0x05, 0x06, 0x03, 0x01, 0x02, 0x03]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation { + policy_identifier: vec![0, 1, 2, 3], + policy_qualifiers: Vec::new(), + } + .encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_new_oid_qualifiers_der() { + use crate::{PolicyInformation, PolicyQualifierInfo}; + const EXPECTED_DER: &[u8] = &[ + 0x30, 0x2C, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x30, 0x25, 0x30, 0x12, 0x06, 0x04, 0x2A, + 0x03, 0x04, 0x00, 0x0C, 0x0A, 0x55, 0x54, 0x46, 0x38, 0x53, 0x74, 0x72, 0x69, 0x6E, + 0x67, 0x30, 0x0F, 0x06, 0x04, 0x2A, 0x03, 0x04, 0x01, 0x12, 0x07, 0x31, 0x32, 0x38, + 0x20, 0x32, 0x35, 0x36, + ]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation { + policy_identifier: vec![1, 2, 3, 4], + policy_qualifiers: vec![ + PolicyQualifierInfo { + policy_qualifier_id: vec![1, 2, 3, 4, 0], + qualifier: yasna::construct_der(|writer| { + writer.write_utf8string("UTF8String") + }), + }, + PolicyQualifierInfo { + policy_qualifier_id: vec![1, 2, 3, 4, 1], + qualifier: yasna::construct_der(|writer| { + writer.write_numeric_string("128 256") + }), + }, + ], + } + .encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_empty() { + use crate::CertificatePolicies; + let none = CertificatePolicies::new(false, vec![]) + .expect("Empty CertificatePolicies don't cause an error but must return None"); + assert!(none.is_none()); + } + + #[test] + fn test_policy_information_non_unique() { + use crate::{CertificatePolicies, PolicyInformation}; + let duplicate_policy_information_error = CertificatePolicies::new( + false, + vec![ + PolicyInformation { + policy_identifier: vec![2, 5, 29, 32, 0], + policy_qualifiers: Vec::new(), + }, + PolicyInformation { + policy_identifier: vec![2, 5, 29, 32, 0], + policy_qualifiers: Vec::new(), + }, + ], + ) + .expect_err("Testing duplicate OID rejection"); + + assert_eq!( + format!("{}", duplicate_policy_information_error), + "Encountered duplicate PolicyInformationOID: 2.5.29.32.0" + ) + } + + #[cfg(feature = "crypto")] + #[test] + fn test_inhibit_any_policy_expected_der() { + const EXPECTED_DER: &[u8] = &[ + 0x30, 0x0D, 0x06, 0x03, 0x55, 0x1D, 0x36, 0x01, 0x01, 0xFF, 0x04, 0x03, 0x02, 0x01, + 0x02, + ]; + let extension_der = + yasna::construct_der(|writer| InhibitAnyPolicy { skip_certs: 2 }.encode_der(writer)); + assert_eq!(EXPECTED_DER, &extension_der) + } + + #[cfg(feature = "crypto")] + #[cfg(feature = "x509-parser")] + #[test] + fn test_inhibit_any_policy_encode_decode() { + let params = CertificateParams { + inhibit_any_policy: Some(InhibitAnyPolicy { skip_certs: 2 }), + ..Default::default() + }; + + let key_pair = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key_pair).unwrap(); + + let parsed = CertificateParams::from_ca_cert_der(cert.der()) + .expect("We should be able to parse the certificate we just created"); + assert_eq!(params.inhibit_any_policy, parsed.inhibit_any_policy,) + } + #[cfg(feature = "crypto")] #[test] fn test_with_key_usages() { diff --git a/rcgen/src/error.rs b/rcgen/src/error.rs index e6ae3961..e4d32383 100644 --- a/rcgen/src/error.rs +++ b/rcgen/src/error.rs @@ -51,6 +51,8 @@ pub enum Error { /// X509 parsing error #[cfg(feature = "x509-parser")] X509(String), + /// A certificate policy OID MUST NOT appear more than once in a certificate policies extension. + DuplicatePolicyInformation(Vec), } impl fmt::Display for Error { @@ -101,6 +103,14 @@ impl fmt::Display for Error { MissingSerialNumber => write!(f, "A serial number must be specified")?, #[cfg(feature = "x509-parser")] X509(e) => write!(f, "X.509 parsing error: {e}")?, + DuplicatePolicyInformation(oid) => write!( + f, + "Encountered duplicate PolicyInformationOID: {}", + oid.iter() + .map(|&node| node.to_string()) + .collect::>() + .join("."), + )?, }; Ok(()) } diff --git a/rcgen/src/lib.rs b/rcgen/src/lib.rs index 83816182..f18f332c 100644 --- a/rcgen/src/lib.rs +++ b/rcgen/src/lib.rs @@ -42,8 +42,10 @@ use std::net::{Ipv4Addr, Ipv6Addr}; use std::ops::Deref; pub use certificate::{ - date_time_ymd, Attribute, BasicConstraints, Certificate, CertificateParams, CidrSubnet, - CustomExtension, DnType, ExtendedKeyUsagePurpose, GeneralSubtree, IsCa, NameConstraints, + date_time_ymd, Attribute, BasicConstraints, Certificate, CertificateParams, + CertificatePolicies, CidrSubnet, CustomExtension, DnType, ExtendedKeyUsagePurpose, + GeneralSubtree, InhibitAnyPolicy, IsCa, NameConstraints, PolicyInformation, + PolicyQualifierInfo, }; pub use crl::{ CertificateRevocationList, CertificateRevocationListParams, CrlDistributionPoint, diff --git a/rcgen/src/oid.rs b/rcgen/src/oid.rs index 3b1c0eb9..1171eca4 100644 --- a/rcgen/src/oid.rs +++ b/rcgen/src/oid.rs @@ -41,6 +41,12 @@ pub(crate) const RSASSA_PSS: &[u64] = &[1, 2, 840, 113549, 1, 1, 10]; /// id-ce-keyUsage in [RFC 5280](https://tools.ietf.org/html/rfc5280#appendix-A.2) pub(crate) const KEY_USAGE: &[u64] = &[2, 5, 29, 15]; +/// id-ce-certificatePolicies in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.4) +pub(crate) const CERTIFICATE_POLICIES: &[u64] = &[2, 5, 29, 32]; + +/// id-ce-inhibitAnyPolicy in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.14) +pub(crate) const INHIBIT_ANY_POLICY: &[u64] = &[2, 5, 29, 54]; + /// id-ce-subjectAltName in [RFC 5280](https://tools.ietf.org/html/rfc5280#appendix-A.2) pub(crate) const SUBJECT_ALT_NAME: &[u64] = &[2, 5, 29, 17];