Update lego

This commit is contained in:
Ed Robinson 2017-04-07 10:53:39 +01:00
parent 65284441fa
commit a3b95f798b
No known key found for this signature in database
GPG key ID: EC501FCA6421CCF0
61 changed files with 3453 additions and 1536 deletions

View file

@ -11,6 +11,7 @@ import (
"io/ioutil"
"log"
"net"
"net/http"
"regexp"
"strconv"
"strings"
@ -22,6 +23,16 @@ var (
Logger *log.Logger
)
const (
// maxBodySize is the maximum size of body that we will read.
maxBodySize = 1024 * 1024
// overallRequestLimit is the overall number of request per second limited on 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
overallRequestLimit = 18
)
// logf writes a log entry. It uses Logger if not
// nil, otherwise it uses the default log.Logger.
func logf(format string, args ...interface{}) {
@ -518,7 +529,11 @@ func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver
func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) {
resc, errc := make(chan authorizationResource), make(chan domainError)
delay := time.Second / overallRequestLimit
for _, domain := range domains {
time.Sleep(delay)
go func(domain string) {
authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}}
var authz authorization
@ -531,6 +546,7 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s
links := parseLinks(hdr["Link"])
if links["next"] == "" {
logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
errc <- domainError{Domain: domain, Error: errors.New("Server did not provide next link to proceed")}
return
}
@ -556,12 +572,20 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s
}
}
logAuthz(challenges)
close(resc)
close(errc)
return challenges, failures
}
func logAuthz(authz []authorizationResource) {
for _, auth := range authz {
logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL)
}
}
func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) {
if len(authz) == 0 {
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
@ -610,73 +634,95 @@ func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle
return CertificateResource{}, err
}
cerRes := CertificateResource{
certRes := CertificateResource{
Domain: commonName.Domain,
CertURL: resp.Header.Get("Location"),
PrivateKey: privateKeyPem}
PrivateKey: privateKeyPem,
}
for {
switch resp.StatusCode {
case 201, 202:
cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
resp.Body.Close()
if err != nil {
return CertificateResource{}, err
}
// The server returns a body with a length of zero if the
// certificate was not ready at the time this request completed.
// Otherwise the body is the certificate.
if len(cert) > 0 {
cerRes.CertStableURL = resp.Header.Get("Content-Location")
cerRes.AccountRef = c.user.GetRegistration().URI
issuedCert := pemEncode(derCertificateBytes(cert))
// The issuer certificate link is always supplied via an "up" link
// in the response headers of a new certificate.
links := parseLinks(resp.Header["Link"])
issuerCert, err := c.getIssuerCertificate(links["up"])
if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err)
} else {
issuerCert = pemEncode(derCertificateBytes(issuerCert))
// If bundle is true, we want to return a certificate bundle.
// To do this, we append the issuer cert to the issued cert.
if bundle {
issuedCert = append(issuedCert, issuerCert...)
}
}
cerRes.Certificate = issuedCert
cerRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", commonName.Domain)
return cerRes, nil
}
// The certificate was granted but is not yet issued.
// Check retry-after and loop.
ra := resp.Header.Get("Retry-After")
retryAfter, err := strconv.Atoi(ra)
if err != nil {
return CertificateResource{}, err
}
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
break
default:
return CertificateResource{}, handleHTTPError(resp)
}
resp, err = httpGet(cerRes.CertURL)
maxChecks := 1000
for i := 0; i < maxChecks; i++ {
done, err := c.checkCertResponse(resp, &certRes, bundle)
resp.Body.Close()
if err != nil {
return CertificateResource{}, err
}
if done {
break
}
if i == maxChecks-1 {
return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i)
}
resp, err = httpGet(certRes.CertURL)
if err != nil {
return CertificateResource{}, err
}
}
return certRes, nil
}
// checkCertResponse checks resp to see if a certificate is contained in the
// response, and if so, loads it into certRes and returns true. If the cert
// is not yet ready, it returns false. This function honors the waiting period
// required by the Retry-After header of the response, if specified. This
// function may read from resp.Body but does NOT close it. The certRes input
// should already have the Domain (common name) field populated. If bundle is
// true, the certificate will be bundled with the issuer's cert.
func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateResource, bundle bool) (bool, error) {
switch resp.StatusCode {
case 201, 202:
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return false, err
}
// The server returns a body with a length of zero if the
// certificate was not ready at the time this request completed.
// Otherwise the body is the certificate.
if len(cert) > 0 {
certRes.CertStableURL = resp.Header.Get("Content-Location")
certRes.AccountRef = c.user.GetRegistration().URI
issuedCert := pemEncode(derCertificateBytes(cert))
// The issuer certificate link is always supplied via an "up" link
// in the response headers of a new certificate.
links := parseLinks(resp.Header["Link"])
issuerCert, err := c.getIssuerCertificate(links["up"])
if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
} else {
issuerCert = pemEncode(derCertificateBytes(issuerCert))
// If bundle is true, we want to return a certificate bundle.
// To do this, we append the issuer cert to the issued cert.
if bundle {
issuedCert = append(issuedCert, issuerCert...)
}
}
certRes.Certificate = issuedCert
certRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
return true, nil
}
// The certificate was granted but is not yet issued.
// Check retry-after and loop.
ra := resp.Header.Get("Retry-After")
retryAfter, err := strconv.Atoi(ra)
if err != nil {
return false, err
}
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", certRes.Domain, retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
return false, nil
default:
return false, handleHTTPError(resp)
}
}
@ -689,7 +735,7 @@ func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
}
defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return nil, err
}

View file

