From f8e6a814fe4b229f1fb58fae746729c690cf0f21 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:55:04 +0100 Subject: [PATCH 01/16] Add id-ce-certificatePolicies OID and contributor question --- rcgen/src/oid.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rcgen/src/oid.rs b/rcgen/src/oid.rs index 3b1c0eb9..3c3e49a4 100644 --- a/rcgen/src/oid.rs +++ b/rcgen/src/oid.rs @@ -41,6 +41,16 @@ 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]; +// Contributor Question: +// Many docstrings refer to appendix A of the RFC instead of the section +// of the respective OID. Why is that? Should I change the link accordingly? +// +// Note: Every other RFC5280 rfc-editor.org link is referring to a nonexisting +// appendix-A section. + +/// 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-subjectAltName in [RFC 5280](https://tools.ietf.org/html/rfc5280#appendix-A.2) pub(crate) const SUBJECT_ALT_NAME: &[u64] = &[2, 5, 29, 17]; From a50c1ffc8d557cd2da5d2ccf6e1bcdb48eb90a06 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:43:34 +0100 Subject: [PATCH 02/16] Add id-ce-inhibitAnyPolicy --- rcgen/src/oid.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rcgen/src/oid.rs b/rcgen/src/oid.rs index 3c3e49a4..7a3f63c1 100644 --- a/rcgen/src/oid.rs +++ b/rcgen/src/oid.rs @@ -51,6 +51,9 @@ 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]; From dce5d5dccd9e4e76a0d5b54e94b371b39c1a7296 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sat, 17 Jan 2026 08:38:52 +0100 Subject: [PATCH 03/16] Works --- rcgen/examples/certificate_policies.rs | 53 +++ rcgen/src/certificate.rs | 585 ++++++++++++++++++++++++- rcgen/src/error.rs | 5 + rcgen/src/lib.rs | 5 +- 4 files changed, 645 insertions(+), 3 deletions(-) create mode 100644 rcgen/examples/certificate_policies.rs diff --git a/rcgen/examples/certificate_policies.rs b/rcgen/examples/certificate_policies.rs new file mode 100644 index 00000000..3c1eb6f9 --- /dev/null +++ b/rcgen/examples/certificate_policies.rs @@ -0,0 +1,53 @@ +// This example is a copy from `simple.rs` with the addition of the CertificatePolicies extension + +use std::fs; + +use rcgen::{ + date_time_ymd, string::Ia5String, CertificateParams, CertificatePolicies, DistinguishedName, + DnType, InhibitAnyPolicy, KeyPair, PolicyInformation, SanType, UserNotice, +}; + +fn main() -> Result<(), Box> { + let mut params: CertificateParams = Default::default(); + params.not_before = date_time_ymd(1975, 1, 1); + params.not_after = date_time_ymd(4096, 1, 1); + params.distinguished_name = DistinguishedName::new(); + params + .distinguished_name + .push(DnType::OrganizationName, "Crab widgits SE"); + params + .distinguished_name + .push(DnType::CommonName, "Master Cert"); + params.subject_alt_names = vec![ + SanType::DnsName("crabs.crabs".try_into()?), + SanType::DnsName("localhost".try_into()?), + ]; + params.certificate_policies = Some( + CertificatePolicies::new(false, PolicyInformation::any_policy()) + .add_policy_unchecked(PolicyInformation::domain_validated()) + .add_policy_unchecked(PolicyInformation::cps_uri(vec![ + Ia5String::try_from("https://cps.example.com")?, + Ia5String::try_from("https://cps.example.org")?, + ])) + .add_policy_unchecked(PolicyInformation::user_notice( + &UserNotice::new_explicit_text("Test".into()), + )), + ); + + params.inhibit_any_policy = Some(InhibitAnyPolicy::new(2)); + + let key_pair = KeyPair::generate()?; + let cert = params.self_signed(&key_pair)?; + + let pem_serialized = cert.pem(); + let pem = pem::parse(&pem_serialized)?; + let der_serialized = pem.contents(); + println!("{pem_serialized}"); + println!("{}", key_pair.serialize_pem()); + fs::create_dir_all("certs/")?; + fs::write("certs/cert.pem", pem_serialized.as_bytes())?; + fs::write("certs/cert.der", der_serialized)?; + fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?; + fs::write("certs/key.der", key_pair.serialize_der())?; + Ok(()) +} diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 6a238862..d6dc3936 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -16,7 +16,7 @@ use crate::ring_like::digest; #[cfg(feature = "pem")] use crate::ENCODE_CONFIG; use crate::{ - oid, write_distinguished_name, write_dt_utc_or_generalized, + oid, string, write_distinguished_name, write_dt_utc_or_generalized, write_x509_authority_key_identifier, write_x509_extension, DistinguishedName, Error, Issuer, KeyIdMethod, KeyUsagePurpose, SanType, SerialNumber, SigningKey, }; @@ -61,6 +61,19 @@ pub struct CertificateParams { pub distinguished_name: DistinguishedName, pub is_ca: IsCa, pub key_usages: Vec, + /// "\[If the optional extension is present, t\]he certificate policies extension contains a sequence of one or more + /// policy information terms, each of which consists of an object + /// identifier (OID) and optional qualifiers. + /// + /// 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."[^1] + /// + /// [^1]: + 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 +106,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 +196,7 @@ 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)?, ..Default::default() }) } @@ -330,6 +346,8 @@ impl CertificateParams { distinguished_name, is_ca, key_usages, + certificate_policies, + inhibit_any_policy, extended_key_usages, name_constraints, crl_distribution_points, @@ -353,6 +371,8 @@ impl CertificateParams { if serial_number.is_some() || *is_ca != IsCa::NoCa || name_constraints.is_some() + || certificate_policies.is_some() // I think this extension must be set by the CA and not the requesting party + || inhibit_any_policy.is_some() // Same as policies above || !crl_distribution_points.is_empty() || *use_authority_key_identifier_extension { @@ -584,6 +604,27 @@ impl CertificateParams { IsCa::NoCa => {}, } + if let Some(certificate_policies) = &self.certificate_policies { + write_x509_extension( + writer.next(), + oid::CERTIFICATE_POLICIES, + certificate_policies.critical, + |writer| { + writer.write_sequence_of(|writer| { + for policy in &certificate_policies.policy_information { + writer.next().write_der(&yasna::encode_der(policy)) + } + }) + }, + ); + } + + if let Some(inhibit_any_policy) = &self.inhibit_any_policy { + write_x509_extension(writer.next(), oid::INHIBIT_ANY_POLICY, true, |writer| { + writer.write_i64(inhibit_any_policy.skip_certs as i64) + }); + } + // Write the custom extensions for ext in &self.custom_extensions { write_x509_extension(writer.next(), &ext.oid, ext.critical, |writer| { @@ -635,6 +676,548 @@ 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. + 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, + // Contributor note: + // Would it be better/feasible to use a HashSet where the hash is determined by + // the OID to ensure RFC compliance here? Is ensuring valid extensions part of + // this crate's responsibilities or is it the consumers responsibility? +} + +// Contributer note: Strongly inspired by NameConstraints impl +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 { + // Contributor note: + // Since we use get_extension_unique with the ext_certificate_policies and CertificatePolicies + // is implemented, this should be `unreachable!()`. I am unsure about the error to return here. + // Returning None would probably be worse since the extension is known to be present. + // The available parser errors don't seem to be applicable here. + // What would have to happen for this branch to be chosen? + return Err(Error::UnsupportedExtension); + }; + + let mut policy_information: Vec = Vec::with_capacity(policies.len()); + for policy in policies.iter().cloned() { + policy_information.push(policy.try_into()?) + } + + Ok(Some(Self { + critical: ext.critical, + policy_information, + })) + } +} + +impl yasna::DEREncodable for 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::encode_der(policy)) + } + }) + }); + } +} + +impl CertificatePolicies { + /// Create a new [`CertificatePolicies`] extension + /// + /// Encorces validity by requiring you to add the first policy. + /// Add more policies by using the [`Self::add_policy`] method + /// or multiple at once by using the [`Self::add_policies`] method. + pub fn new(criticality: bool, policy: PolicyInformation) -> Self { + Self { + critical: criticality, + policy_information: vec![policy], + } + } + + /// Add policy and check if it is already present. + /// Returns [`Error::Other`] if the + /// PolicyInformation OID is already present + pub fn add_policy(self, policy: PolicyInformation) -> Result { + let mut registered_policies = self.policy_information; + for policy_information in ®istered_policies { + if policy_information.policy_identifier == policy.policy_identifier { + return Err(Error::Other); // PolicyInformation must be unique + } + } + registered_policies.push(policy); + Ok(Self { + critical: self.critical, + policy_information: registered_policies, + }) + } + + /// Add once policy at a time + /// Does not validate if the PolicyInformation OID is unique. + pub fn add_policy_unchecked(self, policy: PolicyInformation) -> Self { + let mut registered_policies = self.policy_information.clone(); + registered_policies.push(policy); + Self { + critical: self.critical, + policy_information: registered_policies, + } + } + + /// Add multiple policies at once + /// Does not validate if the PolicyInformation OID is unique. + pub fn add_policies_unchecked(self, policies: &[PolicyInformation]) -> Self { + let mut registered_policies = self.policy_information.clone(); + registered_policies.extend_from_slice(policies); + Self { + critical: self.critical, + policy_information: registered_policies, + } + } +} + +/// > 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 { + policy_identifier: Vec, + policy_qualifiers: Option>, +} + +impl yasna::DEREncodable for PolicyInformation { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + writer.write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(&self.policy_identifier)); + if let Some(policy_qualifiers) = &self.policy_qualifiers { + writer.next().write_sequence_of(|writer| { + for policy_qualifier in policy_qualifiers { + writer + .next() + .write_der(&yasna::encode_der(policy_qualifier)); + } + }) + } + }) + } +} + +impl PolicyInformation { + /// > To promote interoperability, this profile RECOMMENDS that policy information terms consist of only an OID. + pub fn new_oid_only(oid: Vec) -> Self { + Self { + policy_identifier: oid, + policy_qualifiers: None, + } + } + + /// Consider using [`Self::new_oid_only`] instead 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 fn new_oid_qualifiers(oid: Vec, qualifiers: Vec) -> Self { + Self { + policy_identifier: oid, + policy_qualifiers: Some(qualifiers), + } + } + + /// 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 }. + pub fn any_policy() -> Self { + Self::new_oid_only(vec![2, 5, 29, 32, 0]) + } + + /// Certificate issued in compliance with the Extended Validation Guidelines + pub fn extended_validation() -> Self { + Self::new_oid_only(vec![2, 23, 140, 1, 1]) + } + + /// Certificate issued in compliance with the TLS Baseline Requirements – No entity identity asserted + pub fn domain_validated() -> Self { + Self::new_oid_only(vec![2, 23, 140, 1, 2, 1]) + } + + /// Certificate issued in compliance with the TLS Baseline Requirements – Organization identity asserted + pub fn organization_validated() -> Self { + Self::new_oid_only(vec![2, 23, 140, 1, 2, 2]) + } + + /// Certificate issued in compliance with the TLS Baseline Requirements – Individual identity asserted + pub fn individual_validated() -> Self { + Self::new_oid_only(vec![2, 23, 140, 1, 2, 3]) + } + + /// > The CPS Pointer qualifier contains a pointer to a Certification + /// > Practice Statement (CPS) published by the CA. The pointer is in the + /// > form of a URI. Processing requirements for this qualifier are a + /// > local matter. No action is mandated by this specification regardless + /// > of the criticality value asserted for the extension. + pub fn cps_uri(cps_uris: Vec) -> Self { + Self::new_oid_qualifiers( + // Didn't find this one in RFC5280 and took it from PKIOverheid (Dutch Government PKI) using Firefox certificate inspection + // Is it plausible, that this is matching the PolicyQualifierInfo OID? + vec![1, 3, 6, 1, 5, 5, 7, 2, 1], + cps_uris + .iter() + .map(PolicyQualifierInfo::new_cps_uri) + .collect(), + ) + } + + /// User notice is intended for display to a relying party when a + /// certificate is used. Only user notices returned as a result of path + /// validation are intended for display to the user. + pub fn user_notice(user_notice: &UserNotice) -> Self { + Self::new_oid_qualifiers( + // Didn't find this one in RFC5280 and took it from https://github.com/rustls/rcgen/issues/370#issuecomment-3183832371 -> Firefox + // Is it plausible, that this is matching the PolicyQualifierInfo OID? + vec![1, 3, 6, 1, 5, 5, 7, 2, 2], + vec![PolicyQualifierInfo::new_user_notice(user_notice)], + ) + } +} + +#[cfg(all(test, feature = "x509-parser"))] +impl TryFrom> for PolicyInformation { + type Error = Error; + fn try_from(value: x509_parser::extensions::PolicyInformation) -> Result { + let mut policy_identifier = Vec::new(); + + // Contributor question: What error should be returned here? Is this something that can happen? + for v in value.policy_id.iter().ok_or(Error::X509(String::from( + "PolicyInformation without a policy_identifier is invalid", + )))? { + policy_identifier.push(v); + } + + // // This is an alternative way to convert the [`Oid<'_>`] but I think it may yield incorrect results because an arc can be larger than [`u8`] + // for arc in value.policy_id.as_bytes() { + // policy_identifier.push(arc.to_owned() as u64) + // } + + let Some(qualifiers) = value.policy_qualifiers else { + return Ok(Self { + policy_identifier, + policy_qualifiers: None, + }); + }; + + let mut policy_qualifiers = Vec::new(); + for qualifier in qualifiers { + policy_qualifiers.push(qualifier.into()) + } + + Ok(Self { + policy_identifier, + policy_qualifiers: Some(policy_qualifiers), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PolicyQualifierInfo { + policy_qualifier_id: Vec, + /// The DER encoded qualifier + /// + /// > ANY DEFINED BY policyQualifierId + qualifier: Vec, +} + +#[cfg(all(test, feature = "x509-parser"))] +impl From> for PolicyQualifierInfo { + fn from(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Self { + let mut oid = Vec::new(); + for arc in value.policy_qualifier_id.as_bytes() { + oid.push(arc.to_owned() as u64) + } + + Self { + policy_qualifier_id: oid, + qualifier: value.qualifier.to_owned(), + } + } +} + +impl PolicyQualifierInfo { + // id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } + // id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 } + fn new_cps_uri(cps_uri: &string::Ia5String) -> Self { + Self { + policy_qualifier_id: vec![1, 3, 6, 1, 5, 5, 7, 2, 1], + qualifier: yasna::construct_der(|writer| writer.write_ia5_string(cps_uri.as_str())), + } + } + + // id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } + // id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 } + fn new_user_notice(user_notice: &UserNotice) -> Self { + Self { + policy_qualifier_id: vec![1, 3, 6, 1, 5, 5, 7, 2, 2], + qualifier: yasna::encode_der(user_notice), + } + } +} + +impl yasna::DEREncodable for 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); + }); + } +} + +/// > The user notice has two optional fields: the noticeRef field and the +/// > explicitText field. Conforming CAs SHOULD NOT use the noticeRef +/// > option. +/// +/// > If both the noticeRef and explicitText options are included in the +/// > one qualifier and if the application software can locate the notice +/// > text indicated by the noticeRef option, then that text SHOULD be +/// > displayed; otherwise, the explicitText string SHOULD be displayed. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct UserNotice { + /// > The noticeRef field, if used, names an organization and + /// > identifies, by number, a particular textual statement prepared by + /// > that organization. For example, it might identify the + /// > organization "CertsRUs" and notice number 1. In a typical + /// > implementation, the application software will have a notice file + /// > containing the current set of notices for CertsRUs; the + /// > application will extract the notice text from the file and display + /// > it. Messages MAY be multilingual, allowing the software to select + /// > the particular language message for its own environment. + notice_ref: Option, + /// > An explicitText field includes the textual statement directly in + /// > the certificate. The explicitText field is a string with a + /// > maximum size of 200 characters. Conforming CAs SHOULD use the + /// > UTF8String encoding for explicitText, but MAY use IA5String. + /// > Conforming CAs MUST NOT encode explicitText as VisibleString or + /// > BMPString. The explicitText string SHOULD NOT include any control + /// > characters (e.g., U+0000 to U+001F and U+007F to U+009F). When + /// > the UTF8String encoding is used, all character sequences SHOULD be + /// > normalized according to Unicode normalization form C (NFC)[^1] \[[NFC](https://www.rfc-editor.org/rfc/rfc5280#ref-NFC)\]. + /// + /// [^1]: + explicit_text: Option, +} + +impl UserNotice { + /// Creates a new [`UserNotice`] with only the + /// [`UserNotice::explicit_text`] field populated + /// + /// Recommended + pub fn new_explicit_text(msg: DisplayText) -> Self { + Self { + notice_ref: None, + explicit_text: Some(msg), + } + } + + /// Creates a new [`UserNotice`] with all fields populated + /// + /// Consider using [`Self::new_explicit_text`] instead + /// Conforming CAs SHOULD NOT use the noticeRef option. + pub fn new_full(organization: DisplayText, notice_numbers: Vec, msg: String) -> Self { + Self { + notice_ref: Some(NoticeReference { + organization, + notice_numbers, + }), + explicit_text: Some(DisplayText::Utf8String(msg)), // MSG is defined as DisplayText but RECOMMENDED to be UTF8String. Should we expose all options including those that are discouraged? + } + } + + /// Creates a new [`UserNotice`] with all fields populated + /// + /// Consider using [`Self::new_explicit_text`] instead + /// Conforming CAs SHOULD NOT use the noticeRef option. + pub fn new_notice_reference(organization: String, notice_numbers: Vec) -> Self { + Self { + notice_ref: Some(NoticeReference { + organization: DisplayText::Utf8String(organization), + notice_numbers, + }), + explicit_text: None, + } + } +} + +impl yasna::DEREncodable for UserNotice { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + use yasna::encode_der; + writer.write_sequence(|writer| { + if let Some(notice_ref) = &self.notice_ref { + writer.next().write_der(&encode_der(notice_ref)); + } + if let Some(explicit_text) = &self.explicit_text { + writer.next().write_der(&encode_der(explicit_text)); + } + }) + } +} + +/// Consider using [`UserNotice::explicit_text`] instead. +/// +/// > Conforming CAs SHOULD NOT use the noticeRef option. +#[derive(Debug, PartialEq, Eq, Clone)] +struct NoticeReference { + organization: DisplayText, + notice_numbers: Vec, +} + +impl yasna::DEREncodable for NoticeReference { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + use yasna::encode_der; + writer.write_sequence(|writer| { + writer.next().write_der(&encode_der(&self.organization)); + writer.next().write_der(&self.notice_numbers); + }) + } +} + +/// ```ASN.1 +/// DisplayText ::= CHOICE { +/// ia5String IA5String (SIZE (1..200)), +/// visibleString VisibleString (SIZE (1..200)), +/// bmpString BMPString (SIZE (1..200)), +/// utf8String UTF8String (SIZE (1..200)) } +/// ``` +/// +/// Note: While the explicitText has a maximum size of 200 +/// characters, some non-conforming CAs exceed this limit. +/// Therefore, certificate users SHOULD gracefully handle +/// explicitText with more than 200 characters. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum DisplayText { + Ia5String(string::Ia5String), + Utf8String(String), + // Contributor question: + // Should we make non-conformant options available at all? + + // VisibleString(string::VisibleString), // Not implemented yet. Could be imported/inspired from x509_parser + // BmpString(string::BmpString), +} + +impl From<&str> for DisplayText { + fn from(value: &str) -> Self { + Self::Utf8String(value.to_string()) + } +} + +impl From for DisplayText { + fn from(value: String) -> Self { + Self::Utf8String(value) + } +} + +impl yasna::DEREncodable for DisplayText { + fn encode_der<'a>(&self, writer: DERWriter<'a>) { + match self { + DisplayText::Ia5String(string) => writer.write_ia5_string(string.as_str()), + DisplayText::Utf8String(string) => writer.write_utf8_string(string.as_str()), + // DisplayText::BmpString(string) => { + // // [`writer.write_bmp_string`] expects [`&str`]. + // // Would I use write_bytes for this? + // let bytes = string.as_bytes(); + // writer.write_bitvec_bytes(bytes, bytes.len()) + // }, + } + } +} + +/// 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 { + skip_certs: u32, +} + +impl InhibitAnyPolicy { + /// Builds a new InhibitAnyPolicy Extension. + /// + /// > 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. + pub fn new(skip_certs: u32) -> Self { + Self { skip_certs } + } +} + +impl yasna::DEREncodable for 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_i64(self.skip_certs as i64) + }); + } +} + /// A PKCS #10 CSR attribute, as defined in [RFC 5280] and constrained /// by [RFC 2986]. /// diff --git a/rcgen/src/error.rs b/rcgen/src/error.rs index e6ae3961..9881d5d3 100644 --- a/rcgen/src/error.rs +++ b/rcgen/src/error.rs @@ -51,6 +51,10 @@ pub enum Error { /// X509 parsing error #[cfg(feature = "x509-parser")] X509(String), + /// A placeholder error until I can get some feedback on the right error to use + /// + /// Currently used for invalid builder operations + Other, } impl fmt::Display for Error { @@ -101,6 +105,7 @@ 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}")?, + Other => write!(f, "Placeholder error until I get some feedback")?, }; Ok(()) } diff --git a/rcgen/src/lib.rs b/rcgen/src/lib.rs index 83816182..d4dfac04 100644 --- a/rcgen/src/lib.rs +++ b/rcgen/src/lib.rs @@ -42,8 +42,9 @@ 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, UserNotice, }; pub use crl::{ CertificateRevocationList, CertificateRevocationListParams, CrlDistributionPoint, From 8d7eed9b1c9d0f215d95f3b3044947e1d933e9a5 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:44:43 +0100 Subject: [PATCH 04/16] Fix bad noticeNumbers DER; Hide external types in public API --- rcgen/examples/certificate_policies.rs | 8 +- rcgen/src/certificate.rs | 128 +++++++++++++++++-------- 2 files changed, 95 insertions(+), 41 deletions(-) diff --git a/rcgen/examples/certificate_policies.rs b/rcgen/examples/certificate_policies.rs index 3c1eb6f9..e86a7a2a 100644 --- a/rcgen/examples/certificate_policies.rs +++ b/rcgen/examples/certificate_policies.rs @@ -29,9 +29,11 @@ fn main() -> Result<(), Box> { Ia5String::try_from("https://cps.example.com")?, Ia5String::try_from("https://cps.example.org")?, ])) - .add_policy_unchecked(PolicyInformation::user_notice( - &UserNotice::new_explicit_text("Test".into()), - )), + .add_policy_unchecked(PolicyInformation::user_notice(&UserNotice::new_full( + "Example Org".into(), + vec![0, 1, 2, 3], + "Test message".into(), + ))), ); params.inhibit_any_policy = Some(InhibitAnyPolicy::new(2)); diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index d6dc3936..70f6448e 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -605,24 +605,35 @@ impl CertificateParams { } if let Some(certificate_policies) = &self.certificate_policies { - write_x509_extension( - writer.next(), - oid::CERTIFICATE_POLICIES, - certificate_policies.critical, - |writer| { - writer.write_sequence_of(|writer| { - for policy in &certificate_policies.policy_information { - writer.next().write_der(&yasna::encode_der(policy)) - } - }) - }, - ); + // write_x509_extension( + // writer.next(), + // oid::CERTIFICATE_POLICIES, + // certificate_policies.critical, + // |writer| { + // writer.write_sequence_of(|writer| { + // for policy in &certificate_policies.policy_information { + // // writer.next().write_der(&yasna::encode_der(policy)) + + // // Unwrapped equivalent to the above trait use + // writer.next().write_der(&yasna::construct_der(|writer| { + // policy.encode_der(writer) + // })) + // } + // }) + // }, + // ); + writer.next().write_der(&yasna::construct_der(|writer| { + certificate_policies.encode_der(writer); + })) } if let Some(inhibit_any_policy) = &self.inhibit_any_policy { - write_x509_extension(writer.next(), oid::INHIBIT_ANY_POLICY, true, |writer| { - writer.write_i64(inhibit_any_policy.skip_certs as i64) - }); + // write_x509_extension(writer.next(), oid::INHIBIT_ANY_POLICY, true, |writer| { + // writer.write_i64(inhibit_any_policy.skip_certs as i64) + // }); + writer.next().write_der(&yasna::construct_der(|writer| { + inhibit_any_policy.encode_der(writer) + })) } // Write the custom extensions @@ -736,12 +747,18 @@ impl CertificatePolicies { } } -impl yasna::DEREncodable for CertificatePolicies { +// 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::encode_der(policy)) + // writer.next().write_der(&yasna::encode_der(policy)) + + // Unwrapped equivalent to the above trait use + writer + .next() + .write_der(&yasna::construct_der(|writer| policy.encode_der(writer))) } }) }); @@ -818,7 +835,8 @@ pub struct PolicyInformation { policy_qualifiers: Option>, } -impl yasna::DEREncodable for PolicyInformation { +// impl yasna::DEREncodable for PolicyInformation { +impl PolicyInformation { fn encode_der<'a>(&self, writer: DERWriter<'a>) { writer.write_sequence(|writer| { writer @@ -829,7 +847,11 @@ impl yasna::DEREncodable for PolicyInformation { for policy_qualifier in policy_qualifiers { writer .next() - .write_der(&yasna::encode_der(policy_qualifier)); + // .write_der(&yasna::encode_der(policy_qualifier)); + // Unwrapped equivalent to the above trait use + .write_der(&yasna::construct_der(|writer| { + policy_qualifier.encode_der(writer) + })) } }) } @@ -944,11 +966,11 @@ impl TryFrom> for PolicyInformati policy_qualifiers: None, }); }; - - let mut policy_qualifiers = Vec::new(); - for qualifier in qualifiers { - policy_qualifiers.push(qualifier.into()) - } + // let policy_qualifiers = qualifiers.into_iter().map(PolicyQualifierInfo::from).collect(); + let policy_qualifiers = qualifiers + .into_iter() + .map(PolicyQualifierInfo::from_x509) + .collect(); Ok(Self { policy_identifier, @@ -967,8 +989,10 @@ pub struct PolicyQualifierInfo { } #[cfg(all(test, feature = "x509-parser"))] -impl From> for PolicyQualifierInfo { - fn from(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Self { +// impl From> for PolicyQualifierInfo { +// fn from(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Self { +impl PolicyQualifierInfo { + fn from_x509(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Self { let mut oid = Vec::new(); for arc in value.policy_qualifier_id.as_bytes() { oid.push(arc.to_owned() as u64) @@ -996,12 +1020,18 @@ impl PolicyQualifierInfo { fn new_user_notice(user_notice: &UserNotice) -> Self { Self { policy_qualifier_id: vec![1, 3, 6, 1, 5, 5, 7, 2, 2], - qualifier: yasna::encode_der(user_notice), + // qualifier: yasna::encode_der(user_notice), + + // Unwrapped equivalent to the above trait use + qualifier: yasna::construct_der(|writer| { + user_notice.encode_der(writer); + }), } } } -impl yasna::DEREncodable for PolicyQualifierInfo { +// impl yasna::DEREncodable for PolicyQualifierInfo { +impl PolicyQualifierInfo { fn encode_der<'a>(&self, writer: DERWriter<'a>) { writer.write_sequence(|writer| { writer @@ -1062,7 +1092,7 @@ impl UserNotice { /// /// Consider using [`Self::new_explicit_text`] instead /// Conforming CAs SHOULD NOT use the noticeRef option. - pub fn new_full(organization: DisplayText, notice_numbers: Vec, msg: String) -> Self { + pub fn new_full(organization: DisplayText, notice_numbers: NoticeNumbers, msg: String) -> Self { Self { notice_ref: Some(NoticeReference { organization, @@ -1076,7 +1106,7 @@ impl UserNotice { /// /// Consider using [`Self::new_explicit_text`] instead /// Conforming CAs SHOULD NOT use the noticeRef option. - pub fn new_notice_reference(organization: String, notice_numbers: Vec) -> Self { + pub fn new_notice_reference(organization: String, notice_numbers: NoticeNumbers) -> Self { Self { notice_ref: Some(NoticeReference { organization: DisplayText::Utf8String(organization), @@ -1087,7 +1117,8 @@ impl UserNotice { } } -impl yasna::DEREncodable for UserNotice { +// impl yasna::DEREncodable for UserNotice { +impl UserNotice { fn encode_der<'a>(&self, writer: DERWriter<'a>) { use yasna::encode_der; writer.write_sequence(|writer| { @@ -1095,27 +1126,46 @@ impl yasna::DEREncodable for UserNotice { writer.next().write_der(&encode_der(notice_ref)); } if let Some(explicit_text) = &self.explicit_text { - writer.next().write_der(&encode_der(explicit_text)); + // writer.next().write_der(&encode_der(explicit_text)); + + // Unwrapped equivalent to the above trait use + writer.next().write_der(&yasna::construct_der(|writer| { + explicit_text.encode_der(writer); + })) } }) } } +/// ```ASN.1 +/// noticeNumbers SEQUENCE OF INTEGER +/// ``` +type NoticeNumbers = Vec; // Does INTEGER translate to i64? OpenSSL successfully decodes it + /// Consider using [`UserNotice::explicit_text`] instead. /// /// > Conforming CAs SHOULD NOT use the noticeRef option. #[derive(Debug, PartialEq, Eq, Clone)] struct NoticeReference { organization: DisplayText, - notice_numbers: Vec, + notice_numbers: NoticeNumbers, } +// NoticeRef is private. Should I still remove the Trait impl? impl yasna::DEREncodable for NoticeReference { fn encode_der<'a>(&self, writer: DERWriter<'a>) { - use yasna::encode_der; writer.write_sequence(|writer| { - writer.next().write_der(&encode_der(&self.organization)); - writer.next().write_der(&self.notice_numbers); + // writer.next().write_der(&encode_der(&self.organization)); + writer.next().write_der(&yasna::construct_der(|writer| { + self.organization.encode_der(writer); + })); + // This is incorrect + // writer.next().write_der(&self.notice_numbers); + writer.next().write_sequence_of(|writer| { + for val in &self.notice_numbers { + writer.next().write_i64(*val); + } + }); }) } } @@ -1155,7 +1205,8 @@ impl From for DisplayText { } } -impl yasna::DEREncodable for DisplayText { +// impl yasna::DEREncodable for DisplayText { +impl DisplayText { fn encode_der<'a>(&self, writer: DERWriter<'a>) { match self { DisplayText::Ia5String(string) => writer.write_ia5_string(string.as_str()), @@ -1209,7 +1260,8 @@ impl InhibitAnyPolicy { } } -impl yasna::DEREncodable for InhibitAnyPolicy { +// 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| { From 3dc087d8d9445165956600bd23ea4f3f2bfa94e6 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:48:38 +0100 Subject: [PATCH 05/16] Fix check: build-windows --- rcgen/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rcgen/Cargo.toml b/rcgen/Cargo.toml index 8d1ab2da..04e48de9 100644 --- a/rcgen/Cargo.toml +++ b/rcgen/Cargo.toml @@ -31,6 +31,10 @@ zeroize = { workspace = true, optional = true } [target."cfg(unix)".dev-dependencies] openssl = { workspace = true } +[[example]] +name = "certificate_policies" +required-features = ["pem"] + [[example]] name = "rsa-irc-openssl" required-features = ["pem"] From eea90bf1b157d95eae1f845ea8a7a14ee5814cde Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:50:44 +0100 Subject: [PATCH 06/16] Apply unstable formatting cargo fmt --all -- --config-path .rustfmt.unstable.toml --- rcgen/examples/certificate_policies.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rcgen/examples/certificate_policies.rs b/rcgen/examples/certificate_policies.rs index e86a7a2a..c634ecad 100644 --- a/rcgen/examples/certificate_policies.rs +++ b/rcgen/examples/certificate_policies.rs @@ -2,9 +2,10 @@ use std::fs; +use rcgen::string::Ia5String; use rcgen::{ - date_time_ymd, string::Ia5String, CertificateParams, CertificatePolicies, DistinguishedName, - DnType, InhibitAnyPolicy, KeyPair, PolicyInformation, SanType, UserNotice, + date_time_ymd, CertificateParams, CertificatePolicies, DistinguishedName, DnType, + InhibitAnyPolicy, KeyPair, PolicyInformation, SanType, UserNotice, }; fn main() -> Result<(), Box> { From 4677cbc88c04e0091c8eee8311ed00de6874c3e6 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:59:50 +0100 Subject: [PATCH 07/16] Add checked add_policies method --- rcgen/src/certificate.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 70f6448e..45e76dd1 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -795,6 +795,24 @@ impl CertificatePolicies { }) } + /// Add multiple policies at once + /// Returns [`Error::Other`] if any of the + /// PolicyInformation OIDs are already present + pub fn add_policies(self, policies: &[PolicyInformation]) -> Result { + let mut registered_policies = self.policy_information.clone(); + registered_policies.extend_from_slice(policies); + let mut oids = std::collections::HashSet::<&[u64]>::new(); + for policy in ®istered_policies { + if !oids.insert(&policy.policy_identifier) { + return Err(Error::Other); + } + } + Ok(Self { + critical: self.critical, + policy_information: registered_policies, + }) + } + /// Add once policy at a time /// Does not validate if the PolicyInformation OID is unique. pub fn add_policy_unchecked(self, policy: PolicyInformation) -> Self { From 363fff3ec499282caf036c2d3532c3f84416da9e Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:04:18 +0100 Subject: [PATCH 08/16] Fix doc complaints cargo doc --features ring,pem,x509-parser --document-private-items --- rcgen/src/certificate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 45e76dd1..119c915f 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -1096,7 +1096,7 @@ pub struct UserNotice { impl UserNotice { /// Creates a new [`UserNotice`] with only the - /// [`UserNotice::explicit_text`] field populated + /// explicit_text field populated /// /// Recommended pub fn new_explicit_text(msg: DisplayText) -> Self { @@ -1239,7 +1239,7 @@ impl DisplayText { } } -/// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.14 +/// 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 From 2063aec8018be544ea59f349fe76eef4e787a03f Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:16:15 +0100 Subject: [PATCH 09/16] Remove some redundant comments, add missing from_x509 implementation for InhibitAnyPolicy --- rcgen/src/certificate.rs | 49 ++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 119c915f..e092e8ca 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -197,6 +197,7 @@ impl CertificateParams { 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() }) } @@ -702,13 +703,8 @@ pub struct CertificatePolicies { /// A certificate policy OID MUST NOT appear more than once in a /// certificate policies extension. policy_information: Vec, - // Contributor note: - // Would it be better/feasible to use a HashSet where the hash is determined by - // the OID to ensure RFC compliance here? Is ensuring valid extensions part of - // this crate's responsibilities or is it the consumers responsibility? } -// Contributer note: Strongly inspired by NameConstraints impl impl CertificatePolicies { #[cfg(all(test, feature = "x509-parser"))] fn from_x509( @@ -737,7 +733,8 @@ impl CertificatePolicies { let mut policy_information: Vec = Vec::with_capacity(policies.len()); for policy in policies.iter().cloned() { - policy_information.push(policy.try_into()?) + // policy_information.push(policy.try_into()?) + policy_information.push(PolicyInformation::from_x509(policy)?); } Ok(Some(Self { @@ -961,9 +958,11 @@ impl PolicyInformation { } #[cfg(all(test, feature = "x509-parser"))] -impl TryFrom> for PolicyInformation { - type Error = Error; - fn try_from(value: x509_parser::extensions::PolicyInformation) -> Result { +// impl TryFrom> for PolicyInformation { +// type Error = Error; +// fn try_from(value: x509_parser::extensions::PolicyInformation) -> Result { +impl PolicyInformation { + fn from_x509(value: x509_parser::extensions::PolicyInformation) -> Result { let mut policy_identifier = Vec::new(); // Contributor question: What error should be returned here? Is this something that can happen? @@ -973,11 +972,6 @@ impl TryFrom> for PolicyInformati policy_identifier.push(v); } - // // This is an alternative way to convert the [`Oid<'_>`] but I think it may yield incorrect results because an arc can be larger than [`u8`] - // for arc in value.policy_id.as_bytes() { - // policy_identifier.push(arc.to_owned() as u64) - // } - let Some(qualifiers) = value.policy_qualifiers else { return Ok(Self { policy_identifier, @@ -1278,6 +1272,33 @@ impl InhibitAnyPolicy { } } +// #[cfg(feature = "x509-parser")] +// impl From for InhibitAnyPolicy { +// fn from(value: x509_parser::extensions::InhibitAnyPolicy) -> Self { +// Self { +// skip_certs: value.skip_certs +// } +// } +// } +#[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>) { From bed67c350b29f2a45c2c464313bb26cf8c15194c Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sun, 18 Jan 2026 17:05:58 +0100 Subject: [PATCH 10/16] Fix parser issue, add some tests --- rcgen/src/certificate.rs | 215 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 207 insertions(+), 8 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index e092e8ca..6ae59510 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -473,6 +473,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(()); @@ -768,6 +770,12 @@ impl CertificatePolicies { /// Encorces validity by requiring you to add the first policy. /// Add more policies by using the [`Self::add_policy`] method /// or multiple at once by using the [`Self::add_policies`] method. + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation}; + /// + /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()); + /// ``` pub fn new(criticality: bool, policy: PolicyInformation) -> Self { Self { critical: criticality, @@ -778,6 +786,37 @@ impl CertificatePolicies { /// Add policy and check if it is already present. /// Returns [`Error::Other`] if the /// PolicyInformation OID is already present + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) + /// .add_policy(PolicyInformation::domain_validated()); + /// + /// assert!(policies.is_ok()); + /// ``` + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) + /// .add_policy(PolicyInformation::any_policy()); + /// + /// assert_eq!(policies, Err(Error::Other)); + /// ``` + /// + /// This does not prevent logic errors such as adding OIDs with conflicting meaning. + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// let policies = CertificatePolicies::new(false, PolicyInformation::extended_validation()) + /// .add_policy(PolicyInformation::domain_validated()).expect("This OID wasn't added yet") + /// .add_policy(PolicyInformation::organization_validated()).expect("This OID wasn't added yet") + /// .add_policy(PolicyInformation::individual_validated()); + /// + /// assert!(policies.is_ok()); + /// ``` pub fn add_policy(self, policy: PolicyInformation) -> Result { let mut registered_policies = self.policy_information; for policy_information in ®istered_policies { @@ -793,8 +832,48 @@ impl CertificatePolicies { } /// Add multiple policies at once + /// /// Returns [`Error::Other`] if any of the /// PolicyInformation OIDs are already present + /// + /// --- + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) + /// .add_policies(&[PolicyInformation::domain_validated()]); + /// + /// assert!(policies.is_ok()); + /// ``` + /// + /// --- + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) + /// .add_policies(&[PolicyInformation::any_policy()]); + /// + /// assert_eq!(policies, Err(Error::Other)); + /// ``` + /// + /// --- + /// + /// This does not prevent logic errors such as adding OIDs with conflicting meaning. + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// let policies = CertificatePolicies::new(false, PolicyInformation::extended_validation()) + /// .add_policies(&[ + /// PolicyInformation::domain_validated(), + /// PolicyInformation::organization_validated(), + /// PolicyInformation::individual_validated(), + /// ]); + /// + /// assert!(policies.is_ok()); + /// ``` pub fn add_policies(self, policies: &[PolicyInformation]) -> Result { let mut registered_policies = self.policy_information.clone(); registered_policies.extend_from_slice(policies); @@ -812,6 +891,14 @@ impl CertificatePolicies { /// Add once policy at a time /// Does not validate if the PolicyInformation OID is unique. + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// // Duplicate policies are added without checks. It is your responsibility to prevent duplicates when using this method. + /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) + /// .add_policy_unchecked(PolicyInformation::any_policy()); // Bad + /// ``` pub fn add_policy_unchecked(self, policy: PolicyInformation) -> Self { let mut registered_policies = self.policy_information.clone(); registered_policies.push(policy); @@ -823,6 +910,14 @@ impl CertificatePolicies { /// Add multiple policies at once /// Does not validate if the PolicyInformation OID is unique. + /// + /// ```rust + /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; + /// + /// // Duplicate policies are added without checks. It is your responsibility to prevent duplicates when using this method. + /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) + /// .add_policies_unchecked(&[PolicyInformation::any_policy()]); // Bad + /// ``` pub fn add_policies_unchecked(self, policies: &[PolicyInformation]) -> Self { let mut registered_policies = self.policy_information.clone(); registered_policies.extend_from_slice(policies); @@ -982,7 +1077,7 @@ impl PolicyInformation { let policy_qualifiers = qualifiers .into_iter() .map(PolicyQualifierInfo::from_x509) - .collect(); + .collect::, Error>>()?; Ok(Self { policy_identifier, @@ -1001,19 +1096,25 @@ pub struct PolicyQualifierInfo { } #[cfg(all(test, feature = "x509-parser"))] -// impl From> for PolicyQualifierInfo { -// fn from(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Self { +// impl TryFrom> for PolicyQualifierInfo { +// type Error = Error; +// fn try_from(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Result { impl PolicyQualifierInfo { - fn from_x509(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Self { + fn from_x509(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Result { let mut oid = Vec::new(); - for arc in value.policy_qualifier_id.as_bytes() { - oid.push(arc.to_owned() as u64) + for arc in value + .policy_qualifier_id + .iter() + .ok_or(Error::X509(String::from( + "PolicyInformation without a policy_identifier is invalid", + )))? { + oid.push(arc); } - Self { + Ok(Self { policy_qualifier_id: oid, qualifier: value.qualifier.to_owned(), - } + }) } } @@ -1796,6 +1897,104 @@ mod tests { #[cfg(feature = "crypto")] use crate::KeyPair; + #[test] + /// This is more or less only a regression test for later. + /// The below hex is from a certificate that was created with rcgen + /// and represents the CertificatePolicies extension + /// + /// We can assume that it is valid because it successfully decoded in , OpenSSL, Chromium (Edge) and Gecko (Firefox) + /// + /// [Source](https://lapo.it/asn1js/#MIICWTCCAf-gAwIBAgIUKv3YJ-6n4Wx68Zu3PVnCJOk5hDswCgYIKoZIzj0EAwIwMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDAgFw03NTAxMDEwMDAwMDBaGA80MDk2MDEwMTAwMDAwMFowMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABC_pBjrEyqtvBXKiws6h7Q2mPePPNJspjk-HbEqkg_WVRJZWQL90IPIhPFIJvp0Eho0uWISSFbm3hXWwmtcHmxqjgfQwgfEwIQYDVR0RBBowGIILY3JhYnMuY3JhYnOCCWxvY2FsaG9zdDCBvAYDVR0gBIG0MIGxMAYGBFUdIAAwCAYGZ4EMAQIBMFYGCCsGAQUFBwIBMEowIwYIKwYBBQUHAgEWF2h0dHBzOi8vY3BzLmV4YW1wbGUuY29tMCMGCCsGAQUFBwIBFhdodHRwczovL2Nwcy5leGFtcGxlLm9yZzBFBggrBgEFBQcCAjA5MDcGCCsGAQUFBwICMCswGwwLRXhhbXBsZSBPcmcwDAIBAAIBAQIBAgIBAwwMVGVzdCBtZXNzYWdlMA0GA1UdNgEB_wQDAgECMAoGCCqGSM49BAMCA0gAMEUCIDqnBSHeWgQ0tsqgmd8GioEp68y5imF4duQxYxtx0JYDAiEAp4RbNjrVN5PTnbFnE6ySCNuefB70j-29TOyuR2FfD7A) + fn test_certificate_policies_expected_der() { + const EXPECTED_DER: &[u8] = &[ + 0x30, 0x81, 0xBC, 0x06, 0x03, 0x55, 0x1D, 0x20, 0x04, 0x81, 0xB4, 0x30, 0x81, 0xB1, + 0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00, 0x30, 0x08, 0x06, 0x06, 0x67, 0x81, + 0x0C, 0x01, 0x02, 0x01, 0x30, 0x56, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x02, 0x01, 0x30, 0x4A, 0x30, 0x23, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x02, 0x01, 0x16, 0x17, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x63, 0x70, + 0x73, 0x2E, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x30, + 0x23, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x17, 0x68, + 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x63, 0x70, 0x73, 0x2E, 0x65, 0x78, 0x61, + 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x6F, 0x72, 0x67, 0x30, 0x45, 0x06, 0x08, 0x2B, 0x06, + 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x39, 0x30, 0x37, 0x06, 0x08, 0x2B, 0x06, + 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, 0x30, 0x1B, 0x0C, 0x0B, 0x45, 0x78, + 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x4F, 0x72, 0x67, 0x30, 0x0C, 0x02, 0x01, 0x00, + 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x03, 0x0C, 0x0C, 0x54, 0x65, 0x73, + 0x74, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + ]; + let extension_der = yasna::construct_der(|writer| { + CertificatePolicies::new(false, PolicyInformation::any_policy()) + .add_policy_unchecked(PolicyInformation::domain_validated()) + .add_policy_unchecked(PolicyInformation::cps_uri(vec![ + string::Ia5String::try_from("https://cps.example.com").unwrap(), + string::Ia5String::try_from("https://cps.example.org").unwrap(), + ])) + .add_policy_unchecked(PolicyInformation::user_notice(&UserNotice::new_full( + "Example Org".into(), + vec![0, 1, 2, 3], + "Test message".into(), + ))) + .encode_der(writer) + }); + assert_eq!(EXPECTED_DER, &extension_der) + } + + #[test] + fn test_certificate_policies_encode_decode() { + let params = CertificateParams { + certificate_policies: Some( + CertificatePolicies::new(false, PolicyInformation::any_policy()) + .add_policy_unchecked(PolicyInformation::domain_validated()) + .add_policy_unchecked(PolicyInformation::cps_uri(vec![ + string::Ia5String::try_from("https://cps.example.com").unwrap(), + string::Ia5String::try_from("https://cps.example.org").unwrap(), + ])) + .add_policy_unchecked(PolicyInformation::user_notice(&UserNotice::new_full( + "Example Org".into(), + vec![0, 1, 2, 3], + "Test message".into(), + ))), + ), + ..Default::default() + }; + + let key_pair = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key_pair).unwrap(); + + let parsed_params = 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_params.certificate_policies + ) + } + + #[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::new(2).encode_der(writer)); + assert_eq!(EXPECTED_DER, &extension_der) + } + + #[test] + fn test_inhibit_any_policy_encode_decode() { + let params = CertificateParams { + inhibit_any_policy: Some(InhibitAnyPolicy::new(2)), + ..Default::default() + }; + + let key_pair = KeyPair::generate().unwrap(); + let cert = params.self_signed(&key_pair).unwrap(); + + let parsed_params = 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_params.inhibit_any_policy,) + } + #[cfg(feature = "crypto")] #[test] fn test_with_key_usages() { From 22866f81927c12503780df90c6acda9a68588232 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:02:55 +0100 Subject: [PATCH 11/16] Add more tests, export forgotten structs that are needed when constructing custom policies --- rcgen/src/certificate.rs | 258 +++++++++++++++++++++++++++++++++++++-- rcgen/src/lib.rs | 5 +- 2 files changed, 252 insertions(+), 11 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 6ae59510..9d01acb8 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -1087,6 +1087,15 @@ impl PolicyInformation { } #[derive(Debug, PartialEq, Eq, Clone)] +/// ```ASN.1 +/// PolicyQualifierInfo ::= SEQUENCE { +/// policyQualifierId PolicyQualifierId, +/// qualifier ANY DEFINED BY policyQualifierId } +/// ``` +/// +/// Unless you want to create a custom qualifier, you should probably use a convenience method from [`PolicyInformation`] +/// +/// RFC5280 strongly recommends that no custom qualifiers are used. pub struct PolicyQualifierInfo { policy_qualifier_id: Vec, /// The DER encoded qualifier @@ -1119,18 +1128,37 @@ impl PolicyQualifierInfo { } impl PolicyQualifierInfo { - // id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } - // id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 } - fn new_cps_uri(cps_uri: &string::Ia5String) -> Self { + // This seems like a sensible method but I don't think we can expose this trait bound. + // /// Create a custom [`PolicyQualifierInfo`] from an OID and any [`yasna::DEREncodable`] object. + // pub fn new_custom(policy_qualifier_id: Vec, qualifier: Q) -> Self { + // Self { + // policy_qualifier_id, + // qualifier: yasna::encode_der(&qualifier), + // } + // } + + /// Create a custom [`PolicyQualifierInfo`] + /// + /// It is your responsibility to provide valid DER for the qualifier. + pub fn new_custom(policy_qualifier_id: Vec, qualifier: Vec) -> Self { + Self { + policy_qualifier_id, + qualifier, + } + } + + /// id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } + /// id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 } + pub fn new_cps_uri(cps_uri: &string::Ia5String) -> Self { Self { policy_qualifier_id: vec![1, 3, 6, 1, 5, 5, 7, 2, 1], qualifier: yasna::construct_der(|writer| writer.write_ia5_string(cps_uri.as_str())), } } - // id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } - // id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 } - fn new_user_notice(user_notice: &UserNotice) -> Self { + /// id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } + /// id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 } + pub fn new_user_notice(user_notice: &UserNotice) -> Self { Self { policy_qualifier_id: vec![1, 3, 6, 1, 5, 5, 7, 2, 2], // qualifier: yasna::encode_der(user_notice), @@ -1297,7 +1325,9 @@ impl yasna::DEREncodable for NoticeReference { /// explicitText with more than 200 characters. #[derive(Debug, PartialEq, Eq, Clone)] pub enum DisplayText { + /// Prefer [`Self::Utf8String`] Ia5String(string::Ia5String), + /// Usually what you want Utf8String(String), // Contributor question: // Should we make non-conformant options available at all? @@ -1308,7 +1338,7 @@ pub enum DisplayText { impl From<&str> for DisplayText { fn from(value: &str) -> Self { - Self::Utf8String(value.to_string()) + Self::from(value.to_string()) } } @@ -1897,6 +1927,211 @@ mod tests { #[cfg(feature = "crypto")] use crate::KeyPair; + #[test] + fn test_policy_information_any_policy_der() { + const EXPECTED_DER: &[u8] = &[0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::any_policy().encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_domain_validated_der() { + const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x01]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::domain_validated().encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_extended_validation_der() { + const EXPECTED_DER: &[u8] = &[0x30, 0x07, 0x06, 0x05, 0x67, 0x81, 0x0C, 0x01, 0x01]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::extended_validation().encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + /// Note: ASN.1 JavaScript Decoder didn't recognize this OID + fn test_policy_information_individual_validated_der() { + const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x03]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::individual_validated().encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_organization_validated_der() { + const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x02]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::organization_validated().encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_cps_uri_der() { + const EXPECTED_DER: &[u8] = &[ + 0x30, 0x31, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x30, 0x25, + 0x30, 0x23, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x17, + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x63, 0x70, 0x73, 0x2E, 0x65, 0x78, + 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x6F, 0x72, 0x67, + ]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::cps_uri(vec!["https://cps.example.org".try_into().unwrap()]) + .encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_new_oid_only_der() { + const EXPECTED_DER: &[u8] = &[0x30, 0x05, 0x06, 0x03, 0x01, 0x02, 0x03]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::new_oid_only(vec![0, 1, 2, 3]).encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der) + } + + #[test] + fn test_policy_information_new_oid_qualifiers_der() { + 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::new_oid_qualifiers( + vec![1, 2, 3, 4], + vec![ + PolicyQualifierInfo::new_custom( + vec![1, 2, 3, 4, 0], + yasna::construct_der(|writer| writer.write_utf8string("UTF8String")), + ), + PolicyQualifierInfo::new_custom( + vec![1, 2, 3, 4, 1], + 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_user_notice_explicit_text_der() { + const EXPECTED_DER: &[u8] = &[ + 0x30, 0x45, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x39, + 0x30, 0x37, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, + 0x16, 0x29, 0x59, 0x6F, 0x75, 0x20, 0x73, 0x68, 0x6F, 0x75, 0x6C, 0x64, 0x20, 0x75, + 0x73, 0x75, 0x61, 0x6C, 0x6C, 0x79, 0x20, 0x75, 0x73, 0x65, 0x20, 0x61, 0x6E, 0x20, + 0x55, 0x54, 0x46, 0x38, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x20, 0x68, 0x65, 0x72, + 0x65, + ]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::user_notice(&UserNotice::new_explicit_text(DisplayText::Ia5String( + "You should usually use an UTF8String here" + .try_into() + .unwrap(), + ))) + .encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &policy_information_der); + } + + #[test] + fn test_policy_information_user_notice_reference_der() { + const EXPECTED_DER: &[u8] = &[ + 0x30, 0x37, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, + 0x30, 0x29, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x1D, + 0x30, 0x1B, 0x0C, 0x0B, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x4F, 0x72, + 0x67, 0x30, 0x0C, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, + 0x03, + ]; + let policy_information_der = yasna::construct_der(|writer| { + PolicyInformation::user_notice(&UserNotice::new_notice_reference( + "Example Org".to_string(), + vec![0, 1, 2, 3], + )) + .encode_der(writer) + }); + assert_eq!(EXPECTED_DER, &policy_information_der); + } + + #[cfg(feature = "crypto")] + #[test] + /// [DER Source](https://lapo.it/asn1js/#MIICsTCCAlagAwIBAgIUNIeLHUZBtAK7N0HeSWhxrNt7PpkwCgYIKoZIzj0EAwIwMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDAgFw03NTAxMDEwMDAwMDBaGA80MDk2MDEwMTAwMDAwMFowMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKWGpFh06ExqLv-IX2QYQFSeglg3hakmqouV7ghFx_CQp6PKF3uEEniibaJ4OSXivEfigCkiWYAzsTJgs5jTQrSjggFKMIIBRjAhBgNVHREEGjAYggtjcmFicy5jcmFic4IJbG9jYWxob3N0MIIBEAYDVR0gBIIBBzCCAQMwBgYEVR0gADAIBgZngQwBAgEwBwYFZ4EMAQEwCAYGZ4EMAQIDMAgGBmeBDAECAjAFBgMBAgMwLAYDKgMEMCUwEgYEKgMEAAwKVVRGOFN0cmluZzAPBgQqAwQBEgcxMjggMjU2MFYGCCsGAQUFBwIBMEowIwYIKwYBBQUHAgEWF2h0dHBzOi8vY3BzLmV4YW1wbGUuY29tMCMGCCsGAQUFBwIBFhdodHRwczovL2Nwcy5leGFtcGxlLm9yZzBFBggrBgEFBQcCAjA5MDcGCCsGAQUFBwICMCswGwwLRXhhbXBsZSBPcmcwDAIBAAIBAQIBAgIBAwwMVGVzdCBtZXNzYWdlMA0GA1UdNgEB_wQDAgECMAoGCCqGSM49BAMCA0kAMEYCIQCzJmW-eUcSHqdi3V-eb30ZsRIY-tBVohkP06JVxt6IxAIhANKV7alaTWWojOtTXgLkH4nmqEiXGFIb1RXPsNfIhf1H) + fn test_certificate_policies_expected_der() { + use crate::string::Ia5String; + const EXPECTED_DER: &[u8] = &[ + 0x30, 0x82, 0x01, 0x10, 0x06, 0x03, 0x55, 0x1D, 0x20, 0x04, 0x82, 0x01, 0x07, 0x30, + 0x82, 0x01, 0x03, 0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00, 0x30, 0x08, 0x06, + 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x01, 0x30, 0x07, 0x06, 0x05, 0x67, 0x81, 0x0C, + 0x01, 0x01, 0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x03, 0x30, 0x08, + 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x02, 0x30, 0x05, 0x06, 0x03, 0x01, 0x02, + 0x03, 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, 0x30, 0x56, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, + 0x07, 0x02, 0x01, 0x30, 0x4A, 0x30, 0x23, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, + 0x07, 0x02, 0x01, 0x16, 0x17, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x63, + 0x70, 0x73, 0x2E, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, + 0x30, 0x23, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x17, + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x63, 0x70, 0x73, 0x2E, 0x65, 0x78, + 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x6F, 0x72, 0x67, 0x30, 0x45, 0x06, 0x08, 0x2B, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x39, 0x30, 0x37, 0x06, 0x08, 0x2B, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, 0x30, 0x1B, 0x0C, 0x0B, 0x45, + 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x20, 0x4F, 0x72, 0x67, 0x30, 0x0C, 0x02, 0x01, + 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x03, 0x0C, 0x0C, 0x54, 0x65, + 0x73, 0x74, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + ]; + let extension_der = yasna::construct_der(|writer| { + CertificatePolicies::new(false, PolicyInformation::any_policy()) + .add_policies_unchecked(&[ + PolicyInformation::domain_validated(), + PolicyInformation::extended_validation(), + PolicyInformation::individual_validated(), + PolicyInformation::organization_validated(), + PolicyInformation::new_oid_only(vec![0, 1, 2, 3]), + PolicyInformation::new_oid_qualifiers( + vec![1, 2, 3, 4], + vec![ + PolicyQualifierInfo::new_custom( + vec![1, 2, 3, 4, 0], + yasna::construct_der(|writer| { + writer.write_utf8string("UTF8String") + }), + ), + PolicyQualifierInfo::new_custom( + vec![1, 2, 3, 4, 1], + yasna::construct_der(|writer| { + writer.write_numeric_string("128 256") + }), + ), + ], + ), + PolicyInformation::cps_uri(vec![ + Ia5String::try_from("https://cps.example.com").unwrap(), + Ia5String::try_from("https://cps.example.org").unwrap(), + ]), + PolicyInformation::user_notice(&UserNotice::new_full( + "Example Org".into(), + vec![0, 1, 2, 3], + "Test message".into(), + )), + ]) + .encode_der(writer); + }); + assert_eq!(EXPECTED_DER, &extension_der); + } + + #[cfg(feature = "crypto")] #[test] /// This is more or less only a regression test for later. /// The below hex is from a certificate that was created with rcgen @@ -1905,7 +2140,7 @@ mod tests { /// We can assume that it is valid because it successfully decoded in , OpenSSL, Chromium (Edge) and Gecko (Firefox) /// /// [Source](https://lapo.it/asn1js/#MIICWTCCAf-gAwIBAgIUKv3YJ-6n4Wx68Zu3PVnCJOk5hDswCgYIKoZIzj0EAwIwMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDAgFw03NTAxMDEwMDAwMDBaGA80MDk2MDEwMTAwMDAwMFowMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABC_pBjrEyqtvBXKiws6h7Q2mPePPNJspjk-HbEqkg_WVRJZWQL90IPIhPFIJvp0Eho0uWISSFbm3hXWwmtcHmxqjgfQwgfEwIQYDVR0RBBowGIILY3JhYnMuY3JhYnOCCWxvY2FsaG9zdDCBvAYDVR0gBIG0MIGxMAYGBFUdIAAwCAYGZ4EMAQIBMFYGCCsGAQUFBwIBMEowIwYIKwYBBQUHAgEWF2h0dHBzOi8vY3BzLmV4YW1wbGUuY29tMCMGCCsGAQUFBwIBFhdodHRwczovL2Nwcy5leGFtcGxlLm9yZzBFBggrBgEFBQcCAjA5MDcGCCsGAQUFBwICMCswGwwLRXhhbXBsZSBPcmcwDAIBAAIBAQIBAgIBAwwMVGVzdCBtZXNzYWdlMA0GA1UdNgEB_wQDAgECMAoGCCqGSM49BAMCA0gAMEUCIDqnBSHeWgQ0tsqgmd8GioEp68y5imF4duQxYxtx0JYDAiEAp4RbNjrVN5PTnbFnE6ySCNuefB70j-29TOyuR2FfD7A) - fn test_certificate_policies_expected_der() { + fn test_reasonable_certificate_policies_expected_der() { const EXPECTED_DER: &[u8] = &[ 0x30, 0x81, 0xBC, 0x06, 0x03, 0x55, 0x1D, 0x20, 0x04, 0x81, 0xB4, 0x30, 0x81, 0xB1, 0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00, 0x30, 0x08, 0x06, 0x06, 0x67, 0x81, @@ -1936,9 +2171,11 @@ mod tests { ))) .encode_der(writer) }); - assert_eq!(EXPECTED_DER, &extension_der) + assert_eq!(EXPECTED_DER, &extension_der); } + #[cfg(feature = "crypto")] + #[cfg(feature = "x509-parser")] #[test] fn test_certificate_policies_encode_decode() { let params = CertificateParams { @@ -1969,6 +2206,7 @@ mod tests { ) } + #[cfg(feature = "crypto")] #[test] fn test_inhibit_any_policy_expected_der() { const EXPECTED_DER: &[u8] = &[ @@ -1980,6 +2218,8 @@ mod tests { assert_eq!(EXPECTED_DER, &extension_der) } + #[cfg(feature = "crypto")] + #[cfg(feature = "x509-parser")] #[test] fn test_inhibit_any_policy_encode_decode() { let params = CertificateParams { diff --git a/rcgen/src/lib.rs b/rcgen/src/lib.rs index d4dfac04..cbba001b 100644 --- a/rcgen/src/lib.rs +++ b/rcgen/src/lib.rs @@ -43,8 +43,9 @@ use std::ops::Deref; pub use certificate::{ date_time_ymd, Attribute, BasicConstraints, Certificate, CertificateParams, - CertificatePolicies, CidrSubnet, CustomExtension, DnType, ExtendedKeyUsagePurpose, - GeneralSubtree, InhibitAnyPolicy, IsCa, NameConstraints, PolicyInformation, UserNotice, + CertificatePolicies, CidrSubnet, CustomExtension, DisplayText, DnType, ExtendedKeyUsagePurpose, + GeneralSubtree, InhibitAnyPolicy, IsCa, NameConstraints, PolicyInformation, + PolicyQualifierInfo, UserNotice, }; pub use crl::{ CertificateRevocationList, CertificateRevocationListParams, CrlDistributionPoint, From 96291d567320e03c2322e5dce8712b3c4937fa52 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:21:31 +0100 Subject: [PATCH 12/16] Fix failed checks Ran every failed command locally --- rcgen/src/certificate.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 9d01acb8..cc198883 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -1929,6 +1929,7 @@ mod tests { #[test] fn test_policy_information_any_policy_der() { + use crate::PolicyInformation; const EXPECTED_DER: &[u8] = &[0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00]; let policy_information_der = yasna::construct_der(|writer| { PolicyInformation::any_policy().encode_der(writer); @@ -1938,6 +1939,7 @@ mod tests { #[test] fn test_policy_information_domain_validated_der() { + use crate::PolicyInformation; const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x01]; let policy_information_der = yasna::construct_der(|writer| { PolicyInformation::domain_validated().encode_der(writer); @@ -1947,6 +1949,7 @@ mod tests { #[test] fn test_policy_information_extended_validation_der() { + use crate::PolicyInformation; const EXPECTED_DER: &[u8] = &[0x30, 0x07, 0x06, 0x05, 0x67, 0x81, 0x0C, 0x01, 0x01]; let policy_information_der = yasna::construct_der(|writer| { PolicyInformation::extended_validation().encode_der(writer); @@ -1957,6 +1960,7 @@ mod tests { #[test] /// Note: ASN.1 JavaScript Decoder didn't recognize this OID fn test_policy_information_individual_validated_der() { + use crate::PolicyInformation; const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x03]; let policy_information_der = yasna::construct_der(|writer| { PolicyInformation::individual_validated().encode_der(writer); @@ -1966,6 +1970,7 @@ mod tests { #[test] fn test_policy_information_organization_validated_der() { + use crate::PolicyInformation; const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x02]; let policy_information_der = yasna::construct_der(|writer| { PolicyInformation::organization_validated().encode_der(writer); @@ -1975,6 +1980,7 @@ mod tests { #[test] fn test_policy_information_cps_uri_der() { + use crate::PolicyInformation; const EXPECTED_DER: &[u8] = &[ 0x30, 0x31, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x30, 0x25, 0x30, 0x23, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x17, @@ -1990,6 +1996,7 @@ mod tests { #[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::new_oid_only(vec![0, 1, 2, 3]).encode_der(writer); @@ -1999,6 +2006,7 @@ mod tests { #[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, @@ -2026,6 +2034,7 @@ mod tests { #[test] fn test_policy_information_user_notice_explicit_text_der() { + use crate::{DisplayText, PolicyInformation, UserNotice}; const EXPECTED_DER: &[u8] = &[ 0x30, 0x45, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x39, 0x30, 0x37, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, @@ -2047,6 +2056,7 @@ mod tests { #[test] fn test_policy_information_user_notice_reference_der() { + use crate::{PolicyInformation, UserNotice}; const EXPECTED_DER: &[u8] = &[ 0x30, 0x37, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, 0x30, 0x29, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x1D, @@ -2068,7 +2078,13 @@ mod tests { #[test] /// [DER Source](https://lapo.it/asn1js/#MIICsTCCAlagAwIBAgIUNIeLHUZBtAK7N0HeSWhxrNt7PpkwCgYIKoZIzj0EAwIwMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDAgFw03NTAxMDEwMDAwMDBaGA80MDk2MDEwMTAwMDAwMFowMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKWGpFh06ExqLv-IX2QYQFSeglg3hakmqouV7ghFx_CQp6PKF3uEEniibaJ4OSXivEfigCkiWYAzsTJgs5jTQrSjggFKMIIBRjAhBgNVHREEGjAYggtjcmFicy5jcmFic4IJbG9jYWxob3N0MIIBEAYDVR0gBIIBBzCCAQMwBgYEVR0gADAIBgZngQwBAgEwBwYFZ4EMAQEwCAYGZ4EMAQIDMAgGBmeBDAECAjAFBgMBAgMwLAYDKgMEMCUwEgYEKgMEAAwKVVRGOFN0cmluZzAPBgQqAwQBEgcxMjggMjU2MFYGCCsGAQUFBwIBMEowIwYIKwYBBQUHAgEWF2h0dHBzOi8vY3BzLmV4YW1wbGUuY29tMCMGCCsGAQUFBwIBFhdodHRwczovL2Nwcy5leGFtcGxlLm9yZzBFBggrBgEFBQcCAjA5MDcGCCsGAQUFBwICMCswGwwLRXhhbXBsZSBPcmcwDAIBAAIBAQIBAgIBAwwMVGVzdCBtZXNzYWdlMA0GA1UdNgEB_wQDAgECMAoGCCqGSM49BAMCA0kAMEYCIQCzJmW-eUcSHqdi3V-eb30ZsRIY-tBVohkP06JVxt6IxAIhANKV7alaTWWojOtTXgLkH4nmqEiXGFIb1RXPsNfIhf1H) fn test_certificate_policies_expected_der() { - use crate::string::Ia5String; + use crate::{ + string::Ia5String, + CertificatePolicies, + PolicyInformation, + PolicyQualifierInfo, + UserNotice, + }; const EXPECTED_DER: &[u8] = &[ 0x30, 0x82, 0x01, 0x10, 0x06, 0x03, 0x55, 0x1D, 0x20, 0x04, 0x82, 0x01, 0x07, 0x30, 0x82, 0x01, 0x03, 0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00, 0x30, 0x08, 0x06, From 29dfd649047831b23c0893170f0abf9abca13ece Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:22:36 +0100 Subject: [PATCH 13/16] formatting... --- rcgen/src/certificate.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index cc198883..f9e9b649 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -2078,13 +2078,8 @@ mod tests { #[test] /// [DER Source](https://lapo.it/asn1js/#MIICsTCCAlagAwIBAgIUNIeLHUZBtAK7N0HeSWhxrNt7PpkwCgYIKoZIzj0EAwIwMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDAgFw03NTAxMDEwMDAwMDBaGA80MDk2MDEwMTAwMDAwMFowMDEYMBYGA1UECgwPQ3JhYiB3aWRnaXRzIFNFMRQwEgYDVQQDDAtNYXN0ZXIgQ2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKWGpFh06ExqLv-IX2QYQFSeglg3hakmqouV7ghFx_CQp6PKF3uEEniibaJ4OSXivEfigCkiWYAzsTJgs5jTQrSjggFKMIIBRjAhBgNVHREEGjAYggtjcmFicy5jcmFic4IJbG9jYWxob3N0MIIBEAYDVR0gBIIBBzCCAQMwBgYEVR0gADAIBgZngQwBAgEwBwYFZ4EMAQEwCAYGZ4EMAQIDMAgGBmeBDAECAjAFBgMBAgMwLAYDKgMEMCUwEgYEKgMEAAwKVVRGOFN0cmluZzAPBgQqAwQBEgcxMjggMjU2MFYGCCsGAQUFBwIBMEowIwYIKwYBBQUHAgEWF2h0dHBzOi8vY3BzLmV4YW1wbGUuY29tMCMGCCsGAQUFBwIBFhdodHRwczovL2Nwcy5leGFtcGxlLm9yZzBFBggrBgEFBQcCAjA5MDcGCCsGAQUFBwICMCswGwwLRXhhbXBsZSBPcmcwDAIBAAIBAQIBAgIBAwwMVGVzdCBtZXNzYWdlMA0GA1UdNgEB_wQDAgECMAoGCCqGSM49BAMCA0kAMEYCIQCzJmW-eUcSHqdi3V-eb30ZsRIY-tBVohkP06JVxt6IxAIhANKV7alaTWWojOtTXgLkH4nmqEiXGFIb1RXPsNfIhf1H) fn test_certificate_policies_expected_der() { - use crate::{ - string::Ia5String, - CertificatePolicies, - PolicyInformation, - PolicyQualifierInfo, - UserNotice, - }; + use crate::string::Ia5String; + use crate::{CertificatePolicies, PolicyInformation, PolicyQualifierInfo, UserNotice}; const EXPECTED_DER: &[u8] = &[ 0x30, 0x82, 0x01, 0x10, 0x06, 0x03, 0x55, 0x1D, 0x20, 0x04, 0x82, 0x01, 0x07, 0x30, 0x82, 0x01, 0x03, 0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00, 0x30, 0x08, 0x06, From a24dae24d119c9905e2a4f6a648aa1d5a58928ae Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:39:19 +0100 Subject: [PATCH 14/16] Apply requested changes 1st run --- rcgen/Cargo.toml | 4 - rcgen/examples/certificate_policies.rs | 56 --- rcgen/src/certificate.rs | 463 ++++++++----------------- 3 files changed, 136 insertions(+), 387 deletions(-) delete mode 100644 rcgen/examples/certificate_policies.rs diff --git a/rcgen/Cargo.toml b/rcgen/Cargo.toml index 04e48de9..8d1ab2da 100644 --- a/rcgen/Cargo.toml +++ b/rcgen/Cargo.toml @@ -31,10 +31,6 @@ zeroize = { workspace = true, optional = true } [target."cfg(unix)".dev-dependencies] openssl = { workspace = true } -[[example]] -name = "certificate_policies" -required-features = ["pem"] - [[example]] name = "rsa-irc-openssl" required-features = ["pem"] diff --git a/rcgen/examples/certificate_policies.rs b/rcgen/examples/certificate_policies.rs deleted file mode 100644 index c634ecad..00000000 --- a/rcgen/examples/certificate_policies.rs +++ /dev/null @@ -1,56 +0,0 @@ -// This example is a copy from `simple.rs` with the addition of the CertificatePolicies extension - -use std::fs; - -use rcgen::string::Ia5String; -use rcgen::{ - date_time_ymd, CertificateParams, CertificatePolicies, DistinguishedName, DnType, - InhibitAnyPolicy, KeyPair, PolicyInformation, SanType, UserNotice, -}; - -fn main() -> Result<(), Box> { - let mut params: CertificateParams = Default::default(); - params.not_before = date_time_ymd(1975, 1, 1); - params.not_after = date_time_ymd(4096, 1, 1); - params.distinguished_name = DistinguishedName::new(); - params - .distinguished_name - .push(DnType::OrganizationName, "Crab widgits SE"); - params - .distinguished_name - .push(DnType::CommonName, "Master Cert"); - params.subject_alt_names = vec![ - SanType::DnsName("crabs.crabs".try_into()?), - SanType::DnsName("localhost".try_into()?), - ]; - params.certificate_policies = Some( - CertificatePolicies::new(false, PolicyInformation::any_policy()) - .add_policy_unchecked(PolicyInformation::domain_validated()) - .add_policy_unchecked(PolicyInformation::cps_uri(vec![ - Ia5String::try_from("https://cps.example.com")?, - Ia5String::try_from("https://cps.example.org")?, - ])) - .add_policy_unchecked(PolicyInformation::user_notice(&UserNotice::new_full( - "Example Org".into(), - vec![0, 1, 2, 3], - "Test message".into(), - ))), - ); - - params.inhibit_any_policy = Some(InhibitAnyPolicy::new(2)); - - let key_pair = KeyPair::generate()?; - let cert = params.self_signed(&key_pair)?; - - let pem_serialized = cert.pem(); - let pem = pem::parse(&pem_serialized)?; - let der_serialized = pem.contents(); - println!("{pem_serialized}"); - println!("{}", key_pair.serialize_pem()); - fs::create_dir_all("certs/")?; - fs::write("certs/cert.pem", pem_serialized.as_bytes())?; - fs::write("certs/cert.der", der_serialized)?; - fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?; - fs::write("certs/key.der", key_pair.serialize_der())?; - Ok(()) -} diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index f9e9b649..84e53bb2 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -61,17 +61,6 @@ pub struct CertificateParams { pub distinguished_name: DistinguishedName, pub is_ca: IsCa, pub key_usages: Vec, - /// "\[If the optional extension is present, t\]he certificate policies extension contains a sequence of one or more - /// policy information terms, each of which consists of an object - /// identifier (OID) and optional qualifiers. - /// - /// 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."[^1] - /// - /// [^1]: pub certificate_policies: Option, pub inhibit_any_policy: Option, pub extended_key_usages: Vec, @@ -372,8 +361,8 @@ impl CertificateParams { if serial_number.is_some() || *is_ca != IsCa::NoCa || name_constraints.is_some() - || certificate_policies.is_some() // I think this extension must be set by the CA and not the requesting party - || inhibit_any_policy.is_some() // Same as policies above + || certificate_policies.is_some() + || inhibit_any_policy.is_some() || !crl_distribution_points.is_empty() || *use_authority_key_identifier_extension { @@ -608,32 +597,12 @@ impl CertificateParams { } if let Some(certificate_policies) = &self.certificate_policies { - // write_x509_extension( - // writer.next(), - // oid::CERTIFICATE_POLICIES, - // certificate_policies.critical, - // |writer| { - // writer.write_sequence_of(|writer| { - // for policy in &certificate_policies.policy_information { - // // writer.next().write_der(&yasna::encode_der(policy)) - - // // Unwrapped equivalent to the above trait use - // writer.next().write_der(&yasna::construct_der(|writer| { - // policy.encode_der(writer) - // })) - // } - // }) - // }, - // ); writer.next().write_der(&yasna::construct_der(|writer| { certificate_policies.encode_der(writer); })) } if let Some(inhibit_any_policy) = &self.inhibit_any_policy { - // write_x509_extension(writer.next(), oid::INHIBIT_ANY_POLICY, true, |writer| { - // writer.write_i64(inhibit_any_policy.skip_certs as i64) - // }); writer.next().write_der(&yasna::construct_der(|writer| { inhibit_any_policy.encode_der(writer) })) @@ -724,18 +693,11 @@ impl CertificatePolicies { }; let ParsedExtension::CertificatePolicies(policies) = ext.parsed_extension() else { - // Contributor note: - // Since we use get_extension_unique with the ext_certificate_policies and CertificatePolicies - // is implemented, this should be `unreachable!()`. I am unsure about the error to return here. - // Returning None would probably be worse since the extension is known to be present. - // The available parser errors don't seem to be applicable here. - // What would have to happen for this branch to be chosen? - return Err(Error::UnsupportedExtension); + 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(policy.try_into()?) policy_information.push(PolicyInformation::from_x509(policy)?); } @@ -774,7 +736,7 @@ impl CertificatePolicies { /// ```rust /// use rcgen::{CertificatePolicies, PolicyInformation}; /// - /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()); + /// let policies = CertificatePolicies::new(false, PolicyInformation::new_oid_only(vec![2, 5, 29, 32, 0])); /// ``` pub fn new(criticality: bool, policy: PolicyInformation) -> Self { Self { @@ -790,30 +752,8 @@ impl CertificatePolicies { /// ```rust /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; /// - /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) - /// .add_policy(PolicyInformation::domain_validated()); - /// - /// assert!(policies.is_ok()); - /// ``` - /// - /// ```rust - /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; - /// - /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) - /// .add_policy(PolicyInformation::any_policy()); - /// - /// assert_eq!(policies, Err(Error::Other)); - /// ``` - /// - /// This does not prevent logic errors such as adding OIDs with conflicting meaning. - /// - /// ```rust - /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; - /// - /// let policies = CertificatePolicies::new(false, PolicyInformation::extended_validation()) - /// .add_policy(PolicyInformation::domain_validated()).expect("This OID wasn't added yet") - /// .add_policy(PolicyInformation::organization_validated()).expect("This OID wasn't added yet") - /// .add_policy(PolicyInformation::individual_validated()); + /// let policies = CertificatePolicies::new(false, PolicyInformation::new_oid_only(vec![2, 5, 29, 32, 0])) + /// .add_policy(PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 2, 1])); // domain_validated /// /// assert!(policies.is_ok()); /// ``` @@ -841,36 +781,8 @@ impl CertificatePolicies { /// ```rust /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; /// - /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) - /// .add_policies(&[PolicyInformation::domain_validated()]); - /// - /// assert!(policies.is_ok()); - /// ``` - /// - /// --- - /// - /// ```rust - /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; - /// - /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) - /// .add_policies(&[PolicyInformation::any_policy()]); - /// - /// assert_eq!(policies, Err(Error::Other)); - /// ``` - /// - /// --- - /// - /// This does not prevent logic errors such as adding OIDs with conflicting meaning. - /// - /// ```rust - /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; - /// - /// let policies = CertificatePolicies::new(false, PolicyInformation::extended_validation()) - /// .add_policies(&[ - /// PolicyInformation::domain_validated(), - /// PolicyInformation::organization_validated(), - /// PolicyInformation::individual_validated(), - /// ]); + /// let policies = CertificatePolicies::new(false, PolicyInformation::new_oid_only(vec![2, 5, 29, 32, 0])) + /// .add_policies(&[PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 2, 1])]); // domain_validated /// /// assert!(policies.is_ok()); /// ``` @@ -888,44 +800,6 @@ impl CertificatePolicies { policy_information: registered_policies, }) } - - /// Add once policy at a time - /// Does not validate if the PolicyInformation OID is unique. - /// - /// ```rust - /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; - /// - /// // Duplicate policies are added without checks. It is your responsibility to prevent duplicates when using this method. - /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) - /// .add_policy_unchecked(PolicyInformation::any_policy()); // Bad - /// ``` - pub fn add_policy_unchecked(self, policy: PolicyInformation) -> Self { - let mut registered_policies = self.policy_information.clone(); - registered_policies.push(policy); - Self { - critical: self.critical, - policy_information: registered_policies, - } - } - - /// Add multiple policies at once - /// Does not validate if the PolicyInformation OID is unique. - /// - /// ```rust - /// use rcgen::{CertificatePolicies, PolicyInformation, Error}; - /// - /// // Duplicate policies are added without checks. It is your responsibility to prevent duplicates when using this method. - /// let policies = CertificatePolicies::new(false, PolicyInformation::any_policy()) - /// .add_policies_unchecked(&[PolicyInformation::any_policy()]); // Bad - /// ``` - pub fn add_policies_unchecked(self, policies: &[PolicyInformation]) -> Self { - let mut registered_policies = self.policy_information.clone(); - registered_policies.extend_from_slice(policies); - Self { - critical: self.critical, - policy_information: registered_policies, - } - } } /// > A certificate policy OID MUST NOT appear more than once in a @@ -952,19 +826,18 @@ impl PolicyInformation { writer .next() .write_oid(&ObjectIdentifier::from_slice(&self.policy_identifier)); - if let Some(policy_qualifiers) = &self.policy_qualifiers { - writer.next().write_sequence_of(|writer| { - for policy_qualifier in policy_qualifiers { - writer - .next() - // .write_der(&yasna::encode_der(policy_qualifier)); - // Unwrapped equivalent to the above trait use - .write_der(&yasna::construct_der(|writer| { - policy_qualifier.encode_der(writer) - })) - } - }) - } + + let Some(policy_qualifiers) = &self.policy_qualifiers else { + return; + }; + + writer.next().write_sequence_of(|writer| { + for policy_qualifier in policy_qualifiers { + writer.next().write_der(&yasna::construct_der(|writer| { + policy_qualifier.encode_der(writer) + })) + } + }) }) } } @@ -993,69 +866,9 @@ impl PolicyInformation { policy_qualifiers: Some(qualifiers), } } - - /// 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 }. - pub fn any_policy() -> Self { - Self::new_oid_only(vec![2, 5, 29, 32, 0]) - } - - /// Certificate issued in compliance with the Extended Validation Guidelines - pub fn extended_validation() -> Self { - Self::new_oid_only(vec![2, 23, 140, 1, 1]) - } - - /// Certificate issued in compliance with the TLS Baseline Requirements – No entity identity asserted - pub fn domain_validated() -> Self { - Self::new_oid_only(vec![2, 23, 140, 1, 2, 1]) - } - - /// Certificate issued in compliance with the TLS Baseline Requirements – Organization identity asserted - pub fn organization_validated() -> Self { - Self::new_oid_only(vec![2, 23, 140, 1, 2, 2]) - } - - /// Certificate issued in compliance with the TLS Baseline Requirements – Individual identity asserted - pub fn individual_validated() -> Self { - Self::new_oid_only(vec![2, 23, 140, 1, 2, 3]) - } - - /// > The CPS Pointer qualifier contains a pointer to a Certification - /// > Practice Statement (CPS) published by the CA. The pointer is in the - /// > form of a URI. Processing requirements for this qualifier are a - /// > local matter. No action is mandated by this specification regardless - /// > of the criticality value asserted for the extension. - pub fn cps_uri(cps_uris: Vec) -> Self { - Self::new_oid_qualifiers( - // Didn't find this one in RFC5280 and took it from PKIOverheid (Dutch Government PKI) using Firefox certificate inspection - // Is it plausible, that this is matching the PolicyQualifierInfo OID? - vec![1, 3, 6, 1, 5, 5, 7, 2, 1], - cps_uris - .iter() - .map(PolicyQualifierInfo::new_cps_uri) - .collect(), - ) - } - - /// User notice is intended for display to a relying party when a - /// certificate is used. Only user notices returned as a result of path - /// validation are intended for display to the user. - pub fn user_notice(user_notice: &UserNotice) -> Self { - Self::new_oid_qualifiers( - // Didn't find this one in RFC5280 and took it from https://github.com/rustls/rcgen/issues/370#issuecomment-3183832371 -> Firefox - // Is it plausible, that this is matching the PolicyQualifierInfo OID? - vec![1, 3, 6, 1, 5, 5, 7, 2, 2], - vec![PolicyQualifierInfo::new_user_notice(user_notice)], - ) - } } #[cfg(all(test, feature = "x509-parser"))] -// impl TryFrom> for PolicyInformation { -// type Error = Error; -// fn try_from(value: x509_parser::extensions::PolicyInformation) -> Result { impl PolicyInformation { fn from_x509(value: x509_parser::extensions::PolicyInformation) -> Result { let mut policy_identifier = Vec::new(); @@ -1073,7 +886,6 @@ impl PolicyInformation { policy_qualifiers: None, }); }; - // let policy_qualifiers = qualifiers.into_iter().map(PolicyQualifierInfo::from).collect(); let policy_qualifiers = qualifiers .into_iter() .map(PolicyQualifierInfo::from_x509) @@ -1105,9 +917,6 @@ pub struct PolicyQualifierInfo { } #[cfg(all(test, feature = "x509-parser"))] -// impl TryFrom> for PolicyQualifierInfo { -// type Error = Error; -// fn try_from(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Result { impl PolicyQualifierInfo { fn from_x509(value: x509_parser::extensions::PolicyQualifierInfo<'_>) -> Result { let mut oid = Vec::new(); @@ -1147,7 +956,7 @@ impl PolicyQualifierInfo { } } - /// id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } + /// id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }\ /// id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 } pub fn new_cps_uri(cps_uri: &string::Ia5String) -> Self { Self { @@ -1156,14 +965,11 @@ impl PolicyQualifierInfo { } } - /// id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } + /// id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }\ /// id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 } pub fn new_user_notice(user_notice: &UserNotice) -> Self { Self { policy_qualifier_id: vec![1, 3, 6, 1, 5, 5, 7, 2, 2], - // qualifier: yasna::encode_der(user_notice), - - // Unwrapped equivalent to the above trait use qualifier: yasna::construct_der(|writer| { user_notice.encode_der(writer); }), @@ -1927,57 +1733,6 @@ mod tests { #[cfg(feature = "crypto")] use crate::KeyPair; - #[test] - fn test_policy_information_any_policy_der() { - use crate::PolicyInformation; - const EXPECTED_DER: &[u8] = &[0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00]; - let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::any_policy().encode_der(writer); - }); - assert_eq!(EXPECTED_DER, &policy_information_der) - } - - #[test] - fn test_policy_information_domain_validated_der() { - use crate::PolicyInformation; - const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x01]; - let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::domain_validated().encode_der(writer); - }); - assert_eq!(EXPECTED_DER, &policy_information_der) - } - - #[test] - fn test_policy_information_extended_validation_der() { - use crate::PolicyInformation; - const EXPECTED_DER: &[u8] = &[0x30, 0x07, 0x06, 0x05, 0x67, 0x81, 0x0C, 0x01, 0x01]; - let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::extended_validation().encode_der(writer); - }); - assert_eq!(EXPECTED_DER, &policy_information_der) - } - - #[test] - /// Note: ASN.1 JavaScript Decoder didn't recognize this OID - fn test_policy_information_individual_validated_der() { - use crate::PolicyInformation; - const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x03]; - let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::individual_validated().encode_der(writer); - }); - assert_eq!(EXPECTED_DER, &policy_information_der) - } - - #[test] - fn test_policy_information_organization_validated_der() { - use crate::PolicyInformation; - const EXPECTED_DER: &[u8] = &[0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0C, 0x01, 0x02, 0x02]; - let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::organization_validated().encode_der(writer); - }); - assert_eq!(EXPECTED_DER, &policy_information_der) - } - #[test] fn test_policy_information_cps_uri_der() { use crate::PolicyInformation; @@ -1988,8 +1743,13 @@ mod tests { 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x6F, 0x72, 0x67, ]; let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::cps_uri(vec!["https://cps.example.org".try_into().unwrap()]) - .encode_der(writer); + PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 1], + vec![PolicyQualifierInfo::new_cps_uri( + &"https://cps.example.org".try_into().unwrap(), + )], + ) + .encode_der(writer); }); assert_eq!(EXPECTED_DER, &policy_information_der) } @@ -2044,11 +1804,16 @@ mod tests { 0x65, ]; let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::user_notice(&UserNotice::new_explicit_text(DisplayText::Ia5String( - "You should usually use an UTF8String here" - .try_into() - .unwrap(), - ))) + PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 2], + vec![PolicyQualifierInfo::new_user_notice( + &UserNotice::new_explicit_text(DisplayText::Ia5String( + "You should usually use an UTF8String here" + .try_into() + .unwrap(), + )), + )], + ) .encode_der(writer); }); assert_eq!(EXPECTED_DER, &policy_information_der); @@ -2065,10 +1830,12 @@ mod tests { 0x03, ]; let policy_information_der = yasna::construct_der(|writer| { - PolicyInformation::user_notice(&UserNotice::new_notice_reference( - "Example Org".to_string(), - vec![0, 1, 2, 3], - )) + PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 2], + vec![PolicyQualifierInfo::new_user_notice( + &UserNotice::new_notice_reference("Example Org".to_string(), vec![0, 1, 2, 3]), + )], + ) .encode_der(writer) }); assert_eq!(EXPECTED_DER, &policy_information_der); @@ -2103,41 +1870,51 @@ mod tests { 0x73, 0x74, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, ]; let extension_der = yasna::construct_der(|writer| { - CertificatePolicies::new(false, PolicyInformation::any_policy()) - .add_policies_unchecked(&[ - PolicyInformation::domain_validated(), - PolicyInformation::extended_validation(), - PolicyInformation::individual_validated(), - PolicyInformation::organization_validated(), - PolicyInformation::new_oid_only(vec![0, 1, 2, 3]), - PolicyInformation::new_oid_qualifiers( - vec![1, 2, 3, 4], - vec![ - PolicyQualifierInfo::new_custom( - vec![1, 2, 3, 4, 0], - yasna::construct_der(|writer| { - writer.write_utf8string("UTF8String") - }), - ), - PolicyQualifierInfo::new_custom( - vec![1, 2, 3, 4, 1], - yasna::construct_der(|writer| { - writer.write_numeric_string("128 256") - }), - ), - ], - ), - PolicyInformation::cps_uri(vec![ - Ia5String::try_from("https://cps.example.com").unwrap(), - Ia5String::try_from("https://cps.example.org").unwrap(), - ]), - PolicyInformation::user_notice(&UserNotice::new_full( + CertificatePolicies::new( + false, + PolicyInformation::new_oid_only(vec![2, 5, 29, 32, 0]), // any_policy + ) + .add_policies(&[ + PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 2, 1]), // domain_validated + PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 1]), // extended_validation + PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 2, 3]), // individual_validated + PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 2, 2]), // organization_validated + PolicyInformation::new_oid_only(vec![0, 1, 2, 3]), + PolicyInformation::new_oid_qualifiers( + vec![1, 2, 3, 4], + vec![ + PolicyQualifierInfo::new_custom( + vec![1, 2, 3, 4, 0], + yasna::construct_der(|writer| writer.write_utf8string("UTF8String")), + ), + PolicyQualifierInfo::new_custom( + vec![1, 2, 3, 4, 1], + yasna::construct_der(|writer| writer.write_numeric_string("128 256")), + ), + ], + ), + PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 1], + vec![ + PolicyQualifierInfo::new_cps_uri( + &Ia5String::try_from("https://cps.example.com").unwrap(), + ), + PolicyQualifierInfo::new_cps_uri( + &Ia5String::try_from("https://cps.example.org").unwrap(), + ), + ], + ), + PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 2], + vec![PolicyQualifierInfo::new_user_notice(&UserNotice::new_full( "Example Org".into(), vec![0, 1, 2, 3], "Test message".into(), - )), - ]) - .encode_der(writer); + ))], + ), + ]) + .unwrap() + .encode_der(writer); }); assert_eq!(EXPECTED_DER, &extension_der); } @@ -2169,18 +1946,34 @@ mod tests { 0x74, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, ]; let extension_der = yasna::construct_der(|writer| { - CertificatePolicies::new(false, PolicyInformation::any_policy()) - .add_policy_unchecked(PolicyInformation::domain_validated()) - .add_policy_unchecked(PolicyInformation::cps_uri(vec![ - string::Ia5String::try_from("https://cps.example.com").unwrap(), - string::Ia5String::try_from("https://cps.example.org").unwrap(), - ])) - .add_policy_unchecked(PolicyInformation::user_notice(&UserNotice::new_full( + CertificatePolicies::new( + false, + PolicyInformation::new_oid_only(vec![2, 5, 29, 32, 0]), // any_policy + ) + .add_policy(PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 2, 1])) // domain_validated + .unwrap() + .add_policy(PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 1], + vec![ + PolicyQualifierInfo::new_cps_uri( + &string::Ia5String::try_from("https://cps.example.com").unwrap(), + ), + PolicyQualifierInfo::new_cps_uri( + &string::Ia5String::try_from("https://cps.example.org").unwrap(), + ), + ], + )) + .unwrap() + .add_policy(PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 2], + vec![PolicyQualifierInfo::new_user_notice(&UserNotice::new_full( "Example Org".into(), vec![0, 1, 2, 3], "Test message".into(), - ))) - .encode_der(writer) + ))], + )) + .unwrap() + .encode_der(writer) }); assert_eq!(EXPECTED_DER, &extension_der); } @@ -2191,17 +1984,33 @@ mod tests { fn test_certificate_policies_encode_decode() { let params = CertificateParams { certificate_policies: Some( - CertificatePolicies::new(false, PolicyInformation::any_policy()) - .add_policy_unchecked(PolicyInformation::domain_validated()) - .add_policy_unchecked(PolicyInformation::cps_uri(vec![ - string::Ia5String::try_from("https://cps.example.com").unwrap(), - string::Ia5String::try_from("https://cps.example.org").unwrap(), - ])) - .add_policy_unchecked(PolicyInformation::user_notice(&UserNotice::new_full( + CertificatePolicies::new( + false, + PolicyInformation::new_oid_only(vec![2, 5, 29, 32, 0]), // any_policy + ) + .add_policy(PolicyInformation::new_oid_only(vec![2, 23, 140, 1, 2, 1])) // domain_validated + .unwrap() + .add_policy(PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 1], + vec![ + PolicyQualifierInfo::new_cps_uri( + &string::Ia5String::try_from("https://cps.example.com").unwrap(), + ), + PolicyQualifierInfo::new_cps_uri( + &string::Ia5String::try_from("https://cps.example.org").unwrap(), + ), + ], + )) + .unwrap() + .add_policy(PolicyInformation::new_oid_qualifiers( + vec![1, 3, 6, 1, 5, 5, 7, 2, 2], + vec![PolicyQualifierInfo::new_user_notice(&UserNotice::new_full( "Example Org".into(), vec![0, 1, 2, 3], "Test message".into(), - ))), + ))], + )) + .unwrap(), ), ..Default::default() }; From 0096508de5997e2a15553d8476d661be9867d5b1 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:44:30 +0100 Subject: [PATCH 15/16] More cleanup --- rcgen/src/certificate.rs | 43 ++++------------------------------------ rcgen/src/oid.rs | 7 ------- 2 files changed, 4 insertions(+), 46 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 84e53bb2..b1f14967 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -714,9 +714,6 @@ impl CertificatePolicies { 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::encode_der(policy)) - - // Unwrapped equivalent to the above trait use writer .next() .write_der(&yasna::construct_der(|writer| policy.encode_der(writer))) @@ -873,7 +870,7 @@ impl PolicyInformation { fn from_x509(value: x509_parser::extensions::PolicyInformation) -> Result { let mut policy_identifier = Vec::new(); - // Contributor question: What error should be returned here? Is this something that can happen? + // Contributor question: What error should be returned here? for v in value.policy_id.iter().ok_or(Error::X509(String::from( "PolicyInformation without a policy_identifier is invalid", )))? { @@ -937,15 +934,6 @@ impl PolicyQualifierInfo { } impl PolicyQualifierInfo { - // This seems like a sensible method but I don't think we can expose this trait bound. - // /// Create a custom [`PolicyQualifierInfo`] from an OID and any [`yasna::DEREncodable`] object. - // pub fn new_custom(policy_qualifier_id: Vec, qualifier: Q) -> Self { - // Self { - // policy_qualifier_id, - // qualifier: yasna::encode_der(&qualifier), - // } - // } - /// Create a custom [`PolicyQualifierInfo`] /// /// It is your responsibility to provide valid DER for the qualifier. @@ -1045,7 +1033,7 @@ impl UserNotice { organization, notice_numbers, }), - explicit_text: Some(DisplayText::Utf8String(msg)), // MSG is defined as DisplayText but RECOMMENDED to be UTF8String. Should we expose all options including those that are discouraged? + explicit_text: Some(DisplayText::Utf8String(msg)), } } @@ -1073,9 +1061,6 @@ impl UserNotice { writer.next().write_der(&encode_der(notice_ref)); } if let Some(explicit_text) = &self.explicit_text { - // writer.next().write_der(&encode_der(explicit_text)); - - // Unwrapped equivalent to the above trait use writer.next().write_der(&yasna::construct_der(|writer| { explicit_text.encode_der(writer); })) @@ -1098,16 +1083,13 @@ struct NoticeReference { notice_numbers: NoticeNumbers, } -// NoticeRef is private. Should I still remove the Trait impl? +// NoticeRef is private so we can impl DEREncodable impl yasna::DEREncodable for NoticeReference { fn encode_der<'a>(&self, writer: DERWriter<'a>) { writer.write_sequence(|writer| { - // writer.next().write_der(&encode_der(&self.organization)); writer.next().write_der(&yasna::construct_der(|writer| { self.organization.encode_der(writer); })); - // This is incorrect - // writer.next().write_der(&self.notice_numbers); writer.next().write_sequence_of(|writer| { for val in &self.notice_numbers { writer.next().write_i64(*val); @@ -1136,10 +1118,7 @@ pub enum DisplayText { /// Usually what you want Utf8String(String), // Contributor question: - // Should we make non-conformant options available at all? - - // VisibleString(string::VisibleString), // Not implemented yet. Could be imported/inspired from x509_parser - // BmpString(string::BmpString), + // Should we make non-conformant/not recommended options available at all? } impl From<&str> for DisplayText { @@ -1160,12 +1139,6 @@ impl DisplayText { match self { DisplayText::Ia5String(string) => writer.write_ia5_string(string.as_str()), DisplayText::Utf8String(string) => writer.write_utf8_string(string.as_str()), - // DisplayText::BmpString(string) => { - // // [`writer.write_bmp_string`] expects [`&str`]. - // // Would I use write_bytes for this? - // let bytes = string.as_bytes(); - // writer.write_bitvec_bytes(bytes, bytes.len()) - // }, } } } @@ -1209,14 +1182,6 @@ impl InhibitAnyPolicy { } } -// #[cfg(feature = "x509-parser")] -// impl From for InhibitAnyPolicy { -// fn from(value: x509_parser::extensions::InhibitAnyPolicy) -> Self { -// Self { -// skip_certs: value.skip_certs -// } -// } -// } #[cfg(all(test, feature = "x509-parser"))] impl InhibitAnyPolicy { fn from_x509( diff --git a/rcgen/src/oid.rs b/rcgen/src/oid.rs index 7a3f63c1..1171eca4 100644 --- a/rcgen/src/oid.rs +++ b/rcgen/src/oid.rs @@ -41,13 +41,6 @@ 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]; -// Contributor Question: -// Many docstrings refer to appendix A of the RFC instead of the section -// of the respective OID. Why is that? Should I change the link accordingly? -// -// Note: Every other RFC5280 rfc-editor.org link is referring to a nonexisting -// appendix-A section. - /// 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]; From 275c30cf4002f3c07000902deaf02c980a860704 Mon Sep 17 00:00:00 2001 From: Gabgobie <105999094+Gabgobie@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:55:53 +0100 Subject: [PATCH 16/16] Fix test imports on --no-default-features --- rcgen/src/certificate.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index b1f14967..eaf3a364 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -1700,7 +1700,7 @@ mod tests { #[test] fn test_policy_information_cps_uri_der() { - use crate::PolicyInformation; + use crate::{PolicyInformation, PolicyQualifierInfo}; const EXPECTED_DER: &[u8] = &[ 0x30, 0x31, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x30, 0x25, 0x30, 0x23, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x17, @@ -1759,7 +1759,7 @@ mod tests { #[test] fn test_policy_information_user_notice_explicit_text_der() { - use crate::{DisplayText, PolicyInformation, UserNotice}; + use crate::{DisplayText, PolicyInformation, PolicyQualifierInfo, UserNotice}; const EXPECTED_DER: &[u8] = &[ 0x30, 0x45, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x39, 0x30, 0x37, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, @@ -1786,7 +1786,7 @@ mod tests { #[test] fn test_policy_information_user_notice_reference_der() { - use crate::{PolicyInformation, UserNotice}; + use crate::{PolicyInformation, PolicyQualifierInfo, UserNotice}; const EXPECTED_DER: &[u8] = &[ 0x30, 0x37, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x2B, 0x30, 0x29, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x1D,