Commit 8f583dcf authored by Matthew Holt's avatar Matthew Holt

vendor: Update github.com/xenolf/lego/acme to latest

parent 09188981
...@@ -7,9 +7,11 @@ const ( ...@@ -7,9 +7,11 @@ const (
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http // 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 // Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
HTTP01 = Challenge("http-01") HTTP01 = Challenge("http-01")
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns // 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 // Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01") DNS01 = Challenge("dns-01")
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01 // TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
TLSALPN01 = Challenge("tls-alpn-01") TLSALPN01 = Challenge("tls-alpn-01")
) )
...@@ -26,6 +26,9 @@ const ( ...@@ -26,6 +26,9 @@ const (
// “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the // “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the
// limitation is 20 requests per second, but using 20 as value doesn't work but 18 do // limitation is 20 requests per second, but using 20 as value doesn't work but 18 do
overallRequestLimit = 18 overallRequestLimit = 18
statusValid = "valid"
statusInvalid = "invalid"
) )
// User interface is to be implemented by users of this library. // User interface is to be implemented by users of this library.
...@@ -41,6 +44,17 @@ type solver interface { ...@@ -41,6 +44,17 @@ type solver interface {
Solve(challenge challenge, domain string) error Solve(challenge challenge, domain string) error
} }
// Interface for challenges like dns, where we can set a record in advance for ALL challenges.
// This saves quite a bit of time vs creating the records and solving them serially.
type preSolver interface {
PreSolve(challenge challenge, domain string) error
}
// Interface for challenges like dns, where we can solve all the challenges before to delete them.
type cleanup interface {
CleanUp(challenge challenge, domain string) error
}
type validateFunc func(j *jws, domain, uri string, chlng challenge) error type validateFunc func(j *jws, domain, uri string, chlng challenge) error
// Client is the user-friendy way to ACME // Client is the user-friendy way to ACME
...@@ -374,8 +388,10 @@ DNSNames: ...@@ -374,8 +388,10 @@ DNSNames:
} }
} }
// Add the CSR to the certificate so that it can be used for renewals. if cert != nil {
cert.CSR = pemEncode(&csr) // Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = pemEncode(&csr)
}
// do not return an empty failures map, because // do not return an empty failures map, because
// it would still be a non-nil error value // it would still be a non-nil error value
...@@ -396,7 +412,7 @@ DNSNames: ...@@ -396,7 +412,7 @@ DNSNames:
// the whole certificate will fail. // the whole certificate will fail.
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
if len(domains) == 0 { if len(domains) == 0 {
return nil, errors.New("No domains to obtain a certificate for") return nil, errors.New("no domains to obtain a certificate for")
} }
if bundle { if bundle {
...@@ -489,9 +505,9 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b ...@@ -489,9 +505,9 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
// Start by checking to see if the certificate was based off a CSR, and // Start by checking to see if the certificate was based off a CSR, and
// use that if it's defined. // use that if it's defined.
if len(cert.CSR) > 0 { if len(cert.CSR) > 0 {
csr, err := pemDecodeTox509CSR(cert.CSR) csr, errP := pemDecodeTox509CSR(cert.CSR)
if err != nil { if errP != nil {
return nil, err return nil, errP
} }
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
return newCert, failures return newCert, failures
...@@ -524,7 +540,6 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b ...@@ -524,7 +540,6 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
} }
func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) { func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) {
var identifiers []identifier var identifiers []identifier
for _, domain := range domains { for _, domain := range domains {
identifiers = append(identifiers, identifier{Type: "dns", Value: domain}) identifiers = append(identifiers, identifier{Type: "dns", Value: domain})
...@@ -548,29 +563,75 @@ func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, err ...@@ -548,29 +563,75 @@ func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, err
return orderRes, nil return orderRes, nil
} }
// an authz with the solver we have chosen and the index of the challenge associated with it
type selectedAuthSolver struct {
authz authorization
challengeIndex int
solver solver
}
// Looks through the challenge combinations to find a solvable match. // Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns. // Then solves the challenges in series and returns.
func (c *Client) solveChallengeForAuthz(authorizations []authorization) error { func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
failures := make(ObtainError) failures := make(ObtainError)
// loop through the resources, basically through the domains. authSolvers := []*selectedAuthSolver{}
// loop through the resources, basically through the domains. First pass just selects a solver for each authz.
for _, authz := range authorizations { for _, authz := range authorizations {
if authz.Status == "valid" { if authz.Status == statusValid {
// Boulder might recycle recent validated authz (see issue #267) // Boulder might recycle recent validated authz (see issue #267)
log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
continue continue
} }
if i, solvr := c.chooseSolver(authz, authz.Identifier.Value); solvr != nil {
authSolvers = append(authSolvers, &selectedAuthSolver{
authz: authz,
challengeIndex: i,
solver: solvr,
})
} else {
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
}
}
// no solvers - no solving // for all valid presolvers, first submit the challenges so they have max time to propagate
if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil { for _, item := range authSolvers {
err := solver.Solve(authz.Challenges[i], authz.Identifier.Value) authz := item.authz
if err != nil { i := item.challengeIndex
//c.disableAuthz(authz.Identifier) if presolver, ok := item.solver.(preSolver); ok {
if err := presolver.PreSolve(authz.Challenges[i], authz.Identifier.Value); err != nil {
failures[authz.Identifier.Value] = err failures[authz.Identifier.Value] = err
} }
} else { }
//c.disableAuthz(authz) }
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
defer func() {
// clean all created TXT records
for _, item := range authSolvers {
if clean, ok := item.solver.(cleanup); ok {
if failures[item.authz.Identifier.Value] != nil {
// already failed in previous loop
continue
}
err := clean.CleanUp(item.authz.Challenges[item.challengeIndex], item.authz.Identifier.Value)
if err != nil {
log.Warnf("Error cleaning up %s: %v ", item.authz.Identifier.Value, err)
}
}
}
}()
// finally solve all challenges for real
for _, item := range authSolvers {
authz := item.authz
i := item.challengeIndex
if failures[authz.Identifier.Value] != nil {
// already failed in previous loop
continue
}
if err := item.solver.Solve(authz.Challenges[i], authz.Identifier.Value); err != nil {
failures[authz.Identifier.Value] = err
} }
} }
...@@ -604,7 +665,7 @@ func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) ...@@ -604,7 +665,7 @@ func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error)
go func(authzURL string) { go func(authzURL string) {
var authz authorization var authz authorization
_, err := getJSON(authzURL, &authz) _, err := postAsGet(c.jws, authzURL, &authz)
if err != nil { if err != nil {
errc <- domainError{Domain: authz.Identifier.Value, Error: err} errc <- domainError{Domain: authz.Identifier.Value, Error: err}
return return
...@@ -696,7 +757,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr ...@@ -696,7 +757,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
return nil, err return nil, err
} }
if retOrder.Status == "invalid" { if retOrder.Status == statusInvalid {
return nil, err return nil, err
} }
...@@ -706,7 +767,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr ...@@ -706,7 +767,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
PrivateKey: privateKeyPem, PrivateKey: privateKeyPem,
} }
if retOrder.Status == "valid" { if retOrder.Status == statusValid {
// if the certificate is available right away, short cut! // if the certificate is available right away, short cut!
ok, err := c.checkCertResponse(retOrder, &certRes, bundle) ok, err := c.checkCertResponse(retOrder, &certRes, bundle)
if err != nil { if err != nil {
...@@ -728,7 +789,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr ...@@ -728,7 +789,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
case <-stopTimer.C: case <-stopTimer.C:
return nil, errors.New("certificate polling timed out") return nil, errors.New("certificate polling timed out")
case <-retryTick.C: case <-retryTick.C:
_, err := getJSON(order.URL, &retOrder) _, err := postAsGet(c.jws, order.URL, &retOrder)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -750,10 +811,9 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr ...@@ -750,10 +811,9 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
// should already have the Domain (common name) field populated. If bundle is // should already have the Domain (common name) field populated. If bundle is
// true, the certificate will be bundled with the issuer's cert. // true, the certificate will be bundled with the issuer's cert.
func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) { func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) {
switch order.Status { switch order.Status {
case "valid": case statusValid:
resp, err := httpGet(order.Certificate) resp, err := postAsGet(c.jws, order.Certificate, nil)
if err != nil { if err != nil {
return false, err return false, err
} }
...@@ -801,7 +861,7 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou ...@@ -801,7 +861,7 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou
case "processing": case "processing":
return false, nil return false, nil
case "invalid": case statusInvalid:
return false, errors.New("order has invalid state: invalid") return false, errors.New("order has invalid state: invalid")
default: default:
return false, nil return false, nil
...@@ -811,7 +871,7 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou ...@@ -811,7 +871,7 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou
// getIssuerCertificate requests the issuer certificate // getIssuerCertificate requests the issuer certificate
func (c *Client) getIssuerCertificate(url string) ([]byte, error) { func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
log.Infof("acme: Requesting issuer cert from %s", url) log.Infof("acme: Requesting issuer cert from %s", url)
resp, err := httpGet(url) resp, err := postAsGet(c.jws, url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -854,7 +914,10 @@ func parseLinks(links []string) map[string]string { ...@@ -854,7 +914,10 @@ func parseLinks(links []string) map[string]string {
func validate(j *jws, domain, uri string, c challenge) error { func validate(j *jws, domain, uri string, c challenge) error {
var chlng challenge var chlng challenge
hdr, err := postJSON(j, uri, c, &chlng) // Challenge initiation is done by sending a JWS payload containing the
// trivial JSON object `{}`. We use an empty struct instance as the postJSON
// payload here to achieve this result.
hdr, err := postJSON(j, uri, struct{}{}, &chlng)
if err != nil { if err != nil {
return err return err
} }
...@@ -863,12 +926,12 @@ func validate(j *jws, domain, uri string, c challenge) error { ...@@ -863,12 +926,12 @@ func validate(j *jws, domain, uri string, c challenge) error {
// Repeatedly check the server for an updated status on our request. // Repeatedly check the server for an updated status on our request.
for { for {
switch chlng.Status { switch chlng.Status {
case "valid": case statusValid:
log.Infof("[%s] The server validated our request", domain) log.Infof("[%s] The server validated our request", domain)
return nil return nil
case "pending": case "pending":
case "processing": case "processing":
case "invalid": case statusInvalid:
return handleChallengeError(chlng) return handleChallengeError(chlng)
default: default:
return errors.New("the server returned an unexpected state") return errors.New("the server returned an unexpected state")
...@@ -880,11 +943,15 @@ func validate(j *jws, domain, uri string, c challenge) error { ...@@ -880,11 +943,15 @@ func validate(j *jws, domain, uri string, c challenge) error {
// If it doesn't, we'll just poll hard. // If it doesn't, we'll just poll hard.
ra = 5 ra = 5
} }
time.Sleep(time.Duration(ra) * time.Second) time.Sleep(time.Duration(ra) * time.Second)
hdr, err = getJSON(uri, &chlng) resp, err := postAsGet(j, uri, &chlng)
if err != nil { if err != nil {
return err return err
} }
if resp != nil {
hdr = resp.Header
}
} }
} }
...@@ -81,20 +81,20 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { ...@@ -81,20 +81,20 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
return nil, nil, errors.New("no issuing certificate URL") return nil, nil, errors.New("no issuing certificate URL")
} }
resp, err := httpGet(issuedCert.IssuingCertificateURL[0]) resp, errC := httpGet(issuedCert.IssuingCertificateURL[0])
if err != nil { if errC != nil {
return nil, nil, err return nil, nil, errC
} }
defer resp.Body.Close() defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) issuerBytes, errC := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
if err != nil { if errC != nil {
return nil, nil, err return nil, nil, errC
} }
issuerCert, err := x509.ParseCertificate(issuerBytes) issuerCert, errC := x509.ParseCertificate(issuerBytes)
if err != nil { if errC != nil {
return nil, nil, err return nil, nil, errC
} }
// Insert it into the slice on position 0 // Insert it into the slice on position 0
...@@ -258,15 +258,6 @@ func pemDecode(data []byte) (*pem.Block, error) { ...@@ -258,15 +258,6 @@ func pemDecode(data []byte) (*pem.Block, error) {
return pemBlock, nil return pemBlock, nil
} }
func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {
return nil, err
}
return x509.ParseCertificate(pemBlock.Bytes)
}
func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) { func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
pemBlock, err := pemDecode(pem) pemBlock, err := pemDecode(pem)
if pemBlock == nil { if pemBlock == nil {
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
...@@ -18,18 +19,30 @@ type preCheckDNSFunc func(fqdn, value string) (bool, error) ...@@ -18,18 +19,30 @@ type preCheckDNSFunc func(fqdn, value string) (bool, error)
var ( var (
// PreCheckDNS checks DNS propagation before notifying ACME that // PreCheckDNS checks DNS propagation before notifying ACME that
// the DNS challenge is ready. // the DNS challenge is ready.
PreCheckDNS preCheckDNSFunc = checkDNSPropagation PreCheckDNS preCheckDNSFunc = checkDNSPropagation
fqdnToZone = map[string]string{} fqdnToZone = map[string]string{}
muFqdnToZone sync.Mutex
) )
const defaultResolvConf = "/etc/resolv.conf" const defaultResolvConf = "/etc/resolv.conf"
const (
// DefaultPropagationTimeout default propagation timeout
DefaultPropagationTimeout = 60 * time.Second
// DefaultPollingInterval default polling interval
DefaultPollingInterval = 2 * time.Second
// DefaultTTL default TTL
DefaultTTL = 120
)
var defaultNameservers = []string{ var defaultNameservers = []string{
"google-public-dns-a.google.com:53", "google-public-dns-a.google.com:53",
"google-public-dns-b.google.com:53", "google-public-dns-b.google.com:53",
} }
// RecursiveNameservers are used to pre-check DNS propagations // RecursiveNameservers are used to pre-check DNS propagation
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
// DNSTimeout is used to override the default DNS timeout of 10 seconds. // DNSTimeout is used to override the default DNS timeout of 10 seconds.
...@@ -59,7 +72,7 @@ func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) { ...@@ -59,7 +72,7 @@ func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding // base64URL encoding without padding
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
ttl = 120 ttl = DefaultTTL
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain) fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
return return
} }
...@@ -71,8 +84,10 @@ type dnsChallenge struct { ...@@ -71,8 +84,10 @@ type dnsChallenge struct {
provider ChallengeProvider provider ChallengeProvider
} }
func (s *dnsChallenge) Solve(chlng challenge, domain string) error { // PreSolve just submits the txt record to the dns provider. It does not validate record propagation, or
log.Infof("[%s] acme: Trying to solve DNS-01", domain) // do anything at all with the acme server.
func (s *dnsChallenge) PreSolve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
if s.provider == nil { if s.provider == nil {
return errors.New("no DNS Provider configured") return errors.New("no DNS Provider configured")
...@@ -88,12 +103,18 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { ...@@ -88,12 +103,18 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
if err != nil { if err != nil {
return fmt.Errorf("error presenting token: %s", err) return fmt.Errorf("error presenting token: %s", err)
} }
defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth) return nil
if err != nil { }
log.Warnf("Error cleaning up %s: %v ", domain, err)
} func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
}() log.Infof("[%s] acme: Trying to solve DNS-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
fqdn, value, _ := DNS01Record(domain, keyAuth) fqdn, value, _ := DNS01Record(domain, keyAuth)
...@@ -104,7 +125,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { ...@@ -104,7 +125,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
case ChallengeProviderTimeout: case ChallengeProviderTimeout:
timeout, interval = provider.Timeout() timeout, interval = provider.Timeout()
default: default:
timeout, interval = 60*time.Second, 2*time.Second timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
} }
err = WaitFor(timeout, interval, func() (bool, error) { err = WaitFor(timeout, interval, func() (bool, error) {
...@@ -117,6 +138,15 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { ...@@ -117,6 +138,15 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
} }
// CleanUp cleans the challenge
func (s *dnsChallenge) CleanUp(chlng challenge, domain string) error {
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
return s.provider.CleanUp(domain, chlng.Token, keyAuth)
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. // checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string) (bool, error) { func checkDNSPropagation(fqdn, value string) (bool, error) {
// Initial attempt to resolve at the recursive NS // Initial attempt to resolve at the recursive NS
...@@ -124,6 +154,7 @@ func checkDNSPropagation(fqdn, value string) (bool, error) { ...@@ -124,6 +154,7 @@ func checkDNSPropagation(fqdn, value string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
if r.Rcode == dns.RcodeSuccess { if r.Rcode == dns.RcodeSuccess {
// If we see a CNAME here then use the alias // If we see a CNAME here then use the alias
for _, rr := range r.Answer { for _, rr := range r.Answer {
...@@ -167,7 +198,7 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro ...@@ -167,7 +198,7 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro
} }
if !found { if !found {
return false, fmt.Errorf("NS %s did not return the expected TXT record", ns) return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s]", ns, fqdn)
} }
} }
...@@ -210,7 +241,7 @@ func lookupNameservers(fqdn string) ([]string, error) { ...@@ -210,7 +241,7 @@ func lookupNameservers(fqdn string) ([]string, error) {
zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
if err != nil { if err != nil {
return nil, fmt.Errorf("Could not determine the zone: %v", err) return nil, fmt.Errorf("could not determine the zone: %v", err)
} }
r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true) r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true)
...@@ -227,12 +258,15 @@ func lookupNameservers(fqdn string) ([]string, error) { ...@@ -227,12 +258,15 @@ func lookupNameservers(fqdn string) ([]string, error) {
if len(authoritativeNss) > 0 { if len(authoritativeNss) > 0 {
return authoritativeNss, nil return authoritativeNss, nil
} }
return nil, fmt.Errorf("Could not determine authoritative nameservers") return nil, fmt.Errorf("could not determine authoritative nameservers")
} }
// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the // FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the
// domain labels until the nameserver returns a SOA record in the answer section. // domain labels until the nameserver returns a SOA record in the answer section.
func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) { func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
muFqdnToZone.Lock()
defer muFqdnToZone.Unlock()
// Do we have it cached? // Do we have it cached?
if zone, ok := fqdnToZone[fqdn]; ok { if zone, ok := fqdnToZone[fqdn]; ok {
return zone, nil return zone, nil
...@@ -249,7 +283,7 @@ func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) { ...@@ -249,7 +283,7 @@ func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
// Any response code other than NOERROR and NXDOMAIN is treated as error // Any response code other than NOERROR and NXDOMAIN is treated as error
if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess { if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess {
return "", fmt.Errorf("Unexpected response code '%s' for %s", return "", fmt.Errorf("unexpected response code '%s' for %s",
dns.RcodeToString[in.Rcode], domain) dns.RcodeToString[in.Rcode], domain)
} }
...@@ -272,7 +306,7 @@ func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) { ...@@ -272,7 +306,7 @@ func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
} }
} }
return "", fmt.Errorf("Could not find the start of authority") return "", fmt.Errorf("could not find the start of authority")
} }
// dnsMsgContainsCNAME checks for a CNAME answer in msg // dnsMsgContainsCNAME checks for a CNAME answer in msg
......
...@@ -42,12 +42,14 @@ var ( ...@@ -42,12 +42,14 @@ var (
) )
const ( const (
// defaultGoUserAgent is the Go HTTP package user agent string. Too
// bad it isn't exported. If it changes, we should update it here, too.
defaultGoUserAgent = "Go-http-client/1.1"
// ourUserAgent is the User-Agent of this underlying library package. // ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme" // NOTE: Update this with each tagged release.
ourUserAgent = "xenolf-acme/1.2.1"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "detach"
// caCertificatesEnvVar is the environment variable name that can be used to // caCertificatesEnvVar is the environment variable name that can be used to
// specify the path to PEM encoded CA Certificates that can be used to // specify the path to PEM encoded CA Certificates that can be used to
...@@ -148,59 +150,63 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) { ...@@ -148,59 +150,63 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) {
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) { func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
jsonBytes, err := json.Marshal(reqBody) jsonBytes, err := json.Marshal(reqBody)
if err != nil { if err != nil {
return nil, errors.New("Failed to marshal network message") return nil, errors.New("failed to marshal network message")
} }
resp, err := j.post(uri, jsonBytes) resp, err := post(j, uri, jsonBytes, respBody)
if err != nil { if resp == nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err) return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest { return resp.Header, err
}
err := handleHTTPError(resp) func postAsGet(j *jws, uri string, respBody interface{}) (*http.Response, error) {
return post(j, uri, []byte{}, respBody)
}
switch err.(type) { func post(j *jws, uri string, reqBody []byte, respBody interface{}) (*http.Response, error) {
resp, err := j.post(uri, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to post JWS message. -> %v", err)
}
if resp.StatusCode >= http.StatusBadRequest {
err = handleHTTPError(resp)
switch err.(type) {
case NonceError: case NonceError:
// Retry once if the nonce was invalidated // Retry once if the nonce was invalidated
retryResp, err := j.post(uri, jsonBytes) retryResp, errP := j.post(uri, reqBody)
if err != nil { if errP != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err) return nil, fmt.Errorf("failed to post JWS message. -> %v", errP)
} }
defer retryResp.Body.Close()
if retryResp.StatusCode >= http.StatusBadRequest { if retryResp.StatusCode >= http.StatusBadRequest {
return retryResp.Header, handleHTTPError(retryResp) return retryResp, handleHTTPError(retryResp)
} }
if respBody == nil { if respBody == nil {
return retryResp.Header, nil return retryResp, nil
} }
return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody) return retryResp, json.NewDecoder(retryResp.Body).Decode(respBody)
default: default:
return resp.Header, err return resp, err
} }
} }
if respBody == nil { if respBody == nil {
return resp.Header, nil return resp, nil
} }
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) return resp, json.NewDecoder(resp.Body).Decode(respBody)
} }
// userAgent builds and returns the User-Agent string to use in requests. // userAgent builds and returns the User-Agent string to use in requests.
func userAgent() string { func userAgent() string {
ua := fmt.Sprintf("%s %s (%s; %s) %s", UserAgent, ourUserAgent, runtime.GOOS, runtime.GOARCH, defaultGoUserAgent) ua := fmt.Sprintf("%s %s (%s; %s; %s)", UserAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
return strings.TrimSpace(ua) return strings.TrimSpace(ua)
} }
...@@ -35,7 +35,7 @@ func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error { ...@@ -35,7 +35,7 @@ func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
var err error var err error
s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port)) s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
if err != nil { if err != nil {
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err) return fmt.Errorf("could not start HTTP server for challenge -> %v", err)
} }
s.done = make(chan bool) s.done = make(chan bool)
...@@ -62,20 +62,31 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { ...@@ -62,20 +62,31 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, domain) && r.Method == http.MethodGet { if strings.HasPrefix(r.Host, domain) && r.Method == http.MethodGet {
w.Header().Add("Content-Type", "text/plain") w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(keyAuth)) _, err := w.Write([]byte(keyAuth))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Infof("[%s] Served key authentication", domain) log.Infof("[%s] Served key authentication", domain)
} else { } else {
log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method) log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method)
w.Write([]byte("TEST")) _, err := w.Write([]byte("TEST"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} }
}) })
httpServer := &http.Server{ httpServer := &http.Server{Handler: mux}
Handler: mux,
}
// Once httpServer is shut down we don't want any lingering // Once httpServer is shut down we don't want any lingering
// connections, so disable KeepAlives. // connections, so disable KeepAlives.
httpServer.SetKeepAlivesEnabled(false) httpServer.SetKeepAlivesEnabled(false)
httpServer.Serve(s.listener)
err := httpServer.Serve(s.listener)
if err != nil {
log.Println(err)
}
s.done <- true s.done <- true
} }
...@@ -12,8 +12,8 @@ import ( ...@@ -12,8 +12,8 @@ import (
) )
// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. // idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.1 // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1
var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
type tlsALPNChallenge struct { type tlsALPNChallenge struct {
jws *jws jws *jws
...@@ -58,7 +58,7 @@ func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) { ...@@ -58,7 +58,7 @@ func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {
// Add the keyAuth digest as the acmeValidation-v1 extension // Add the keyAuth digest as the acmeValidation-v1 extension
// (marked as critical such that it won't be used by non-ACME software). // (marked as critical such that it won't be used by non-ACME software).
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3 // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3
extensions := []pkix.Extension{ extensions := []pkix.Extension{
{ {
Id: idPeAcmeIdentifierV1, Id: idPeAcmeIdentifierV1,
......
...@@ -3,6 +3,7 @@ package acme ...@@ -3,6 +3,7 @@ package acme
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"log"
"net" "net"
"net/http" "net/http"
) )
...@@ -65,7 +66,10 @@ func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error { ...@@ -65,7 +66,10 @@ func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error {
// Shut the server down when we're finished. // Shut the server down when we're finished.
go func() { go func() {
http.Serve(t.listener, nil) err := http.Serve(t.listener, nil)
if err != nil {
log.Println(err)
}
}() }()
return nil return nil
......
...@@ -3,16 +3,20 @@ package acme ...@@ -3,16 +3,20 @@ package acme
import ( import (
"fmt" "fmt"
"time" "time"
"github.com/xenolf/lego/log"
) )
// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'. // WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
log.Infof("Wait [timeout: %s, interval: %s]", timeout, interval)
var lastErr string var lastErr string
timeup := time.After(timeout) timeUp := time.After(timeout)
for { for {
select { select {
case <-timeup: case <-timeUp:
return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr) return fmt.Errorf("time limit exceeded: last error: %s", lastErr)
default: default:
} }
......
...@@ -170,7 +170,7 @@ ...@@ -170,7 +170,7 @@
"importpath": "github.com/xenolf/lego/acme", "importpath": "github.com/xenolf/lego/acme",
"repository": "https://github.com/xenolf/lego", "repository": "https://github.com/xenolf/lego",
"vcs": "git", "vcs": "git",
"revision": "04e2d74406d42a3727e7a132c1a39735ac527f51", "revision": "4e842a5eb6dcb9520e03db70cd5896f1df14b72a",
"branch": "master", "branch": "master",
"path": "/acme", "path": "/acme",
"notests": true "notests": true
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment