summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/xenolf/lego/acme
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/xenolf/lego/acme')
-rw-r--r--vendor/github.com/xenolf/lego/acme/challenges.go16
-rw-r--r--vendor/github.com/xenolf/lego/acme/client.go804
-rw-r--r--vendor/github.com/xenolf/lego/acme/client_test.go198
-rw-r--r--vendor/github.com/xenolf/lego/acme/crypto.go332
-rw-r--r--vendor/github.com/xenolf/lego/acme/crypto_test.go93
-rw-r--r--vendor/github.com/xenolf/lego/acme/dns_challenge.go282
-rw-r--r--vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go53
-rw-r--r--vendor/github.com/xenolf/lego/acme/dns_challenge_test.go185
-rw-r--r--vendor/github.com/xenolf/lego/acme/error.go86
-rw-r--r--vendor/github.com/xenolf/lego/acme/http.go117
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_challenge.go41
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_challenge_server.go79
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_challenge_test.go57
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_test.go100
-rw-r--r--vendor/github.com/xenolf/lego/acme/jws.go115
-rw-r--r--vendor/github.com/xenolf/lego/acme/messages.go117
-rw-r--r--vendor/github.com/xenolf/lego/acme/pop_challenge.go1
-rw-r--r--vendor/github.com/xenolf/lego/acme/provider.go28
-rw-r--r--vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go67
-rw-r--r--vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go62
-rw-r--r--vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go65
-rw-r--r--vendor/github.com/xenolf/lego/acme/utils.go29
-rw-r--r--vendor/github.com/xenolf/lego/acme/utils_test.go26
23 files changed, 2953 insertions, 0 deletions
diff --git a/vendor/github.com/xenolf/lego/acme/challenges.go b/vendor/github.com/xenolf/lego/acme/challenges.go
new file mode 100644
index 000000000..857900507
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/challenges.go
@@ -0,0 +1,16 @@
+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")
+ // 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/vendor/github.com/xenolf/lego/acme/client.go b/vendor/github.com/xenolf/lego/acme/client.go
new file mode 100644
index 000000000..5eae8d26a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/client.go
@@ -0,0 +1,804 @@
+// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers.
+package acme
+
+import (
+ "crypto"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var (
+ // Logger is an optional custom logger.
+ Logger *log.Logger
+)
+
+// 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
+ issuerCert []byte
+ 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.NewRegURL == "" {
+ return nil, errors.New("directory missing new registration URL")
+ }
+ if dir.NewAuthzURL == "" {
+ return nil, errors.New("directory missing new authz URL")
+ }
+ if dir.NewCertURL == "" {
+ return nil, errors.New("directory missing new certificate URL")
+ }
+ if dir.RevokeCertURL == "" {
+ return nil, errors.New("directory missing revoke certificate URL")
+ }
+
+ jws := &jws{privKey: privKey, directoryURL: caDirURL}
+
+ // 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
+}
+
+// SetChallengeProvider specifies a custom provider that will make the solution available
+func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error {
+ switch challenge {
+ case HTTP01:
+ c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
+ case TLSSNI01:
+ c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p}
+ case DNS01:
+ c.solvers[challenge] = &dnsChallenge{jws: c.jws, 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.
+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
+}
+
+// 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.
+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.
+ for _, challenge := range challenges {
+ delete(c.solvers, challenge)
+ }
+}
+
+// Register the current account to the ACME server.
+func (c *Client) Register() (*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",
+ }
+ if c.user.GetEmail() != "" {
+ regMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
+ } else {
+ regMsg.Contact = []string{}
+ }
+
+ var serverReg Registration
+ var regURI string
+ hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &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")
+ }
+
+ return reg, nil
+}
+
+// DeleteRegistration deletes the client's user registration from the ACME
+// server.
+func (c *Client) DeleteRegistration() error {
+ if c == nil || c.user == nil {
+ return errors.New("acme: cannot unregister a nil client or user")
+ }
+ logf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
+
+ regMsg := registrationMessage{
+ Resource: "reg",
+ Delete: true,
+ }
+
+ _, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// QueryRegistration runs a POST request on the client's registration and
+// returns the result.
+//
+// This is similar to the Register function, but acting on an existing
+// registration link and resource.
+func (c *Client) QueryRegistration() (*RegistrationResource, error) {
+ if c == nil || c.user == nil {
+ return nil, errors.New("acme: cannot query the registration of a nil client or user")
+ }
+ // Log the URL here instead of the email as the email may not be set
+ logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
+
+ regMsg := registrationMessage{
+ Resource: "reg",
+ }
+
+ var serverReg Registration
+ hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg)
+ if err != nil {
+ return nil, err
+ }
+
+ reg := &RegistrationResource{Body: serverReg}
+
+ links := parseLinks(hdr["Link"])
+ // Location: header is not returned so this needs to be populated off of
+ // existing URI
+ reg.URI = c.user.GetRegistration().URI
+ if links["terms-of-service"] != "" {
+ reg.TosURL = links["terms-of-service"]
+ }
+
+ if links["next"] != "" {
+ reg.NewAuthzURL = links["next"]
+ } else {
+ return nil, errors.New("acme: No new-authz link in response to registration query")
+ }
+
+ return reg, nil
+}
+
+// AgreeToTOS updates the Client registration and sends the agreement to
+// the server.
+func (c *Client) AgreeToTOS() error {
+ 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.
+// If bundle is true, the []byte contains both the issuer certificate and
+// your issued certificate as a bundle.
+// This function will never return a partial certificate. If one domain in the list fails,
+// the whole certificate will fail.
+func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) {
+ // figure out what domains it concerns
+ // start with the common name
+ domains := []string{csr.Subject.CommonName}
+
+ // loop over the SubjectAltName DNS names
+DNSNames:
+ for _, sanName := range csr.DNSNames {
+ for _, existingName := range domains {
+ if existingName == sanName {
+ // duplicate; skip this name
+ continue DNSNames
+ }
+ }
+
+ // name is unique
+ domains = append(domains, sanName)
+ }
+
+ if bundle {
+ logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
+ } else {
+ logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
+ }
+
+ challenges, failures := c.getChallenges(domains)
+ // If any challenge fails - return. Do not generate partial SAN certificates.
+ if len(failures) > 0 {
+ return CertificateResource{}, failures
+ }
+
+ errs := c.solveChallenges(challenges)
+ // If any challenge fails - return. Do not generate partial SAN certificates.
+ if len(errs) > 0 {
+ return CertificateResource{}, errs
+ }
+
+ logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
+
+ cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil)
+ if err != nil {
+ for _, chln := range challenges {
+ failures[chln.Domain] = err
+ }
+ }
+
+ // Add the CSR to the certificate so that it can be used for renewals.
+ cert.CSR = pemEncode(&csr)
+
+ return cert, failures
+}
+
+// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
+// The first domain in domains is used for the CommonName field of the certificate, all other
+// domains are added using the Subject Alternate Names extension. A new private key is generated
+// 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) (CertificateResource, map[string]error) {
+ 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 {
+ return CertificateResource{}, failures
+ }
+
+ errs := c.solveChallenges(challenges)
+ // If any challenge fails - return. Do not generate partial SAN certificates.
+ if len(errs) > 0 {
+ return CertificateResource{}, errs
+ }
+
+ logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
+
+ cert, err := c.requestCertificate(challenges, bundle, privKey)
+ if err != nil {
+ for _, chln := range challenges {
+ failures[chln.Domain] = err
+ }
+ }
+
+ return cert, failures
+}
+
+// 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{Resource: "revoke-cert", 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 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
+ }
+
+ x509Cert := certificates[0]
+ if x509Cert.IsCA {
+ return CertificateResource{}, 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()))
+
+ // The first step of renewal is to check if we get a renewed cert
+ // directly from the cert URL.
+ resp, err := httpGet(cert.CertURL)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ defer resp.Body.Close()
+ serverCertBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ serverCert, err := x509.ParseCertificate(serverCertBytes)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ // If the server responds with a different certificate we are effectively renewed.
+ // TODO: Further test if we can actually use the new certificate (Our private key works)
+ if !x509Cert.Equal(serverCert) {
+ logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain)
+ issuedCert := pemEncode(derCertificateBytes(serverCertBytes))
+ // If bundle is true, we want to return a certificate bundle.
+ // To do this, we need the issuer certificate.
+ if bundle {
+ // The issuer certificate link is always supplied via an "up" link
+ // in the response headers of a new certificate.
+ links := parseLinks(resp.Header["Link"])
+ issuerCert, err := c.getIssuerCertificate(links["up"])
+ if err != nil {
+ // If we fail to acquire the issuer cert, return the issued certificate - do not fail.
+ logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err)
+ } else {
+ // Success - append the issuer cert to the issued cert.
+ issuerCert = pemEncode(derCertificateBytes(issuerCert))
+ issuedCert = append(issuedCert, issuerCert...)
+ }
+ }
+
+ cert.Certificate = issuedCert
+ return cert, nil
+ }
+
+ // If the certificate is the same, then we need to request a new certificate.
+ // Start by checking to see if the certificate was based off a CSR, and
+ // use that if it's defined.
+ if len(cert.CSR) > 0 {
+ csr, err := pemDecodeTox509CSR(cert.CSR)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
+ return newCert, failures[cert.Domain]
+ }
+
+ var privKey crypto.PrivateKey
+ if cert.PrivateKey != nil {
+ privKey, err = parsePEMPrivateKey(cert.PrivateKey)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ }
+
+ var domains []string
+ var failures map[string]error
+ // 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, failures := c.ObtainCertificate(domains, bundle, privKey)
+ return newCert, failures[cert.Domain]
+}
+
+// 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 {
+ // loop through the resources, basically through the domains.
+ failures := make(map[string]error)
+ for _, authz := range challenges {
+ if authz.Body.Status == "valid" {
+ // Boulder might recycle recent validated authz (see issue #267)
+ logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain)
+ continue
+ }
+ // no solvers - no solving
+ if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
+ for i, solver := range solvers {
+ // TODO: do not immediately fail if one domain fails to validate.
+ err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
+ if err != nil {
+ failures[authz.Domain] = err
+ }
+ }
+ } else {
+ failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain)
+ }
+ }
+
+ 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
+ }
+ }
+ return 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)
+
+ for _, domain := range domains {
+ go func(domain string) {
+ authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}}
+ var authz authorization
+ hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz)
+ if err != nil {
+ errc <- domainError{Domain: domain, Error: err}
+ return
+ }
+
+ links := parseLinks(hdr["Link"])
+ if links["next"] == "" {
+ logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
+ return
+ }
+
+ resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain}
+ }(domain)
+ }
+
+ responses := make(map[string]authorizationResource)
+ failures := make(map[string]error)
+ for i := 0; i < len(domains); i++ {
+ select {
+ case res := <-resc:
+ responses[res.Domain] = 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)
+ }
+ }
+
+ close(resc)
+ close(errc)
+
+ return challenges, failures
+}
+
+func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey) (CertificateResource, error) {
+ if len(authz) == 0 {
+ return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
+ }
+
+ var err error
+ if privKey == nil {
+ privKey, err = generatePrivateKey(c.keyType)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ }
+
+ // determine certificate name(s) based on the authorization resources
+ commonName := authz[0]
+ var san []string
+ for _, auth := range authz[1:] {
+ san = append(san, auth.Domain)
+ }
+
+ // TODO: should the CSR be customizable?
+ csr, err := generateCsr(privKey, commonName.Domain, san)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey))
+}
+
+func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) {
+ commonName := authz[0]
+
+ var authURLs []string
+ for _, auth := range authz[1:] {
+ authURLs = append(authURLs, auth.AuthURL)
+ }
+
+ csrString := base64.URLEncoding.EncodeToString(csr)
+ jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ resp, err := c.jws.post(commonName.NewCertURL, jsonBytes)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ cerRes := CertificateResource{
+ Domain: commonName.Domain,
+ CertURL: resp.Header.Get("Location"),
+ PrivateKey: privateKeyPem}
+
+ for {
+ switch resp.StatusCode {
+ case 201, 202:
+ cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
+ resp.Body.Close()
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ // The server returns a body with a length of zero if the
+ // certificate was not ready at the time this request completed.
+ // Otherwise the body is the certificate.
+ if len(cert) > 0 {
+
+ cerRes.CertStableURL = resp.Header.Get("Content-Location")
+ cerRes.AccountRef = c.user.GetRegistration().URI
+
+ issuedCert := pemEncode(derCertificateBytes(cert))
+ // If bundle is true, we want to return a certificate bundle.
+ // To do this, we need the issuer certificate.
+ if bundle {
+ // The issuer certificate link is always supplied via an "up" link
+ // in the response headers of a new certificate.
+ links := parseLinks(resp.Header["Link"])
+ issuerCert, err := c.getIssuerCertificate(links["up"])
+ if err != nil {
+ // If we fail to acquire the issuer cert, return the issued certificate - do not fail.
+ logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err)
+ } else {
+ // Success - append the issuer cert to the issued cert.
+ issuerCert = pemEncode(derCertificateBytes(issuerCert))
+ issuedCert = append(issuedCert, issuerCert...)
+ }
+ }
+
+ cerRes.Certificate = issuedCert
+ logf("[INFO][%s] Server responded with a certificate.", commonName.Domain)
+ return cerRes, nil
+ }
+
+ // The certificate was granted but is not yet issued.
+ // Check retry-after and loop.
+ ra := resp.Header.Get("Retry-After")
+ retryAfter, err := strconv.Atoi(ra)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter)
+ time.Sleep(time.Duration(retryAfter) * time.Second)
+
+ break
+ default:
+ return CertificateResource{}, handleHTTPError(resp)
+ }
+
+ resp, err = httpGet(cerRes.CertURL)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ }
+}
+
+// getIssuerCertificate requests the issuer certificate and caches it for
+// subsequent requests.
+func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
+ logf("[INFO] acme: Requesting issuer cert from %s", url)
+ if c.issuerCert != nil {
+ return c.issuerCert, nil
+ }
+
+ resp, err := httpGet(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = x509.ParseCertificate(issuerBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ c.issuerCert = issuerBytes
+ 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, chlng challenge) error {
+ var challengeResponse challenge
+
+ hdr, err := postJSON(j, uri, chlng, &challengeResponse)
+ 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 challengeResponse.Status {
+ case "valid":
+ logf("[INFO][%s] The server validated our request", domain)
+ return nil
+ case "pending":
+ break
+ case "invalid":
+ return handleChallengeError(challengeResponse)
+ 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 = 1
+ }
+ time.Sleep(time.Duration(ra) * time.Second)
+
+ hdr, err = getJSON(uri, &challengeResponse)
+ if err != nil {
+ return err
+ }
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/client_test.go b/vendor/github.com/xenolf/lego/acme/client_test.go
new file mode 100644
index 000000000..e309554f3
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/client_test.go
@@ -0,0 +1,198 @@
+package acme
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/json"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+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{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "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 := 2, 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{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "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))
+ client.SetTLSAddress(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))
+ 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 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, URI: "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"})
+
+ default:
+ http.Error(w, r.Method, http.StatusMethodNotAllowed)
+ }
+ }))
+ defer ts.Close()
+
+ privKey, _ := rsa.GenerateKey(rand.Reader, 512)
+ j := &jws{privKey: privKey, directoryURL: ts.URL}
+
+ tsts := []struct {
+ name string
+ statuses []string
+ want string
+ }{
+ {"POST-unexpected", []string{"weird"}, "unexpected"},
+ {"POST-valid", []string{"valid"}, ""},
+ {"POST-invalid", []string{"invalid"}, "Error Detail"},
+ {"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
+ {"GET-valid", []string{"pending", "valid"}, ""},
+ {"GET-invalid", []string{"pending", "invalid"}, "Error Detail"},
+ }
+
+ 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)
+ }
+ }
+}
+
+// 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/vendor/github.com/xenolf/lego/acme/crypto.go b/vendor/github.com/xenolf/lego/acme/crypto.go
new file mode 100644
index 000000000..af97f5d1e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/crypto.go
@@ -0,0 +1,332 @@
+package acme
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "math/big"
+ "net/http"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/ocsp"
+)
+
+// 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
+)
+
+// 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 := keyAsJWK(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.URLEncoding.EncodeToString(thumbBytes)
+ index := strings.Index(keyThumb, "=")
+ if index != -1 {
+ keyThumb = keyThumb[:index]
+ }
+
+ 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) ([]byte, error) {
+ template := x509.CertificateRequest{
+ Subject: pkix.Name{
+ CommonName: domain,
+ },
+ }
+
+ if len(san) > 0 {
+ template.DNSNames = san
+ }
+
+ 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/vendor/github.com/xenolf/lego/acme/crypto_test.go b/vendor/github.com/xenolf/lego/acme/crypto_test.go
new file mode 100644
index 000000000..d2fc5088b
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/crypto_test.go
@@ -0,0 +1,93 @@
+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)
+ 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/vendor/github.com/xenolf/lego/acme/dns_challenge.go b/vendor/github.com/xenolf/lego/acme/dns_challenge.go
new file mode 100644
index 000000000..c5fd354a1
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/dns_challenge.go
@@ -0,0 +1,282 @@
+package acme
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/publicsuffix"
+)
+
+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{}
+)
+
+var RecursiveNameservers = []string{
+ "google-public-dns-a.google.com:53",
+ "google-public-dns-b.google.com:53",
+}
+
+// DNSTimeout is used to override the default DNS timeout of 10 seconds.
+var DNSTimeout = 10 * time.Second
+
+// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge
+func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
+ keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
+ // base64URL encoding without padding
+ keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
+ value = strings.TrimRight(keyAuthSha, "=")
+ ttl = 120
+ fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
+ return
+}
+
+// dnsChallenge implements the dns-01 challenge according to ACME 7.5
+type dnsChallenge struct {
+ jws *jws
+ 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...", domain)
+
+ 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.URI, challenge{Resource: "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 suceeds, 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:]
+ // Give up if we have reached the TLD
+ if isTLD(domain) {
+ break
+ }
+
+ 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 {
+ 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")
+}
+
+func isTLD(domain string) bool {
+ publicsuffix, _ := publicsuffix.PublicSuffix(UnFqdn(domain))
+ if publicsuffix == UnFqdn(domain) {
+ 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/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go b/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
new file mode 100644
index 000000000..240384e60
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
@@ -0,0 +1,53 @@
+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/vendor/github.com/xenolf/lego/acme/dns_challenge_test.go b/vendor/github.com/xenolf/lego/acme/dns_challenge_test.go
new file mode 100644
index 000000000..6e448854b
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/dns_challenge_test.go
@@ -0,0 +1,185 @@
+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",
+ },
+ // invalid domain
+ {"_null.com.",
+ "Could not determine the zone",
+ },
+ // invalid domain
+ {"in-valid.co.uk.",
+ "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
+}
+
+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",
+ },
+}
+
+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, directoryURL: ts.URL}
+ solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
+ clientChallenge := challenge{Type: "dns01", Status: "pending", URI: 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 <nil>", 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 <nil>", 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
+ }
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/error.go b/vendor/github.com/xenolf/lego/acme/error.go
new file mode 100644
index 000000000..2aa690b33
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/error.go
@@ -0,0 +1,86 @@
+package acme
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+const (
+ tosAgreementError = "Must agree to subscriber agreement before any further actions"
+)
+
+// 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
+}
+
+type domainError struct {
+ Domain string
+ Error error
+}
+
+type challengeError struct {
+ RemoteError
+ records []validationRecord
+}
+
+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)
+ }
+
+ return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr)
+}
+
+func handleHTTPError(resp *http.Response) error {
+ var errorDetail RemoteError
+
+ contenType := resp.Header.Get("Content-Type")
+ // try to decode the content as JSON
+ if contenType == "application/json" || contenType == "application/problem+json" {
+ decoder := json.NewDecoder(resp.Body)
+ err := decoder.Decode(&errorDetail)
+ if err != nil {
+ return err
+ }
+ } else {
+ detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
+ 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}
+ }
+
+ return errorDetail
+}
+
+func handleChallengeError(chlng challenge) error {
+ return challengeError{chlng.Error, chlng.ValidationRecords}
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http.go b/vendor/github.com/xenolf/lego/acme/http.go
new file mode 100644
index 000000000..180db786d
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http.go
@@ -0,0 +1,117 @@
+package acme
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "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{Timeout: 10 * 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, err
+ }
+
+ req.Header.Set("User-Agent", userAgent())
+
+ resp, err = HTTPClient.Do(req)
+ if err != nil {
+ return resp, 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, 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, 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 %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 {
+ return resp.Header, handleHTTPError(resp)
+ }
+
+ 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/vendor/github.com/xenolf/lego/acme/http_challenge.go b/vendor/github.com/xenolf/lego/acme/http_challenge.go
new file mode 100644
index 000000000..95cb1fd81
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_challenge.go
@@ -0,0 +1,41 @@
+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.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http_challenge_server.go b/vendor/github.com/xenolf/lego/acme/http_challenge_server.go
new file mode 100644
index 000000000..42541380c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_challenge_server.go
@@ -0,0 +1,79 @@
+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("[INFO] Received request for domain %s with method %s", 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/vendor/github.com/xenolf/lego/acme/http_challenge_test.go b/vendor/github.com/xenolf/lego/acme/http_challenge_test.go
new file mode 100644
index 000000000..fdd8f4d27
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_challenge_test.go
@@ -0,0 +1,57 @@
+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: 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: 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 := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
+ t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http_test.go b/vendor/github.com/xenolf/lego/acme/http_test.go
new file mode 100644
index 000000000..33a48a331
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_test.go
@@ -0,0 +1,100 @@
+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/vendor/github.com/xenolf/lego/acme/jws.go b/vendor/github.com/xenolf/lego/acme/jws.go
new file mode 100644
index 000000000..f70513e38
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/jws.go
@@ -0,0 +1,115 @@
+package acme
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "fmt"
+ "net/http"
+ "sync"
+
+ "gopkg.in/square/go-jose.v1"
+)
+
+type jws struct {
+ directoryURL string
+ privKey crypto.PrivateKey
+ nonces []string
+ sync.Mutex
+}
+
+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
+ }
+}
+
+// Posts a JWS signed message to the specified URL
+func (j *jws) post(url string, content []byte) (*http.Response, error) {
+ signedContent, err := j.signContent(content)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
+ if err != nil {
+ return nil, err
+ }
+
+ j.getNonceFromResponse(resp)
+
+ return resp, err
+}
+
+func (j *jws) signContent(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
+ }
+ }
+
+ signer, err := jose.NewSigner(alg, j.privKey)
+ if err != nil {
+ return nil, err
+ }
+ signer.SetNonceSource(j)
+
+ signed, err := signer.Sign(content)
+ if err != nil {
+ return nil, err
+ }
+ return signed, nil
+}
+
+func (j *jws) getNonceFromResponse(resp *http.Response) error {
+ j.Lock()
+ defer j.Unlock()
+ nonce := resp.Header.Get("Replay-Nonce")
+ if nonce == "" {
+ return fmt.Errorf("Server did not respond with a proper nonce header.")
+ }
+
+ j.nonces = append(j.nonces, nonce)
+ return nil
+}
+
+func (j *jws) getNonce() error {
+ resp, err := httpHead(j.directoryURL)
+ if err != nil {
+ return err
+ }
+
+ return j.getNonceFromResponse(resp)
+}
+
+func (j *jws) Nonce() (string, error) {
+ nonce := ""
+ if len(j.nonces) == 0 {
+ err := j.getNonce()
+ if err != nil {
+ return nonce, err
+ }
+ }
+ if len(j.nonces) == 0 {
+ return "", fmt.Errorf("Can't get nonce")
+ }
+ j.Lock()
+ defer j.Unlock()
+ nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1]
+ return nonce, nil
+}
diff --git a/vendor/github.com/xenolf/lego/acme/messages.go b/vendor/github.com/xenolf/lego/acme/messages.go
new file mode 100644
index 000000000..0efeae674
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/messages.go
@@ -0,0 +1,117 @@
+package acme
+
+import (
+ "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 recoveryKeyMessage struct {
+ Length int `json:"length,omitempty"`
+ Client jose.JsonWebKey `json:"client,omitempty"`
+ Server jose.JsonWebKey `json:"client,omitempty"`
+}
+
+type registrationMessage struct {
+ Resource string `json:"resource"`
+ Contact []string `json:"contact"`
+ Delete bool `json:"delete,omitempty"`
+ // RecoveryKey recoveryKeyMessage `json:"recoveryKey,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"`
+ // RecoveryKey recoveryKeyMessage `json:"recoveryKey,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"`
+}
+
+type authorizationResource struct {
+ Body authorization
+ Domain string
+ NewCertURL string
+ AuthURL string
+}
+
+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"`
+}
+
+type identifier struct {
+ Type string `json:"type"`
+ 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"`
+}
+
+type csrMessage struct {
+ Resource string `json:"resource,omitempty"`
+ Csr string `json:"csr"`
+ Authorizations []string `json:"authorizations"`
+}
+
+type revokeCertMessage struct {
+ Resource string `json:"resource"`
+ Certificate string `json:"certificate"`
+}
+
+// CertificateResource represents a CA issued certificate.
+// PrivateKey and Certificate are both 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:"-"`
+ CSR []byte `json:"-"`
+}
diff --git a/vendor/github.com/xenolf/lego/acme/pop_challenge.go b/vendor/github.com/xenolf/lego/acme/pop_challenge.go
new file mode 100644
index 000000000..8d2a213b0
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/pop_challenge.go
@@ -0,0 +1 @@
+package acme
diff --git a/vendor/github.com/xenolf/lego/acme/provider.go b/vendor/github.com/xenolf/lego/acme/provider.go
new file mode 100644
index 000000000..d177ff07a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/provider.go
@@ -0,0 +1,28 @@
+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/vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go
new file mode 100644
index 000000000..34383cbfa
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go
@@ -0,0 +1,67 @@
+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/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go
new file mode 100644
index 000000000..df00fbb5a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go
@@ -0,0 +1,62 @@
+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/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go
new file mode 100644
index 000000000..3aec74565
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go
@@ -0,0 +1,65 @@
+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 := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
+ t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/utils.go b/vendor/github.com/xenolf/lego/acme/utils.go
new file mode 100644
index 000000000..2fa0db304
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/utils.go
@@ -0,0 +1,29 @@
+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/vendor/github.com/xenolf/lego/acme/utils_test.go b/vendor/github.com/xenolf/lego/acme/utils_test.go
new file mode 100644
index 000000000..158af4116
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/utils_test.go
@@ -0,0 +1,26 @@
+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)
+ }
+ }
+}