-
-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Implement ACME Automated Renewal Information (ARI) #85
base: main
Are you sure you want to change the base?
Conversation
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 🤔 |
Why is it useful for the identifier type to live in pki-types? What would the downside be if it living in this crate? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking pretty good!
/// See <https://www.ietf.org/archive/id/draft-ietf-acme-ari-07.html#section-4.1> for | ||
/// more information. | ||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
pub struct EncodedCertificateIdentifier(pub(crate) String); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we make this contain a Cow<'a, str>
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think since we're always format!()
ing a concatenation of base64 inputs to construct this there's not an opportunity to use a &str
. Am I overlooking something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really -- just wondering if people might have pre-encoded certificate identifiers that they might want to splice in? Like, if you're storing a bunch of certs in your database (like we used to do at my old startup), it wouldn't be crazy to store these directly to make querying ARI easier, and in that case it feels like low-effort future-proofing to enable borrowing this down the line.
Also, how important is the Encoded
prefix here? In my quest to strive for minimal entropy in type/variable names, is there another kind/encoding of certificate identifier that we need to disambiguate from?
Digging into this a little bit more, I noticed that the spec also doesn't really define the concept of a certificate identifier (maybe good spec feedback?). I think my ideal representation might be more like this:
struct CertificateId {
authority_key_identifier: Cow<'a, str>,
serial: Cow<'a, str>,
}
(Perhaps even with pub
fields?) WDYT?
I flip flopped a bit. Initially I was thinking there was a reason to involve
I figured this out locally. It's authz reuse & a logic bug in the test finalization logic carried forward from the |
Previously both the provision example and the Pebble integration test logic created a new order for a set of input identifiers, iterated the authorizations for the resulting order, and then pulled out the identifier/challenge for each **pending** status authz. After POSTing each authz's challenge URL the associated challenge identifiers were used for the CSR sent to finalize the order. Since the logic above only processes pending authz's it would skip any that are already **valid** (e.g. from previous orders). This can end up producing a CSR that has fewer names than the initial order, which provokes an error during finalization when the CA rejects the CSR. The solution is simple: don't tie the CSR identifiers to the challenges at all, use the same list of identifiers as was used to create the order. Everything else remains the same.
It was mistakenly issuing for `dns01.example.com` like the `dns_01()` test.
By default the Pebble server reuses valid authorizations 50% of the time. To shake out authz related bugs in the issuance flow it's helpful to crank this to 100% per-test. Unfortunately since it's controlled by an env var and not part of the Pebble JSON config this requires some extra plumbing.
This commit adds an integration test that uses Pebble w/ 100% valid authorization reuse to confirm the issuance logic works correctly when only one of three identifiers in a subsequent order need challenge validation.
Also add more detail to the pre-reqs for running the Pebble tests.
This type represents 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. Both encodings are then separated by a "." character for use in ARI related requests. For now this can only be constructed directly from a 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.
By optionally specifying a `EncodedCertificateIdentifier` 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.
Add an optional, non-default, feature for `x509-parser` that allows constructing `EncodedCertificateIdentifier` 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 an `EncodedCertificateIdentifier` and returns the `RenewalInfo` the CA suggests for that certificate.
Adds a new `Account.renewal_info()` function that takes an `EncodedCertificateIdentifier` and returns the `RenewalInfo` the CA suggests for that certificate. All of the above is gated by an optional, non-default, `chrono` feature, using that crate to parse the RFC3339 formatted date/time stamps in the renewal information responses.
Updates the ARI integration test to revoke the initial certificate, and then verifying the ARI info suggests immediate replacement before replacing it.
I rebased this on top of #86 and implemented the review feedback so far. |
@@ -1,18 +1,24 @@ | |||
use std::fmt; | |||
|
|||
use crate::BytesResponse; | |||
use crate::crypto::{self, KeyPair}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, biffed the import order in my rebase. I'll fix this.
@@ -23,6 +23,7 @@ async-trait = "0.1" | |||
aws-lc-rs = { version = "1.8.0", optional = true } | |||
base64 = "0.22" | |||
bytes = "1" | |||
chrono = { version = "0.4", features = ["serde"], optional = true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, this commit was meant to be squashed into its parent. Will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, also on consideration, rcgen and yasna depend on time instead of chrono today. I wonder if that mean we should use time instead of chrono here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, make sure that we have doc_auto_cfg
set up so renewal_info()
will be discoverable in the docs even when cfg
ed out by default.
Will fix this & the other nits from self-review shortly. Out of juice for today |
Yup, won't take a look at it until tomorrow either. |
/// See <https://www.ietf.org/archive/id/draft-ietf-acme-ari-07.html#section-4.1> for | ||
/// more information. | ||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
pub struct EncodedCertificateIdentifier(pub(crate) String); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really -- just wondering if people might have pre-encoded certificate identifiers that they might want to splice in? Like, if you're storing a bunch of certs in your database (like we used to do at my old startup), it wouldn't be crazy to store these directly to make querying ARI easier, and in that case it feels like low-effort future-proofing to enable borrowing this down the line.
Also, how important is the Encoded
prefix here? In my quest to strive for minimal entropy in type/variable names, is there another kind/encoding of certificate identifier that we need to disambiguate from?
Digging into this a little bit more, I noticed that the spec also doesn't really define the concept of a certificate identifier (maybe good spec feedback?). I think my ideal representation might be more like this:
struct CertificateId {
authority_key_identifier: Cow<'a, str>,
serial: Cow<'a, str>,
}
(Perhaps even with pub
fields?) WDYT?
@@ -184,6 +184,17 @@ fn try_tracing_init() { | |||
.try_init(); | |||
} | |||
|
|||
fn dns_identifiers<I, T>(dns_names: I) -> Vec<Identifier> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nits: move this down below its user? Also use impl Trait
to remove the indirection, I think we could write names: impl IntoIterator<Item = impl ToString>
?
@@ -23,6 +23,7 @@ async-trait = "0.1" | |||
aws-lc-rs = { version = "1.8.0", optional = true } | |||
base64 = "0.22" | |||
bytes = "1" | |||
chrono = { version = "0.4", features = ["serde"], optional = true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, also on consideration, rcgen and yasna depend on time instead of chrono today. I wonder if that mean we should use time instead of chrono here?
Note to reviewers: opened as a draft while we sort through some details. Extends the commits found in #86.
This branch implements ACME Renewal Information based on
draft-07
. 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:
EncodedCertificateIdentifier
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 relevantpki_types::Der
data pre-extracted from a certificate.x509-parser
dependency we offer a way to go frompki_types::CertificateDer<_>
to aEncodedCertificateIdentifier
when thex509-parser
dependency feature is activated.DirectoryUrls
type gets a newrenewal_info
field to track the optional ARI endpoint used to signal the ACME server supports ARI.chrono
dependency feature is activated, theAccount
struct gains a newrenewal_info()
function that accepts aEncodedCertificateIdentifier
and returnsRenewalInfo
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.NewOrder
struct gains a new optionalreplaces(cert_id: EncodedCertificateIdentifier)
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. LikeAccount.renewal_info()
theAccount.new_order()
function will error if provided aNewOrder
with areplacement
value if the ACME CA doesn't support ARI.I haven't tested this with the live Let's Encrypt staging or production ACME servers, but it does work with the ARI support built into Pebble (and is now included in the integration test coverage).