Skip to content

Conversation

cpu
Copy link
Collaborator

@cpu cpu commented Feb 22, 2025

This branch implements ACME Renewal Information based on draft-08. While not yet a proposed standard RFC this draft is nearly at that stage, and has been deployed by Let's Encrypt (and I believe Google as well).

Support brings a few new bits of API surface:

  • a new CertificateIdentifier type that uniquely identifies a certificate for ARI compatible servers. This requires parsing specific certificate fields to construct, and so without taking a dependency users are expected to provide the relevant pki_types::Der data pre-extracted from a certificate.
  • for users willing to take an optional x509-parser dependency we offer a way to go from pki_types::CertificateDer<_> to a CertificateIdentifier when the x509-parser dependency feature is activated.
  • The DirectoryUrls type gets a new renewal_info field to track the optional ARI endpoint used to signal the ACME server supports ARI.
  • When the optional time dependency feature is activated, the Account struct gains a new renewal_info() function that accepts a CertificateIdentifier and returns RenewalInfo that the caller can use to determine when to replace the identified certificate. This function returns an error if the CA doesn't support ARI.
  • The NewOrder struct gains a new optional replaces(cert_id: CertificateIdentifier) function that can be used to indicate an order is a replacement for a previous order. CAs might use this information to give more generous rate limits, or to know when it's safe to revoke a previously issued certificate due to imposed compliance reasons. Like Account.renewal_info() the Account.new_order() function will error if provided a NewOrder with a replacement value if the ACME CA doesn't support ARI.

@cpu
Copy link
Collaborator Author

cpu commented Feb 22, 2025

I think there might be some kind of a flaky race condition with the Pebble tests that also needs investigating. I'm seeing the occasional spurious error from an order's identifiers not matching the CSR identifiers 🤔

@djc
Copy link
Owner

djc commented Feb 23, 2025

Why is it useful for the identifier type to live in pki-types? What would the downside be if it living in this crate?

Copy link
Owner

@djc djc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking pretty good!

@cpu
Copy link
Collaborator Author

cpu commented Feb 23, 2025

Why is it useful for the identifier type to live in pki-types? What would the downside be if it living in this crate?

I flip flopped a bit. Initially I was thinking there was a reason to involve rcgen, and so it seemed like a helpful bridge between instant-acme and rcgen. In practice I think it made more sense to do the heavy lifting here. I closed the pki-types PR and will rebase with a similar type defined here instead.

I think there might be some kind of a flaky race condition with the Pebble tests that also needs investigating. I'm seeing the occasional spurious error from an order's identifiers not matching the CSR identifiers 🤔

I figured this out locally. It's authz reuse & a logic bug in the test finalization logic carried forward from the provision.rs example. I'll fix shortly.

@cpu
Copy link
Collaborator Author

cpu commented Feb 23, 2025

I rebased this on top of #86 and implemented the review feedback so far.

@cpu
Copy link
Collaborator Author

cpu commented Feb 23, 2025

This branch has conflicts that must be resolved

Will fix this & the other nits from self-review shortly. Out of juice for today

@djc
Copy link
Owner

djc commented Feb 23, 2025

Yup, won't take a look at it until tomorrow either.

@cpu cpu marked this pull request as ready for review February 27, 2025 23:17
@cpu
Copy link
Collaborator Author

cpu commented Feb 27, 2025

cpu marked this pull request as ready for review now

I think this is in pretty good shape now. I'd like to hold merging it until I have a chance to verify it works with LE's staging server but I should be able to do that in the next few days.

Copy link
Owner

@djc djc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great!

Copy link
Owner

@djc djc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, don't forget to update the README -- I think this is worth a feature bullet point.

cpu added 8 commits March 1, 2025 11:33
Also add more detail to the pre-reqs for running the Pebble tests.
This type holds the BASE64 URL-safe encoding of a certificate's
DER encoded authority key information (AKI) ext's keyIdentifier field,
and the BASE64 URL-safe encoding of a certificate's raw DER encoded
serial number. Ser/der is done by consuming/emitting the pair of encoded
fields, '.' separated.

