From 60690c15583a5f1cc62862fe2cf49bc5c031f6e5 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Wed, 19 Apr 2017 17:50:10 -0400 Subject: [PATCH] Adds DNS-01 challenge type support. (#19) This commit adds DNS-01 challenges as a supported challenge type for Pebble. The WFE is modified to construct a DNS-01 challenge along with the HTTP-01 and TLS-SNI-02 challenges for every new pending authz. A new configuration parameter is added to the config file for the upstream DNS resolvers to use. The validation implementation is very close to Boulder's VA & `bdns` implementation, but simplified for just TXT records & a simple DNS-01 challenge mechanism. Note: this commit adds a dependency on miekg/dns and you may need to `go get ./...` since we don't do any sort of godeps magic/vendoring at present. --- acme/common.go | 1 + va/va.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- wfe/wfe.go | 4 ++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/acme/common.go b/acme/common.go index bb382ea7..b66e1053 100644 --- a/acme/common.go +++ b/acme/common.go @@ -21,6 +21,7 @@ const ( ChallengeHTTP01 = "http-01" ChallengeTLSSNI02 = "tls-sni-02" + ChallengeDNS01 = "dns-01" HTTP01BaseURL = ".well-known/acme-challenge/" ) diff --git a/va/va.go b/va/va.go index c6734f4f..c5cdc6c4 100644 --- a/va/va.go +++ b/va/va.go @@ -5,6 +5,7 @@ import ( "crypto/subtle" "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/hex" "fmt" "io/ioutil" @@ -71,7 +72,11 @@ type VAImpl struct { ca *ca.CAImpl } -func New(log *log.Logger, clk clock.Clock, httpPort, tlsPort int, ca *ca.CAImpl) *VAImpl { +func New( + log *log.Logger, + clk clock.Clock, + httpPort, tlsPort int, + ca *ca.CAImpl) *VAImpl { va := &VAImpl{ log: log, clk: clk, @@ -199,17 +204,56 @@ func (va VAImpl) performValidation(task *vaTask, results chan<- *core.Validation va.log.Printf("Sleeping for %s seconds before validating", time.Second*len) va.clk.Sleep(time.Second * len) - // TODO(@cpu): Implement validation for DNS-01 switch task.Challenge.Type { case acme.ChallengeHTTP01: results <- va.validateHTTP01(task) case acme.ChallengeTLSSNI02: results <- va.validateTLSSNI02(task) + case acme.ChallengeDNS01: + results <- va.validateDNS01(task) default: va.log.Printf("Error: performValidation(): Invalid challenge type: %q", task.Challenge.Type) } } +func (va VAImpl) validateDNS01(task *vaTask) *core.ValidationRecord { + const dns01Prefix = "_acme-challenge" + challengeSubdomain := fmt.Sprintf("%s.%s", dns01Prefix, task.Identifier) + + result := &core.ValidationRecord{ + URL: challengeSubdomain, + ValidatedAt: va.clk.Now(), + } + + txts, err := net.LookupTXT(challengeSubdomain) + if err != nil { + result.Error = acme.UnauthorizedProblem("Error retrieving TXT records for DNS challenge") + return result + } + + if len(txts) == 0 { + msg := fmt.Sprintf("No TXT records found for DNS challenge") + result.Error = acme.UnauthorizedProblem(msg) + return result + } + + h := sha256.New() + task.Challenge.RLock() + h.Write([]byte(task.Challenge.KeyAuthorization)) + task.Challenge.RUnlock() + authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + + for _, element := range txts { + if subtle.ConstantTimeCompare([]byte(element), []byte(authorizedKeysDigest)) == 1 { + return result + } + } + + msg := fmt.Sprintf("Correct value not found for DNS challenge") + result.Error = acme.UnauthorizedProblem(msg) + return result +} + func (va VAImpl) validateTLSSNI02(task *vaTask) *core.ValidationRecord { portString := strconv.Itoa(va.tlsPort) hostPort := net.JoinHostPort(task.Identifier, portString) diff --git a/wfe/wfe.go b/wfe/wfe.go index ae34f853..58d82465 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -562,8 +562,8 @@ func (wfe *WebFrontEndImpl) makeChallenge( func (wfe *WebFrontEndImpl) makeChallenges(authz *core.Authorization, request *http.Request) error { var chals []*core.Challenge - // TODO(@cpu): construct challenges for DNS-01 - for _, chalType := range []string{acme.ChallengeHTTP01, acme.ChallengeTLSSNI02} { + enabledChallenges := []string{acme.ChallengeHTTP01, acme.ChallengeTLSSNI02, acme.ChallengeDNS01} + for _, chalType := range enabledChallenges { chal, err := wfe.makeChallenge(chalType, authz, request) if err != nil { return err