package acmeserver import ( "bytes" "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "errors" "fmt" "io/ioutil" "log" "net/http" "golang.org/x/crypto/acme" ) const ( http01Challenge = "http-01" dns01Challenge = "dns-01" ) type validator interface { Fulfill(context.Context, *acme.Client, string, *acme.Challenge) (func(), error) } // ACME is a simple wrapper for an acme.Client that handles the // certificate creation/renewal workflow using http-01 or dns-01 // challenges. type ACME struct { email string accountKeyPath string directoryURL string client *acme.Client handler http.Handler validators map[string]validator defaultChallengeType string } // NewACME returns a new ACME object with the provided config. func NewACME(config *Config) (*ACME, error) { if config.Email == "" { return nil, errors.New("configuration parameter 'email' is unset") } if config.AccountKeyPath == "" { return nil, errors.New("configuration parameter 'account_key_path' is unset") } var h http.Handler v := make(map[string]validator) if config.HTTP.Enabled { httpv := newHTTPValidator() h = httpv v[http01Challenge] = httpv } if config.DNS.Enabled { dnsv, err := newDNSValidator(config) if err != nil { return nil, err } v[dns01Challenge] = dnsv } defaultChalType := config.DefaultChallengeType if defaultChalType == "" { defaultChalType = http01Challenge } if _, ok := v[defaultChalType]; !ok { return nil, fmt.Errorf("the default challenge type '%s' is not configured", defaultChalType) } directoryURL := config.DirectoryURL if directoryURL == "" { directoryURL = acme.LetsEncryptURL } return &ACME{ email: config.Email, accountKeyPath: config.AccountKeyPath, directoryURL: directoryURL, handler: h, validators: v, defaultChallengeType: defaultChalType, }, nil } // Return the account key, possibly initializing it if it does not exist. func (a *ACME) accountKey() (crypto.Signer, error) { if key, err := parsePrivateKeyFromFile(a.accountKeyPath); err == nil { return key, err } log.Printf("generating new account key") eckey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } var buf bytes.Buffer if err = encodeECDSAKey(&buf, eckey); err != nil { return nil, err } if err = ioutil.WriteFile(a.accountKeyPath, buf.Bytes(), 0600); err != nil { return nil, err } return eckey, err } // Return a new acme.Client, possibly initializing the account key if // it does not exist yet. The Client is created on the first call and // cached thereafter. func (a *ACME) acmeClient(ctx context.Context) (*acme.Client, error) { if a.client != nil { return a.client, nil } client := &acme.Client{ DirectoryURL: a.directoryURL, } key, err := a.accountKey() if err != nil { return nil, err } client.Key = key ac := &acme.Account{ Contact: []string{"mailto:" + a.email}, } // Register the account (accept TOS) if necessary. If the // account is already registered we get a StatusConflict, // which we can ignore. acct, err := client.Register(ctx, ac, func(_ string) bool { return true }) if ae, ok := err.(*acme.Error); err == nil || err == acme.ErrAccountAlreadyExists || (ok && ae.StatusCode == http.StatusConflict) { log.Printf("ACME account %s", acct.URI) a.client = client err = nil } return a.client, err } // GetCertificate returns a certificate chain (and the parsed leaf // cert), given a private key and a list of domains. The first domain // will be the subject CN, the others will be added as subjectAltNames. func (a *ACME) GetCertificate(ctx context.Context, key crypto.Signer, c *certConfig) (der [][]byte, leaf *x509.Certificate, err error) { client, err := a.acmeClient(ctx) if err != nil { return nil, nil, err } o, err := a.verifyAll(ctx, client, c) if err != nil { return nil, nil, err } csr, err := certRequest(key, c.Names) if err != nil { return nil, nil, err } der, _, err = client.CreateOrderCert(ctx, o.FinalizeURL, csr, true) if err != nil { return nil, nil, err } leaf, err = validCert(c.Names, der, key) if err != nil { return nil, nil, err } return der, leaf, nil } func (a *ACME) verifyAll(ctx context.Context, client *acme.Client, c *certConfig) (*acme.Order, error) { // Make an authorization request to the ACME server, and // verify that it returns a valid response with challenges. o, err := client.AuthorizeOrder(ctx, acme.DomainIDs(c.Names...)) if err != nil { return nil, fmt.Errorf("AuthorizeOrder failed: %v", err) } switch o.Status { case acme.StatusReady: return o, nil // already authorized case acme.StatusPending: default: return nil, fmt.Errorf("invalid new order status %q", o.Status) } for _, zurl := range o.AuthzURLs { z, err := client.GetAuthorization(ctx, zurl) if err != nil { return nil, fmt.Errorf("GetAuthorization(%s) failed: %v", zurl, err) } if z.Status != acme.StatusPending { continue } // Pick a challenge that matches our preferences and the // available validators. The validator fulfills the challenge, // and returns a cleanup function that we're going to call // before we return. All steps are sequential and idempotent. chal := a.pickChallenge(z.Challenges, c) if chal == nil { return nil, fmt.Errorf("unable to authorize %q", c.Names) } v, ok := a.validators[chal.Type] if !ok { return nil, fmt.Errorf("challenge type '%s' is not available", chal.Type) } for _, domain := range c.Names { cleanup, err := v.Fulfill(ctx, client, domain, chal) if err != nil { return nil, fmt.Errorf("fulfillment failed: %v", err) } defer cleanup() } if _, err := client.Accept(ctx, chal); err != nil { return nil, fmt.Errorf("challenge accept failed: %v", err) } if _, err := client.WaitAuthorization(ctx, z.URI); err != nil { return nil, fmt.Errorf("WaitAuthorization(%s) failed: %v", z.URI, err) } } // Authorizations are satisfied, wait for the CA // to update the order status. if _, err = client.WaitOrder(ctx, o.URI); err != nil { return nil, err } return o, nil } // Pick a challenge with the right type from the Challenge response // returned by the ACME server. We have a pretty specific idea of the // challenge type we want (either the default, or the ChallengeType // specified in the cert config), so we just search for that one. func (a *ACME) pickChallenge(chal []*acme.Challenge, c *certConfig) *acme.Challenge { ctype := a.defaultChallengeType if c.ChallengeType != "" { ctype = c.ChallengeType } for _, ch := range chal { if ch.Type == ctype { return ch } } return nil } // Handler returns the http.Handler that is used to satisfy the // http-01 validation requests. func (a *ACME) Handler() http.Handler { return a.handler }