Skip to content
This repository has been archived by the owner on Feb 13, 2018. It is now read-only.

Commit

Permalink
Added DNS Challenge support
Browse files Browse the repository at this point in the history
  • Loading branch information
jameshartig committed Feb 19, 2016
1 parent 5cbbc14 commit 59374dc
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 12 deletions.
3 changes: 3 additions & 0 deletions acme.go
Expand Up @@ -430,6 +430,9 @@ func (c *Client) signObject(accountKey interface{}, v interface{}) (string, erro
default:
err = errors.New("acme: unsupported private key type")
}
if err != nil {
return "", err
}
data, err := json.Marshal(v)
if err != nil {
return "", err
Expand Down
8 changes: 6 additions & 2 deletions acme_test.go
Expand Up @@ -359,8 +359,12 @@ func TestParseLinks(t *testing.T) {

func requiresEtcHostsEdits(t *testing.T) {
addrs, err := net.LookupHost(testDomain)
if err != nil || len(addrs) != 1 || addrs[0] != "127.0.0.1" {
t.Skip("/etc/hosts file not properly configured, skipping test. see README for required edits")
if err != nil || len(addrs) == 0 || (addrs[0] != "127.0.0.1" && addrs[0] != "::1") {
addr := "NXDOMAIN"
if len(addrs) > 0 {
addr = addrs[0]
}
t.Skipf("/etc/hosts file not properly configured, skipping test. see README for required edits. %s resolved to %s", testDomain, addr)
}
return
}
38 changes: 30 additions & 8 deletions challenge.go
Expand Up @@ -30,8 +30,8 @@ const (
// HTTP returns a URL path and HTTP response body that the ACME server will
// check when verifying the challenge.
func (chal Challenge) HTTP(accountKey interface{}) (urlPath, resource string, err error) {
if chal.Type != "http-01" {
return "", "", fmt.Errorf("challenge type is %s not %s", chal.Type, "http-01")
if chal.Type != ChallengeHTTP {
return "", "", fmt.Errorf("challenge type is %s not %s", chal.Type, ChallengeHTTP)
}

urlPath = path.Join("/.well-known/acme-challenge", chal.Token)
Expand All @@ -43,8 +43,8 @@ func (chal Challenge) HTTP(accountKey interface{}) (urlPath, resource string, er
// The ACME server will make a TLS Server Name Indication handshake with the
// given domain. The domain must present the returned certifiate for each name.
func (chal Challenge) TLSSNI(accountKey interface{}) (map[string]*tls.Certificate, error) {
if chal.Type != "tls-sni-01" {
return nil, fmt.Errorf("challenge type is %s not %s", chal.Type, "tls-sni-01")
if chal.Type != ChallengeTLSSNI {
return nil, fmt.Errorf("challenge type is %s not %s", chal.Type, ChallengeTLSSNI)
}

auth, err := keyAuth(accountKey, chal.Token)
Expand Down Expand Up @@ -103,8 +103,18 @@ func (chal Challenge) TLSSNI(accountKey interface{}) (map[string]*tls.Certificat
}

// Not yet implemented
func (chal Challenge) DNS(accountKey interface{}) (domain, txt string, err error) {
return "", "", errors.New("dns challenges not implemented")
func (chal Challenge) DNS(accountKey interface{}) (subdomain, txt string, err error) {
if chal.Type != ChallengeDNS {
return "", "", fmt.Errorf("challenge type is %s not %s", chal.Type, ChallengeDNS)
}
auth, err := keyAuth(accountKey, chal.Token)
if err != nil {
return "", "", err
}
hash := sha256.Sum256([]byte(auth))
txt = base64.RawURLEncoding.EncodeToString(hash[:])
subdomain = "_acme-challenge"
return
}

// Not yet implemented
Expand All @@ -119,7 +129,7 @@ func (chal Challenge) ProofOfPossession(accountKey, certKey interface{}) (Challe
// result of the status.
func (c *Client) ChallengeReady(accountKey interface{}, chal Challenge) error {
switch chal.Type {
case "http-01", "tls-sni-01":
case ChallengeHTTP, ChallengeTLSSNI, ChallengeDNS:
default:
return fmt.Errorf("unsupported challenge type '%s'", chal.Type)
}
Expand Down Expand Up @@ -158,9 +168,12 @@ func (c *Client) ChallengeReady(accountKey interface{}, chal Challenge) error {
start := time.Now()
for {
if time.Now().Sub(start) > pollTimeout {
if chal.Error != nil {
return chal.Error
}
return errors.New("polling pending challenge timed out")
}
chal, err := c.Challenge(chal.URI)
chal, err = c.Challenge(chal.URI)
if err != nil {
return err
}
Expand All @@ -171,6 +184,15 @@ func (c *Client) ChallengeReady(accountKey interface{}, chal Challenge) error {
if chal.Error == nil {
return errors.New("challenge returned status 'invalid' without explicit error")
}
// if the challenge was DNS, keep trying since the value is cached
if chal.Type == ChallengeDNS {
// unauthorized is the TXT value is wrong or not found
// connection if NXDOMAIN
if chal.Error.Typ == "urn:acme:error:unauthorized" || chal.Error.Typ == "urn:acme:error:connection" {
time.Sleep(pollInterval)
continue
}
}
return chal.Error
case StatusValid:
return nil
Expand Down
64 changes: 62 additions & 2 deletions challenge_test.go
@@ -1,22 +1,26 @@
package letsencrypt

import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

// Specified in boulder's configuration
// See $GOATH/src/github.com/letsencrypt/boulder/test/boulder-config.json
var (
httpPort int = 5002
httpsPort int = 5001
httpPort int = 5002
httpsPort int = 5001
boulderDNSSrv string = "http://localhost:8055/set-txt"
)

func TestHTTPChallenge(t *testing.T) {
Expand Down Expand Up @@ -148,3 +152,59 @@ func TestTLSSNIChallenge(t *testing.T) {
t.Fatal(err)
}
}

func TestDNSChallenge(t *testing.T) {
requiresEtcHostsEdits(t)

priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}

cli, err := NewClient(testURL)
if err != nil {
t.Fatal(err)
}
if _, err := cli.NewRegistration(priv); err != nil {
t.Fatal(err)
}
auth, _, err := cli.NewAuthorization(priv, "dns", testDomain)
if err != nil {
t.Fatal(err)
}

chals := auth.Combinations(ChallengeDNS)
if len(chals) == 0 || len(chals[0]) != 1 {
t.Fatal("no supported challenges")
}
chal := chals[0][0]
subdomain, txt, err := chal.DNS(priv)
if err != nil {
t.Fatal(err)
}

body := struct {
Host string `json:"host"`
Value string `json:"value"`
}{
// end host in a period so its fqdn for dns question
Host: strings.Join([]string{subdomain, testDomain, ""}, "."),
Value: txt,
}
bodyb, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("POST", boulderDNSSrv, bytes.NewReader(bodyb))
if err != nil {
t.Fatal(err)
}
_, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}

if err := cli.ChallengeReady(priv, chal); err != nil {
t.Fatal(err)
}
}

0 comments on commit 59374dc

Please sign in to comment.