Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "dns-account-01" support from draft-ietf-acme-scoped-dns-challenges #435

Merged
merged 3 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions acme/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/"

Expand Down
52 changes: 51 additions & 1 deletion va/va.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base32"
"encoding/base64"
"fmt"
"io"
Expand Down Expand Up @@ -92,6 +93,8 @@ type vaTask struct {
Identifier acme.Identifier
Challenge *core.Challenge
Account *core.Account
AccountURL string
Wildcard bool
}

type VAImpl struct {
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels slightly awkward to pass both a *core.Account and an acctURL, given that one can be constructed from the other, but also given that the construction method is acctURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", acctPath, newAcct.ID)) and half of those values are inaccessible here in the VA, I think this is fine.

task := &vaTask{
Identifier: ident,
Challenge: chal,
Account: acct,
AccountURL: acctURL,
Wildcard: wildcard,
}
// Submit the task for validation
va.tasks <- task
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)

Expand Down
40 changes: 22 additions & 18 deletions wfe/wfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Copy link

Choose a reason for hiding this comment

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


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