Refactor challenge providers to new ChallengeProvider interface

* new ChallengeProvider with Present and CleanUp methods
* new Challenge type describing `http-01`, `tls-sni-01`, `dns-01`
* new client.SetChallengeProvider to support custom implementations
This commit is contained in:
Jehiah Czebotar 2016-01-14 23:06:25 -05:00
parent 2e5ae296cc
commit 617dd4d37c
20 changed files with 317 additions and 185 deletions

15
acme/challenges.go Normal file
View file

@ -0,0 +1,15 @@
package acme
type Challenge string
const (
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
HTTP01 = Challenge("http-01")
// TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni
// Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge
TLSSNI01 = Challenge("tls-sni-01")
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
// Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01")
)

View file

@ -54,7 +54,7 @@ type Client struct {
jws *jws
keyBits int
issuerCert []byte
solvers map[string]solver
solvers map[Challenge]solver
}
// NewClient creates a new ACME client on behalf of the user. The client will depend on
@ -93,13 +93,28 @@ func NewClient(caDirURL string, user User, keyBits int) (*Client, error) {
// REVIEW: best possibility?
// Add all available solvers with the right index as per ACME
// spec to this map. Otherwise they won`t be found.
solvers := make(map[string]solver)
solvers["http-01"] = &httpChallenge{jws: jws, validate: validate}
solvers["tls-sni-01"] = &tlsSNIChallenge{jws: jws, validate: validate}
solvers := make(map[Challenge]solver)
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate}
solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate}
return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil
}
// SetChallengeProvider specifies a custom provider that will make the solution available
func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error {
switch challenge {
case HTTP01:
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
case TLSSNI01:
c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p}
case DNS01:
c.solvers[challenge] = &dnsChallenge{jws: c.jws, provider: p}
default:
return fmt.Errorf("Unknown challenge %v", challenge)
}
return nil
}
// SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges.
// If this option is not used, the default port 80 and all interfaces will be used.
// To only specify a port and no interface use the ":port" notation.
@ -109,9 +124,8 @@ func (c *Client) SetHTTPAddress(iface string) error {
return err
}
if chlng, ok := c.solvers["http-01"]; ok {
chlng.(*httpChallenge).iface = host
chlng.(*httpChallenge).port = port
if chlng, ok := c.solvers[HTTP01]; ok {
chlng.(*httpChallenge).provider = &httpChallengeServer{iface: host, port: port}
}
return nil
@ -126,21 +140,17 @@ func (c *Client) SetTLSAddress(iface string) error {
return err
}
if chlng, ok := c.solvers["tls-sni-01"]; ok {
chlng.(*tlsSNIChallenge).iface = host
chlng.(*tlsSNIChallenge).port = port
if chlng, ok := c.solvers[TLSSNI01]; ok {
chlng.(*tlsSNIChallenge).provider = &tlsSNIChallengeServer{iface: host, port: port}
}
return nil
}
// ExcludeChallenges explicitly removes challenges from the pool for solving.
func (c *Client) ExcludeChallenges(challenges []string) {
func (c *Client) ExcludeChallenges(challenges []Challenge) {
// Loop through all challenges and delete the requested one if found.
for _, challenge := range challenges {
if _, ok := c.solvers[challenge]; ok {
delete(c.solvers, challenge)
}
delete(c.solvers, challenge)
}
}

View file

@ -75,32 +75,32 @@ func TestClientOptPort(t *testing.T) {
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
httpSolver, ok := client.solvers["http-01"].(*httpChallenge)
httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
if !ok {
t.Fatal("Expected http-01 solver to be httpChallenge type")
}
if httpSolver.jws != client.jws {
t.Error("Expected http-01 to have same jws as client")
}
if httpSolver.port != optPort {
t.Errorf("Expected http-01 to have port %s but was %s", optPort, httpSolver.port)
if got := httpSolver.provider.(*httpChallengeServer).port; got != optPort {
t.Errorf("Expected http-01 to have port %s but was %s", optPort, got)
}
if httpSolver.iface != optHost {
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, httpSolver.iface)
if got := httpSolver.provider.(*httpChallengeServer).iface; got != optHost {
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
}
httpsSolver, ok := client.solvers["tls-sni-01"].(*tlsSNIChallenge)
httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge)
if !ok {
t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
}
if httpsSolver.jws != client.jws {
t.Error("Expected tls-sni-01 to have same jws as client")
}
if httpsSolver.port != optPort {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, httpSolver.port)
if got := httpsSolver.provider.(*tlsSNIChallengeServer).port; got != optPort {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
}
if httpsSolver.port != optPort {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, httpSolver.iface)
if got := httpsSolver.provider.(*tlsSNIChallengeServer).iface; got != optHost {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
}
// test setting different host
@ -108,11 +108,11 @@ func TestClientOptPort(t *testing.T) {
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
if httpSolver.iface != optHost {
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, httpSolver.iface)
if got := httpSolver.provider.(*httpChallengeServer).iface; got != optHost {
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
}
if httpsSolver.port != optPort {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, httpSolver.iface)
if got := httpsSolver.provider.(*tlsSNIChallengeServer).port; got != optPort {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
}
}

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
@ -19,37 +20,49 @@ var preCheckDNS preCheckDNSFunc = checkDNS
var preCheckDNSFallbackCount = 5
// DNSProvider represents a service for managing DNS records.
type DNSProvider interface {
CreateTXTRecord(fqdn, value string, ttl int) error
RemoveTXTRecord(fqdn, value string, ttl int) error
// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding
keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
value = strings.TrimRight(keyAuthSha, "=")
ttl = 120
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
return
}
// dnsChallenge implements the dns-01 challenge according to ACME 7.5
type dnsChallenge struct {
jws *jws
provider DNSProvider
provider ChallengeProvider
}
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO] acme: Trying to solve DNS-01")
if s.provider == nil {
return errors.New("No DNS Provider configured")
}
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey)
if err != nil {
return err
}
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding
keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
keyAuthSha = strings.TrimRight(keyAuthSha, "=")
fqdn := fmt.Sprintf("_acme-challenge.%s.", domain)
if err = s.provider.CreateTXTRecord(fqdn, keyAuthSha, 120); err != nil {
return err
err = s.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("Error presenting token %s", err)
}
defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("Error cleaning up %s %v ", domain, err)
}
}()
fqdn, _, _ := DNS01Record(domain, keyAuth)
preCheckDNS(domain, fqdn)
@ -94,10 +107,6 @@ Loop:
resp, err = http.Get(chlng.URI)
}
if err = s.provider.RemoveTXTRecord(fqdn, keyAuthSha, 120); err != nil {
logf("[WARN] acme: Failed to cleanup DNS record. -> %v ", err)
}
return nil
}

