Skip to content

Per-type challenge state + handles#283

Open
cpu wants to merge 9 commits into
mainfrom
ci/cpu-challenge-handles
Open

Per-type challenge state + handles#283
cpu wants to merge 9 commits into
mainfrom
ci/cpu-challenge-handles

Conversation

@cpu

@cpu cpu commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

Extracts the portions of #279 that are not specific to dns-persist-01.

The key motivation of the design I landed on is that while it's convenient to try and abstract away authorization challenge response across multiple challenge types, in reality there are a lot of challenge type specific details one has to contend with even when using challenges that all derive the key auth from the token (e.g. the format of the key authorization: should it be the raw string? a SHA256 digest? the base64 of that digest?).

Further, the new challenge types (dns-persist-01, device-attest-01) don't have as much in common with the "legacy" challenge types and so we should be using the type system more to differentiate the challenge contexts. This also lets us tighten up what challenges are used for which identifier types (e.g. to avoid a situation where you try to use device-attest-01 with a DNS identifier, or dns-01/dns-persist-01 with an IP address identifier).

@cpu cpu self-assigned this Apr 12, 2026
@djc
djc force-pushed the provider-tweaks branch from 43d1840 to 960d28d Compare April 12, 2026 20:52
Base automatically changed from provider-tweaks to main April 12, 2026 21:02
@djc

djc commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Want to rebase this now that #280 has been merged?

@cpu
cpu force-pushed the ci/cpu-challenge-handles branch from 49973d4 to e4e5c47 Compare April 12, 2026 21:38
@cpu

cpu commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator Author

Want to rebase this

Done!

@djc djc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Here's some early feedback. Need to think more about all the different challenge handling stuff...

Comment thread src/types.rs Outdated
Comment thread src/types.rs
Comment on lines +873 to +894
pub struct Http01Challenge {
/// A token for constructing a key authorization to complete this challenge
pub token: String,
}

/// Challenge state for an RFC 8555 dns-01 challenge
///
/// See <https://www.rfc-editor.org/rfc/rfc8555#section-8.4>
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[non_exhaustive]
pub struct Dns01Challenge {
/// A token for constructing a key authorization to complete this challenge
pub token: String,
}

/// Challenge state for an RFC 8737 tls-alpn-01 challenge
///
/// See <https://www.rfc-editor.org/rfc/rfc8737#section-3>
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[non_exhaustive]
pub struct TlsAlpn01Challenge {
/// A token for constructing a key authorization to complete this challenge

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Not totally convinced it's worth having 3 different types for these.

@cpu cpu May 3, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I tried to motivate this in the PR description but I think it's a worthwhile trade-off to make the API prevent misuse. I think it's a mistake to encourage thinking about all challenges as being equivalent. By specification they aren't ever satisfied in a uniform way and I think the API should reflect that.

As one example from the current API send_device_attestation() exists on Order, and at runtime produces an error for challenges other than ChallengeType::DeviceAttest01. It seems preferable to me that there never be a code path that compiles for trying to use the device attest challenge response submission for an HTTP-01/DNS-01/TLS-ALPN-01/etc challenge.

Similarly, the current API lets you get the BASE64 encoding of the SHA-256 digest of the key authorization of an HTTP-01 challenge token, but that's the incorrect format to use for provisioning a HTTP-01 challenge response. That format only make sense for DNS-01, not HTTP-01 or TLS-ALPN-01. Similar for getting the raw SHA-256 digest of the token without the BASE64 encoding, that's specific to TLS-ALPN-01.

Without separate types we have to offer each of these presentation options and rely on documentation to point out which should be used where, and I think it'd be better to let the type system enforce that.

Does that help convince you? I'm open to other solutions if you think there's a middle ground that can offer the same benefits with fewer moving pieces.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@djc Any thoughts here?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Pff, sorry for the long delay.

I'm open to the idea of more strongly anchoring the challenges and their associated state/actions in the type system. I think the per-challenge state types look okay but it feels like there is too much boilerplate in the current iteration of the type-specific challenge handle. I'd also suggest leaving out some of the extra stuff like the PREFIX and webroot_path() stuff at least for now -- I'm inclined to think users probably don't need help solving that part?

Maybe there should also be a separate challenges.rs module with inline modules per challenge type, to make things easier to navigate?

djc and others added 9 commits July 11, 2026 08:53
It's more of a data class and I think fits better alongside the
challenge types (vs the challenge handles).
It's more and more common for the challenge types to be
divergent in state, and authorization mechanism. Let's start to reflect
this better in the type system.
Handling authorization challenges requires per-type specific knowledge,
e.g. whether the challenge should get an empty POST-as-GET to mark it
ready (not applicable for device-attest-01), or whether the challenge
response is provisioned with a key authorization (not applicable to
device-attest-01, dns-persist-01) and which format (e.g. for a DNS-01
TXT record, or a TLS-ALPN-01 challenge certificate extension).
How to provision a challenge response to complete an authz depends on
the challenge type. E.g. when provisioning an HTTP-01 response, you
shouldn't use the SHA256 digest of the key auth (that's specific to
TLS-ALPN-01) or the base64 encoding of that digest (that's specific to
DNS-01). Similarly, there's extra context that's useful to provide to
callers that depends on the challenge type context (e.g. the hostname
for a DNS-01 TXT record, or the file path for an HTTP-01 webroot
response file).

By removing the common `KeyAuthorization` type in favour of
per-challenge type authorization types that the challenge type handles
produce we can use the type system to guide callers to the right
process/data for their challenge.
In theory an ACME CA won't produce challenges that don't support the
authorization's identifier type, but just in case we should make sure
that we don't produce a challenge handle for an identifier type it isn't
spec'd to support.
@cpu
cpu force-pushed the ci/cpu-challenge-handles branch from e4e5c47 to de24537 Compare July 11, 2026 13:06
Comment thread src/types.rs

impl ChallengeState {
/// Get the challenge type associated with this challenge state
pub fn r#type(&self) -> ChallengeType {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: I think this should be below token().

Comment thread src/types.rs
Comment on lines +873 to +894
pub struct Http01Challenge {
/// A token for constructing a key authorization to complete this challenge
pub token: String,
}

/// Challenge state for an RFC 8555 dns-01 challenge
///
/// See <https://www.rfc-editor.org/rfc/rfc8555#section-8.4>
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[non_exhaustive]
pub struct Dns01Challenge {
/// A token for constructing a key authorization to complete this challenge
pub token: String,
}

/// Challenge state for an RFC 8737 tls-alpn-01 challenge
///
/// See <https://www.rfc-editor.org/rfc/rfc8737#section-3>
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[non_exhaustive]
pub struct TlsAlpn01Challenge {
/// A token for constructing a key authorization to complete this challenge

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Pff, sorry for the long delay.

I'm open to the idea of more strongly anchoring the challenges and their associated state/actions in the type system. I think the per-challenge state types look okay but it feels like there is too much boilerplate in the current iteration of the type-specific challenge handle. I'd also suggest leaving out some of the extra stuff like the PREFIX and webroot_path() stuff at least for now -- I'm inclined to think users probably don't need help solving that part?

Maybe there should also be a separate challenges.rs module with inline modules per challenge type, to make things easier to navigate?

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