diff options
Diffstat (limited to 'vendor/github.com/rsc/letsencrypt/vendor/github.com/xenolf/lego/acme/client.go')
-rw-r--r-- | vendor/github.com/rsc/letsencrypt/vendor/github.com/xenolf/lego/acme/client.go | 425 |
1 files changed, 306 insertions, 119 deletions
diff --git a/vendor/github.com/rsc/letsencrypt/vendor/github.com/xenolf/lego/acme/client.go b/vendor/github.com/rsc/letsencrypt/vendor/github.com/xenolf/lego/acme/client.go index 16e4cbe00..bcb844371 100644 --- a/vendor/github.com/rsc/letsencrypt/vendor/github.com/xenolf/lego/acme/client.go +++ b/vendor/github.com/rsc/letsencrypt/vendor/github.com/xenolf/lego/acme/client.go @@ -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{}) { @@ -49,17 +60,17 @@ type validateFunc func(j *jws, domain, uri string, chlng challenge) error // Client is the user-friendy way to ACME type Client struct { - directory directory - user User - jws *jws - keyType KeyType - issuerCert []byte - solvers map[Challenge]solver + directory directory + user User + jws *jws + keyType KeyType + solvers map[Challenge]solver } // NewClient creates a new ACME client on behalf of the user. The client will depend on -// the ACME directory located at caDirURL for the rest of its actions. It will -// generate private keys for certificates of size keyBits. +// the ACME directory located at caDirURL for the rest of its actions. A private +// key of type keyType (see KeyType contants) will be generated when requesting a new +// certificate if one isn't provided. func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { privKey := user.GetPrivateKey() if privKey == nil { @@ -96,13 +107,15 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil } -// SetChallengeProvider specifies a custom provider that will make the solution available +// SetChallengeProvider specifies a custom provider p that can solve the given challenge type. 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, validate: validate, provider: p} default: return fmt.Errorf("Unknown challenge %v", challenge) } @@ -112,6 +125,9 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) // 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. +// +// NOTE: This REPLACES any custom HTTP provider previously set by calling +// c.SetChallengeProvider with the default HTTP challenge provider. func (c *Client) SetHTTPAddress(iface string) error { host, port, err := net.SplitHostPort(iface) if err != nil { @@ -128,6 +144,9 @@ func (c *Client) SetHTTPAddress(iface string) error { // SetTLSAddress specifies a custom interface:port to be used for TLS based challenges. // If this option is not used, the default port 443 and all interfaces will be used. // To only specify a port and no interface use the ":port" notation. +// +// NOTE: This REPLACES any custom TLS-SNI provider previously set by calling +// c.SetChallengeProvider with the default TLS-SNI challenge provider. func (c *Client) SetTLSAddress(iface string) error { host, port, err := net.SplitHostPort(iface) if err != nil { @@ -165,15 +184,31 @@ func (c *Client) Register() (*RegistrationResource, error) { } var serverReg Registration + var regURI string hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg) if err != nil { - return nil, err + remoteErr, ok := err.(RemoteError) + if ok && remoteErr.StatusCode == 409 { + regURI = hdr.Get("Location") + regMsg = registrationMessage{ + Resource: "reg", + } + if hdr, err = postJSON(c.jws, regURI, regMsg, &serverReg); err != nil { + return nil, err + } + } else { + return nil, err + } } reg := &RegistrationResource{Body: serverReg} links := parseLinks(hdr["Link"]) - reg.URI = hdr.Get("Location") + + if regURI == "" { + regURI = hdr.Get("Location") + } + reg.URI = regURI if links["terms-of-service"] != "" { reg.TosURL = links["terms-of-service"] } @@ -187,6 +222,68 @@ func (c *Client) Register() (*RegistrationResource, error) { return reg, nil } +// DeleteRegistration deletes the client's user registration from the ACME +// server. +func (c *Client) DeleteRegistration() error { + if c == nil || c.user == nil { + return errors.New("acme: cannot unregister a nil client or user") + } + logf("[INFO] acme: Deleting account for %s", c.user.GetEmail()) + + regMsg := registrationMessage{ + Resource: "reg", + Delete: true, + } + + _, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil) + if err != nil { + return err + } + + return nil +} + +// QueryRegistration runs a POST request on the client's registration and +// returns the result. +// +// This is similar to the Register function, but acting on an existing +// registration link and resource. +func (c *Client) QueryRegistration() (*RegistrationResource, error) { + if c == nil || c.user == nil { + return nil, errors.New("acme: cannot query the registration of a nil client or user") + } + // Log the URL here instead of the email as the email may not be set + logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI) + + regMsg := registrationMessage{ + Resource: "reg", + } + + var serverReg Registration + hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg) + if err != nil { + return nil, err + } + + reg := &RegistrationResource{Body: serverReg} + + links := parseLinks(hdr["Link"]) + // Location: header is not returned so this needs to be populated off of + // existing URI + reg.URI = c.user.GetRegistration().URI + if links["terms-of-service"] != "" { + reg.TosURL = links["terms-of-service"] + } + + if links["next"] != "" { + reg.NewAuthzURL = links["next"] + } else { + return nil, errors.New("acme: No new-authz link in response to registration query") + } + + return reg, nil +} + // AgreeToTOS updates the Client registration and sends the agreement to // the server. func (c *Client) AgreeToTOS() error { @@ -198,6 +295,69 @@ func (c *Client) AgreeToTOS() error { return err } +// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it. +// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key +// for this CSR is not required. +// If bundle is true, the []byte contains both the issuer certificate and +// your issued certificate as a bundle. +// This function will never return a partial certificate. If one domain in the list fails, +// the whole certificate will fail. +func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) { + // figure out what domains it concerns + // start with the common name + domains := []string{csr.Subject.CommonName} + + // loop over the SubjectAltName DNS names +DNSNames: + for _, sanName := range csr.DNSNames { + for _, existingName := range domains { + if existingName == sanName { + // duplicate; skip this name + continue DNSNames + } + } + + // name is unique + domains = append(domains, sanName) + } + + if bundle { + logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) + } else { + logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) + } + + challenges, failures := c.getChallenges(domains) + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(failures) > 0 { + for _, auth := range challenges { + c.disableAuthz(auth) + } + + return CertificateResource{}, failures + } + + errs := c.solveChallenges(challenges) + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(errs) > 0 { + return CertificateResource{}, errs + } + + logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + + cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil) + if err != nil { + for _, chln := range challenges { + failures[chln.Domain] = err + } + } + + // Add the CSR to the certificate so that it can be used for renewals. + cert.CSR = pemEncode(&csr) + + return cert, failures +} + // ObtainCertificate tries to obtain a single certificate using all domains passed into it. // The first domain in domains is used for the CommonName field of the certificate, all other // domains are added using the Subject Alternate Names extension. A new private key is generated @@ -207,7 +367,7 @@ func (c *Client) AgreeToTOS() error { // your issued certificate as a bundle. // This function will never return a partial certificate. If one domain in the list fails, // the whole certificate will fail. -func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey) (CertificateResource, map[string]error) { +func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, map[string]error) { if bundle { logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { @@ -217,6 +377,10 @@ func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto challenges, failures := c.getChallenges(domains) // If any challenge fails - return. Do not generate partial SAN certificates. if len(failures) > 0 { + for _, auth := range challenges { + c.disableAuthz(auth) + } + return CertificateResource{}, failures } @@ -228,7 +392,7 @@ func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - cert, err := c.requestCertificate(challenges, bundle, privKey) + cert, err := c.requestCertificate(challenges, bundle, privKey, mustStaple) if err != nil { for _, chln := range challenges { failures[chln.Domain] = err @@ -264,7 +428,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error { // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. // For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. -func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (CertificateResource, error) { +func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (CertificateResource, error) { // Input certificate is PEM encoded. Decode it here as we may need the decoded // cert later on in the renewal process. The input may be a bundle or a single certificate. certificates, err := parsePEMBundle(cert.Certificate) @@ -281,47 +445,16 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) - // The first step of renewal is to check if we get a renewed cert - // directly from the cert URL. - resp, err := httpGet(cert.CertURL) - if err != nil { - return CertificateResource{}, err - } - defer resp.Body.Close() - serverCertBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return CertificateResource{}, err - } - - serverCert, err := x509.ParseCertificate(serverCertBytes) - if err != nil { - return CertificateResource{}, err - } - - // If the server responds with a different certificate we are effectively renewed. - // TODO: Further test if we can actually use the new certificate (Our private key works) - if !x509Cert.Equal(serverCert) { - logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain) - issuedCert := pemEncode(derCertificateBytes(serverCertBytes)) - // If bundle is true, we want to return a certificate bundle. - // To do this, we need the issuer certificate. - if bundle { - // 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("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err) - } else { - // Success - append the issuer cert to the issued cert. - issuerCert = pemEncode(derCertificateBytes(issuerCert)) - issuedCert = append(issuedCert, issuerCert...) - } + // We always need to request a new certificate to renew. + // Start by checking to see if the certificate was based off a CSR, and + // use that if it's defined. + if len(cert.CSR) > 0 { + csr, err := pemDecodeTox509CSR(cert.CSR) + if err != nil { + return CertificateResource{}, err } - - cert.Certificate = issuedCert - return cert, nil + newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) + return newCert, failures[cert.Domain] } var privKey crypto.PrivateKey @@ -347,7 +480,7 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif domains = append(domains, x509Cert.Subject.CommonName) } - newCert, failures := c.ObtainCertificate(domains, bundle, privKey) + newCert, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple) return newCert, failures[cert.Domain] } @@ -357,16 +490,23 @@ func (c *Client) solveChallenges(challenges []authorizationResource) map[string] // loop through the resources, basically through the domains. failures := make(map[string]error) for _, authz := range challenges { + if authz.Body.Status == "valid" { + // Boulder might recycle recent validated authz (see issue #267) + logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain) + continue + } // no solvers - no solving if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil { for i, solver := range solvers { // TODO: do not immediately fail if one domain fails to validate. err := solver.Solve(authz.Body.Challenges[i], authz.Domain) if err != nil { + c.disableAuthz(authz) failures[authz.Domain] = err } } } else { + c.disableAuthz(authz) failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain) } } @@ -399,7 +539,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 @@ -412,6 +556,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 } @@ -437,18 +582,32 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s } } + logAuthz(challenges) + close(resc) close(errc) return challenges, failures } -func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey) (CertificateResource, error) { +func logAuthz(authz []authorizationResource) { + for _, auth := range authz { + logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL) + } +} + +// cleanAuthz loops through the passed in slice and disables any auths which are not "valid" +func (c *Client) disableAuthz(auth authorizationResource) error { + var disabledAuth authorization + _, err := postJSON(c.jws, auth.AuthURL, deactivateAuthMessage{Resource: "authz", Status: "deactivated"}, &disabledAuth) + return err +} + +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!") } - commonName := authz[0] var err error if privKey == nil { privKey, err = generatePrivateKey(c.keyType) @@ -457,19 +616,30 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, } } + // determine certificate name(s) based on the authorization resources + commonName := authz[0] var san []string - var authURLs []string for _, auth := range authz[1:] { san = append(san, auth.Domain) - authURLs = append(authURLs, auth.AuthURL) } // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey, commonName.Domain, san) + csr, err := generateCsr(privKey, commonName.Domain, san, mustStaple) if err != nil { return CertificateResource{}, err } + return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey)) +} + +func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) { + commonName := authz[0] + + var authURLs []string + for _, auth := range authz[1:] { + authURLs = append(authURLs, auth.AuthURL) + } + csrString := base64.URLEncoding.EncodeToString(csr) jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs}) if err != nil { @@ -481,90 +651,108 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, return CertificateResource{}, err } - privateKeyPem := pemEncode(privKey) - 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 - } + 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 + } + } - // 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 { + return certRes, nil +} - cerRes.CertStableURL = resp.Header.Get("Content-Location") - cerRes.AccountRef = c.user.GetRegistration().URI +// 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 + } - issuedCert := pemEncode(derCertificateBytes(cert)) - // If bundle is true, we want to return a certificate bundle. - // To do this, we need the issuer certificate. - if bundle { - // 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 { - // Success - append the issuer cert to the issued cert. - issuerCert = pemEncode(derCertificateBytes(issuerCert)) - issuedCert = append(issuedCert, issuerCert...) - } - } + // 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 - cerRes.Certificate = issuedCert - logf("[INFO][%s] Server responded with a certificate.", commonName.Domain) - return cerRes, nil - } + issuedCert := pemEncode(derCertificateBytes(cert)) - // 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) + // 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 { - return CertificateResource{}, err - } + // 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)) - logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter) - time.Sleep(time.Duration(retryAfter) * time.Second) + // 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...) + } + } - break - default: - return CertificateResource{}, handleHTTPError(resp) + certRes.Certificate = issuedCert + certRes.IssuerCertificate = issuerCert + logf("[INFO][%s] Server responded with a certificate.", certRes.Domain) + return true, nil } - resp, err = httpGet(cerRes.CertURL) + // 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 + 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) } } -// getIssuerCertificate requests the issuer certificate and caches it for -// subsequent requests. +// getIssuerCertificate requests the issuer certificate func (c *Client) getIssuerCertificate(url string) ([]byte, error) { logf("[INFO] acme: Requesting issuer cert from %s", url) - if c.issuerCert != nil { - return c.issuerCert, nil - } - resp, err := httpGet(url) if err != nil { return nil, err } 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 } @@ -574,7 +762,6 @@ func (c *Client) getIssuerCertificate(url string) ([]byte, error) { return nil, err } - c.issuerCert = issuerBytes return issuerBytes, err } |