Conversation
0e1d34e to
33f178f
Compare
33f178f to
6171866
Compare
The enum represents the optional pathLenConstraint field of the basic constraints extension, not the extension itself. Renaming it frees the `BasicConstraints` name for the extension type introduced next, without needing to disambiguate between the two.
Port the BasicConstraints extension into an `ext::BasicConstraints` static extension whose `from_params` constructor owns the presence decision (extension omitted entirely for `IsCa::NoCa`), removing `write_ca_extensions()`.
Implement `Extension` for `&CustomExtension` so the certificate and CSR paths write user-supplied extensions through `ext::write_extension()` like the built-in ones, borrowing them from the params rather than cloning. `CustomExtension` now stores a `Criticality` instead of a `bool`, converting at the public accessors.
Port both CRL-level extensions to static extensions: a `CrlNumber` type in the ext module built via `From<&SerialNumber>`, and a `StaticExtension` impl directly on the existing `CrlIssuingDistributionPoint` params type, replacing the inline writers in the CRL serialization path.
Port the reasonCode and invalidityDate CRL entry extensions into `ext::ReasonCode` and `ext::InvalidityDate` static extensions. The `from_params` constructors own the presence decisions (filtering `unspecified(0)` per RFC 5280 §5.3.1), so the crlEntryExtensions presence gate and the writers now share a single source of truth. The unconditional GeneralizedTime encoding for invalidityDate (RFC 5280 §5.3.2) is preserved and remains covered by the existing CRL tests. With no callers left, the `write_x509_extension()` helper is deleted.
…riting Add an `ext::Extensions` collection that preserves insertion order and rejects duplicate OIDs with a new `Error::DuplicateExtension` variant. The collection borrows the extensions (and through them, the params payloads) for the duration of one serialization. The certificate path now builds the full collection via `CertificateParams::extensions()` and only emits the extensions field of the certificate when the built collection is non-empty. This deletes the `should_write_exts` predicate whose drift from the writers previously caused requested extensions to be silently dropped. Presence is now observed from what was built rather than predicted from the params, removing that bug class structurally. Params that request two extensions with the same OID (previously serialized as an invalid duplicate) now fail with `Error::DuplicateExtension`.
Replace `write_extension_request_attribute()` and the `write_extension_request` predicate with a `csr_extensions()` collection builder. Like the certificate extensions field, the PKCS #9 extensionRequest attribute elides itself when the built collection is empty, claiming a slot in the attributes SET only when there is something to write (yasna rejects set elements that produce no output).
Build the crlExtensions field (AKI, CRL number, optional IDP) and each entry's crlEntryExtensions through `ext::Extensions`, eliding the fields when the built collections are empty. The reasonCode/invalidityDate presence gate is gone: the collection's emptiness is the single source of truth. With all writers converted, `write_extension()` becomes private to the ext module.
Previously SKI was only written for `IsCa::Ca`/`ExplicitNoCa` certificates. RFC 5280 §4.2.1.2 describes the SKI as a MUST for CA certificates and a SHOULD for end entity certificates, so emit it unconditionally. CSRs are unchanged (no SKI is requested).
CSRs previously requested extensions as KU, SAN, EKU while certificates emit SAN, KU, EKU. Use the certificate order for the CSR extension request attribute so both paths serialize extensions consistently.
Move the type next to its `Extension` impl, adding whitespace between the impl's methods; the crate root re-export path is unchanged.
Make `Criticality` public and restructure `CustomExtension` with public `oid`, `criticality` and `der_value` fields, dropping the `set_criticality()`/`criticality()`/`content()` accessors. `from_oid_content()` now takes the criticality directly instead of defaulting to non-critical with later mutation.
Add a dedicated `AcmeIdentifier` type for RFC 8737 TLS-ALPN-01 challenge response extensions. It converts into a `CustomExtension` for use in `CertificateParams::custom_extensions`, always critical per RFC 8737 §3. It stays a `CustomExtension` conversion rather than becoming a first-class params field: the extension never appears in CSRs, and nearly every `CertificateParams` would carry a `None` for it. The `TryFrom<&[u8]>` constructor returns the new `Error::InvalidAcmeIdentifierLength` instead of panicking on wrong-length digests like `new_acme_identifier` did.
Move the CSR extension parsing into `from_parsed` constructors on the ext types (`KeyUsage`, `SubjectAlternativeName`, `ExtendedKeyUsage`, `BasicConstraints`), symmetric with their `from_params` serializing counterparts. Unknown requested extensions still yield `Error::UnsupportedExtension`. Custom EKU purpose OIDs now parse into `ExtendedKeyUsagePurpose::Other` and round-trip, where `from_der` previously rejected them with `Error::UnsupportedExtension`.
Rewrite the test-only `CertificateParams::from_ca_cert_der()` as a single pass over the parsed extensions using the shared `from_parsed` constructors, adding test-only `from_parsed` for `NameConstraints` and `SubjectKeyIdentifier`. This deletes the per-field `from_x509` helpers that each re-scanned the certificate (`SanType`, `ExtendedKeyUsagePurpose`, `NameConstraints`, `IsCa`). `KeyUsagePurpose::from_x509` and `KeyIdMethod::from_x509` remain for `Issuer::from_ca_cert_der`.
Serialize params exercising every certificate extension writer, then assert the exact extension count and that parsing recovers what was requested. A certificate missing a requested extension is still well-formed, so presence regressions can only be caught by comparing the output against the requested params.
`CertificateSigningRequestParams::from_der` previously returned `Error::UnsupportedExtension` for any requested extension other than SAN/KU/EKU/BasicConstraints - including custom extensions rcgen itself wrote via `serialize_request`. Iterate the raw extension request attribute (rather than x509-parser's pre-parsed view, which discards OIDs) and recover anything unhandled into `CertificateParams::custom_extensions` via a new `CustomExtension::from_parsed` constructor, preserving OID, criticality and value so that serializing the recovered params reproduces the request.
RFC 5280 §4.2 forbids multiple instances of the same extension. A CSR requesting an extension twice previously either merged both instances into the recovered `CertificateParams` (for natively handled types) or preserved both as custom extensions, deferring the failure to re-serialization. Reject the duplicate up front with `Error::DuplicateExtension`, mirroring the write-side collection invariant.
Like the CSR extension request path, `from_ca_cert_der()` previously merged duplicate instances of natively handled extensions into params silently. Reject them with `Error::DuplicateExtension` instead, mirroring the write-side collection invariant.
6171866 to
c624890
Compare
Rebased on |
djc
left a comment
There was a problem hiding this comment.
Suggest we split this once more, before the AcmeIdentifier inclusion?
Two concerns remaining (somewhat from the previous PR):
- It feels like in a number of cases, this is wrapping a local type in another local type, when we could arguably implement
Extensionfor the existing type rather than wrapping it in another layer. - The
from_params(params: ..) -> Option<Self>pattern feels somewhat overwrought? Especially to the extent we can reuse existing types, can we just havewrite()yieldboolinstead of having this indirection between something that exists, then is written?
| pub struct CustomExtension { | ||
| oid: Vec<u64>, | ||
| critical: bool, | ||
| pub(crate) oid: Vec<u64>, |
There was a problem hiding this comment.
Suggest inserting a commit before this one that moves CustomExtension into extension? Definitely want to keep the type together with its Extension implementation, too.
(I did find this commit later on, but would be better to do it here IMO.)
| } | ||
| } | ||
|
|
||
| /// A certificate revocation list (CRL) issuing distribution point, to be included in a CRL's |
There was a problem hiding this comment.
Hmm, on main the crl module is 480 lines and extension is already 1276 lines. Might be better to keep CRL-only extensions in crl?
(Also better to keep moves and refactoring separate in terms of commits.)
| /// | ||
| /// Returns [`Error::DuplicateExtension`] if the extension's OID is already present | ||
| /// in the collection. | ||
| pub(crate) fn add_extension( |
There was a problem hiding this comment.
Nit: suggest just calling this push() to mimic Vec::push()?
| /// built collection, not predicted from the params, so an empty extensions | ||
| /// field is never emitted and requested extensions can never be silently | ||
| /// dropped. | ||
| pub(crate) fn write_exts_der(&self, writer: DERWriter) { |
There was a problem hiding this comment.
Nit: the exts_ infix feels superfluous here?
Oh, I guess this is necessary because we only want to write the extensions wrapper when we know there are 1+ extensions to write? Still wondering if there isn't a more light-weight pattern at least for some of the extensions. |
I will noodle on this when I split it up & address your other feedback. This part of the solution is more or less as-is from 2023 (time flies!) and I wouldn't be surprised if in 2026 I have a better idea 😆 |
This revives and finishes the extension rework from #446 and continues where #449 left off by starting on breaking changes. Each extension is now its own type in a new ext module, with the presence decision living next to the encoding.
Serialization builds an
Extensionscollection first and only writes what was actually built, so the bug class that produced silently dropped extensions is addressed more structurally.Throughout duplicate OIDs are rejected instead of silently emitted, on both the write and parse sides (#155).
CSR parsing also got friendlier. Requested extensions rcgen doesn't handle natively used to fail the whole parse with
UnsupportedExtension. Now they're preserved as custom extensions, so a CSR round-trips throughfrom_derwithout losing anything (#150). Same deal for extended key usages with purpose OIDs rcgen doesn't know, those now parse intoExtendedKeyUsagePurpose::Otherinstead of erroring.Along the way this grew some related changes:
PathLenConstraintto free up theBasicConstraintsname for the extension type and because it's more precise.CustomExtension/ACME identifier API got a rework.For most users this will all be fairly uneventful and require no code changes since the parameters+issuance APIs remain mostly the same, but some changes are semver-breaking so this bumps to 0.15.0 so we can use
mainto prepare the next major release (#418).Resolves #150
Resolves #155
Resolves #446
Resolves #122