View file

@ -34,8 +34,9 @@ func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProvid
return c, nil
}
// CreateTXTRecord creates a TXT record using the specified parameters
func (c *DNSProviderCloudFlare) CreateTXTRecord(fqdn, value string, ttl int) error {
// Present creates a TXT record to fulfil the dns-01 challenge
func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
@ -50,8 +51,9 @@ func (c *DNSProviderCloudFlare) CreateTXTRecord(fqdn, value string, ttl int) err
return nil
}
// RemoveTXTRecord removes the TXT record matching the specified parameters
func (c *DNSProviderCloudFlare) RemoveTXTRecord(fqdn, value string, ttl int) error {
// CleanUp removes the TXT record matching the specified parameters
func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := DNS01Record(domain, keyAuth)
records, err := c.findTxtRecords(fqdn)
if err != nil {
return err
@ -60,10 +62,9 @@ func (c *DNSProviderCloudFlare) RemoveTXTRecord(fqdn, value string, ttl int) err
for _, rec := range records {
err := c.client.Records.Delete(c.ctx, rec.ZoneID, rec.ID)
if err != nil {
return fmt.Errorf("CloudFlare API call has failed: %v", err)
return err
}
}
return nil
}
@ -140,12 +141,14 @@ func unFqdn(name string) string {
// TTL must be between 120 and 86400 seconds
func sanitizeTTL(ttl int) int {
if ttl < 120 {
ttl = 120
} else if ttl > 86400 {
ttl = 86400
switch {
case ttl < 120:
return 120
case ttl > 86400:
return 86400
default:
return ttl
}
return ttl
}
func envAuth() (email, apiKey string) {

View file

@ -1,7 +1,6 @@
package acme
import (
"fmt"
"os"
"testing"
"time"
@ -54,7 +53,7 @@ func TestNewDNSProviderCloudFlareMissingCredErr(t *testing.T) {
restoreCloudFlareEnv()
}
func TestCloudFlareCreateTXTRecord(t *testing.T) {
func TestCloudFlarePresent(t *testing.T) {
if !cflareLiveTest {
t.Skip("skipping live test")
}
@ -62,12 +61,11 @@ func TestCloudFlareCreateTXTRecord(t *testing.T) {
provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey)
assert.NoError(t, err)
fqdn := fmt.Sprintf("_acme-challenge.123.%s.", cflareDomain)
err = provider.CreateTXTRecord(fqdn, "123d==", 120)
err = provider.Present(cflareDomain, "", "123d==")
assert.NoError(t, err)
}
func TestCloudFlareRemoveTXTRecord(t *testing.T) {
func TestCloudFlareCleanUp(t *testing.T) {
if !cflareLiveTest {
t.Skip("skipping live test")
}
@ -77,7 +75,6 @@ func TestCloudFlareRemoveTXTRecord(t *testing.T) {
provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey)
assert.NoError(t, err)
fqdn := fmt.Sprintf("_acme-challenge.123.%s.", cflareDomain)
err = provider.RemoveTXTRecord(fqdn, "123d==", 120)
err = provider.CleanUp(cflareDomain, "", "123d==")
assert.NoError(t, err)
}

View file

@ -10,7 +10,7 @@ const (
dnsTemplate = "%s %d IN TXT \"%s\""
)
// DNSProviderManual is an implementation of the DNSProvider interface
// DNSProviderManual is an implementation of the ChallengeProvider interface
type DNSProviderManual struct{}
// NewDNSProviderManual returns a DNSProviderManual instance.
@ -18,8 +18,9 @@ func NewDNSProviderManual() (*DNSProviderManual, error) {
return &DNSProviderManual{}, nil
}
// CreateTXTRecord prints instructions for manually creating the TXT record
func (*DNSProviderManual) CreateTXTRecord(fqdn, value string, ttl int) error {
// Present prints instructions for manually creating the TXT record
func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
logf("[INFO] acme: Please create the following TXT record in your DNS zone:")
logf("[INFO] acme: %s", dnsRecord)
@ -29,9 +30,10 @@ func (*DNSProviderManual) CreateTXTRecord(fqdn, value string, ttl int) error {
return nil
}
// RemoveTXTRecord prints instructions for manually removing the TXT record
func (*DNSProviderManual) RemoveTXTRecord(fqdn, value string, ttl int) error {
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
// CleanUp prints instructions for manually removing the TXT record
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
fqdn, _, ttl := DNS01Record(domain, keyAuth)
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, "...")
logf("[INFO] acme: You can now remove this TXT record from your DNS zone:")
logf("[INFO] acme: %s", dnsRecord)
return nil

View file

@ -6,13 +6,14 @@ import (
"time"
)
// DNSProviderRFC2136 is an implementation of the DNSProvider interface that
// DNSProviderRFC2136 is an implementation of the ChallengeProvider interface that
// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
type DNSProviderRFC2136 struct {
nameserver string
zone string
tsigKey string
tsigSecret string
records map[string]string
}
// NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance.
@ -23,6 +24,7 @@ func NewDNSProviderRFC2136(nameserver, zone, tsigKey, tsigSecret string) (*DNSPr
d := &DNSProviderRFC2136{
nameserver: nameserver,
zone: zone,
records: make(map[string]string),
}
if len(tsigKey) > 0 && len(tsigSecret) > 0 {
d.tsigKey = tsigKey
@ -32,13 +34,17 @@ func NewDNSProviderRFC2136(nameserver, zone, tsigKey, tsigSecret string) (*DNSPr
return d, nil
}
// CreateTXTRecord creates a TXT record using the specified parameters
func (r *DNSProviderRFC2136) CreateTXTRecord(fqdn, value string, ttl int) error {
// Present creates a TXT record using the specified parameters
func (r *DNSProviderRFC2136) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
r.records[fqdn] = value
return r.changeRecord("INSERT", fqdn, value, ttl)
}
// RemoveTXTRecord removes the TXT record matching the specified parameters
func (r *DNSProviderRFC2136) RemoveTXTRecord(fqdn, value string, ttl int) error {
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error {
fqdn, _, ttl := DNS01Record(domain, keyAuth)
value := r.records[fqdn]
return r.changeRecord("REMOVE", fqdn, value, ttl)
}

View file

@ -11,7 +11,9 @@ import (
)
var (
rfc2136TestValue = "so6ZGir4GaZqI11h9UccBB=="
rfc2136TestDomain = "123456789.www.example.com"
rfc2136TestKeyAuth = "123d=="
rfc2136TestValue = "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"
rfc2136TestFqdn = "_acme-challenge.123456789.www.example.com."
rfc2136TestZone = "example.com."
rfc2136TestTTL = 120
@ -58,8 +60,8 @@ func TestRFC2136ServerSuccess(t *testing.T) {
if err != nil {
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
}
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err != nil {
t.Errorf("Expected CreateTXTRecord() to return no error but the error was -> %v", err)
if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
t.Errorf("Expected Present() to return no error but the error was -> %v", err)
}
}
@ -77,10 +79,10 @@ func TestRFC2136ServerError(t *testing.T) {
if err != nil {
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
}
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err == nil {
t.Errorf("Expected CreateTXTRecord() to return an error but it did not.")
if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err == nil {
t.Errorf("Expected Present() to return an error but it did not.")
} else if !strings.Contains(err.Error(), "NOTZONE") {
t.Errorf("Expected CreateTXTRecord() to return an error with the 'NOTZONE' rcode string but it did not.")
t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string but it did not.")
}
}
@ -98,8 +100,8 @@ func TestRFC2136TsigClient(t *testing.T) {
if err != nil {
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
}
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err != nil {
t.Errorf("Expected CreateTXTRecord() to return no error but the error was -> %v", err)
if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
t.Errorf("Expected Present() to return no error but the error was -> %v", err)
}
}
@ -136,8 +138,9 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) {
if err != nil {
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
}
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err != nil {
t.Errorf("Expected CreateTXTRecord() to return no error but the error was -> %v", err)
if err := provider.Present(rfc2136TestDomain, "", "1234d=="); err != nil {
t.Errorf("Expected Present() to return no error but the error was -> %v", err)
}
rcvMsg := <-reqChan

View file

@ -40,13 +40,15 @@ func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*D
return &DNSProviderRoute53{client: client}, nil
}
// CreateTXTRecord creates a TXT record using the specified parameters
func (r *DNSProviderRoute53) CreateTXTRecord(fqdn, value string, ttl int) error {
// Present creates a TXT record using the specified parameters
func (r *DNSProviderRoute53) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
return r.changeRecord("UPSERT", fqdn, value, ttl)
}
// RemoveTXTRecord removes the TXT record matching the specified parameters
func (r *DNSProviderRoute53) RemoveTXTRecord(fqdn, value string, ttl int) error {
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProviderRoute53) CleanUp(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
return r.changeRecord("DELETE", fqdn, value, ttl)
}
@ -110,7 +112,7 @@ func newTXTRecordSet(fqdn, value string, ttl int) route53.ResourceRecordSet {
// Route53 API has pretty strict rate limits (5req/s globally per account)
// Hence we check if we are being throttled to maybe retry the request
func rateExceeded (err error) bool {
func rateExceeded(err error) bool {
if strings.Contains(err.Error(), "Throttling") {
return true
}

View file

@ -108,19 +108,22 @@ func TestNewDNSProviderRoute53InvalidRegionErr(t *testing.T) {
assert.EqualError(t, err, "Invalid AWS region name us-east-3")
}
func TestRoute53CreateTXTRecord(t *testing.T) {
func TestRoute53Present(t *testing.T) {
assert := assert.New(t)
testServer := makeRoute53TestServer()
provider := makeRoute53Provider(testServer)
testServer.ResponseMap(2, serverResponseMap)
err := provider.CreateTXTRecord("_acme-challenge.123.example.com.", "123456d==", 120)
assert.NoError(err, "Expected CreateTXTRecord to return no error")
domain := "example.com"
keyAuth := "123456d=="
err := provider.Present(domain, "", keyAuth)
assert.NoError(err, "Expected Present to return no error")
httpReqs := testServer.WaitRequests(2)
httpReq := httpReqs[1]
assert.Equal("/2013-04-01/hostedzone/Z2K123214213123/rrset", httpReq.URL.Path,
"Expected CreateTXTRecord to select the correct hostedzone")
"Expected Present to select the correct hostedzone")
}

View file

@ -2,73 +2,44 @@ package acme
import (
"fmt"
"net"
"net/http"
"strings"
"log"
)
type httpChallenge struct {
jws *jws
validate validateFunc
iface string
port string
done chan bool
provider ChallengeProvider
}
// HTTP01ChallengePath returns the URL path for the `http-01` challenge
func HTTP01ChallengePath(token string) string {
return "/.well-known/acme-challenge/" + token
}
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
s.done = make(chan bool)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey)
if err != nil {
return err
}
// Allow for CLI port override
port := "80"
if s.port != "" {
port = s.port
if s.provider == nil {
s.provider = &httpChallengeServer{}
}
iface := ""
if s.iface != "" {
iface = s.iface
}
listener, err := net.Listen("tcp", net.JoinHostPort(iface, port))
err = s.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
return fmt.Errorf("Error presenting token %s", err)
}
path := "/.well-known/acme-challenge/" + chlng.Token
go s.serve(listener, path, keyAuth, domain)
err = s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
listener.Close()
<-s.done
return err
}
func (s *httpChallenge) serve(listener net.Listener, path, keyAuth, domain string) {
// The handler validates the HOST header and request type.
// For validation it then writes the token the server returned with the challenge
mux := http.NewServeMux()
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(keyAuth))
logf("[INFO][%s] Served key authentication", domain)
} else {
logf("[INFO] Received request for domain %s with method %s", r.Host, r.Method)
w.Write([]byte("TEST"))
defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("Error cleaning up %s %v ", domain, err)
}
})
}()
http.Serve(listener, mux)
s.done <- true
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}

View file

@ -0,0 +1,63 @@
package acme
import (
"fmt"
"net"
"net/http"
"strings"
)
// httpChallengeServer implements ChallengeProvider for `http-01` challenge
type httpChallengeServer struct {
iface string
port string
done chan bool
listener net.Listener
}
// Present makes the token available at `HTTP01ChallengePath(token)`
func (s *httpChallengeServer) Present(domain, token, keyAuth string) error {
if s.port == "" {
s.port = "80"
}
var err error
s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
if err != nil {
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
}
s.done = make(chan bool)
go s.serve(domain, token, keyAuth)
return nil
}
func (s *httpChallengeServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
s.listener.Close()
<-s.done
return nil
}
func (s *httpChallengeServer) serve(domain, token, keyAuth string) {
path := HTTP01ChallengePath(token)
// The handler validates the HOST header and request type.
// For validation it then writes the token the server returned with the challenge
mux := http.NewServeMux()
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(keyAuth))
logf("[INFO][%s] Served key authentication", domain)
} else {
logf("[INFO] Received request for domain %s with method %s", r.Host, r.Method)
w.Write([]byte("TEST"))
}
})
http.Serve(s.listener, mux)
s.done <- true
}

View file

@ -10,7 +10,7 @@ import (
func TestHTTPChallenge(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "http-01", Token: "http1"}
clientChallenge := challenge{Type: HTTP01, Token: "http1"}
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
resp, err := httpGet(uri)
@ -35,7 +35,7 @@ func TestHTTPChallenge(t *testing.T) {
return nil
}
solver := &httpChallenge{jws: j, validate: mockValidate, port: "23457"}
solver := &httpChallenge{jws: j, validate: mockValidate, provider: &httpChallengeServer{port: "23457"}}
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
t.Errorf("Solve error: got %v, want nil", err)
@ -45,8 +45,8 @@ func TestHTTPChallenge(t *testing.T) {
func TestHTTPChallengeInvalidPort(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "http-01", Token: "http2"}
solver := &httpChallenge{jws: j, validate: stubValidate, port: "123456"}
clientChallenge := challenge{Type: HTTP01, Token: "http2"}
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &httpChallengeServer{port: "123456"}}
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
t.Errorf("Solve error: got %v, want error", err)

View file

@ -82,7 +82,7 @@ type validationRecord struct {
type challenge struct {
Resource string `json:"resource,omitempty"`
Type string `json:"type,omitempty"`
Type Challenge `json:"type,omitempty"`
Status string `json:"status,omitempty"`
URI string `json:"uri,omitempty"`
Token string `json:"token,omitempty"`

8
acme/provider.go Normal file
View file

@ -0,0 +1,8 @@
package acme
// ChallengeProvider presents the solution to a challenge available to be solved
// CleanUp will be called by the challenge if Present ends in a non-error state.
type ChallengeProvider interface {
Present(domain, token, keyAuth string) error
CleanUp(domain, token, keyAuth string) error
}

View file

@ -6,15 +6,13 @@ import (
"crypto/tls"
"encoding/hex"
"fmt"
"net"
"net/http"
"log"
)
type tlsSNIChallenge struct {
jws *jws
validate validateFunc
iface string
port string
provider ChallengeProvider
}
func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
@ -29,41 +27,25 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
return err
}
cert, err := t.generateCertificate(keyAuth)
if t.provider == nil {
t.provider = &tlsSNIChallengeServer{}
}
err = t.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return err
return fmt.Errorf("Error presenting token %s", err)
}
// Allow for CLI port override
port := "443"
if t.port != "" {
port = t.port
}
iface := ""
if t.iface != "" {
iface = t.iface
}
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{cert}
listener, err := tls.Listen("tcp", net.JoinHostPort(iface, port), tlsConf)
if err != nil {
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
}
defer listener.Close()
go http.Serve(listener, nil)
defer func() {
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("Error cleaning up %s %v ", domain, err)
}
}()
return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
func (t *tlsSNIChallenge) generateCertificate(keyAuth string) (tls.Certificate, error) {
zBytes := sha256.Sum256([]byte(keyAuth))
z := hex.EncodeToString(zBytes[:sha256.Size])
// TLSSNI01ChallengeCert returns a certificate for the `tls-sni-01` challenge
func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, error) {
// generate a new RSA key for the certificates
tempPrivKey, err := generatePrivateKey(rsakey, 2048)
if err != nil {
@ -72,6 +54,8 @@ func (t *tlsSNIChallenge) generateCertificate(keyAuth string) (tls.Certificate,
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
rsaPrivPEM := pemEncode(rsaPrivKey)
zBytes := sha256.Sum256([]byte(keyAuth))
z := hex.EncodeToString(zBytes[:sha256.Size])
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
if err != nil {

View file

@ -0,0 +1,52 @@
package acme
import (
"crypto/tls"
"fmt"
"net"
"net/http"
)
// tlsSNIChallengeServer implements ChallengeProvider for `TLS-SNI-01` challenge
type tlsSNIChallengeServer struct {
iface string
port string
done chan bool
listener net.Listener
}
// Present makes the keyAuth available as a cert
func (s *tlsSNIChallengeServer) Present(domain, token, keyAuth string) error {
if s.port == "" {
s.port = "443"
}
cert, err := TLSSNI01ChallengeCert(keyAuth)
if err != nil {
return err
}
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{cert}
s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf)
if err != nil {
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
}
s.done = make(chan bool)
go func() {
http.Serve(s.listener, nil)
s.done <- true
}()
return nil
}
func (s *tlsSNIChallengeServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
s.listener.Close()
<-s.done
return nil
}

View file

@ -13,7 +13,7 @@ import (
func TestTLSSNIChallenge(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "tls-sni-01", Token: "tlssni1"}
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"}
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
InsecureSkipVerify: true,
@ -43,7 +43,7 @@ func TestTLSSNIChallenge(t *testing.T) {
return nil
}
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, port: "23457"}
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &tlsSNIChallengeServer{port: "23457"}}
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
t.Errorf("Solve error: got %v, want nil", err)
@ -53,8 +53,8 @@ func TestTLSSNIChallenge(t *testing.T) {
func TestTLSSNIChallengeInvalidPort(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: "tls-sni-01", Token: "tlssni2"}
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, port: "123456"}
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"}
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &tlsSNIChallengeServer{port: "123456"}}
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
t.Errorf("Solve error: got %v, want error", err)

View file

@ -7,6 +7,7 @@ import (
"strings"
"github.com/codegangsta/cli"
"github.com/xenolf/lego/acme"
)
// Configuration type from CLI and config files.
@ -24,8 +25,11 @@ func (c *Configuration) RsaBits() int {
return c.context.GlobalInt("rsa-key-size")
}
func (c *Configuration) ExcludedSolvers() []string {
return c.context.GlobalStringSlice("exclude")
func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) {
for _, s := range c.context.GlobalStringSlice("exclude") {
cc = append(cc, acme.Challenge(s))
}
return
}
// ServerPath returns the OS dependent path to the data for a specific CA