diff --git a/acme/common.go b/acme/common.go index 34a0a524..ab926d26 100644 --- a/acme/common.go +++ b/acme/common.go @@ -15,9 +15,10 @@ const ( IdentifierDNS = "dns" IdentifierIP = "ip" - ChallengeHTTP01 = "http-01" - ChallengeTLSALPN01 = "tls-alpn-01" - ChallengeDNS01 = "dns-01" + ChallengeHTTP01 = "http-01" + ChallengeTLSALPN01 = "tls-alpn-01" + ChallengeDNS01 = "dns-01" + ChallengeDNSAccount01 = "dns-account-01" HTTP01BaseURL = ".well-known/acme-challenge/" diff --git a/va/va.go b/va/va.go index 08a94377..f4cffaa3 100644 --- a/va/va.go +++ b/va/va.go @@ -7,6 +7,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/asn1" + "encoding/base32" "encoding/base64" "fmt" "io" @@ -92,6 +93,8 @@ type vaTask struct { Identifier acme.Identifier Challenge *core.Challenge Account *core.Account + AccountURL string + Wildcard bool } type VAImpl struct { @@ -157,11 +160,13 @@ func New( return va } -func (va VAImpl) ValidateChallenge(ident acme.Identifier, chal *core.Challenge, acct *core.Account) { +func (va VAImpl) ValidateChallenge(ident acme.Identifier, chal *core.Challenge, acct *core.Account, acctURL string, wildcard bool) { task := &vaTask{ Identifier: ident, Challenge: chal, Account: acct, + AccountURL: acctURL, + Wildcard: wildcard, } // Submit the task for validation va.tasks <- task @@ -299,6 +304,8 @@ func (va VAImpl) performValidation(task *vaTask, results chan<- *core.Validation results <- va.validateTLSALPN01(task) case acme.ChallengeDNS01: results <- va.validateDNS01(task) + case acme.ChallengeDNSAccount01: + results <- va.validateDNSAccount01(task) default: va.log.Printf("Error: performValidation(): Invalid challenge type: %q", task.Challenge.Type) } @@ -342,6 +349,49 @@ func (va VAImpl) validateDNS01(task *vaTask) *core.ValidationRecord { return result } +func (va VAImpl) validateDNSAccount01(task *vaTask) *core.ValidationRecord { + acctHash := sha256.Sum256([]byte(task.AccountURL)) + acctLabel := strings.ToLower(base32.StdEncoding.EncodeToString(acctHash[0:10])) + scope := "host" + if task.Wildcard { + scope = "wildcard" + } + challengeSubdomain := fmt.Sprintf("_%s._acme-%s-challenge.%s", acctLabel, scope, task.Identifier.Value) + + result := &core.ValidationRecord{ + URL: challengeSubdomain, + ValidatedAt: time.Now(), + } + + txts, err := va.getTXTEntry(challengeSubdomain) + if err != nil { + result.Error = acme.UnauthorizedProblem(fmt.Sprintf("Error retrieving TXT records for DNS-ACCOUNT-01 challenge (%q)", err)) + return result + } + + if len(txts) == 0 { + msg := "No TXT records found for DNS-ACCOUNT-01 challenge" + result.Error = acme.UnauthorizedProblem(msg) + return result + } + + task.Challenge.RLock() + expectedKeyAuthorization := task.Challenge.ExpectedKeyAuthorization(task.Account.Key) + h := sha256.Sum256([]byte(expectedKeyAuthorization)) + task.Challenge.RUnlock() + authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h[:]) + + for _, element := range txts { + if subtle.ConstantTimeCompare([]byte(element), []byte(authorizedKeysDigest)) == 1 { + return result + } + } + + msg := "Correct value not found for DNS-ACCOUNT-01 challenge" + result.Error = acme.UnauthorizedProblem(msg) + return result +} + func (va VAImpl) validateTLSALPN01(task *vaTask) *core.ValidationRecord { portString := strconv.Itoa(va.tlsPort) diff --git a/wfe/wfe.go b/wfe/wfe.go index a627d53f..7d83257e 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -1597,30 +1597,27 @@ func (wfe *WebFrontEndImpl) makeChallenge( func (wfe *WebFrontEndImpl) makeChallenges(authz *core.Authorization, request *http.Request) error { var chals []*core.Challenge - // Authorizations for a wildcard identifier only get a DNS-01 challenges to - // match Boulder/Let's Encrypt wildcard issuance policy + // Determine which challenge types are enabled for this identifier + var enabledChallenges []string if strings.HasPrefix(authz.Identifier.Value, "*.") { - chal, err := wfe.makeChallenge(acme.ChallengeDNS01, authz, request) - if err != nil { - return err - } - chals = []*core.Challenge{chal} + // Authorizations for a wildcard identifier get DNS-based challenges to + // match Boulder/Let's Encrypt wildcard issuance policy + enabledChallenges = []string{acme.ChallengeDNS01, acme.ChallengeDNSAccount01} } else { // IP addresses get HTTP-01 and TLS-ALPN challenges - var enabledChallenges []string if authz.Identifier.Type == acme.IdentifierIP { enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01} } else { // Non-wildcard, non-IP identifier authorizations get all of the enabled challenge types - enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01, acme.ChallengeDNS01} + enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01, acme.ChallengeDNS01, acme.ChallengeDNSAccount01} } - for _, chalType := range enabledChallenges { - chal, err := wfe.makeChallenge(chalType, authz, request) - if err != nil { - return err - } - chals = append(chals, chal) + } + for _, chalType := range enabledChallenges { + chal, err := wfe.makeChallenge(chalType, authz, request) + if err != nil { + return err } + chals = append(chals, chal) } // Lock the authorization for writing to update the challenges @@ -2377,8 +2374,12 @@ func (wfe *WebFrontEndImpl) updateChallenge( // If the identifier value is for a wildcard domain then strip the wildcard // prefix before dispatching the validation to ensure the base domain is - // validated. - ident.Value = strings.TrimPrefix(ident.Value, "*.") + // validated. Set a flag to indicate validation scope. + wildcard := false + if strings.HasPrefix(ident.Value, "*.") { + ident.Value = strings.TrimPrefix(ident.Value, "*.") + wildcard = true + } // Confirm challenge status again and update it immediately before sending it to the VA prob = nil @@ -2395,8 +2396,11 @@ func (wfe *WebFrontEndImpl) updateChallenge( return } + // Reconstruct account URL for use in scoped validation methods + acctURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", acctPath, existingAcct.ID)) + // Submit a validation job to the VA, this will be processed asynchronously - wfe.va.ValidateChallenge(ident, existingChal, existingAcct) + wfe.va.ValidateChallenge(ident, existingChal, existingAcct, acctURL, wildcard) // Lock the challenge for reading in order to write the response existingChal.RLock()