@ -10,6 +10,7 @@ import (
const (
tosAgreementError = "Must agree to subscriber agreement before any further actions"
invalidNonceError = "JWS has invalid anti-replay nonce"
)
// RemoteError is the base type for all errors specific to the ACME protocol.
@ -30,6 +31,12 @@ type TOSError struct {
RemoteError
}
// NonceError represents the error which is returned if the
// nonce sent by the client was not accepted by the server.
type NonceError struct {
RemoteError
}
type domainError struct {
Domain string
Error error
@ -54,20 +61,17 @@ func (c challengeError) Error() string {
func handleHTTPError(resp *http.Response) error {
var errorDetail RemoteError
contenType := resp.Header.Get("Content-Type")
// try to decode the content as JSON
if contenType == "application/json" || contenType == "application/problem+json" {
decoder := json.NewDecoder(resp.Body)
err := decoder.Decode(&errorDetail)
contentType := resp.Header.Get("Content-Type")
if contentType == "application/json" || contentType == "application/problem+json" {
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
if err != nil {
return err
}
} else {
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return err
}
errorDetail.Detail = string(detailBytes)
}
@ -78,6 +82,10 @@ func handleHTTPError(resp *http.Response) error {
return TOSError{errorDetail}
}
if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) {
return NonceError{errorDetail}
}
return errorDetail
}

View file

@ -31,14 +31,14 @@ const (
func httpHead(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to head %q: %v", url, err)
}
req.Header.Set("User-Agent", userAgent())
resp, err = HTTPClient.Do(req)
if err != nil {
return resp, err
return resp, fmt.Errorf("failed to do head %q: %v", url, err)
}
resp.Body.Close()
return resp, err
@ -49,7 +49,7 @@ func httpHead(url string) (resp *http.Response, err error) {
func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to post %q: %v", url, err)
}
req.Header.Set("Content-Type", bodyType)
req.Header.Set("User-Agent", userAgent())
@ -62,7 +62,7 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response,
func httpGet(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get %q: %v", url, err)
}
req.Header.Set("User-Agent", userAgent())
@ -74,7 +74,7 @@ func httpGet(url string) (resp *http.Response, err error) {
func getJSON(uri string, respBody interface{}) (http.Header, error) {
resp, err := httpGet(uri)
if err != nil {
return nil, fmt.Errorf("failed to get %q: %v", uri, err)
return nil, fmt.Errorf("failed to get json %q: %v", uri, err)
}
defer resp.Body.Close()
@ -97,10 +97,41 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return resp.Header, handleHTTPError(resp)
err := handleHTTPError(resp)
switch err.(type) {
case NonceError:
// Retry once if the nonce was invalidated
retryResp, err := j.post(uri, jsonBytes)
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
}
defer retryResp.Body.Close()
if retryResp.StatusCode >= http.StatusBadRequest {
return retryResp.Header, handleHTTPError(retryResp)
}
if respBody == nil {
return retryResp.Header, nil
}
return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody)
default:
return resp.Header, err
}
}
if respBody == nil {

View file

@ -16,8 +16,7 @@ import (
type jws struct {
directoryURL string
privKey crypto.PrivateKey
nonces []string
sync.Mutex
nonces nonceManager
}
func keyAsJWK(key interface{}) *jose.JsonWebKey {
@ -32,23 +31,26 @@ func keyAsJWK(key interface{}) *jose.JsonWebKey {
}
}
// Posts a JWS signed message to the specified URL
// Posts a JWS signed message to the specified URL.
// It does NOT close the response body, so the caller must
// do that if no error was returned.
func (j *jws) post(url string, content []byte) (*http.Response, error) {
signedContent, err := j.signContent(content)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
}
resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
}
j.Lock()
defer j.Unlock()
j.getNonceFromResponse(resp)
nonce, nonceErr := getNonceFromResponse(resp)
if nonceErr == nil {
j.nonces.Push(nonce)
}
return resp, err
return resp, nil
}
func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
@ -67,49 +69,63 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
signer, err := jose.NewSigner(alg, j.privKey)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
}
signer.SetNonceSource(j)
signed, err := signer.Sign(content)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
}
return signed, nil
}
func (j *jws) getNonceFromResponse(resp *http.Response) error {
func (j *jws) Nonce() (string, error) {
if nonce, ok := j.nonces.Pop(); ok {
return nonce, nil
}
return getNonce(j.directoryURL)
}
type nonceManager struct {
nonces []string
sync.Mutex
}
func (n *nonceManager) Pop() (string, bool) {
n.Lock()
defer n.Unlock()
if len(n.nonces) == 0 {
return "", false
}
nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true
}
func (n *nonceManager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
func getNonce(url string) (string, error) {
resp, err := httpHead(url)
if err != nil {
return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error())
}
return getNonceFromResponse(resp)
}
func getNonceFromResponse(resp *http.Response) (string, error) {
nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" {
return fmt.Errorf("Server did not respond with a proper nonce header.")
return "", fmt.Errorf("Server did not respond with a proper nonce header.")
}
j.nonces = append(j.nonces, nonce)
return nil
}
func (j *jws) getNonce() error {
resp, err := httpHead(j.directoryURL)
if err != nil {
return err
}
return j.getNonceFromResponse(resp)
}
func (j *jws) Nonce() (string, error) {
j.Lock()
defer j.Unlock()
nonce := ""
if len(j.nonces) == 0 {
err := j.getNonce()
if err != nil {
return nonce, err
}
}
if len(j.nonces) == 0 {
return "", fmt.Errorf("Can't get nonce")
}
nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1]
return nonce, nil
}