For now this can only be constructed directly from raw DER and the
caller is responsible for extracting the right values from
a certificate. Subsequent commits will offer a way to construct one from
certificate DER using an optional x509-parser dependency.
This error type can be used when a feature is requested that we know the
ACME server doesn't support (e.g. based on the absence of a URL from the
directory resource).
Instead of constructing a `NewOrder` instance by populating the `pub
identifiers` field directly, use `NewOrder::new()` and make the internal
fields private. This will let us switch to a builder-model for future
customization.
@cpu
Copy link
Collaborator Author

cpu commented Mar 1, 2025

Also, don't forget to update the README -- I think this is worth a feature bullet point.

Done! I also bumped the crate version to reflect the breaking changes. Moving on to testing with a real server.

cpu added 12 commits March 1, 2025 15:45
By optionally specifying a `CertificateIdentifier` when
constructing a `NewOrder` it's possible to indicate to the ACME CA that
the order is a replacement for a pre-existing certificate that was
previously ordered.

When accepted, an order created in this way will echo back a `replaces`
field with the serialized `CertificateIdentifier` the order replaces.
Add an optional, non-default, feature for `x509-parser` that allows
constructing `CertificateIdentifier` instances from certificate
DER.
This will allow using the issued certificate for further testing (e.g.
revocation, replacement order).
In the near future it may contain IP address identifiers.
This will support making it easier for each test to construct its own
identifiers up-front.
This will make it ergonomic to test things like order replacement by
calling `replaces()` on the `NewOrder` before providing it to `test()`.
Also activate the optional `x509-parser` feature for the CI invocation.
Adds a new `Account.renewal_info()` function that takes a
`CertificateIdentifier` and returns the `RenewalInfo` the CA
suggests for that certificate.

Requires opting in to a `time` dependency feature.
Updates the ARI integration test to revoke the initial certificate, and
then verifying the ARI info suggests immediate replacement before
replacing it.
Notably we have to avoid activating the `fips` feature since it won't
build in the docs env.
@cpu
Copy link
Collaborator Author

cpu commented Mar 1, 2025

Moving on to testing with a real server.

This ended up being a bit of a rabbit hole and ultimately resulted in one code change. Fetching renewal info works great, but creating replacement orders was throwing an error because the reflected order doesn't have a matching replaces field.

Previously in new_order() I was following the spec text where it says:

If the Server accepts a new-order request with a "replaces" field, it MUST reflect that field in the response and in subsequent requests for the corresponding Order object.

If we sent replaces, I asserted the server echoed the right replaces value. That works great with Pebble 👍

With Let's Encrypt staging and production I never get back a reflected replaces in an echoed order object. I thought at first this was related to "order re-use" since Pebble doesn't implement this, but even after I implemented de-activating an order (PR forthcoming) a brand new order created with a replaces in the NewOrder req still had no replaces echoed.

From my cursory look at Boulder, there doesn't seem to be a place to set that field at all. The internal corepb.Order protobuf message doesn't have the information, and neither does the rendered wfe2.orderJSON type marshalled into the response.

I'll file an upstream issue but for now I've relaxed the check in 9d7563b and suggest we can merge this as-is. @djc WDYT?

@cpu
Copy link
Collaborator Author

cpu commented Mar 1, 2025

I'll file an upstream issue

letsencrypt/boulder#8034

@cpu cpu changed the title Implement ACME Automated Renewal Information (ARI) Implement ACME Renewal Information (ARI) Mar 1, 2025
@djc
Copy link
Owner

djc commented Mar 1, 2025

I'll file an upstream issue but for now I've relaxed the check in 9d7563b and suggest we can merge this as-is. @djc WDYT?

LGTM!

@cpu cpu merged commit 4a22662 into djc:main Mar 1, 2025
9 checks passed
@cpu cpu deleted the cpu-ari-ci branch March 1, 2025 21:36
@djc djc mentioned this pull request Jul 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants