From 76cb7789ce7fbca5394be05580059392e0efe00d Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sun, 27 May 2018 23:55:19 +0200 Subject: [PATCH] feat: replace v1 by v2. --- account.go | 2 +- acme/challenges.go | 3 - acme/client.go | 615 +++++++++--------- acme/client_test.go | 105 +++- acme/crypto.go | 14 +- acme/dns_challenge.go | 6 +- acme/dns_challenge_test.go | 4 +- acme/error.go | 31 +- acme/http.go | 2 +- acme/http_challenge.go | 2 +- acme/http_challenge_test.go | 4 +- acme/jws.go | 82 ++- acme/messages.go | 123 ++-- acme/tls_sni_challenge.go | 67 -- acme/tls_sni_challenge_server.go | 62 -- acme/tls_sni_challenge_test.go | 65 -- acmev2/challenges.go | 13 - acmev2/client.go | 867 -------------------------- acmev2/client_test.go | 330 ---------- acmev2/crypto.go | 342 ---------- acmev2/crypto_test.go | 93 --- acmev2/dns_challenge.go | 309 --------- acmev2/dns_challenge_manual.go | 53 -- acmev2/dns_challenge_test.go | 200 ------ acmev2/error.go | 91 --- acmev2/http.go | 160 ----- acmev2/http_challenge.go | 41 -- acmev2/http_challenge_server.go | 79 --- acmev2/http_challenge_test.go | 57 -- acmev2/http_test.go | 100 --- acmev2/jws.go | 167 ----- acmev2/messages.go | 106 ---- acmev2/pop_challenge.go | 1 - acmev2/provider.go | 28 - acmev2/testdata/resolv.conf.1 | 5 - acmev2/utils.go | 29 - acmev2/utils_test.go | 26 - cli.go | 2 +- cli_handlers.go | 3 +- configuration.go | 2 +- providers/dns/gandi/gandi_test.go | 58 -- providers/dns/gandiv5/gandiv5_test.go | 58 -- 42 files changed, 559 insertions(+), 3848 deletions(-) delete mode 100644 acme/tls_sni_challenge.go delete mode 100644 acme/tls_sni_challenge_server.go delete mode 100644 acme/tls_sni_challenge_test.go delete mode 100644 acmev2/challenges.go delete mode 100644 acmev2/client.go delete mode 100644 acmev2/client_test.go delete mode 100644 acmev2/crypto.go delete mode 100644 acmev2/crypto_test.go delete mode 100644 acmev2/dns_challenge.go delete mode 100644 acmev2/dns_challenge_manual.go delete mode 100644 acmev2/dns_challenge_test.go delete mode 100644 acmev2/error.go delete mode 100644 acmev2/http.go delete mode 100644 acmev2/http_challenge.go delete mode 100644 acmev2/http_challenge_server.go delete mode 100644 acmev2/http_challenge_test.go delete mode 100644 acmev2/http_test.go delete mode 100644 acmev2/jws.go delete mode 100644 acmev2/messages.go delete mode 100644 acmev2/pop_challenge.go delete mode 100644 acmev2/provider.go delete mode 100644 acmev2/testdata/resolv.conf.1 delete mode 100644 acmev2/utils.go delete mode 100644 acmev2/utils_test.go diff --git a/account.go b/account.go index 2c7dcf93..31dc0253 100644 --- a/account.go +++ b/account.go @@ -7,7 +7,7 @@ import ( "os" "path" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // Account represents a users local saved credentials diff --git a/acme/challenges.go b/acme/challenges.go index 85790050..cf7bd7f7 100644 --- a/acme/challenges.go +++ b/acme/challenges.go @@ -7,9 +7,6 @@ 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") diff --git a/acme/client.go b/acme/client.go index bcb84437..001f8cc0 100644 --- a/acme/client.go +++ b/acme/client.go @@ -5,13 +5,11 @@ import ( "crypto" "crypto/x509" "encoding/base64" - "encoding/json" "errors" "fmt" "io/ioutil" "log" "net" - "net/http" "regexp" "strconv" "strings" @@ -82,27 +80,26 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) } - if dir.NewRegURL == "" { + if dir.NewAccountURL == "" { return nil, errors.New("directory missing new registration URL") } - if dir.NewAuthzURL == "" { - return nil, errors.New("directory missing new authz URL") + if dir.NewOrderURL == "" { + return nil, errors.New("directory missing new order URL") } - if dir.NewCertURL == "" { - return nil, errors.New("directory missing new certificate URL") - } - if dir.RevokeCertURL == "" { + /*if dir.RevokeCertURL == "" { return nil, errors.New("directory missing revoke certificate URL") - } + }*/ - jws := &jws{privKey: privKey, directoryURL: caDirURL} + jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL} + if reg := user.GetRegistration(); reg != nil { + jws.kid = reg.URI + } // 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[Challenge]solver) solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} - solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}} return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil } @@ -112,8 +109,6 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) 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: @@ -141,24 +136,6 @@ func (c *Client) SetHTTPAddress(iface string) error { return nil } -// 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 { - return err - } - - if chlng, ok := c.solvers[TLSSNI01]; ok { - chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port) - } - return nil -} - // ExcludeChallenges explicitly removes challenges from the pool for solving. func (c *Client) ExcludeChallenges(challenges []Challenge) { // Loop through all challenges and delete the requested one if found. @@ -167,61 +144,124 @@ func (c *Client) ExcludeChallenges(challenges []Challenge) { } } +// GetToSURL returns the current ToS URL from the Directory +func (c *Client) GetToSURL() string { + return c.directory.Meta.TermsOfService +} + +// GetExternalAccountRequired returns the External Account Binding requirement of the Directory +func (c *Client) GetExternalAccountRequired() bool { + return c.directory.Meta.ExternalAccountRequired +} + // Register the current account to the ACME server. -func (c *Client) Register() (*RegistrationResource, error) { +func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { if c == nil || c.user == nil { return nil, errors.New("acme: cannot register a nil client or user") } logf("[INFO] acme: Registering account for %s", c.user.GetEmail()) - regMsg := registrationMessage{ - Resource: "new-reg", - } + accMsg := accountMessage{} if c.user.GetEmail() != "" { - regMsg.Contact = []string{"mailto:" + c.user.GetEmail()} + accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} } else { - regMsg.Contact = []string{} + accMsg.Contact = []string{} } + accMsg.TermsOfServiceAgreed = tosAgreed - var serverReg Registration - var regURI string - hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg) + var serverReg accountMessage + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) if err != nil { 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"]) - - if regURI == "" { - regURI = hdr.Get("Location") - } - reg.URI = regURI - if links["terms-of-service"] != "" { - reg.TosURL = links["terms-of-service"] - } - - if links["next"] != "" { - reg.NewAuthzURL = links["next"] - } else { - return nil, errors.New("acme: The server did not return 'next' link to proceed") + reg := &RegistrationResource{ + URI: hdr.Get("Location"), + Body: serverReg, } + c.jws.kid = reg.URI return reg, nil } +// Register the current account to the ACME server. +func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) { + if c == nil || c.user == nil { + return nil, errors.New("acme: cannot register a nil client or user") + } + logf("[INFO] acme: Registering account (EAB) for %s", c.user.GetEmail()) + + accMsg := accountMessage{} + if c.user.GetEmail() != "" { + accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} + } else { + accMsg.Contact = []string{} + } + accMsg.TermsOfServiceAgreed = tosAgreed + + hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) + if err != nil { + return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error()) + } + + eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac) + if err != nil { + return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error()) + } + + eabPayload := eabJWS.FullSerialize() + + accMsg.ExternalAccountBinding = []byte(eabPayload) + + var serverReg accountMessage + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) + if err != nil { + remoteErr, ok := err.(RemoteError) + if ok && remoteErr.StatusCode == 409 { + } else { + return nil, err + } + } + + reg := &RegistrationResource{ + URI: hdr.Get("Location"), + Body: serverReg, + } + c.jws.kid = reg.URI + + return reg, nil +} + +// ResolveAccountByKey will attempt to look up an account using the given account key +// and return its registration resource. +func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { + logf("[INFO] acme: Trying to resolve account by key") + + acc := accountMessage{OnlyReturnExisting: true} + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil) + if err != nil { + return nil, err + } + + accountLink := hdr.Get("Location") + if accountLink == "" { + return nil, errors.New("Server did not return the account link") + } + + var retAccount accountMessage + c.jws.kid = accountLink + hdr, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount) + if err != nil { + return nil, err + } + + return &RegistrationResource{URI: accountLink, Body: retAccount}, nil +} + // DeleteRegistration deletes the client's user registration from the ACME // server. func (c *Client) DeleteRegistration() error { @@ -230,12 +270,11 @@ func (c *Client) DeleteRegistration() error { } logf("[INFO] acme: Deleting account for %s", c.user.GetEmail()) - regMsg := registrationMessage{ - Resource: "reg", - Delete: true, + accMsg := accountMessage{ + Status: "deactivated", } - _, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil) + _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil) if err != nil { return err } @@ -255,46 +294,23 @@ func (c *Client) QueryRegistration() (*RegistrationResource, error) { // 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", - } + accMsg := accountMessage{} - var serverReg Registration - hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg) + var serverReg accountMessage + _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &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 { - reg := c.user.GetRegistration() - - reg.Body.Agreement = c.user.GetRegistration().TosURL - reg.Body.Resource = "reg" - _, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil) - 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. @@ -302,7 +318,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) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) { +func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (*CertificateResource, error) { // figure out what domains it concerns // start with the common name domains := []string{csr.Subject.CommonName} @@ -327,35 +343,44 @@ DNSNames: 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 { + order, err := c.createOrderForIdentifiers(domains) + if err != nil { + return nil, err + } + authz, err := c.getAuthzForOrder(order) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + /*for _, auth := range authz { c.disableAuthz(auth) - } - - return CertificateResource{}, failures + }*/ + return nil, err } - errs := c.solveChallenges(challenges) - // If any challenge fails - return. Do not generate partial SAN certificates. - if len(errs) > 0 { - return CertificateResource{}, errs + err = c.solveChallengeForAuthz(authz) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + return nil, err } logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil) + failures := make(ObtainError) + cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil) if err != nil { - for _, chln := range challenges { - failures[chln.Domain] = err + for _, chln := range authz { + failures[chln.Identifier.Value] = err } } // Add the CSR to the certificate so that it can be used for renewals. cert.CSR = pemEncode(&csr) - return cert, failures + // do not return an empty failures map, because + // it would still be a non-nil error value + if len(failures) > 0 { + return cert, failures + } + return cert, nil } // ObtainCertificate tries to obtain a single certificate using all domains passed into it. @@ -367,39 +392,52 @@ DNSNames: // 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, mustStaple bool) (CertificateResource, map[string]error) { +func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { + if len(domains) == 0 { + return nil, errors.New("No domains to obtain a certificate for") + } + if bundle { logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { logf("[INFO][%s] acme: Obtaining SAN certificate", 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 { + order, err := c.createOrderForIdentifiers(domains) + if err != nil { + return nil, err + } + authz, err := c.getAuthzForOrder(order) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + /*for _, auth := range authz { c.disableAuthz(auth) - } - - return CertificateResource{}, failures + }*/ + return nil, err } - errs := c.solveChallenges(challenges) - // If any challenge fails - return. Do not generate partial SAN certificates. - if len(errs) > 0 { - return CertificateResource{}, errs + err = c.solveChallengeForAuthz(authz) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + return nil, err } logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - cert, err := c.requestCertificate(challenges, bundle, privKey, mustStaple) + failures := make(ObtainError) + cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple) if err != nil { - for _, chln := range challenges { - failures[chln.Domain] = err + for _, auth := range authz { + failures[auth.Identifier.Value] = err } } - return cert, failures + // do not return an empty failures map, because + // it would still be a non-nil error value + if len(failures) > 0 { + return cert, failures + } + return cert, nil } // RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. @@ -416,7 +454,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error { encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw) - _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil) + _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil) return err } @@ -428,17 +466,17 @@ 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, mustStaple 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) if err != nil { - return CertificateResource{}, err + return nil, err } x509Cert := certificates[0] if x509Cert.IsCA { - return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) + return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) } // This is just meant to be informal for the user. @@ -451,22 +489,21 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b if len(cert.CSR) > 0 { csr, err := pemDecodeTox509CSR(cert.CSR) if err != nil { - return CertificateResource{}, err + return nil, err } newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) - return newCert, failures[cert.Domain] + return newCert, failures } var privKey crypto.PrivateKey if cert.PrivateKey != nil { privKey, err = parsePEMPrivateKey(cert.PrivateKey) if err != nil { - return CertificateResource{}, err + return nil, err } } var domains []string - var failures map[string]error // check for SAN certificate if len(x509Cert.DNSNames) > 1 { domains = append(domains, x509Cert.Subject.CommonName) @@ -480,233 +517,242 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b domains = append(domains, x509Cert.Subject.CommonName) } - newCert, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple) - return newCert, failures[cert.Domain] + newCert, err := c.ObtainCertificate(domains, bundle, privKey, mustStaple) + return newCert, err +} + +func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) { + + var identifiers []identifier + for _, domain := range domains { + identifiers = append(identifiers, identifier{Type: "dns", Value: domain}) + } + + order := orderMessage{ + Identifiers: identifiers, + } + + var response orderMessage + hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response) + if err != nil { + return orderResource{}, err + } + + orderRes := orderResource{ + URL: hdr.Get("Location"), + Domains: domains, + orderMessage: response, + } + return orderRes, nil } // Looks through the challenge combinations to find a solvable match. // Then solves the challenges in series and returns. -func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error { +func (c *Client) solveChallengeForAuthz(authorizations []authorization) error { + failures := make(ObtainError) + // loop through the resources, basically through the domains. - failures := make(map[string]error) - for _, authz := range challenges { - if authz.Body.Status == "valid" { + for _, authz := range authorizations { + if authz.Status == "valid" { // Boulder might recycle recent validated authz (see issue #267) - logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain) + logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) 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 - } + if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil { + err := solver.Solve(authz.Challenges[i], authz.Identifier.Value) + if err != nil { + //c.disableAuthz(authz.Identifier) + failures[authz.Identifier.Value] = err } } else { - c.disableAuthz(authz) - failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain) + //c.disableAuthz(authz) + failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value) } } - return failures -} - -// Checks all combinations from the server and returns an array of -// solvers which should get executed in series. -func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver { - for _, combination := range auth.Combinations { - solvers := make(map[int]solver) - for _, idx := range combination { - if solver, ok := c.solvers[auth.Challenges[idx].Type]; ok { - solvers[idx] = solver - } else { - logf("[INFO][%s] acme: Could not find solver for: %s", domain, auth.Challenges[idx].Type) - } - } - - // If we can solve the whole combination, return the solvers - if len(solvers) == len(combination) { - return solvers - } + // be careful not to return an empty failures map, for + // even an empty ObtainError is a non-nil error value + if len(failures) > 0 { + return failures } return nil } +// Checks all challenges from the server in order and returns the first matching solver. +func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) { + for i, challenge := range auth.Challenges { + if solver, ok := c.solvers[Challenge(challenge.Type)]; ok { + return i, solver + } + logf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type) + } + return 0, nil +} + // Get the challenges needed to proof our identifier to the ACME server. -func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) { - resc, errc := make(chan authorizationResource), make(chan domainError) +func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) { + resc, errc := make(chan authorization), make(chan domainError) delay := time.Second / overallRequestLimit - for _, domain := range domains { + for _, authzURL := range order.Authorizations { time.Sleep(delay) - go func(domain string) { - authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}} + go func(authzURL string) { var authz authorization - hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz) + _, err := getJSON(authzURL, &authz) if err != nil { - errc <- domainError{Domain: domain, Error: err} + errc <- domainError{Domain: authz.Identifier.Value, Error: err} return } - 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 - } - - resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain} - }(domain) + resc <- authz + }(authzURL) } - responses := make(map[string]authorizationResource) - failures := make(map[string]error) - for i := 0; i < len(domains); i++ { + var responses []authorization + failures := make(ObtainError) + for i := 0; i < len(order.Authorizations); i++ { select { case res := <-resc: - responses[res.Domain] = res + responses = append(responses, res) case err := <-errc: failures[err.Domain] = err.Error } } - challenges := make([]authorizationResource, 0, len(responses)) - for _, domain := range domains { - if challenge, ok := responses[domain]; ok { - challenges = append(challenges, challenge) - } - } - - logAuthz(challenges) + logAuthz(order) close(resc) close(errc) - return challenges, failures + // be careful to not return an empty failures map; + // even if empty, they become non-nil error values + if len(failures) > 0 { + return responses, failures + } + return responses, nil } -func logAuthz(authz []authorizationResource) { - for _, auth := range authz { - logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL) +func logAuthz(order orderResource) { + for i, auth := range order.Authorizations { + logf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth) } } // cleanAuthz loops through the passed in slice and disables any auths which are not "valid" -func (c *Client) disableAuthz(auth authorizationResource) error { +func (c *Client) disableAuthz(authURL string) error { var disabledAuth authorization - _, err := postJSON(c.jws, auth.AuthURL, deactivateAuthMessage{Resource: "authz", Status: "deactivated"}, &disabledAuth) + _, err := postJSON(c.jws, authURL, deactivateAuthMessage{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!") - } +func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { var err error if privKey == nil { privKey, err = generatePrivateKey(c.keyType) if err != nil { - return CertificateResource{}, err + return nil, err } } // determine certificate name(s) based on the authorization resources - commonName := authz[0] + commonName := order.Domains[0] var san []string - for _, auth := range authz[1:] { - san = append(san, auth.Domain) + for _, auth := range order.Identifiers { + san = append(san, auth.Value) } // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey, commonName.Domain, san, mustStaple) + csr, err := generateCsr(privKey, commonName, san, mustStaple) if err != nil { - return CertificateResource{}, err + return nil, err } - return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey)) + return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey)) } -func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) { - commonName := authz[0] +func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) { + commonName := order.Domains[0] - var authURLs []string - for _, auth := range authz[1:] { - authURLs = append(authURLs, auth.AuthURL) + csrString := base64.RawURLEncoding.EncodeToString(csr) + var retOrder orderMessage + _, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder) + if error != nil { + return nil, error } - csrString := base64.URLEncoding.EncodeToString(csr) - jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs}) - if err != nil { - return CertificateResource{}, err - } - - resp, err := c.jws.post(commonName.NewCertURL, jsonBytes) - if err != nil { - return CertificateResource{}, err + if retOrder.Status == "invalid" { + return nil, error } certRes := CertificateResource{ - Domain: commonName.Domain, - CertURL: resp.Header.Get("Location"), + Domain: commonName, + CertURL: retOrder.Certificate, PrivateKey: privateKeyPem, } + if retOrder.Status == "valid" { + // if the certificate is available right away, short cut! + ok, err := c.checkCertResponse(retOrder, &certRes, bundle) + if err != nil { + return nil, err + } + + if ok { + return &certRes, nil + } + } + maxChecks := 1000 for i := 0; i < maxChecks; i++ { - done, err := c.checkCertResponse(resp, &certRes, bundle) - resp.Body.Close() + _, err := getJSON(order.URL, &retOrder) if err != nil { - return CertificateResource{}, err + return nil, err + } + done, err := c.checkCertResponse(retOrder, &certRes, bundle) + if err != nil { + return nil, 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 nil, fmt.Errorf("polled for certificate %d times; giving up", i) } } - return certRes, nil + 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 +// checkCertResponse checks to see if the certificate is ready and a link is contained in the +// response. if so, loads it into certRes and returns true. If the cert +// is not yet ready, it returns false. 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: +func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) { + + switch order.Status { + case "valid": + resp, err := httpGet(order.Certificate) + if err != nil { + return false, err + } + 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 + // The issuer certificate link is always supplied via an "up" link + // in the response headers of a new certificate. + links := parseLinks(resp.Header["Link"]) + if link, ok := links["up"]; ok { + issuerCert, err := c.getIssuerCertificate(link) - 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) @@ -716,31 +762,26 @@ func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateReso // 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...) + cert = append(cert, issuerCert...) } + + certRes.IssuerCertificate = 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) + certRes.Certificate = cert + certRes.CertURL = order.Certificate + certRes.CertStableURL = order.Certificate + logf("[INFO][%s] Server responded with a certificate.", certRes.Domain) + return true, nil + case "processing": return false, nil - default: - return false, handleHTTPError(resp) + case "invalid": + return false, errors.New("Order has invalid state: invalid") } + + return false, nil } // getIssuerCertificate requests the issuer certificate @@ -786,10 +827,10 @@ func parseLinks(links []string) map[string]string { // validate makes the ACME server start validating a // challenge response, only returning once it is done. -func validate(j *jws, domain, uri string, chlng challenge) error { - var challengeResponse challenge +func validate(j *jws, domain, uri string, c challenge) error { + var chlng challenge - hdr, err := postJSON(j, uri, chlng, &challengeResponse) + hdr, err := postJSON(j, uri, c, &chlng) if err != nil { return err } @@ -797,27 +838,27 @@ func validate(j *jws, domain, uri string, chlng challenge) error { // After the path is sent, the ACME server will access our server. // Repeatedly check the server for an updated status on our request. for { - switch challengeResponse.Status { + switch chlng.Status { case "valid": logf("[INFO][%s] The server validated our request", domain) return nil case "pending": break case "invalid": - return handleChallengeError(challengeResponse) + return handleChallengeError(chlng) default: - return errors.New("The server returned an unexpected state.") + return errors.New("The server returned an unexpected state") } ra, err := strconv.Atoi(hdr.Get("Retry-After")) if err != nil { // The ACME server MUST return a Retry-After. // If it doesn't, we'll just poll hard. - ra = 1 + ra = 5 } time.Sleep(time.Duration(ra) * time.Second) - hdr, err = getJSON(uri, &challengeResponse) + hdr, err = getJSON(uri, &chlng) if err != nil { return err } diff --git a/acme/client_test.go b/acme/client_test.go index b18334c8..d30dd3ba 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -27,7 +27,13 @@ func TestNewClient(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"}) + data, _ := json.Marshal(directory{ + NewNonceURL: "http://test", + NewAccountURL: "http://test", + NewOrderURL: "http://test", + RevokeCertURL: "http://test", + KeyChangeURL: "http://test", + }) w.Write(data) })) @@ -47,7 +53,7 @@ func TestNewClient(t *testing.T) { t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType) } - if expected, actual := 2, len(client.solvers); actual != expected { + if expected, actual := 1, len(client.solvers); actual != expected { t.Fatalf("Expected %d solver(s), got %d", expected, actual) } } @@ -65,7 +71,13 @@ func TestClientOptPort(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"}) + data, _ := json.Marshal(directory{ + NewNonceURL: "http://test", + NewAccountURL: "http://test", + NewOrderURL: "http://test", + RevokeCertURL: "http://test", + KeyChangeURL: "http://test", + }) w.Write(data) })) @@ -76,7 +88,6 @@ func TestClientOptPort(t *testing.T) { t.Fatalf("Could not create client: %v", err) } client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - client.SetTLSAddress(net.JoinHostPort(optHost, optPort)) httpSolver, ok := client.solvers[HTTP01].(*httpChallenge) if !ok { @@ -92,7 +103,7 @@ func TestClientOptPort(t *testing.T) { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } - httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge) + /* httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge) if !ok { t.Fatal("Expected tls-sni-01 solver to be httpChallenge type") } @@ -104,19 +115,15 @@ func TestClientOptPort(t *testing.T) { } if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost { t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got) - } + } */ // test setting different host optHost = "127.0.0.1" client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - client.SetTLSAddress(net.JoinHostPort(optHost, optPort)) if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } - if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { - t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) - } } func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { @@ -124,12 +131,12 @@ func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { time.Sleep(250 * time.Millisecond) w.Header().Add("Replay-Nonce", "12345") w.Header().Add("Retry-After", "0") - writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URI: "http://example.com/", Token: "token"}) + writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) })) defer ts.Close() privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey, directoryURL: ts.URL} + j := &jws{privKey: privKey, getNonceURL: ts.URL} ch := make(chan bool) resultCh := make(chan bool) go func() { @@ -163,12 +170,12 @@ func TestValidate(t *testing.T) { case "POST": st := statuses[0] statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"}) + writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) case "GET": st := statuses[0] statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"}) + writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) default: http.Error(w, r.Method, http.StatusMethodNotAllowed) @@ -177,7 +184,7 @@ func TestValidate(t *testing.T) { defer ts.Close() privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey, directoryURL: ts.URL} + j := &jws{privKey: privKey, getNonceURL: ts.URL} tsts := []struct { name string @@ -186,10 +193,10 @@ func TestValidate(t *testing.T) { }{ {"POST-unexpected", []string{"weird"}, "unexpected"}, {"POST-valid", []string{"valid"}, ""}, - {"POST-invalid", []string{"invalid"}, "Error Detail"}, + {"POST-invalid", []string{"invalid"}, "Error"}, {"GET-unexpected", []string{"pending", "weird"}, "unexpected"}, {"GET-valid", []string{"pending", "valid"}, ""}, - {"GET-invalid", []string{"pending", "invalid"}, "Error Detail"}, + {"GET-invalid", []string{"pending", "invalid"}, "Error"}, } for _, tst := range tsts { @@ -209,9 +216,15 @@ func TestGetChallenges(t *testing.T) { case "GET", "HEAD": w.Header().Add("Replay-Nonce", "12345") w.Header().Add("Retry-After", "0") - writeJSONResponse(w, directory{NewAuthzURL: ts.URL, NewCertURL: ts.URL, NewRegURL: ts.URL, RevokeCertURL: ts.URL}) + writeJSONResponse(w, directory{ + NewNonceURL: ts.URL, + NewAccountURL: ts.URL, + NewOrderURL: ts.URL, + RevokeCertURL: ts.URL, + KeyChangeURL: ts.URL, + }) case "POST": - writeJSONResponse(w, authorization{}) + writeJSONResponse(w, orderMessage{}) } })) defer ts.Close() @@ -224,7 +237,7 @@ func TestGetChallenges(t *testing.T) { } user := mockUser{ email: "test@test.com", - regres: &RegistrationResource{NewAuthzURL: ts.URL}, + regres: &RegistrationResource{URI: ts.URL}, privatekey: key, } @@ -233,12 +246,60 @@ func TestGetChallenges(t *testing.T) { t.Fatalf("Could not create client: %v", err) } - _, failures := client.getChallenges([]string{"example.com"}) - if failures["example.com"] == nil { + _, err = client.createOrderForIdentifiers([]string{"example.com"}) + if err != nil { t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil") } } +func TestResolveAccountByKey(t *testing.T) { + keyBits := 512 + keyType := RSA2048 + key, err := rsa.GenerateKey(rand.Reader, keyBits) + if err != nil { + t.Fatal("Could not generate test key:", err) + } + user := mockUser{ + email: "test@test.com", + regres: new(RegistrationResource), + privatekey: key, + } + + var ts *httptest.Server + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/directory": + writeJSONResponse(w, directory{ + NewNonceURL: ts.URL + "/nonce", + NewAccountURL: ts.URL + "/account", + NewOrderURL: ts.URL + "/newOrder", + RevokeCertURL: ts.URL + "/revokeCert", + KeyChangeURL: ts.URL + "/keyChange", + }) + case "/nonce": + w.Header().Add("Replay-Nonce", "12345") + w.Header().Add("Retry-After", "0") + case "/account": + w.Header().Set("Location", ts.URL+"/account_recovery") + case "/account_recovery": + writeJSONResponse(w, accountMessage{ + Status: "valid", + }) + } + })) + + client, err := NewClient(ts.URL+"/directory", user, keyType) + if err != nil { + t.Fatalf("Could not create client: %v", err) + } + + if res, err := client.ResolveAccountByKey(); err != nil { + t.Fatalf("Unexpected error resolving account by key: %v", err) + } else if res.Body.Status != "valid" { + t.Errorf("Unexpected account status: %v", res.Body.Status) + } +} + // writeJSONResponse marshals the body as JSON and writes it to the response. func writeJSONResponse(w http.ResponseWriter, body interface{}) { bs, err := json.Marshal(body) diff --git a/acme/crypto.go b/acme/crypto.go index 74d0c6d2..7adc34d4 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -18,10 +18,10 @@ import ( "io/ioutil" "math/big" "net/http" - "strings" "time" "golang.org/x/crypto/ocsp" + jose "gopkg.in/square/go-jose.v2" ) // KeyType represents the key algo as well as the key size or curve to use. @@ -135,9 +135,9 @@ func getKeyAuthorization(token string, key interface{}) (string, error) { } // Generate the Key Authorization for the challenge - jwk := keyAsJWK(publicKey) + jwk := &jose.JSONWebKey{Key: publicKey} if jwk == nil { - return "", errors.New("Could not generate JWK from key.") + return "", errors.New("Could not generate JWK from key") } thumbBytes, err := jwk.Thumbprint(crypto.SHA256) if err != nil { @@ -145,11 +145,7 @@ func getKeyAuthorization(token string, key interface{}) (string, error) { } // unpad the base64URL - keyThumb := base64.URLEncoding.EncodeToString(thumbBytes) - index := strings.Index(keyThumb, "=") - if index != -1 { - keyThumb = keyThumb[:index] - } + keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes) return token + "." + keyThumb, nil } @@ -176,7 +172,7 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { } if len(certificates) == 0 { - return nil, errors.New("No certificates were found while parsing the bundle.") + return nil, errors.New("No certificates were found while parsing the bundle") } return certificates, nil diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index d6844dcd..d129dacc 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -29,6 +29,7 @@ var defaultNameservers = []string{ "google-public-dns-b.google.com:53", } +// RecursiveNameservers are used to pre-check DNS propagations var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) // DNSTimeout is used to override the default DNS timeout of 10 seconds. @@ -57,8 +58,7 @@ func getNameservers(path string, defaults []string) []string { 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, "=") + value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) ttl = 120 fqdn = fmt.Sprintf("_acme-challenge.%s.", domain) return @@ -114,7 +114,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { return err } - return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "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}) } // checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 117ac303..e561739b 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -100,9 +100,9 @@ func TestDNSValidServerResponse(t *testing.T) { })) manualProvider, _ := NewDNSProviderManual() - jws := &jws{privKey: privKey, directoryURL: ts.URL} + jws := &jws{privKey: privKey, getNonceURL: ts.URL} solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider} - clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"} + clientChallenge := challenge{Type: "dns01", Status: "pending", URL: ts.URL, Token: "http8"} go func() { time.Sleep(time.Second * 2) diff --git a/acme/error.go b/acme/error.go index e4bc934c..78694deb 100644 --- a/acme/error.go +++ b/acme/error.go @@ -1,6 +1,7 @@ package acme import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -9,8 +10,8 @@ import ( ) const ( - tosAgreementError = "Must agree to subscriber agreement before any further actions" - invalidNonceError = "JWS has invalid anti-replay nonce" + tosAgreementError = "Terms of service have changed" + invalidNonceError = "urn:ietf:params:acme:error:badNonce" ) // RemoteError is the base type for all errors specific to the ACME protocol. @@ -42,27 +43,23 @@ type domainError struct { Error error } -type challengeError struct { - RemoteError - records []validationRecord -} +// ObtainError is returned when there are specific errors available +// per domain. For example in ObtainCertificate +type ObtainError map[string]error -func (c challengeError) Error() string { - - var errStr string - for _, validation := range c.records { - errStr = errStr + fmt.Sprintf("\tValidation for %s:%s\n\tResolved to:\n\t\t%s\n\tUsed: %s\n\n", - validation.Hostname, validation.Port, strings.Join(validation.ResolvedAddresses, "\n\t\t"), validation.UsedAddress) +func (e ObtainError) Error() string { + buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n") + for dom, err := range e { + buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err)) } - - return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr) + return buffer.String() } func handleHTTPError(resp *http.Response) error { var errorDetail RemoteError contentType := resp.Header.Get("Content-Type") - if contentType == "application/json" || contentType == "application/problem+json" { + if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") { err := json.NewDecoder(resp.Body).Decode(&errorDetail) if err != nil { return err @@ -82,7 +79,7 @@ func handleHTTPError(resp *http.Response) error { return TOSError{errorDetail} } - if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) { + if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError { return NonceError{errorDetail} } @@ -90,5 +87,5 @@ func handleHTTPError(resp *http.Response) error { } func handleChallengeError(chlng challenge) error { - return challengeError{chlng.Error, chlng.ValidationRecords} + return chlng.Error } diff --git a/acme/http.go b/acme/http.go index e469e0de..b93e5344 100644 --- a/acme/http.go +++ b/acme/http.go @@ -102,7 +102,7 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) { func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) { jsonBytes, err := json.Marshal(reqBody) 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) diff --git a/acme/http_challenge.go b/acme/http_challenge.go index 95cb1fd8..b6c969fe 100644 --- a/acme/http_challenge.go +++ b/acme/http_challenge.go @@ -37,5 +37,5 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error { } }() - return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "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}) } diff --git a/acme/http_challenge_test.go b/acme/http_challenge_test.go index 7400f56d..10f92028 100644 --- a/acme/http_challenge_test.go +++ b/acme/http_challenge_test.go @@ -11,7 +11,7 @@ import ( func TestHTTPChallenge(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 512) j := &jws{privKey: privKey} - clientChallenge := challenge{Type: HTTP01, Token: "http1"} + clientChallenge := challenge{Type: string(HTTP01), Token: "http1"} mockValidate := func(_ *jws, _, _ string, chlng challenge) error { uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token resp, err := httpGet(uri) @@ -46,7 +46,7 @@ func TestHTTPChallenge(t *testing.T) { func TestHTTPChallengeInvalidPort(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 128) j := &jws{privKey: privKey} - clientChallenge := challenge{Type: HTTP01, Token: "http2"} + clientChallenge := challenge{Type: string(HTTP01), Token: "http2"} solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}} if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { diff --git a/acme/jws.go b/acme/jws.go index a3943434..8c06baa5 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -10,37 +10,27 @@ import ( "net/http" "sync" - "gopkg.in/square/go-jose.v1" + "gopkg.in/square/go-jose.v2" ) type jws struct { - directoryURL string - privKey crypto.PrivateKey - nonces nonceManager -} - -func keyAsJWK(key interface{}) *jose.JsonWebKey { - switch k := key.(type) { - case *ecdsa.PublicKey: - return &jose.JsonWebKey{Key: k, Algorithm: "EC"} - case *rsa.PublicKey: - return &jose.JsonWebKey{Key: k, Algorithm: "RSA"} - - default: - return nil - } + getNonceURL string + privKey crypto.PrivateKey + kid string + nonces nonceManager } // 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) + signedContent, err := j.signContent(url, content) if err != nil { return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) } - resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize()))) + data := bytes.NewBuffer([]byte(signedContent.FullSerialize())) + resp, err := httpPost(url, "application/jose+json", data) if err != nil { return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error()) } @@ -53,7 +43,7 @@ func (j *jws) post(url string, content []byte) (*http.Response, error) { return resp, nil } -func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { +func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) { var alg jose.SignatureAlgorithm switch k := j.privKey.(type) { @@ -67,11 +57,28 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { } } - signer, err := jose.NewSigner(alg, j.privKey) + jsonKey := jose.JSONWebKey{ + Key: j.privKey, + KeyID: j.kid, + } + + signKey := jose.SigningKey{ + Algorithm: alg, + Key: jsonKey, + } + options := jose.SignerOptions{ + NonceSource: j, + ExtraHeaders: make(map[jose.HeaderKey]interface{}), + } + options.ExtraHeaders["url"] = url + if j.kid == "" { + options.EmbedJWK = true + } + + signer, err := jose.NewSigner(signKey, &options) if err != nil { return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error()) } - signer.SetNonceSource(j) signed, err := signer.Sign(content) if err != nil { @@ -80,12 +87,41 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { return signed, nil } +func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { + jwk := jose.JSONWebKey{Key: j.privKey} + jwkJSON, err := jwk.Public().MarshalJSON() + if err != nil { + return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error()) + } + + signer, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, + &jose.SignerOptions{ + EmbedJWK: false, + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": kid, + "url": url, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("Failed to create External Account Binding jose signer -> %s", err.Error()) + } + + signed, err := signer.Sign(jwkJSON) + if err != nil { + return nil, fmt.Errorf("Failed to External Account Binding sign content -> %s", err.Error()) + } + + return signed, nil +} + func (j *jws) Nonce() (string, error) { if nonce, ok := j.nonces.Pop(); ok { return nonce, nil } - return getNonce(j.directoryURL) + return getNonce(j.getNonceURL) } type nonceManager struct { @@ -124,7 +160,7 @@ func getNonce(url string) (string, error) { 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") } return nonce, nil diff --git a/acme/messages.go b/acme/messages.go index 79ccf154..b5c60a24 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -1,59 +1,62 @@ package acme import ( + "encoding/json" "time" - - "gopkg.in/square/go-jose.v1" ) -type directory struct { - NewAuthzURL string `json:"new-authz"` - NewCertURL string `json:"new-cert"` - NewRegURL string `json:"new-reg"` - RevokeCertURL string `json:"revoke-cert"` -} - -type registrationMessage struct { - Resource string `json:"resource"` - Contact []string `json:"contact"` - Delete bool `json:"delete,omitempty"` -} - -// Registration is returned by the ACME server after the registration -// The client implementation should save this registration somewhere. -type Registration struct { - Resource string `json:"resource,omitempty"` - ID int `json:"id"` - Key jose.JsonWebKey `json:"key"` - Contact []string `json:"contact"` - Agreement string `json:"agreement,omitempty"` - Authorizations string `json:"authorizations,omitempty"` - Certificates string `json:"certificates,omitempty"` -} - // RegistrationResource represents all important informations about a registration // of which the client needs to keep track itself. type RegistrationResource struct { - Body Registration `json:"body,omitempty"` - URI string `json:"uri,omitempty"` - NewAuthzURL string `json:"new_authzr_uri,omitempty"` - TosURL string `json:"terms_of_service,omitempty"` + Body accountMessage `json:"body,omitempty"` + URI string `json:"uri,omitempty"` } -type authorizationResource struct { - Body authorization - Domain string - NewCertURL string - AuthURL string +type directory struct { + NewNonceURL string `json:"newNonce"` + NewAccountURL string `json:"newAccount"` + NewOrderURL string `json:"newOrder"` + RevokeCertURL string `json:"revokeCert"` + KeyChangeURL string `json:"keyChange"` + Meta struct { + TermsOfService string `json:"termsOfService"` + Website string `json:"website"` + CaaIdentities []string `json:"caaIdentities"` + ExternalAccountRequired bool `json:"externalAccountRequired"` + } `json:"meta"` +} + +type accountMessage struct { + Status string `json:"status,omitempty"` + Contact []string `json:"contact,omitempty"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` + Orders string `json:"orders,omitempty"` + OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` + ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` +} + +type orderResource struct { + URL string `json:"url,omitempty"` + Domains []string `json:"domains,omitempty"` + orderMessage `json:"body,omitempty"` +} + +type orderMessage struct { + Status string `json:"status,omitempty"` + Expires string `json:"expires,omitempty"` + Identifiers []identifier `json:"identifiers"` + NotBefore string `json:"notBefore,omitempty"` + NotAfter string `json:"notAfter,omitempty"` + Authorizations []string `json:"authorizations,omitempty"` + Finalize string `json:"finalize,omitempty"` + Certificate string `json:"certificate,omitempty"` } type authorization struct { - Resource string `json:"resource,omitempty"` - Identifier identifier `json:"identifier"` - Status string `json:"status,omitempty"` - Expires time.Time `json:"expires,omitempty"` - Challenges []challenge `json:"challenges,omitempty"` - Combinations [][]int `json:"combinations,omitempty"` + Status string `json:"status"` + Expires time.Time `json:"expires"` + Identifier identifier `json:"identifier"` + Challenges []challenge `json:"challenges"` } type identifier struct { @@ -61,41 +64,29 @@ type identifier struct { Value string `json:"value"` } -type validationRecord struct { - URI string `json:"url,omitempty"` - Hostname string `json:"hostname,omitempty"` - Port string `json:"port,omitempty"` - ResolvedAddresses []string `json:"addressesResolved,omitempty"` - UsedAddress string `json:"addressUsed,omitempty"` -} - type challenge struct { - Resource string `json:"resource,omitempty"` - Type Challenge `json:"type,omitempty"` - Status string `json:"status,omitempty"` - URI string `json:"uri,omitempty"` - Token string `json:"token,omitempty"` - KeyAuthorization string `json:"keyAuthorization,omitempty"` - TLS bool `json:"tls,omitempty"` - Iterations int `json:"n,omitempty"` - Error RemoteError `json:"error,omitempty"` - ValidationRecords []validationRecord `json:"validationRecord,omitempty"` + URL string `json:"url"` + Type string `json:"type"` + Status string `json:"status"` + Token string `json:"token"` + Validated time.Time `json:"validated"` + KeyAuthorization string `json:"keyAuthorization"` + Error RemoteError `json:"error"` } type csrMessage struct { - Resource string `json:"resource,omitempty"` - Csr string `json:"csr"` - Authorizations []string `json:"authorizations"` + Csr string `json:"csr"` +} + +type emptyObjectMessage struct { } type revokeCertMessage struct { - Resource string `json:"resource"` Certificate string `json:"certificate"` } type deactivateAuthMessage struct { - Resource string `json:"resource,omitempty"` - Status string `jsom:"status"` + Status string `jsom:"status"` } // CertificateResource represents a CA issued certificate. diff --git a/acme/tls_sni_challenge.go b/acme/tls_sni_challenge.go deleted file mode 100644 index 34383cbf..00000000 --- a/acme/tls_sni_challenge.go +++ /dev/null @@ -1,67 +0,0 @@ -package acme - -import ( - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "fmt" - "log" -) - -type tlsSNIChallenge struct { - jws *jws - validate validateFunc - provider ChallengeProvider -} - -func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { - // FIXME: https://github.com/ietf-wg-acme/acme/pull/22 - // Currently we implement this challenge to track boulder, not the current spec! - - logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain) - - // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey) - if err != nil { - return err - } - - err = t.provider.Present(domain, chlng.Token, keyAuth) - if err != nil { - return fmt.Errorf("[%s] error presenting token: %v", domain, err) - } - defer func() { - err := t.provider.CleanUp(domain, chlng.Token, keyAuth) - if err != nil { - log.Printf("[%s] error cleaning up: %v", domain, err) - } - }() - return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) -} - -// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge -func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, string, error) { - // generate a new RSA key for the certificates - tempPrivKey, err := generatePrivateKey(RSA2048) - if err != nil { - return tls.Certificate{}, "", err - } - 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 { - return tls.Certificate{}, "", err - } - - certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) - if err != nil { - return tls.Certificate{}, "", err - } - - return certificate, domain, nil -} diff --git a/acme/tls_sni_challenge_server.go b/acme/tls_sni_challenge_server.go deleted file mode 100644 index df00fbb5..00000000 --- a/acme/tls_sni_challenge_server.go +++ /dev/null @@ -1,62 +0,0 @@ -package acme - -import ( - "crypto/tls" - "fmt" - "net" - "net/http" -) - -// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge -// It may be instantiated without using the NewTLSProviderServer function if -// you want only to use the default values. -type TLSProviderServer struct { - iface string - port string - done chan bool - listener net.Listener -} - -// NewTLSProviderServer creates a new TLSProviderServer on the selected interface and port. -// Setting iface and / or port to an empty string will make the server fall back to -// the "any" interface and port 443 respectively. -func NewTLSProviderServer(iface, port string) *TLSProviderServer { - return &TLSProviderServer{iface: iface, port: port} -} - -// Present makes the keyAuth available as a cert -func (s *TLSProviderServer) 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 -} - -// CleanUp closes the HTTP server. -func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error { - if s.listener == nil { - return nil - } - s.listener.Close() - <-s.done - return nil -} diff --git a/acme/tls_sni_challenge_test.go b/acme/tls_sni_challenge_test.go deleted file mode 100644 index 83b2833a..00000000 --- a/acme/tls_sni_challenge_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package acme - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "fmt" - "strings" - "testing" -) - -func TestTLSSNIChallenge(t *testing.T) { - privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey} - clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"} - mockValidate := func(_ *jws, _, _ string, chlng challenge) error { - conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{ - InsecureSkipVerify: true, - }) - if err != nil { - t.Errorf("Expected to connect to challenge server without an error. %s", err.Error()) - } - - // Expect the server to only return one certificate - connState := conn.ConnectionState() - if count := len(connState.PeerCertificates); count != 1 { - t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count) - } - - remoteCert := connState.PeerCertificates[0] - if count := len(remoteCert.DNSNames); count != 1 { - t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count) - } - - zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization)) - z := hex.EncodeToString(zBytes[:sha256.Size]) - domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:]) - - if remoteCert.DNSNames[0] != domain { - t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0]) - } - - return nil - } - solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}} - - if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil { - t.Errorf("Solve error: got %v, want nil", err) - } -} - -func TestTLSSNIChallengeInvalidPort(t *testing.T) { - privKey, _ := rsa.GenerateKey(rand.Reader, 128) - j := &jws{privKey: privKey} - clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"} - solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}} - - if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { - t.Errorf("Solve error: got %v, want error", err) - } else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) { - t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want) - } -} diff --git a/acmev2/challenges.go b/acmev2/challenges.go deleted file mode 100644 index cf7bd7f7..00000000 --- a/acmev2/challenges.go +++ /dev/null @@ -1,13 +0,0 @@ -package acme - -// Challenge is a string that identifies a particular type and version of ACME challenge. -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") - // 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") -) diff --git a/acmev2/client.go b/acmev2/client.go deleted file mode 100644 index a329769a..00000000 --- a/acmev2/client.go +++ /dev/null @@ -1,867 +0,0 @@ -// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers. -package acme - -import ( - "crypto" - "crypto/x509" - "encoding/base64" - "errors" - "fmt" - "io/ioutil" - "log" - "net" - "regexp" - "strconv" - "strings" - "time" -) - -var ( - // Logger is an optional custom logger. - 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{}) { - if Logger != nil { - Logger.Printf(format, args...) - } else { - log.Printf(format, args...) - } -} - -// User interface is to be implemented by users of this library. -// It is used by the client type to get user specific information. -type User interface { - GetEmail() string - GetRegistration() *RegistrationResource - GetPrivateKey() crypto.PrivateKey -} - -// Interface for all challenge solvers to implement. -type solver interface { - Solve(challenge challenge, domain string) error -} - -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 - 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. 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 { - return nil, errors.New("private key was nil") - } - - var dir directory - if _, err := getJSON(caDirURL, &dir); err != nil { - return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) - } - - if dir.NewAccountURL == "" { - return nil, errors.New("directory missing new registration URL") - } - if dir.NewOrderURL == "" { - return nil, errors.New("directory missing new order URL") - } - /*if dir.RevokeCertURL == "" { - return nil, errors.New("directory missing revoke certificate URL") - }*/ - - jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL} - if reg := user.GetRegistration(); reg != nil { - jws.kid = reg.URI - } - - // 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[Challenge]solver) - solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} - - return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil -} - -// 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 DNS01: - c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, 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. -// -// 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 { - return err - } - - if chlng, ok := c.solvers[HTTP01]; ok { - chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port) - } - - return nil -} - -// ExcludeChallenges explicitly removes challenges from the pool for solving. -func (c *Client) ExcludeChallenges(challenges []Challenge) { - // Loop through all challenges and delete the requested one if found. - for _, challenge := range challenges { - delete(c.solvers, challenge) - } -} - -// GetToSURL returns the current ToS URL from the Directory -func (c *Client) GetToSURL() string { - return c.directory.Meta.TermsOfService -} - -// GetExternalAccountRequired returns the External Account Binding requirement of the Directory -func (c *Client) GetExternalAccountRequired() bool { - return c.directory.Meta.ExternalAccountRequired -} - -// Register the current account to the ACME server. -func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { - if c == nil || c.user == nil { - return nil, errors.New("acme: cannot register a nil client or user") - } - logf("[INFO] acme: Registering account for %s", c.user.GetEmail()) - - accMsg := accountMessage{} - if c.user.GetEmail() != "" { - accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} - } else { - accMsg.Contact = []string{} - } - accMsg.TermsOfServiceAgreed = tosAgreed - - var serverReg accountMessage - hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) - if err != nil { - remoteErr, ok := err.(RemoteError) - if ok && remoteErr.StatusCode == 409 { - } else { - return nil, err - } - } - - reg := &RegistrationResource{ - URI: hdr.Get("Location"), - Body: serverReg, - } - c.jws.kid = reg.URI - - return reg, nil -} - -// Register the current account to the ACME server. -func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) { - if c == nil || c.user == nil { - return nil, errors.New("acme: cannot register a nil client or user") - } - logf("[INFO] acme: Registering account (EAB) for %s", c.user.GetEmail()) - - accMsg := accountMessage{} - if c.user.GetEmail() != "" { - accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} - } else { - accMsg.Contact = []string{} - } - accMsg.TermsOfServiceAgreed = tosAgreed - - hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) - if err != nil { - return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error()) - } - - eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac) - if err != nil { - return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error()) - } - - eabPayload := eabJWS.FullSerialize() - - accMsg.ExternalAccountBinding = []byte(eabPayload) - - var serverReg accountMessage - hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) - if err != nil { - remoteErr, ok := err.(RemoteError) - if ok && remoteErr.StatusCode == 409 { - } else { - return nil, err - } - } - - reg := &RegistrationResource{ - URI: hdr.Get("Location"), - Body: serverReg, - } - c.jws.kid = reg.URI - - return reg, nil -} - - -// ResolveAccountByKey will attempt to look up an account using the given account key -// and return its registration resource. -func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { - logf("[INFO] acme: Trying to resolve account by key") - - acc := accountMessage{OnlyReturnExisting: true} - hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil) - if err != nil { - return nil, err - } - - accountLink := hdr.Get("Location") - if accountLink == "" { - return nil, errors.New("Server did not return the account link") - } - - var retAccount accountMessage - c.jws.kid = accountLink - hdr, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount) - if err != nil { - return nil, err - } - - return &RegistrationResource{URI: accountLink, Body: retAccount}, 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()) - - accMsg := accountMessage{ - Status: "deactivated", - } - - _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, 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) - - accMsg := accountMessage{} - - var serverReg accountMessage - _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &serverReg) - if err != nil { - return nil, err - } - - reg := &RegistrationResource{Body: serverReg} - - // Location: header is not returned so this needs to be populated off of - // existing URI - reg.URI = c.user.GetRegistration().URI - - return reg, nil -} - -// 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, 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, ", ")) - } - - order, err := c.createOrderForIdentifiers(domains) - if err != nil { - return nil, err - } - authz, err := c.getAuthzForOrder(order) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - /*for _, auth := range authz { - c.disableAuthz(auth) - }*/ - return nil, err - } - - err = c.solveChallengeForAuthz(authz) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - return nil, err - } - - logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - - failures := make(ObtainError) - cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil) - if err != nil { - for _, chln := range authz { - failures[chln.Identifier.Value] = err - } - } - - // 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 - // it would still be a non-nil error value - if len(failures) > 0 { - return cert, failures - } - return cert, nil -} - -// 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 -// for every invocation of this function. If you do not want that you can supply your own private key -// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one. -// 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) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { - if len(domains) == 0 { - return nil, errors.New("No domains to obtain a certificate for") - } - - if bundle { - logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) - } else { - logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) - } - - order, err := c.createOrderForIdentifiers(domains) - if err != nil { - return nil, err - } - authz, err := c.getAuthzForOrder(order) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - /*for _, auth := range authz { - c.disableAuthz(auth) - }*/ - return nil, err - } - - err = c.solveChallengeForAuthz(authz) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - return nil, err - } - - logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - - failures := make(ObtainError) - cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple) - if err != nil { - for _, auth := range authz { - failures[auth.Identifier.Value] = err - } - } - - // do not return an empty failures map, because - // it would still be a non-nil error value - if len(failures) > 0 { - return cert, failures - } - return cert, nil -} - -// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. -func (c *Client) RevokeCertificate(certificate []byte) error { - certificates, err := parsePEMBundle(certificate) - if err != nil { - return err - } - - x509Cert := certificates[0] - if x509Cert.IsCA { - return fmt.Errorf("Certificate bundle starts with a CA certificate") - } - - encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw) - - _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil) - return err -} - -// RenewCertificate takes a CertificateResource and tries to renew the certificate. -// If the renewal process succeeds, the new certificate will ge returned in a new CertResource. -// Please be aware that this function will return a new certificate in ANY case that is not an error. -// If the server does not provide us with a new cert on a GET request to the CertURL -// this function will start a new-cert flow where a new certificate gets generated. -// 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, 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) - if err != nil { - return nil, err - } - - x509Cert := certificates[0] - if x509Cert.IsCA { - return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) - } - - // This is just meant to be informal for the user. - timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) - logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) - - // 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 nil, err - } - newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) - return newCert, failures - } - - var privKey crypto.PrivateKey - if cert.PrivateKey != nil { - privKey, err = parsePEMPrivateKey(cert.PrivateKey) - if err != nil { - return nil, err - } - } - - var domains []string - // check for SAN certificate - if len(x509Cert.DNSNames) > 1 { - domains = append(domains, x509Cert.Subject.CommonName) - for _, sanDomain := range x509Cert.DNSNames { - if sanDomain == x509Cert.Subject.CommonName { - continue - } - domains = append(domains, sanDomain) - } - } else { - domains = append(domains, x509Cert.Subject.CommonName) - } - - newCert, err := c.ObtainCertificate(domains, bundle, privKey, mustStaple) - return newCert, err -} - -func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) { - - var identifiers []identifier - for _, domain := range domains { - identifiers = append(identifiers, identifier{Type: "dns", Value: domain}) - } - - order := orderMessage{ - Identifiers: identifiers, - } - - var response orderMessage - hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response) - if err != nil { - return orderResource{}, err - } - - orderRes := orderResource{ - URL: hdr.Get("Location"), - Domains: domains, - orderMessage: response, - } - return orderRes, nil -} - -// Looks through the challenge combinations to find a solvable match. -// Then solves the challenges in series and returns. -func (c *Client) solveChallengeForAuthz(authorizations []authorization) error { - failures := make(ObtainError) - - // loop through the resources, basically through the domains. - for _, authz := range authorizations { - if authz.Status == "valid" { - // Boulder might recycle recent validated authz (see issue #267) - logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) - continue - } - - // no solvers - no solving - if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil { - err := solver.Solve(authz.Challenges[i], authz.Identifier.Value) - if err != nil { - //c.disableAuthz(authz.Identifier) - failures[authz.Identifier.Value] = err - } - } else { - //c.disableAuthz(authz) - failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value) - } - } - - // be careful not to return an empty failures map, for - // even an empty ObtainError is a non-nil error value - if len(failures) > 0 { - return failures - } - return nil -} - -// Checks all challenges from the server in order and returns the first matching solver. -func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) { - for i, challenge := range auth.Challenges { - if solver, ok := c.solvers[Challenge(challenge.Type)]; ok { - return i, solver - } - logf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type) - } - return 0, nil -} - -// Get the challenges needed to proof our identifier to the ACME server. -func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) { - resc, errc := make(chan authorization), make(chan domainError) - - delay := time.Second / overallRequestLimit - - for _, authzURL := range order.Authorizations { - time.Sleep(delay) - - go func(authzURL string) { - var authz authorization - _, err := getJSON(authzURL, &authz) - if err != nil { - errc <- domainError{Domain: authz.Identifier.Value, Error: err} - return - } - - resc <- authz - }(authzURL) - } - - var responses []authorization - failures := make(ObtainError) - for i := 0; i < len(order.Authorizations); i++ { - select { - case res := <-resc: - responses = append(responses, res) - case err := <-errc: - failures[err.Domain] = err.Error - } - } - - logAuthz(order) - - close(resc) - close(errc) - - // be careful to not return an empty failures map; - // even if empty, they become non-nil error values - if len(failures) > 0 { - return responses, failures - } - return responses, nil -} - -func logAuthz(order orderResource) { - for i, auth := range order.Authorizations { - logf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth) - } -} - -// cleanAuthz loops through the passed in slice and disables any auths which are not "valid" -func (c *Client) disableAuthz(authURL string) error { - var disabledAuth authorization - _, err := postJSON(c.jws, authURL, deactivateAuthMessage{Status: "deactivated"}, &disabledAuth) - return err -} - -func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { - - var err error - if privKey == nil { - privKey, err = generatePrivateKey(c.keyType) - if err != nil { - return nil, err - } - } - - // determine certificate name(s) based on the authorization resources - commonName := order.Domains[0] - var san []string - for _, auth := range order.Identifiers { - san = append(san, auth.Value) - } - - // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey, commonName, san, mustStaple) - if err != nil { - return nil, err - } - - return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey)) -} - -func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) { - commonName := order.Domains[0] - - csrString := base64.RawURLEncoding.EncodeToString(csr) - var retOrder orderMessage - _, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder) - if error != nil { - return nil, error - } - - if retOrder.Status == "invalid" { - return nil, error - } - - certRes := CertificateResource{ - Domain: commonName, - CertURL: retOrder.Certificate, - PrivateKey: privateKeyPem, - } - - if retOrder.Status == "valid" { - // if the certificate is available right away, short cut! - ok, err := c.checkCertResponse(retOrder, &certRes, bundle) - if err != nil { - return nil, err - } - - if ok { - return &certRes, nil - } - } - - maxChecks := 1000 - for i := 0; i < maxChecks; i++ { - _, err := getJSON(order.URL, &retOrder) - if err != nil { - return nil, err - } - done, err := c.checkCertResponse(retOrder, &certRes, bundle) - if err != nil { - return nil, err - } - if done { - break - } - if i == maxChecks-1 { - return nil, fmt.Errorf("polled for certificate %d times; giving up", i) - } - } - - return &certRes, nil -} - -// checkCertResponse checks to see if the certificate is ready and a link is contained in the -// response. if so, loads it into certRes and returns true. If the cert -// is not yet ready, it returns false. 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(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) { - - switch order.Status { - case "valid": - resp, err := httpGet(order.Certificate) - if err != nil { - return false, err - } - - cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) - if err != nil { - return false, err - } - - // The issuer certificate link is always supplied via an "up" link - // in the response headers of a new certificate. - links := parseLinks(resp.Header["Link"]) - if link, ok := links["up"]; ok { - issuerCert, err := c.getIssuerCertificate(link) - - 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 { - cert = append(cert, issuerCert...) - } - - certRes.IssuerCertificate = issuerCert - } - } - - certRes.Certificate = cert - certRes.CertURL = order.Certificate - certRes.CertStableURL = order.Certificate - logf("[INFO][%s] Server responded with a certificate.", certRes.Domain) - return true, nil - - case "processing": - return false, nil - case "invalid": - return false, errors.New("Order has invalid state: invalid") - } - - return false, nil -} - -// getIssuerCertificate requests the issuer certificate -func (c *Client) getIssuerCertificate(url string) ([]byte, error) { - logf("[INFO] acme: Requesting issuer cert from %s", url) - resp, err := httpGet(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) - if err != nil { - return nil, err - } - - _, err = x509.ParseCertificate(issuerBytes) - if err != nil { - return nil, err - } - - return issuerBytes, err -} - -func parseLinks(links []string) map[string]string { - aBrkt := regexp.MustCompile("[<>]") - slver := regexp.MustCompile("(.+) *= *\"(.+)\"") - linkMap := make(map[string]string) - - for _, link := range links { - - link = aBrkt.ReplaceAllString(link, "") - parts := strings.Split(link, ";") - - matches := slver.FindStringSubmatch(parts[1]) - if len(matches) > 0 { - linkMap[matches[2]] = parts[0] - } - } - - return linkMap -} - -// validate makes the ACME server start validating a -// challenge response, only returning once it is done. -func validate(j *jws, domain, uri string, c challenge) error { - var chlng challenge - - hdr, err := postJSON(j, uri, c, &chlng) - if err != nil { - return err - } - - // After the path is sent, the ACME server will access our server. - // Repeatedly check the server for an updated status on our request. - for { - switch chlng.Status { - case "valid": - logf("[INFO][%s] The server validated our request", domain) - return nil - case "pending": - break - case "invalid": - return handleChallengeError(chlng) - default: - return errors.New("The server returned an unexpected state") - } - - ra, err := strconv.Atoi(hdr.Get("Retry-After")) - if err != nil { - // The ACME server MUST return a Retry-After. - // If it doesn't, we'll just poll hard. - ra = 5 - } - time.Sleep(time.Duration(ra) * time.Second) - - hdr, err = getJSON(uri, &chlng) - if err != nil { - return err - } - } -} diff --git a/acmev2/client_test.go b/acmev2/client_test.go deleted file mode 100644 index d30dd3ba..00000000 --- a/acmev2/client_test.go +++ /dev/null @@ -1,330 +0,0 @@ -package acme - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "encoding/json" - "net" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" -) - -func TestNewClient(t *testing.T) { - keyBits := 32 // small value keeps test fast - keyType := RSA2048 - key, err := rsa.GenerateKey(rand.Reader, keyBits) - if err != nil { - t.Fatal("Could not generate test key:", err) - } - user := mockUser{ - email: "test@test.com", - regres: new(RegistrationResource), - privatekey: key, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{ - NewNonceURL: "http://test", - NewAccountURL: "http://test", - NewOrderURL: "http://test", - RevokeCertURL: "http://test", - KeyChangeURL: "http://test", - }) - w.Write(data) - })) - - client, err := NewClient(ts.URL, user, keyType) - if err != nil { - t.Fatalf("Could not create client: %v", err) - } - - if client.jws == nil { - t.Fatalf("Expected client.jws to not be nil") - } - if expected, actual := key, client.jws.privKey; actual != expected { - t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual) - } - - if client.keyType != keyType { - t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType) - } - - if expected, actual := 1, len(client.solvers); actual != expected { - t.Fatalf("Expected %d solver(s), got %d", expected, actual) - } -} - -func TestClientOptPort(t *testing.T) { - keyBits := 32 // small value keeps test fast - key, err := rsa.GenerateKey(rand.Reader, keyBits) - if err != nil { - t.Fatal("Could not generate test key:", err) - } - user := mockUser{ - email: "test@test.com", - regres: new(RegistrationResource), - privatekey: key, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{ - NewNonceURL: "http://test", - NewAccountURL: "http://test", - NewOrderURL: "http://test", - RevokeCertURL: "http://test", - KeyChangeURL: "http://test", - }) - w.Write(data) - })) - - optPort := "1234" - optHost := "" - client, err := NewClient(ts.URL, user, RSA2048) - if err != nil { - t.Fatalf("Could not create client: %v", err) - } - client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - - 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 got := httpSolver.provider.(*HTTPProviderServer).port; got != optPort { - t.Errorf("Expected http-01 to have port %s but was %s", optPort, got) - } - if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { - t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) - } - - /* 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 got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { - t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) - } - if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost { - t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got) - } */ - - // test setting different host - optHost = "127.0.0.1" - client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - - if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { - t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) - } -} - -func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(250 * time.Millisecond) - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) - })) - defer ts.Close() - - privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey, getNonceURL: ts.URL} - ch := make(chan bool) - resultCh := make(chan bool) - go func() { - j.Nonce() - ch <- true - }() - go func() { - j.Nonce() - ch <- true - }() - go func() { - <-ch - <-ch - resultCh <- true - }() - select { - case <-resultCh: - case <-time.After(400 * time.Millisecond): - t.Fatal("JWS is probably holding a lock while making HTTP request") - } -} - -func TestValidate(t *testing.T) { - var statuses []string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Minimal stub ACME server for validation. - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - switch r.Method { - case "HEAD": - case "POST": - st := statuses[0] - statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) - - case "GET": - st := statuses[0] - statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) - - default: - http.Error(w, r.Method, http.StatusMethodNotAllowed) - } - })) - defer ts.Close() - - privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey, getNonceURL: ts.URL} - - tsts := []struct { - name string - statuses []string - want string - }{ - {"POST-unexpected", []string{"weird"}, "unexpected"}, - {"POST-valid", []string{"valid"}, ""}, - {"POST-invalid", []string{"invalid"}, "Error"}, - {"GET-unexpected", []string{"pending", "weird"}, "unexpected"}, - {"GET-valid", []string{"pending", "valid"}, ""}, - {"GET-invalid", []string{"pending", "invalid"}, "Error"}, - } - - for _, tst := range tsts { - statuses = tst.statuses - if err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"}); err == nil && tst.want != "" { - t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want) - } else if err != nil && !strings.Contains(err.Error(), tst.want) { - t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want) - } - } -} - -func TestGetChallenges(t *testing.T) { - var ts *httptest.Server - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET", "HEAD": - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - writeJSONResponse(w, directory{ - NewNonceURL: ts.URL, - NewAccountURL: ts.URL, - NewOrderURL: ts.URL, - RevokeCertURL: ts.URL, - KeyChangeURL: ts.URL, - }) - case "POST": - writeJSONResponse(w, orderMessage{}) - } - })) - defer ts.Close() - - keyBits := 512 // small value keeps test fast - keyType := RSA2048 - key, err := rsa.GenerateKey(rand.Reader, keyBits) - if err != nil { - t.Fatal("Could not generate test key:", err) - } - user := mockUser{ - email: "test@test.com", - regres: &RegistrationResource{URI: ts.URL}, - privatekey: key, - } - - client, err := NewClient(ts.URL, user, keyType) - if err != nil { - t.Fatalf("Could not create client: %v", err) - } - - _, err = client.createOrderForIdentifiers([]string{"example.com"}) - if err != nil { - t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil") - } -} - -func TestResolveAccountByKey(t *testing.T) { - keyBits := 512 - keyType := RSA2048 - key, err := rsa.GenerateKey(rand.Reader, keyBits) - if err != nil { - t.Fatal("Could not generate test key:", err) - } - user := mockUser{ - email: "test@test.com", - regres: new(RegistrationResource), - privatekey: key, - } - - var ts *httptest.Server - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.RequestURI { - case "/directory": - writeJSONResponse(w, directory{ - NewNonceURL: ts.URL + "/nonce", - NewAccountURL: ts.URL + "/account", - NewOrderURL: ts.URL + "/newOrder", - RevokeCertURL: ts.URL + "/revokeCert", - KeyChangeURL: ts.URL + "/keyChange", - }) - case "/nonce": - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - case "/account": - w.Header().Set("Location", ts.URL+"/account_recovery") - case "/account_recovery": - writeJSONResponse(w, accountMessage{ - Status: "valid", - }) - } - })) - - client, err := NewClient(ts.URL+"/directory", user, keyType) - if err != nil { - t.Fatalf("Could not create client: %v", err) - } - - if res, err := client.ResolveAccountByKey(); err != nil { - t.Fatalf("Unexpected error resolving account by key: %v", err) - } else if res.Body.Status != "valid" { - t.Errorf("Unexpected account status: %v", res.Body.Status) - } -} - -// writeJSONResponse marshals the body as JSON and writes it to the response. -func writeJSONResponse(w http.ResponseWriter, body interface{}) { - bs, err := json.Marshal(body) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if _, err := w.Write(bs); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -// stubValidate is like validate, except it does nothing. -func stubValidate(j *jws, domain, uri string, chlng challenge) error { - return nil -} - -type mockUser struct { - email string - regres *RegistrationResource - privatekey *rsa.PrivateKey -} - -func (u mockUser) GetEmail() string { return u.email } -func (u mockUser) GetRegistration() *RegistrationResource { return u.regres } -func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } diff --git a/acmev2/crypto.go b/acmev2/crypto.go deleted file mode 100644 index 7adc34d4..00000000 --- a/acmev2/crypto.go +++ /dev/null @@ -1,342 +0,0 @@ -package acme - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/base64" - "encoding/pem" - "errors" - "fmt" - "io" - "io/ioutil" - "math/big" - "net/http" - "time" - - "golang.org/x/crypto/ocsp" - jose "gopkg.in/square/go-jose.v2" -) - -// KeyType represents the key algo as well as the key size or curve to use. -type KeyType string -type derCertificateBytes []byte - -// Constants for all key types we support. -const ( - EC256 = KeyType("P256") - EC384 = KeyType("P384") - RSA2048 = KeyType("2048") - RSA4096 = KeyType("4096") - RSA8192 = KeyType("8192") -) - -const ( - // OCSPGood means that the certificate is valid. - OCSPGood = ocsp.Good - // OCSPRevoked means that the certificate has been deliberately revoked. - OCSPRevoked = ocsp.Revoked - // OCSPUnknown means that the OCSP responder doesn't know about the certificate. - OCSPUnknown = ocsp.Unknown - // OCSPServerFailed means that the OCSP responder failed to process the request. - OCSPServerFailed = ocsp.ServerFailed -) - -// Constants for OCSP must staple -var ( - tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} - ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05} -) - -// GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response, -// the parsed response, and an error, if any. The returned []byte can be passed directly -// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the -// issued certificate, this function will try to get the issuer certificate from the -// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return -// values are nil, the OCSP status may be assumed OCSPUnknown. -func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { - certificates, err := parsePEMBundle(bundle) - if err != nil { - return nil, nil, err - } - - // We expect the certificate slice to be ordered downwards the chain. - // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, - // which should always be the first two certificates. If there's no - // OCSP server listed in the leaf cert, there's nothing to do. And if - // we have only one certificate so far, we need to get the issuer cert. - issuedCert := certificates[0] - if len(issuedCert.OCSPServer) == 0 { - return nil, nil, errors.New("no OCSP server specified in cert") - } - if len(certificates) == 1 { - // TODO: build fallback. If this fails, check the remaining array entries. - if len(issuedCert.IssuingCertificateURL) == 0 { - return nil, nil, errors.New("no issuing certificate URL") - } - - resp, err := httpGet(issuedCert.IssuingCertificateURL[0]) - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - - issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) - if err != nil { - return nil, nil, err - } - - issuerCert, err := x509.ParseCertificate(issuerBytes) - if err != nil { - return nil, nil, err - } - - // Insert it into the slice on position 0 - // We want it ordered right SRV CRT -> CA - certificates = append(certificates, issuerCert) - } - issuerCert := certificates[1] - - // Finally kick off the OCSP request. - ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) - if err != nil { - return nil, nil, err - } - - reader := bytes.NewReader(ocspReq) - req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader) - if err != nil { - return nil, nil, err - } - defer req.Body.Close() - - ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024)) - ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) - if err != nil { - return nil, nil, err - } - - return ocspResBytes, ocspRes, nil -} - -func getKeyAuthorization(token string, key interface{}) (string, error) { - var publicKey crypto.PublicKey - switch k := key.(type) { - case *ecdsa.PrivateKey: - publicKey = k.Public() - case *rsa.PrivateKey: - publicKey = k.Public() - } - - // Generate the Key Authorization for the challenge - jwk := &jose.JSONWebKey{Key: publicKey} - if jwk == nil { - return "", errors.New("Could not generate JWK from key") - } - thumbBytes, err := jwk.Thumbprint(crypto.SHA256) - if err != nil { - return "", err - } - - // unpad the base64URL - keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes) - - return token + "." + keyThumb, nil -} - -// parsePEMBundle parses a certificate bundle from top to bottom and returns -// a slice of x509 certificates. This function will error if no certificates are found. -func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { - var certificates []*x509.Certificate - var certDERBlock *pem.Block - - for { - certDERBlock, bundle = pem.Decode(bundle) - if certDERBlock == nil { - break - } - - if certDERBlock.Type == "CERTIFICATE" { - cert, err := x509.ParseCertificate(certDERBlock.Bytes) - if err != nil { - return nil, err - } - certificates = append(certificates, cert) - } - } - - if len(certificates) == 0 { - return nil, errors.New("No certificates were found while parsing the bundle") - } - - return certificates, nil -} - -func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { - keyBlock, _ := pem.Decode(key) - - switch keyBlock.Type { - case "RSA PRIVATE KEY": - return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) - case "EC PRIVATE KEY": - return x509.ParseECPrivateKey(keyBlock.Bytes) - default: - return nil, errors.New("Unknown PEM header value") - } -} - -func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { - - switch keyType { - case EC256: - return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case EC384: - return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case RSA2048: - return rsa.GenerateKey(rand.Reader, 2048) - case RSA4096: - return rsa.GenerateKey(rand.Reader, 4096) - case RSA8192: - return rsa.GenerateKey(rand.Reader, 8192) - } - - return nil, fmt.Errorf("Invalid KeyType: %s", keyType) -} - -func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { - template := x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: domain, - }, - } - - if len(san) > 0 { - template.DNSNames = san - } - - if mustStaple { - template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ - Id: tlsFeatureExtensionOID, - Value: ocspMustStapleFeature, - }) - } - - return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) -} - -func pemEncode(data interface{}) []byte { - var pemBlock *pem.Block - switch key := data.(type) { - case *ecdsa.PrivateKey: - keyBytes, _ := x509.MarshalECPrivateKey(key) - pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} - case *rsa.PrivateKey: - pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} - break - case *x509.CertificateRequest: - pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} - break - case derCertificateBytes: - pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} - } - - return pem.EncodeToMemory(pemBlock) -} - -func pemDecode(data []byte) (*pem.Block, error) { - pemBlock, _ := pem.Decode(data) - if pemBlock == nil { - return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?") - } - - 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) { - pemBlock, err := pemDecode(pem) - if pemBlock == nil { - return nil, err - } - - if pemBlock.Type != "CERTIFICATE REQUEST" { - return nil, fmt.Errorf("PEM block is not a certificate request") - } - - return x509.ParseCertificateRequest(pemBlock.Bytes) -} - -// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate. -// The certificate has to be PEM encoded. Any other encodings like DER will fail. -func GetPEMCertExpiration(cert []byte) (time.Time, error) { - pemBlock, err := pemDecode(cert) - if pemBlock == nil { - return time.Time{}, err - } - - return getCertExpiration(pemBlock.Bytes) -} - -// getCertExpiration returns the "NotAfter" date of a DER encoded certificate. -func getCertExpiration(cert []byte) (time.Time, error) { - pCert, err := x509.ParseCertificate(cert) - if err != nil { - return time.Time{}, err - } - - return pCert.NotAfter, nil -} - -func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { - derBytes, err := generateDerCert(privKey, time.Time{}, domain) - if err != nil { - return nil, err - } - - return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil -} - -func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return nil, err - } - - if expiration.IsZero() { - expiration = time.Now().Add(365) - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - CommonName: "ACME Challenge TEMP", - }, - NotBefore: time.Now(), - NotAfter: expiration, - - KeyUsage: x509.KeyUsageKeyEncipherment, - BasicConstraintsValid: true, - DNSNames: []string{domain}, - } - - return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) -} - -func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser { - return http.MaxBytesReader(nil, rd, numBytes) -} diff --git a/acmev2/crypto_test.go b/acmev2/crypto_test.go deleted file mode 100644 index 6f43835f..00000000 --- a/acmev2/crypto_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package acme - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "testing" - "time" -) - -func TestGeneratePrivateKey(t *testing.T) { - key, err := generatePrivateKey(RSA2048) - if err != nil { - t.Error("Error generating private key:", err) - } - if key == nil { - t.Error("Expected key to not be nil, but it was") - } -} - -func TestGenerateCSR(t *testing.T) { - key, err := rsa.GenerateKey(rand.Reader, 512) - if err != nil { - t.Fatal("Error generating private key:", err) - } - - csr, err := generateCsr(key, "fizz.buzz", nil, true) - if err != nil { - t.Error("Error generating CSR:", err) - } - if csr == nil || len(csr) == 0 { - t.Error("Expected CSR with data, but it was nil or length 0") - } -} - -func TestPEMEncode(t *testing.T) { - buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") - - reader := MockRandReader{b: buf} - key, err := rsa.GenerateKey(reader, 32) - if err != nil { - t.Fatal("Error generating private key:", err) - } - - data := pemEncode(key) - - if data == nil { - t.Fatal("Expected result to not be nil, but it was") - } - if len(data) != 127 { - t.Errorf("Expected PEM encoding to be length 127, but it was %d", len(data)) - } -} - -func TestPEMCertExpiration(t *testing.T) { - privKey, err := generatePrivateKey(RSA2048) - if err != nil { - t.Fatal("Error generating private key:", err) - } - - expiration := time.Now().Add(365) - expiration = expiration.Round(time.Second) - certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com") - if err != nil { - t.Fatal("Error generating cert:", err) - } - - buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") - - // Some random string should return an error. - if ctime, err := GetPEMCertExpiration(buf.Bytes()); err == nil { - t.Errorf("Expected getCertExpiration to return an error for garbage string but returned %v", ctime) - } - - // A DER encoded certificate should return an error. - if _, err := GetPEMCertExpiration(certBytes); err == nil { - t.Errorf("Expected getCertExpiration to return an error for DER certificates but returned none.") - } - - // A PEM encoded certificate should work ok. - pemCert := pemEncode(derCertificateBytes(certBytes)) - if ctime, err := GetPEMCertExpiration(pemCert); err != nil || !ctime.Equal(expiration.UTC()) { - t.Errorf("Expected getCertExpiration to return %v but returned %v. Error: %v", expiration, ctime, err) - } -} - -type MockRandReader struct { - b *bytes.Buffer -} - -func (r MockRandReader) Read(p []byte) (int, error) { - return r.b.Read(p) -} diff --git a/acmev2/dns_challenge.go b/acmev2/dns_challenge.go deleted file mode 100644 index d129dacc..00000000 --- a/acmev2/dns_challenge.go +++ /dev/null @@ -1,309 +0,0 @@ -package acme - -import ( - "crypto/sha256" - "encoding/base64" - "errors" - "fmt" - "log" - "net" - "strings" - "time" - - "github.com/miekg/dns" -) - -type preCheckDNSFunc func(fqdn, value string) (bool, error) - -var ( - // PreCheckDNS checks DNS propagation before notifying ACME that - // the DNS challenge is ready. - PreCheckDNS preCheckDNSFunc = checkDNSPropagation - fqdnToZone = map[string]string{} -) - -const defaultResolvConf = "/etc/resolv.conf" - -var defaultNameservers = []string{ - "google-public-dns-a.google.com:53", - "google-public-dns-b.google.com:53", -} - -// RecursiveNameservers are used to pre-check DNS propagations -var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) - -// DNSTimeout is used to override the default DNS timeout of 10 seconds. -var DNSTimeout = 10 * time.Second - -// getNameservers attempts to get systems nameservers before falling back to the defaults -func getNameservers(path string, defaults []string) []string { - config, err := dns.ClientConfigFromFile(path) - if err != nil || len(config.Servers) == 0 { - return defaults - } - - systemNameservers := []string{} - for _, server := range config.Servers { - // ensure all servers have a port number - if _, _, err := net.SplitHostPort(server); err != nil { - systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53")) - } else { - systemNameservers = append(systemNameservers, server) - } - } - return systemNameservers -} - -// 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 - value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) - 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 - validate validateFunc - provider ChallengeProvider -} - -func (s *dnsChallenge) Solve(chlng challenge, domain string) error { - logf("[INFO][%s] acme: Trying to solve DNS-01", domain) - - 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) - if 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, value, _ := DNS01Record(domain, keyAuth) - - logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) - - var timeout, interval time.Duration - switch provider := s.provider.(type) { - case ChallengeProviderTimeout: - timeout, interval = provider.Timeout() - default: - timeout, interval = 60*time.Second, 2*time.Second - } - - err = WaitFor(timeout, interval, func() (bool, error) { - return PreCheckDNS(fqdn, value) - }) - if err != nil { - return err - } - - return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) -} - -// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. -func checkDNSPropagation(fqdn, value string) (bool, error) { - // Initial attempt to resolve at the recursive NS - r, err := dnsQuery(fqdn, dns.TypeTXT, RecursiveNameservers, true) - if err != nil { - return false, err - } - if r.Rcode == dns.RcodeSuccess { - // If we see a CNAME here then use the alias - for _, rr := range r.Answer { - if cn, ok := rr.(*dns.CNAME); ok { - if cn.Hdr.Name == fqdn { - fqdn = cn.Target - break - } - } - } - } - - authoritativeNss, err := lookupNameservers(fqdn) - if err != nil { - return false, err - } - - return checkAuthoritativeNss(fqdn, value, authoritativeNss) -} - -// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. -func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { - for _, ns := range nameservers { - r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) - if err != nil { - return false, err - } - - if r.Rcode != dns.RcodeSuccess { - return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) - } - - var found bool - for _, rr := range r.Answer { - if txt, ok := rr.(*dns.TXT); ok { - if strings.Join(txt.Txt, "") == value { - found = true - break - } - } - } - - if !found { - return false, fmt.Errorf("NS %s did not return the expected TXT record", ns) - } - } - - return true, nil -} - -// dnsQuery will query a nameserver, iterating through the supplied servers as it retries -// The nameserver should include a port, to facilitate testing where we talk to a mock dns server. -func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) { - m := new(dns.Msg) - m.SetQuestion(fqdn, rtype) - m.SetEdns0(4096, false) - - if !recursive { - m.RecursionDesired = false - } - - // Will retry the request based on the number of servers (n+1) - for i := 1; i <= len(nameservers)+1; i++ { - ns := nameservers[i%len(nameservers)] - udp := &dns.Client{Net: "udp", Timeout: DNSTimeout} - in, _, err = udp.Exchange(m, ns) - - if err == dns.ErrTruncated { - tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout} - // If the TCP request succeeds, the err will reset to nil - in, _, err = tcp.Exchange(m, ns) - } - - if err == nil { - break - } - } - return -} - -// lookupNameservers returns the authoritative nameservers for the given fqdn. -func lookupNameservers(fqdn string) ([]string, error) { - var authoritativeNss []string - - zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) - if err != nil { - return nil, fmt.Errorf("Could not determine the zone: %v", err) - } - - r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true) - if err != nil { - return nil, err - } - - for _, rr := range r.Answer { - if ns, ok := rr.(*dns.NS); ok { - authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) - } - } - - if len(authoritativeNss) > 0 { - return authoritativeNss, nil - } - return nil, fmt.Errorf("Could not determine authoritative nameservers") -} - -// 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. -func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) { - // Do we have it cached? - if zone, ok := fqdnToZone[fqdn]; ok { - return zone, nil - } - - labelIndexes := dns.Split(fqdn) - for _, index := range labelIndexes { - domain := fqdn[index:] - - in, err := dnsQuery(domain, dns.TypeSOA, nameservers, true) - if err != nil { - return "", err - } - - // Any response code other than NOERROR and NXDOMAIN is treated as error - if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess { - return "", fmt.Errorf("Unexpected response code '%s' for %s", - dns.RcodeToString[in.Rcode], domain) - } - - // Check if we got a SOA RR in the answer section - if in.Rcode == dns.RcodeSuccess { - - // CNAME records cannot/should not exist at the root of a zone. - // So we skip a domain when a CNAME is found. - if dnsMsgContainsCNAME(in) { - continue - } - - for _, ans := range in.Answer { - if soa, ok := ans.(*dns.SOA); ok { - zone := soa.Hdr.Name - fqdnToZone[fqdn] = zone - return zone, nil - } - } - } - } - - return "", fmt.Errorf("Could not find the start of authority") -} - -// dnsMsgContainsCNAME checks for a CNAME answer in msg -func dnsMsgContainsCNAME(msg *dns.Msg) bool { - for _, ans := range msg.Answer { - if _, ok := ans.(*dns.CNAME); ok { - return true - } - } - return false -} - -// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. -func ClearFqdnCache() { - fqdnToZone = map[string]string{} -} - -// ToFqdn converts the name into a fqdn appending a trailing dot. -func ToFqdn(name string) string { - n := len(name) - if n == 0 || name[n-1] == '.' { - return name - } - return name + "." -} - -// UnFqdn converts the fqdn into a name removing the trailing dot. -func UnFqdn(name string) string { - n := len(name) - if n != 0 && name[n-1] == '.' { - return name[:n-1] - } - return name -} diff --git a/acmev2/dns_challenge_manual.go b/acmev2/dns_challenge_manual.go deleted file mode 100644 index 240384e6..00000000 --- a/acmev2/dns_challenge_manual.go +++ /dev/null @@ -1,53 +0,0 @@ -package acme - -import ( - "bufio" - "fmt" - "os" -) - -const ( - dnsTemplate = "%s %d IN TXT \"%s\"" -) - -// DNSProviderManual is an implementation of the ChallengeProvider interface -type DNSProviderManual struct{} - -// NewDNSProviderManual returns a DNSProviderManual instance. -func NewDNSProviderManual() (*DNSProviderManual, error) { - return &DNSProviderManual{}, nil -} - -// 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) - - authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) - if err != nil { - return err - } - - logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone) - logf("[INFO] acme: %s", dnsRecord) - logf("[INFO] acme: Press 'Enter' when you are done") - - reader := bufio.NewReader(os.Stdin) - _, _ = reader.ReadString('\n') - return nil -} - -// 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, "...") - - authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) - if err != nil { - return err - } - - logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone) - logf("[INFO] acme: %s", dnsRecord) - return nil -} diff --git a/acmev2/dns_challenge_test.go b/acmev2/dns_challenge_test.go deleted file mode 100644 index e561739b..00000000 --- a/acmev2/dns_challenge_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package acme - -import ( - "bufio" - "crypto/rand" - "crypto/rsa" - "net/http" - "net/http/httptest" - "os" - "reflect" - "sort" - "strings" - "testing" - "time" -) - -var lookupNameserversTestsOK = []struct { - fqdn string - nss []string -}{ - {"books.google.com.ng.", - []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, - }, - {"www.google.com.", - []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, - }, - {"physics.georgetown.edu.", - []string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."}, - }, -} - -var lookupNameserversTestsErr = []struct { - fqdn string - error string -}{ - // invalid tld - {"_null.n0n0.", - "Could not determine the zone", - }, -} - -var findZoneByFqdnTests = []struct { - fqdn string - zone string -}{ - {"mail.google.com.", "google.com."}, // domain is a CNAME - {"foo.google.com.", "google.com."}, // domain is a non-existent subdomain - {"example.com.ac.", "ac."}, // domain is a eTLD - {"cross-zone-example.assets.sh.", "assets.sh."}, // domain is a cross-zone CNAME -} - -var checkAuthoritativeNssTests = []struct { - fqdn, value string - ns []string - ok bool -}{ - // TXT RR w/ expected value - {"8.8.8.8.asn.routeviews.org.", "151698.8.8.024", []string{"asnums.routeviews.org."}, - true, - }, - // No TXT RR - {"ns1.google.com.", "", []string{"ns2.google.com."}, - false, - }, -} - -var checkAuthoritativeNssTestsErr = []struct { - fqdn, value string - ns []string - error string -}{ - // TXT RR /w unexpected value - {"8.8.8.8.asn.routeviews.org.", "fe01=", []string{"asnums.routeviews.org."}, - "did not return the expected TXT record", - }, - // No TXT RR - {"ns1.google.com.", "fe01=", []string{"ns2.google.com."}, - "did not return the expected TXT record", - }, -} - -var checkResolvConfServersTests = []struct { - fixture string - expected []string - defaults []string -}{ - {"testdata/resolv.conf.1", []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"}, []string{"127.0.0.1:53"}}, - {"testdata/resolv.conf.nonexistant", []string{"127.0.0.1:53"}, []string{"127.0.0.1:53"}}, -} - -func TestDNSValidServerResponse(t *testing.T) { - PreCheckDNS = func(fqdn, value string) (bool, error) { - return true, nil - } - privKey, _ := rsa.GenerateKey(rand.Reader, 512) - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Replay-Nonce", "12345") - w.Write([]byte("{\"type\":\"dns01\",\"status\":\"valid\",\"uri\":\"http://some.url\",\"token\":\"http8\"}")) - })) - - manualProvider, _ := NewDNSProviderManual() - jws := &jws{privKey: privKey, getNonceURL: ts.URL} - solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider} - clientChallenge := challenge{Type: "dns01", Status: "pending", URL: ts.URL, Token: "http8"} - - go func() { - time.Sleep(time.Second * 2) - f := bufio.NewWriter(os.Stdout) - defer f.Flush() - f.WriteString("\n") - }() - - if err := solver.Solve(clientChallenge, "example.com"); err != nil { - t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err) - } -} - -func TestPreCheckDNS(t *testing.T) { - ok, err := PreCheckDNS("acme-staging.api.letsencrypt.org", "fe01=") - if err != nil || !ok { - t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org") - } -} - -func TestLookupNameserversOK(t *testing.T) { - for _, tt := range lookupNameserversTestsOK { - nss, err := lookupNameservers(tt.fqdn) - if err != nil { - t.Fatalf("#%s: got %q; want nil", tt.fqdn, err) - } - - sort.Strings(nss) - sort.Strings(tt.nss) - - if !reflect.DeepEqual(nss, tt.nss) { - t.Errorf("#%s: got %v; want %v", tt.fqdn, nss, tt.nss) - } - } -} - -func TestLookupNameserversErr(t *testing.T) { - for _, tt := range lookupNameserversTestsErr { - _, err := lookupNameservers(tt.fqdn) - if err == nil { - t.Fatalf("#%s: expected %q (error); got ", tt.fqdn, tt.error) - } - - if !strings.Contains(err.Error(), tt.error) { - t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err) - continue - } - } -} - -func TestFindZoneByFqdn(t *testing.T) { - for _, tt := range findZoneByFqdnTests { - res, err := FindZoneByFqdn(tt.fqdn, RecursiveNameservers) - if err != nil { - t.Errorf("FindZoneByFqdn failed for %s: %v", tt.fqdn, err) - } - if res != tt.zone { - t.Errorf("%s: got %s; want %s", tt.fqdn, res, tt.zone) - } - } -} - -func TestCheckAuthoritativeNss(t *testing.T) { - for _, tt := range checkAuthoritativeNssTests { - ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) - if ok != tt.ok { - t.Errorf("%s: got %t; want %t", tt.fqdn, ok, tt.ok) - } - } -} - -func TestCheckAuthoritativeNssErr(t *testing.T) { - for _, tt := range checkAuthoritativeNssTestsErr { - _, err := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) - if err == nil { - t.Fatalf("#%s: expected %q (error); got ", tt.fqdn, tt.error) - } - if !strings.Contains(err.Error(), tt.error) { - t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err) - continue - } - } -} - -func TestResolveConfServers(t *testing.T) { - for _, tt := range checkResolvConfServersTests { - result := getNameservers(tt.fixture, tt.defaults) - - sort.Strings(result) - sort.Strings(tt.expected) - if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("#%s: expected %q; got %q", tt.fixture, tt.expected, result) - } - } -} diff --git a/acmev2/error.go b/acmev2/error.go deleted file mode 100644 index 78694deb..00000000 --- a/acmev2/error.go +++ /dev/null @@ -1,91 +0,0 @@ -package acme - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" -) - -const ( - tosAgreementError = "Terms of service have changed" - invalidNonceError = "urn:ietf:params:acme:error:badNonce" -) - -// RemoteError is the base type for all errors specific to the ACME protocol. -type RemoteError struct { - StatusCode int `json:"status,omitempty"` - Type string `json:"type"` - Detail string `json:"detail"` -} - -func (e RemoteError) Error() string { - return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail) -} - -// TOSError represents the error which is returned if the user needs to -// accept the TOS. -// TODO: include the new TOS url if we can somehow obtain it. -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 -} - -// ObtainError is returned when there are specific errors available -// per domain. For example in ObtainCertificate -type ObtainError map[string]error - -func (e ObtainError) Error() string { - buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n") - for dom, err := range e { - buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err)) - } - return buffer.String() -} - -func handleHTTPError(resp *http.Response) error { - var errorDetail RemoteError - - contentType := resp.Header.Get("Content-Type") - if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") { - err := json.NewDecoder(resp.Body).Decode(&errorDetail) - if err != nil { - return err - } - } else { - detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) - if err != nil { - return err - } - errorDetail.Detail = string(detailBytes) - } - - errorDetail.StatusCode = resp.StatusCode - - // Check for errors we handle specifically - if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError { - return TOSError{errorDetail} - } - - if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError { - return NonceError{errorDetail} - } - - return errorDetail -} - -func handleChallengeError(chlng challenge) error { - return chlng.Error -} diff --git a/acmev2/http.go b/acmev2/http.go deleted file mode 100644 index b93e5344..00000000 --- a/acmev2/http.go +++ /dev/null @@ -1,160 +0,0 @@ -package acme - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "runtime" - "strings" - "time" -) - -// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. -var UserAgent string - -// HTTPClient is an HTTP client with a reasonable timeout value. -var HTTPClient = http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 15 * time.Second, - ResponseHeaderTimeout: 15 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, -} - -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 = "xenolf-acme" -) - -// httpHead performs a HEAD request with a proper User-Agent string. -// The response body (resp.Body) is already closed when this function returns. -func httpHead(url string) (resp *http.Response, err error) { - req, err := http.NewRequest("HEAD", url, nil) - if err != nil { - 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, fmt.Errorf("failed to do head %q: %v", url, err) - } - resp.Body.Close() - return resp, err -} - -// httpPost performs a POST request with a proper User-Agent string. -// Callers should close resp.Body when done reading from it. -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, fmt.Errorf("failed to post %q: %v", url, err) - } - req.Header.Set("Content-Type", bodyType) - req.Header.Set("User-Agent", userAgent()) - - return HTTPClient.Do(req) -} - -// httpGet performs a GET request with a proper User-Agent string. -// Callers should close resp.Body when done reading from it. -func httpGet(url string) (resp *http.Response, err error) { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to get %q: %v", url, err) - } - req.Header.Set("User-Agent", userAgent()) - - return HTTPClient.Do(req) -} - -// getJSON performs an HTTP GET request and parses the response body -// as JSON, into the provided respBody object. -func getJSON(uri string, respBody interface{}) (http.Header, error) { - resp, err := httpGet(uri) - if err != nil { - return nil, fmt.Errorf("failed to get json %q: %v", uri, err) - } - defer resp.Body.Close() - - if resp.StatusCode >= http.StatusBadRequest { - return resp.Header, handleHTTPError(resp) - } - - return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) -} - -// postJSON performs an HTTP POST request and parses the response body -// as JSON, into the provided respBody object. -func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) { - jsonBytes, err := json.Marshal(reqBody) - if err != nil { - return nil, errors.New("Failed to marshal network message") - } - - resp, err := j.post(uri, jsonBytes) - if err != nil { - return nil, fmt.Errorf("Failed to post JWS message. -> %v", err) - } - - defer resp.Body.Close() - - if resp.StatusCode >= http.StatusBadRequest { - - 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 { - return resp.Header, nil - } - - return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) -} - -// userAgent builds and returns the User-Agent string to use in requests. -func userAgent() string { - ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent) - return strings.TrimSpace(ua) -} diff --git a/acmev2/http_challenge.go b/acmev2/http_challenge.go deleted file mode 100644 index b6c969fe..00000000 --- a/acmev2/http_challenge.go +++ /dev/null @@ -1,41 +0,0 @@ -package acme - -import ( - "fmt" - "log" -) - -type httpChallenge struct { - jws *jws - validate validateFunc - 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) - - // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) - if err != nil { - return err - } - - err = s.provider.Present(domain, chlng.Token, keyAuth) - if err != nil { - return fmt.Errorf("[%s] error presenting token: %v", domain, err) - } - defer func() { - err := s.provider.CleanUp(domain, chlng.Token, keyAuth) - if err != nil { - log.Printf("[%s] error cleaning up: %v", domain, err) - } - }() - - return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) -} diff --git a/acmev2/http_challenge_server.go b/acmev2/http_challenge_server.go deleted file mode 100644 index 64c6a828..00000000 --- a/acmev2/http_challenge_server.go +++ /dev/null @@ -1,79 +0,0 @@ -package acme - -import ( - "fmt" - "net" - "net/http" - "strings" -) - -// HTTPProviderServer implements ChallengeProvider for `http-01` challenge -// It may be instantiated without using the NewHTTPProviderServer function if -// you want only to use the default values. -type HTTPProviderServer struct { - iface string - port string - done chan bool - listener net.Listener -} - -// NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port. -// Setting iface and / or port to an empty string will make the server fall back to -// the "any" interface and port 80 respectively. -func NewHTTPProviderServer(iface, port string) *HTTPProviderServer { - return &HTTPProviderServer{iface: iface, port: port} -} - -// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests. -func (s *HTTPProviderServer) 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 -} - -// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)` -func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error { - if s.listener == nil { - return nil - } - s.listener.Close() - <-s.done - return nil -} - -func (s *HTTPProviderServer) 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("[WARN] 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")) - } - }) - - httpServer := &http.Server{ - Handler: mux, - } - // Once httpServer is shut down we don't want any lingering - // connections, so disable KeepAlives. - httpServer.SetKeepAlivesEnabled(false) - httpServer.Serve(s.listener) - s.done <- true -} diff --git a/acmev2/http_challenge_test.go b/acmev2/http_challenge_test.go deleted file mode 100644 index 10f92028..00000000 --- a/acmev2/http_challenge_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package acme - -import ( - "crypto/rand" - "crypto/rsa" - "io/ioutil" - "strings" - "testing" -) - -func TestHTTPChallenge(t *testing.T) { - privKey, _ := rsa.GenerateKey(rand.Reader, 512) - j := &jws{privKey: privKey} - clientChallenge := challenge{Type: string(HTTP01), Token: "http1"} - mockValidate := func(_ *jws, _, _ string, chlng challenge) error { - uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token - resp, err := httpGet(uri) - if err != nil { - return err - } - defer resp.Body.Close() - - if want := "text/plain"; resp.Header.Get("Content-Type") != want { - t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want) - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - bodyStr := string(body) - - if bodyStr != chlng.KeyAuthorization { - t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization) - } - - return nil - } - solver := &httpChallenge{jws: j, validate: mockValidate, provider: &HTTPProviderServer{port: "23457"}} - - if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil { - t.Errorf("Solve error: got %v, want nil", err) - } -} - -func TestHTTPChallengeInvalidPort(t *testing.T) { - privKey, _ := rsa.GenerateKey(rand.Reader, 128) - j := &jws{privKey: privKey} - clientChallenge := challenge{Type: string(HTTP01), Token: "http2"} - solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}} - - if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { - t.Errorf("Solve error: got %v, want error", err) - } else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) { - t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want) - } -} diff --git a/acmev2/http_test.go b/acmev2/http_test.go deleted file mode 100644 index 33a48a33..00000000 --- a/acmev2/http_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package acme - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestHTTPHeadUserAgent(t *testing.T) { - var ua, method string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ua = r.Header.Get("User-Agent") - method = r.Method - })) - defer ts.Close() - - _, err := httpHead(ts.URL) - if err != nil { - t.Fatal(err) - } - - if method != "HEAD" { - t.Errorf("Expected method to be HEAD, got %s", method) - } - if !strings.Contains(ua, ourUserAgent) { - t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua) - } -} - -func TestHTTPGetUserAgent(t *testing.T) { - var ua, method string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ua = r.Header.Get("User-Agent") - method = r.Method - })) - defer ts.Close() - - res, err := httpGet(ts.URL) - if err != nil { - t.Fatal(err) - } - res.Body.Close() - - if method != "GET" { - t.Errorf("Expected method to be GET, got %s", method) - } - if !strings.Contains(ua, ourUserAgent) { - t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua) - } -} - -func TestHTTPPostUserAgent(t *testing.T) { - var ua, method string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ua = r.Header.Get("User-Agent") - method = r.Method - })) - defer ts.Close() - - res, err := httpPost(ts.URL, "text/plain", strings.NewReader("falalalala")) - if err != nil { - t.Fatal(err) - } - res.Body.Close() - - if method != "POST" { - t.Errorf("Expected method to be POST, got %s", method) - } - if !strings.Contains(ua, ourUserAgent) { - t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua) - } -} - -func TestUserAgent(t *testing.T) { - ua := userAgent() - - if !strings.Contains(ua, defaultGoUserAgent) { - t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua) - } - if !strings.Contains(ua, ourUserAgent) { - t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua) - } - if strings.HasSuffix(ua, " ") { - t.Errorf("UA should not have trailing spaces; got '%s'", ua) - } - - // customize the UA by appending a value - UserAgent = "MyApp/1.2.3" - ua = userAgent() - if !strings.Contains(ua, defaultGoUserAgent) { - t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua) - } - if !strings.Contains(ua, ourUserAgent) { - t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua) - } - if !strings.Contains(ua, UserAgent) { - t.Errorf("Expected custom UA to contain %s, got '%s'", UserAgent, ua) - } -} diff --git a/acmev2/jws.go b/acmev2/jws.go deleted file mode 100644 index 26fcb20d..00000000 --- a/acmev2/jws.go +++ /dev/null @@ -1,167 +0,0 @@ -package acme - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rsa" - "fmt" - "net/http" - "sync" - - "gopkg.in/square/go-jose.v2" -) - -type jws struct { - getNonceURL string - privKey crypto.PrivateKey - kid string - nonces nonceManager -} - -// 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(url, content) - if err != nil { - return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) - } - - data := bytes.NewBuffer([]byte(signedContent.FullSerialize())) - resp, err := httpPost(url, "application/jose+json", data) - if err != nil { - return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error()) - } - - nonce, nonceErr := getNonceFromResponse(resp) - if nonceErr == nil { - j.nonces.Push(nonce) - } - - return resp, nil -} - -func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) { - - var alg jose.SignatureAlgorithm - switch k := j.privKey.(type) { - case *rsa.PrivateKey: - alg = jose.RS256 - case *ecdsa.PrivateKey: - if k.Curve == elliptic.P256() { - alg = jose.ES256 - } else if k.Curve == elliptic.P384() { - alg = jose.ES384 - } - } - - jsonKey := jose.JSONWebKey{ - Key: j.privKey, - KeyID: j.kid, - } - - signKey := jose.SigningKey{ - Algorithm: alg, - Key: jsonKey, - } - options := jose.SignerOptions{ - NonceSource: j, - ExtraHeaders: make(map[jose.HeaderKey]interface{}), - } - options.ExtraHeaders["url"] = url - if j.kid == "" { - options.EmbedJWK = true - } - - signer, err := jose.NewSigner(signKey, &options) - if err != nil { - return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error()) - } - - signed, err := signer.Sign(content) - if err != nil { - return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) - } - return signed, nil -} - -func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { - jwk := jose.JSONWebKey{Key: j.privKey} - jwkJSON, err := jwk.Public().MarshalJSON() - if err != nil { - return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error()) - } - - signer, err := jose.NewSigner( - jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, - &jose.SignerOptions{ - EmbedJWK: false, - ExtraHeaders: map[jose.HeaderKey]interface{}{ - "kid": kid, - "url": url, - }, - }, - ) - if err != nil { - return nil, fmt.Errorf("Failed to create External Account Binding jose signer -> %s", err.Error()) - } - - signed, err := signer.Sign(jwkJSON) - if err != nil { - return nil, fmt.Errorf("Failed to External Account Binding sign content -> %s", err.Error()) - } - - return signed, nil -} - -func (j *jws) Nonce() (string, error) { - if nonce, ok := j.nonces.Pop(); ok { - return nonce, nil - } - - return getNonce(j.getNonceURL) -} - -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 nonce, nil -} diff --git a/acmev2/messages.go b/acmev2/messages.go deleted file mode 100644 index 696ffeb3..00000000 --- a/acmev2/messages.go +++ /dev/null @@ -1,106 +0,0 @@ -package acme - -import ( - "time" - "encoding/json" -) - -// RegistrationResource represents all important informations about a registration -// of which the client needs to keep track itself. -type RegistrationResource struct { - Body accountMessage `json:"body,omitempty"` - URI string `json:"uri,omitempty"` -} - -type directory struct { - NewNonceURL string `json:"newNonce"` - NewAccountURL string `json:"newAccount"` - NewOrderURL string `json:"newOrder"` - RevokeCertURL string `json:"revokeCert"` - KeyChangeURL string `json:"keyChange"` - Meta struct { - TermsOfService string `json:"termsOfService"` - Website string `json:"website"` - CaaIdentities []string `json:"caaIdentities"` - ExternalAccountRequired bool `json:"externalAccountRequired"` - } `json:"meta"` -} - -type accountMessage struct { - Status string `json:"status,omitempty"` - Contact []string `json:"contact,omitempty"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` - Orders string `json:"orders,omitempty"` - OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` - ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` -} - -type orderResource struct { - URL string `json:"url,omitempty"` - Domains []string `json:"domains,omitempty"` - orderMessage `json:"body,omitempty"` -} - -type orderMessage struct { - Status string `json:"status,omitempty"` - Expires string `json:"expires,omitempty"` - Identifiers []identifier `json:"identifiers"` - NotBefore string `json:"notBefore,omitempty"` - NotAfter string `json:"notAfter,omitempty"` - Authorizations []string `json:"authorizations,omitempty"` - Finalize string `json:"finalize,omitempty"` - Certificate string `json:"certificate,omitempty"` -} - -type authorization struct { - Status string `json:"status"` - Expires time.Time `json:"expires"` - Identifier identifier `json:"identifier"` - Challenges []challenge `json:"challenges"` -} - -type identifier struct { - Type string `json:"type"` - Value string `json:"value"` -} - -type challenge struct { - URL string `json:"url"` - Type string `json:"type"` - Status string `json:"status"` - Token string `json:"token"` - Validated time.Time `json:"validated"` - KeyAuthorization string `json:"keyAuthorization"` - Error RemoteError `json:"error"` -} - -type csrMessage struct { - Csr string `json:"csr"` -} - -type emptyObjectMessage struct { -} - -type revokeCertMessage struct { - Certificate string `json:"certificate"` -} - -type deactivateAuthMessage struct { - Status string `jsom:"status"` -} - -// CertificateResource represents a CA issued certificate. -// PrivateKey, Certificate and IssuerCertificate are all -// already PEM encoded and can be directly written to disk. -// Certificate may be a certificate bundle, depending on the -// options supplied to create it. -type CertificateResource struct { - Domain string `json:"domain"` - CertURL string `json:"certUrl"` - CertStableURL string `json:"certStableUrl"` - AccountRef string `json:"accountRef,omitempty"` - PrivateKey []byte `json:"-"` - Certificate []byte `json:"-"` - IssuerCertificate []byte `json:"-"` - CSR []byte `json:"-"` -} diff --git a/acmev2/pop_challenge.go b/acmev2/pop_challenge.go deleted file mode 100644 index 8d2a213b..00000000 --- a/acmev2/pop_challenge.go +++ /dev/null @@ -1 +0,0 @@ -package acme diff --git a/acmev2/provider.go b/acmev2/provider.go deleted file mode 100644 index d177ff07..00000000 --- a/acmev2/provider.go +++ /dev/null @@ -1,28 +0,0 @@ -package acme - -import "time" - -// ChallengeProvider enables implementing a custom challenge -// provider. Present 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 -} - -// ChallengeProviderTimeout allows for implementing a -// ChallengeProvider where an unusually long timeout is required when -// waiting for an ACME challenge to be satisfied, such as when -// checking for DNS record progagation. If an implementor of a -// ChallengeProvider provides a Timeout method, then the return values -// of the Timeout method will be used when appropriate by the acme -// package. The interval value is the time between checks. -// -// The default values used for timeout and interval are 60 seconds and -// 2 seconds respectively. These are used when no Timeout method is -// defined for the ChallengeProvider. -type ChallengeProviderTimeout interface { - ChallengeProvider - Timeout() (timeout, interval time.Duration) -} diff --git a/acmev2/testdata/resolv.conf.1 b/acmev2/testdata/resolv.conf.1 deleted file mode 100644 index 3098f99b..00000000 --- a/acmev2/testdata/resolv.conf.1 +++ /dev/null @@ -1,5 +0,0 @@ -domain company.com -nameserver 10.200.3.249 -nameserver 10.200.3.250:5353 -nameserver 2001:4860:4860::8844 -nameserver [10.0.0.1]:5353 diff --git a/acmev2/utils.go b/acmev2/utils.go deleted file mode 100644 index 2fa0db30..00000000 --- a/acmev2/utils.go +++ /dev/null @@ -1,29 +0,0 @@ -package acme - -import ( - "fmt" - "time" -) - -// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'. -func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { - var lastErr string - timeup := time.After(timeout) - for { - select { - case <-timeup: - return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr) - default: - } - - stop, err := f() - if stop { - return nil - } - if err != nil { - lastErr = err.Error() - } - - time.Sleep(interval) - } -} diff --git a/acmev2/utils_test.go b/acmev2/utils_test.go deleted file mode 100644 index 158af411..00000000 --- a/acmev2/utils_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package acme - -import ( - "testing" - "time" -) - -func TestWaitForTimeout(t *testing.T) { - c := make(chan error) - go func() { - err := WaitFor(3*time.Second, 1*time.Second, func() (bool, error) { - return false, nil - }) - c <- err - }() - - timeout := time.After(4 * time.Second) - select { - case <-timeout: - t.Fatal("timeout exceeded") - case err := <-c: - if err == nil { - t.Errorf("expected timeout error; got %v", err) - } - } -} diff --git a/cli.go b/cli.go index f8d85f58..95dd9e67 100644 --- a/cli.go +++ b/cli.go @@ -11,7 +11,7 @@ import ( "text/tabwriter" "github.com/urfave/cli" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // Logger is used to log errors; if nil, the default log.Logger is used. diff --git a/cli_handlers.go b/cli_handlers.go index dd71c9ed..3eeabf6e 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -15,7 +15,7 @@ import ( "time" "github.com/urfave/cli" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" "github.com/xenolf/lego/providers/dns" "github.com/xenolf/lego/providers/http/memcached" "github.com/xenolf/lego/providers/http/webroot" @@ -302,7 +302,6 @@ func run(c *cli.Context) error { } var cert *acme.CertificateResource - var err error if hasDomains { // obtain a certificate, generating a new private key diff --git a/configuration.go b/configuration.go index 4ee0b43b..f92c1fe9 100644 --- a/configuration.go +++ b/configuration.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/urfave/cli" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // Configuration type from CLI and config files. diff --git a/providers/dns/gandi/gandi_test.go b/providers/dns/gandi/gandi_test.go index 451333ca..c3899bd5 100644 --- a/providers/dns/gandi/gandi_test.go +++ b/providers/dns/gandi/gandi_test.go @@ -2,13 +2,10 @@ package gandi import ( "crypto" - "crypto/rand" - "crypto/rsa" "io" "io/ioutil" "net/http" "net/http/httptest" - "os" "regexp" "strings" "testing" @@ -92,61 +89,6 @@ func TestDNSProvider(t *testing.T) { } } -// TestDNSProviderLive performs a live test to obtain a certificate -// using the Let's Encrypt staging server. It runs provided that both -// the environment variables GANDI_API_KEY and GANDI_TEST_DOMAIN are -// set. Otherwise the test is skipped. -// -// To complete this test, go test must be run with the -timeout=40m -// flag, since the default timeout of 10m is insufficient. -func TestDNSProviderLive(t *testing.T) { - apiKey := os.Getenv("GANDI_API_KEY") - domain := os.Getenv("GANDI_TEST_DOMAIN") - if apiKey == "" || domain == "" { - t.Skip("skipping live test") - } - // create a user. - const rsaKeySize = 2048 - privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) - if err != nil { - t.Fatal(err) - } - myUser := user{ - Email: "test@example.com", - key: privateKey, - } - // create a client using staging server - client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048) - if err != nil { - t.Fatal(err) - } - provider, err := NewDNSProviderCredentials(apiKey) - if err != nil { - t.Fatal(err) - } - err = client.SetChallengeProvider(acme.DNS01, provider) - if err != nil { - t.Fatal(err) - } - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) - // register and agree tos - reg, err := client.Register() - if err != nil { - t.Fatal(err) - } - myUser.Registration = reg - err = client.AgreeToTOS() - if err != nil { - t.Fatal(err) - } - // complete the challenge - bundle := false - _, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false) - if len(failures) > 0 { - t.Fatal(failures) - } -} - // serverResponses is the XML-RPC Request->Response map used by the // fake RPC server. It was generated by recording a real RPC session // which resulted in the successful issue of a cert, and then diff --git a/providers/dns/gandiv5/gandiv5_test.go b/providers/dns/gandiv5/gandiv5_test.go index 56e63915..888ef92f 100644 --- a/providers/dns/gandiv5/gandiv5_test.go +++ b/providers/dns/gandiv5/gandiv5_test.go @@ -2,13 +2,10 @@ package gandiv5 import ( "crypto" - "crypto/rand" - "crypto/rsa" "io" "io/ioutil" "net/http" "net/http/httptest" - "os" "regexp" "strings" "testing" @@ -92,61 +89,6 @@ func TestDNSProvider(t *testing.T) { } } -// TestDNSProviderLive performs a live test to obtain a certificate -// using the Let's Encrypt staging server. It runs provided that both -// the environment variables GANDIV5_API_KEY and GANDI_TEST_DOMAIN are -// set. Otherwise the test is skipped. -// -// To complete this test, go test must be run with the -timeout=40m -// flag, since the default timeout of 10m is insufficient. -func TestDNSProviderLive(t *testing.T) { - apiKey := os.Getenv("GANDIV5_API_KEY") - domain := os.Getenv("GANDI_TEST_DOMAIN") - if apiKey == "" || domain == "" { - t.Skip("skipping live test") - } - // create a user. - const rsaKeySize = 2048 - privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) - if err != nil { - t.Fatal(err) - } - myUser := user{ - Email: "test@example.com", - key: privateKey, - } - // create a client using staging server - client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048) - if err != nil { - t.Fatal(err) - } - provider, err := NewDNSProviderCredentials(apiKey) - if err != nil { - t.Fatal(err) - } - err = client.SetChallengeProvider(acme.DNS01, provider) - if err != nil { - t.Fatal(err) - } - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) - // register and agree tos - reg, err := client.Register() - if err != nil { - t.Fatal(err) - } - myUser.Registration = reg - err = client.AgreeToTOS() - if err != nil { - t.Fatal(err) - } - // complete the challenge - bundle := false - _, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false) - if len(failures) > 0 { - t.Fatal(failures) - } -} - // serverResponses is the JSON Request->Response map used by the // fake JSON server. var serverResponses = map[string]string{