forked from TrueCloudLab/lego
Merge pull request #76 from jehiah/custom_http_challenge_76
http-01: refactor provider for presenting token
This commit is contained in:
commit
0c1adedebe
20 changed files with 317 additions and 185 deletions
15
acme/challenges.go
Normal file
15
acme/challenges.go
Normal 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")
|
||||
)
|
|
@ -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,22 +140,18 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the current account to the ACME server.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
func envAuth() (email, apiKey string) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
63
acme/http_challenge_server.go
Normal file
63
acme/http_challenge_server.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
8
acme/provider.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
defer func() {
|
||||
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
|
||||
log.Printf("Error cleaning up %s %v ", domain, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
go http.Serve(listener, nil)
|
||||
|
||||
}()
|
||||
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 {
|
||||
|
|
52
acme/tls_sni_challenge_server.go
Normal file
52
acme/tls_sni_challenge_server.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue