From 96c76147e533c0772862b1dec83a77f24e70c8ec Mon Sep 17 00:00:00 2001 From: Samantha Date: Tue, 21 Apr 2026 18:03:45 -0400 Subject: [PATCH 1/2] Update draft-ietf-acme-dns-persist-00 support to 01 --- cmd/boulder-wfe2/main.go | 9 +++++++++ core/objects.go | 4 ++++ core/util.go | 2 +- ra/ra.go | 2 +- test/config-next/wfe2.json | 1 + va/dns_persist.go | 7 +++++-- vendor/github.com/eggsampler/acme/v3/types.go | 8 +++++++- wfe2/wfe.go | 16 ++++++++++++++-- wfe2/wfe_test.go | 3 +++ 9 files changed, 45 insertions(+), 7 deletions(-) diff --git a/cmd/boulder-wfe2/main.go b/cmd/boulder-wfe2/main.go index 8290e108e13..7e874e67a74 100644 --- a/cmd/boulder-wfe2/main.go +++ b/cmd/boulder-wfe2/main.go @@ -120,6 +120,14 @@ type Config struct { // will differ in configuration for production and staging. LegacyKeyIDPrefix string `validate:"required,url"` + // AccountURIPrefix is used to construct the "accounturi" field of + // dns-persist-01 challenges (e.g. "https://acme-v02.api.letsencrypt.org/acme/acct/"). + // MUST match the first entry of the VA's AccountURIPrefixes field. + // + // TODO(#8724): Once this field has been set in Production we can make + // it required. + AccountURIPrefix string `validate:"omitempty,url,endswith=/"` + // GoodKey is an embedded config stanza for the goodkey library. GoodKey goodkey.Config @@ -414,6 +422,7 @@ func main() { wfe.AllowOrigins = c.WFE.AllowOrigins wfe.DirectoryWebsite = c.WFE.DirectoryWebsite wfe.LegacyKeyIDPrefix = c.WFE.LegacyKeyIDPrefix + wfe.AccountURIPrefix = c.WFE.AccountURIPrefix if c.WFE.ListenAddress == "" { cmd.Fail("HTTP listen address is not configured") diff --git a/core/objects.go b/core/objects.go index 51861109452..dee0e27ecda 100644 --- a/core/objects.go +++ b/core/objects.go @@ -163,6 +163,10 @@ type Challenge struct { // by all challenges except dns-persist-01. Token string `json:"token,omitempty"` + // AccountURI is the account URI the client includes during dns-persist-01 + // challenge validation. + AccountURI string `json:"accounturi,omitempty"` + // IssuerDomainNames contains the list of issuer domain name values accepted // during dns-persist-01 challenge validation. IssuerDomainNames []string `json:"issuer-domain-names,omitempty"` diff --git a/core/util.go b/core/util.go index 39b72d05080..e1228fbe6b5 100644 --- a/core/util.go +++ b/core/util.go @@ -402,7 +402,7 @@ func Command() string { } // NormalizeIssuerDomainName normalizes an RFC 8659 issuer-domain-name per the -// recommended algorithm in draft-ietf-acme-dns-persist-00, Section 9.1.1: +// recommended algorithm in draft-ietf-acme-dns-persist-01, Section 9.2: // case-fold to lowercase, apply Unicode NFC normalization, convert to A-label // (Punycode), remove any trailing dot, and ensure the result is no more than // 253 octets in length. If normalization fails, an error is returned. diff --git a/ra/ra.go b/ra/ra.go index e46a184f583..63191c93740 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -2211,7 +2211,7 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New } // Never reuse dns-persist-01 authorizations: - // draft-ietf-acme-dns-persist-00 section 7.8 caps the reuse period to + // draft-ietf-acme-dns-persist-01 section 7.8 caps the reuse period to // the TXT record's TTL and BRs section 3.2.2.4.22 caps it at 10 days. // Since TTLs are typically seconds to minutes, the TTL cap is likely to // be the binding constraint; re-validating every order is simpler. diff --git a/test/config-next/wfe2.json b/test/config-next/wfe2.json index 0009838809c..d929140a618 100644 --- a/test/config-next/wfe2.json +++ b/test/config-next/wfe2.json @@ -11,6 +11,7 @@ "directoryCAAIdentity": "happy-hacker-ca.invalid", "directoryWebsite": "https://github.com/letsencrypt/boulder", "legacyKeyIDPrefix": "http://boulder.service.consul:4000/reg/", + "accountURIPrefix": "http://boulder.service.consul:4001/acme/acct/", "goodkey": {}, "maxContactsPerRegistration": 3, "tls": { diff --git a/va/dns_persist.go b/va/dns_persist.go index 6f98b5fbd38..4442c79fb6e 100644 --- a/va/dns_persist.go +++ b/va/dns_persist.go @@ -109,7 +109,7 @@ func parseDNSPersistRecord(record string) (string, *dnsPersistIssueValueParams, params.persistUntil = time.Unix(persistUntilVal, 0).UTC() default: - // Per draft-ietf-acme-dns-persist-00, "the server MUST ignore any + // Per draft-ietf-acme-dns-persist-01, "the server MUST ignore any // parameter within the issue-value that has an unrecognized tag." continue } @@ -125,10 +125,13 @@ func parseDNSPersistRecord(record string) (string, *dnsPersistIssueValueParams, // given time. It returns nil if the record authorizes issuance, or a // berrors.Unauthorized error for authorization failures. func checkDNSPersistRecord(params *dnsPersistIssueValueParams, validAccountURI string, wildcardName bool, validatedAt time.Time) error { + // Per draft-ietf-acme-dns-persist-01 section 4.1, accounturi values are + // compared using Simple String Comparison per RFC 3986 section 6.2.1, with + // no case-folding or other normalization. if params.accountURI != validAccountURI { return berrors.UnauthorizedError("accounturi mismatch: expected %q, got %q", validAccountURI, params.accountURI) } - // Per draft-ietf-acme-dns-persist-00, the policy parameter's tag and + // Per draft-ietf-acme-dns-persist-01 section 4.1, the policy parameter's // defined values MUST be treated as case-insensitive. If the policy // parameter's value is anything other than "wildcard", the CA MUST proceed // as if the policy parameter were not present. diff --git a/vendor/github.com/eggsampler/acme/v3/types.go b/vendor/github.com/eggsampler/acme/v3/types.go index 3357bacf919..dcda2a864b5 100644 --- a/vendor/github.com/eggsampler/acme/v3/types.go +++ b/vendor/github.com/eggsampler/acme/v3/types.go @@ -202,10 +202,16 @@ type Challenge struct { Token string `json:"token"` KeyAuthorization string `json:"keyAuthorization"` + // AccountURI is specific to the dns-persist-01 challenge type. It is the + // URI that the client must include in the accounturi parameter of the DNS + // TXT record when completing the challenge. For more information see: + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-persist-01#section-3.1 + AccountURI string `json:"accounturi,omitempty"` + // IssuerDomainNames is specific to the dns-persist-01 challenge type. It // contains the list of issuer domain names accepted by the CA. For more // information see: - // https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-persist-00#section-3.1 + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-persist-01#section-3.1 IssuerDomainNames []string `json:"issuer-domain-names,omitempty"` // Authorization url provided by the rel="up" Link http header diff --git a/wfe2/wfe.go b/wfe2/wfe.go index 060add84d54..aaaf494170f 100644 --- a/wfe2/wfe.go +++ b/wfe2/wfe.go @@ -140,6 +140,11 @@ type WebFrontEndImpl struct { // `LegacyKeyIDPrefix` for more information. LegacyKeyIDPrefix string + // AccountURIPrefix is required to set the "accounturi" field of + // dns-persist-01 challenges. MUST match the first entry of the VA's + // AccountURIPrefixes field. + AccountURIPrefix string + // Key policy. keyPolicy goodkey.KeyPolicy @@ -1245,19 +1250,26 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay( } if challenge.Type == core.ChallengeTypeDNSPersist01 { - // draft-ietf-acme-dns-persist-00 section 3.1 states, "Servers MUST NOT + // draft-ietf-acme-dns-persist-01 section 3.1 states, "Servers MUST NOT // send more than 10 issuer domain names." Be aware of this if we ever // support configuration of multiple CAA identities. challenge.IssuerDomainNames = []string{wfe.DirectoryCAAIdentity} + // TODO(#8724): Once the configuration of AccountURIPrefix is required + // to be non-empty, this conditional can be removed. + if wfe.AccountURIPrefix != "" { + challenge.AccountURI = fmt.Sprintf("%s%d", wfe.AccountURIPrefix, authz.RegistrationID) + } + // dns-persist-01 does not use a token, but authorizations store a // single token which gets unconditionally assigned to all challenge // types during deserialization. challenge.Token = "" } else { - // Belt and suspenders: we don't expect this to ever be populated + // Belt and suspenders: we don't expect these to ever be populated // outside of this function, but just in case. challenge.IssuerDomainNames = nil + challenge.AccountURI = "" } } diff --git a/wfe2/wfe_test.go b/wfe2/wfe_test.go index e51d43a87cf..ee2a51c3802 100644 --- a/wfe2/wfe_test.go +++ b/wfe2/wfe_test.go @@ -448,6 +448,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { test.AssertNotError(t, err, "Unable to create WFE") wfe.SubscriberAgreementURL = agreementURL + wfe.AccountURIPrefix = "http://localhost/acme/acct/" return wfe, fc, requestSigner{t, inmemNonceService.AsSource()} } @@ -3657,9 +3658,11 @@ func TestPrepAuthzForDisplay(t *testing.T) { if chall.Type == core.ChallengeTypeDNSPersist01 { test.Assert(t, chall.Token == "", fmt.Sprintf("expected %s to have no token", chall.Type)) test.AssertDeepEquals(t, chall.IssuerDomainNames, []string{"letsencrypt.org"}) + test.AssertEquals(t, chall.AccountURI, "http://localhost/acme/acct/1") } else { test.Assert(t, chall.Token != "", fmt.Sprintf("expected %s to have a token", chall.Type)) test.Assert(t, chall.IssuerDomainNames == nil, fmt.Sprintf("expected %s to have no issuer domain names", chall.Type)) + test.AssertEquals(t, chall.AccountURI, "") } } } From 542f3bca577664084dbc0fe0990a57fb56f268d9 Mon Sep 17 00:00:00 2001 From: Samantha Date: Tue, 28 Apr 2026 15:56:14 -0400 Subject: [PATCH 2/2] Update github.com/eggsampler/acme/v3 to v3.8.1 --- go.mod | 2 +- go.sum | 4 ++-- vendor/modules.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1d5bfca1635..2f51cd7a3ea 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.6 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 github.com/aws/smithy-go v1.24.2 - github.com/eggsampler/acme/v3 v3.8.0 + github.com/eggsampler/acme/v3 v3.8.1 github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-logr/stdr v1.2.2 github.com/go-sql-driver/mysql v1.9.1 diff --git a/go.sum b/go.sum index 54908a713bc..c3ae402fabd 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/eggsampler/acme/v3 v3.8.0 h1:MNP3qVZnAl97l86CHkLdh/O9IBXdjL/AEy7c7QgdJj8= -github.com/eggsampler/acme/v3 v3.8.0/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= +github.com/eggsampler/acme/v3 v3.8.1 h1:HmpFs/CIdEXg2NCwSEFBd1BgSzzN8fPzwNZGzp0izrw= +github.com/eggsampler/acme/v3 v3.8.1/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= diff --git a/vendor/modules.txt b/vendor/modules.txt index 546e00c5ce0..c50ff3ac573 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -146,7 +146,7 @@ github.com/cespare/xxhash/v2 # github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ## explicit github.com/dgryski/go-rendezvous -# github.com/eggsampler/acme/v3 v3.8.0 +# github.com/eggsampler/acme/v3 v3.8.1 ## explicit; go 1.11 github.com/eggsampler/acme/v3 # github.com/felixge/httpsnoop v1.0.4