Repository navigation
Conversation
Add the dns-persist-01 challenge type, disabled by default, and the issuerDomainNames and accountHashPrefix properties required by draft-ietf-acme-dns-persist-02. Both properties are validated when the challenge is enabled.
Implement the dns-persist-01 validation method defined in draft-ietf-acme-dns-persist-02. The validation looks up the TXT records at _validation-persist.<domain>, ignores the ones for other issuers, and succeeds if a record binds the domain to the account using the hashed account URI. Wildcard identifiers require policy=wildcard. The persistUntil parameter limits the expiration of the authorization, and orders cannot be finalized once an authorization validated with dns-persist-01 has expired.
Persist the issuer domain names of dns-persist-01 challenges, and allow updating the expiration of an authorization, so it can be limited by the persistUntil parameter of the validation record.
Create dns-persist-01 challenges for DNS identifiers, including wildcards, when the challenge is enabled in the provisioner. The challenges include the issuer domain names instead of a token. The ACME directory advertises the accountHashPrefix and issuerDomainNames metadata required to provision the validation records.
Validation records can be provisioned by a party other than the ACME client, so include the reason a record was rejected in the error detail to make them easier to fix.
Pin the smallstep/linkedca#170 branch until it is released. The newer linkedca requires go 1.26.7.
Map the dns-persist-01 challenge type and the issuerDomainNames and accountHashPrefix properties in both directions. Without this, remote provisioner management and `step ca provisioner update` silently drop them, and a provisioner configured with only dns-persist-01 falls back to the default challenges.
|
|
This was referenced Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for the ACME
dns-persist-01challenge, as defined indraft-ietf-acme-dns-persist-02.
It is disabled by default.
To validate a domain, the CA looks up the TXT records at
_validation-persist.<domain>. A record must nameone of the CA's issuer domain names. Its
accounturimust be the hashed account URI<accountHashPrefix>sha-256/b64url(SHA-256(len ‖ domain ‖ thumbprint ‖ accountURL)), computed either forthe domain or for
*.Configuration
{ "type": "ACME", "name": "acme", "challenges": ["dns-01", "dns-persist-01"], "caaIdentities": ["ca.example.com"], "issuerDomainNames": ["ca.example.com"], "accountHashPrefix": "https://ca.example.com/acme/acme/account-hash/" }Both new properties are required when
dns-persist-01is enabled, and are validated inInit:issuerDomainNames: 1 to 10 names, each a lowercase A-label without a trailing dot. IfcaaIdentitiesisset, every name must also appear in it.
accountHashPrefix: an absolute URL prefix containing only characters allowed in an RFC 8659 parameter value.The directory
metaobject advertises both values when the challenge is enabled.Changes
authority/provisioner: theDNS_PERSIST_01challenge type, the two new properties, and their validation.acme:dns-persist-01validator.accounturi, or an invalidpersistUntilare rejected asmalformed. Unknown tags are ignored.persistUntil, or a wildcard identifier withoutpolicy=wildcardarerejected as
unauthorized.persistUntilcaps the authorization'sexpires.Order.Finalizerefuses to issue once an authorization validated with dns-persist-01 has expired.acme/api:dns-persist-01challenges for DNS identifiers, wildcards included.issuerDomainNamesand notoken.acme/db/nosql: persists the challenge'sissuerDomainNames.UpdateAuthorizationnow also persistsExpiresAt.authority: converts the challenge type and both properties to and from linkedca. This keeps them intact throughremote provisioner management and
step ca provisioner add/update. Without it, a provisioner configured withonly dns-persist-01 would fall back to the default challenges after an update.
go.mod: pins the Add the dns-persist-01 ACME challenge type linkedca#170 branch until linkedca is released. The newer linkedca requiresgo 1.26.7, so thegodirective moves up from1.26.0.Design notes
accountHashPrefixis explicit config. It is not derived from the request host. The draft requires astable prefix (§4.5), and links built by the linker depend on the request
Host.kid. Legacy accounts without a stored location fail with a clear error instead of hashing a guessed URL.tokenis nowomitemptyonacme.Challenge. Existing challenge types always set it, so their JSONoutput is unchanged.
acme.ProvisionergainsGetIssuerDomainNames()andGetAccountHashPrefix(), following theGetAttestationRoots()precedent.pending, like dns-01, so DNS can be fixed and the challenge retried.Account status and stored-location failures mark it
invalid.NewDetailedError, so clients see why a record was rejected, as device-attest-01 does.The details contain only public DNS data and values derived from the requesting account.
persistUntilissuance guard lives inFinalize. It applies only to authorizations with a validdns-persist-01 challenge. An order can still show
readyafter the deadline, but finalizing it then failswith
unauthorized. If the authorization was stillpendingwhen it expired, the existingUpdateStatuslogic marks the order
invalid.Not implemented
draft's concern is recomputing hashes over long key histories, but
keyChangeis not implemented in step-ca.Each account has exactly one key, so checking a record costs two hashes.
keyChangeis ever implemented.
policy=wildcardrecord, and DNSSEC or multi-perspective validation (§7.8). These are MAY or SHOULD in thedraft.
Testing
persistUntil, wildcards, andDB errors.
golangci-lintwith the shared smallstep config: 0 issues.step-castarted with--resolverpointing to an embedded DNSserver. It used a
golang.org/x/crypto/acmeclient that computes the hashed URIs on its own. All 21 checks passed:accountHashPrefixandissuerDomainNames.issuerDomainNamesand notoken.persistUntilcaps theauthorization's
expires, and a certificate is issued.policy=wildcard, with a detailed error. After the record isfixed, the same challenge is retried and the wildcard certificate is issued.
*form validates.unauthorized.unauthorized.malformed.persistUntil:unauthorized.dns.persistUntilpasses, finalizing areadyorder fails. An order whose authorization was stillpendingbecomesinvalid.TestBootstrapClientServerRotation(inca) fails under-raceon unmodifiedmastertoo. It passeswithout
-race.Related
linkedca release before merging.
step ca provisionerflags.