Skip to content

Commit b37975e

Browse files
authored
Add "dns-account-01" support from draft-ietf-acme-scoped-dns-challenges (#435)
This change implements the `dns-account-01` ACME challenge as specified in [draft-ietf-acme-scoped-dns-challenges](https://datatracker.ietf.org/doc/draft-ietf-acme-scoped-dns-challenges/). The relevant [validation label computation](https://github.com/aaomidi/draft-ietf-acme-scoped-dns-challenges/blob/0058e0800056698fb37f3b2cb31a727c826675fb/draft-ietf-acme-scoped-dns-challenges.mkd#dns-account-01-challenge) is: ```plain "_" || base32(SHA-256(<ACCOUNT_RESOURCE_URL>)[0:10]) || "._acme-" || <SCOPE> || "-challenge" ``` where SCOPE is one of { `host`, `wildcard` }. A SCOPE of { `domain` } is unimplemented. This implementation is interoperable with the https://github.com/eggsampler/acme changes in eggsampler/acme#21 and passes the `TestWildcardDNSAccount` test. Solves #425.
1 parent a292a6e commit b37975e

File tree

3 files changed

+77
-22
lines changed

3 files changed

+77
-22
lines changed

acme/common.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ const (
1515
IdentifierDNS = "dns"
1616
IdentifierIP = "ip"
1717

18-
ChallengeHTTP01 = "http-01"
19-
ChallengeTLSALPN01 = "tls-alpn-01"
20-
ChallengeDNS01 = "dns-01"
18+
ChallengeHTTP01 = "http-01"
19+
ChallengeTLSALPN01 = "tls-alpn-01"
20+
ChallengeDNS01 = "dns-01"
21+
ChallengeDNSAccount01 = "dns-account-01"
2122

2223
HTTP01BaseURL = ".well-known/acme-challenge/"
2324

va/va.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"crypto/tls"
88
"crypto/x509"
99
"encoding/asn1"
10+
"encoding/base32"
1011
"encoding/base64"
1112
"fmt"
1213
"io"
@@ -92,6 +93,8 @@ type vaTask struct {
9293
Identifier acme.Identifier
9394
Challenge *core.Challenge
9495
Account *core.Account
96+
AccountURL string
97+
Wildcard bool
9598
}
9699

97100
type VAImpl struct {
@@ -157,11 +160,13 @@ func New(
157160
return va
158161
}
159162

160-
func (va VAImpl) ValidateChallenge(ident acme.Identifier, chal *core.Challenge, acct *core.Account) {
163+
func (va VAImpl) ValidateChallenge(ident acme.Identifier, chal *core.Challenge, acct *core.Account, acctURL string, wildcard bool) {
161164
task := &vaTask{
162165
Identifier: ident,
163166
Challenge: chal,
164167
Account: acct,
168+
AccountURL: acctURL,
169+
Wildcard: wildcard,
165170
}
166171
// Submit the task for validation
167172
va.tasks <- task
@@ -299,6 +304,8 @@ func (va VAImpl) performValidation(task *vaTask, results chan<- *core.Validation
299304
results <- va.validateTLSALPN01(task)
300305
case acme.ChallengeDNS01:
301306
results <- va.validateDNS01(task)
307+
case acme.ChallengeDNSAccount01:
308+
results <- va.validateDNSAccount01(task)
302309
default:
303310
va.log.Printf("Error: performValidation(): Invalid challenge type: %q", task.Challenge.Type)
304311
}
@@ -342,6 +349,49 @@ func (va VAImpl) validateDNS01(task *vaTask) *core.ValidationRecord {
342349
return result
343350
}
344351

352+
func (va VAImpl) validateDNSAccount01(task *vaTask) *core.ValidationRecord {
353+
acctHash := sha256.Sum256([]byte(task.AccountURL))
354+
acctLabel := strings.ToLower(base32.StdEncoding.EncodeToString(acctHash[0:10]))
355+
scope := "host"
356+
if task.Wildcard {
357+
scope = "wildcard"
358+
}
359+
challengeSubdomain := fmt.Sprintf("_%s._acme-%s-challenge.%s", acctLabel, scope, task.Identifier.Value)
360+
361+
result := &core.ValidationRecord{
362+
URL: challengeSubdomain,
363+
ValidatedAt: time.Now(),
364+
}
365+
366+
txts, err := va.getTXTEntry(challengeSubdomain)
367+
if err != nil {
368+
result.Error = acme.UnauthorizedProblem(fmt.Sprintf("Error retrieving TXT records for DNS-ACCOUNT-01 challenge (%q)", err))
369+
return result
370+
}
371+
372+
if len(txts) == 0 {
373+
msg := "No TXT records found for DNS-ACCOUNT-01 challenge"
374+
result.Error = acme.UnauthorizedProblem(msg)
375+
return result
376+
}
377+
378+
task.Challenge.RLock()
379+
expectedKeyAuthorization := task.Challenge.ExpectedKeyAuthorization(task.Account.Key)
380+
h := sha256.Sum256([]byte(expectedKeyAuthorization))
381+
task.Challenge.RUnlock()
382+
authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h[:])
383+
384+
for _, element := range txts {
385+
if subtle.ConstantTimeCompare([]byte(element), []byte(authorizedKeysDigest)) == 1 {
386+
return result
387+
}
388+
}
389+
390+
msg := "Correct value not found for DNS-ACCOUNT-01 challenge"
391+
result.Error = acme.UnauthorizedProblem(msg)
392+
return result
393+
}
394+
345395
func (va VAImpl) validateTLSALPN01(task *vaTask) *core.ValidationRecord {
346396
portString := strconv.Itoa(va.tlsPort)
347397

wfe/wfe.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,30 +1597,27 @@ func (wfe *WebFrontEndImpl) makeChallenge(
15971597
func (wfe *WebFrontEndImpl) makeChallenges(authz *core.Authorization, request *http.Request) error {
15981598
var chals []*core.Challenge
15991599

1600-
// Authorizations for a wildcard identifier only get a DNS-01 challenges to
1601-
// match Boulder/Let's Encrypt wildcard issuance policy
1600+
// Determine which challenge types are enabled for this identifier
1601+
var enabledChallenges []string
16021602
if strings.HasPrefix(authz.Identifier.Value, "*.") {
1603-
chal, err := wfe.makeChallenge(acme.ChallengeDNS01, authz, request)
1604-
if err != nil {
1605-
return err
1606-
}
1607-
chals = []*core.Challenge{chal}
1603+
// Authorizations for a wildcard identifier get DNS-based challenges to
1604+
// match Boulder/Let's Encrypt wildcard issuance policy
1605+
enabledChallenges = []string{acme.ChallengeDNS01, acme.ChallengeDNSAccount01}
16081606
} else {
16091607
// IP addresses get HTTP-01 and TLS-ALPN challenges
1610-
var enabledChallenges []string
16111608
if authz.Identifier.Type == acme.IdentifierIP {
16121609
enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01}
16131610
} else {
16141611
// Non-wildcard, non-IP identifier authorizations get all of the enabled challenge types
1615-
enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01, acme.ChallengeDNS01}
1612+
enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01, acme.ChallengeDNS01, acme.ChallengeDNSAccount01}
16161613
}
1617-
for _, chalType := range enabledChallenges {
1618-
chal, err := wfe.makeChallenge(chalType, authz, request)
1619-
if err != nil {
1620-
return err
1621-
}
1622-
chals = append(chals, chal)
1614+
}
1615+
for _, chalType := range enabledChallenges {
1616+
chal, err := wfe.makeChallenge(chalType, authz, request)
1617+
if err != nil {
1618+
return err
16231619
}
1620+
chals = append(chals, chal)
16241621
}
16251622

16261623
// Lock the authorization for writing to update the challenges
@@ -2377,8 +2374,12 @@ func (wfe *WebFrontEndImpl) updateChallenge(
23772374

23782375
// If the identifier value is for a wildcard domain then strip the wildcard
23792376
// prefix before dispatching the validation to ensure the base domain is
2380-
// validated.
2381-
ident.Value = strings.TrimPrefix(ident.Value, "*.")
2377+
// validated. Set a flag to indicate validation scope.
2378+
wildcard := false
2379+
if strings.HasPrefix(ident.Value, "*.") {
2380+
ident.Value = strings.TrimPrefix(ident.Value, "*.")
2381+
wildcard = true
2382+
}
23822383

23832384
// Confirm challenge status again and update it immediately before sending it to the VA
23842385
prob = nil
@@ -2395,8 +2396,11 @@ func (wfe *WebFrontEndImpl) updateChallenge(
23952396
return
23962397
}
23972398

2399+
// Reconstruct account URL for use in scoped validation methods
2400+
acctURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", acctPath, existingAcct.ID))
2401+
23982402
// Submit a validation job to the VA, this will be processed asynchronously
2399-
wfe.va.ValidateChallenge(ident, existingChal, existingAcct)
2403+
wfe.va.ValidateChallenge(ident, existingChal, existingAcct, acctURL, wildcard)
24002404

24012405
// Lock the challenge for reading in order to write the response
24022406
existingChal.RLock()

0 commit comments

Comments
 (0)