package acmeserver import ( "bytes" "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "errors" "fmt" "io/ioutil" "log" "net/http" "path/filepath" "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") } 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: filepath.Join(config.Dir, "account.key"), 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. _, err = client.Register(ctx, ac, func(_ string) bool { return true }) if ae, ok := err.(*acme.Error); err == nil || ok && ae.StatusCode == http.StatusConflict { 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 } if err = a.verifyAll(ctx, client, c); err != nil { return nil, nil, err } csr, err := certRequest(key, c.Names) if err != nil { return nil, nil, err } der, _, err = client.CreateCert(ctx, csr, 0, 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) error { for _, domain := range c.Names { if err := a.verify(ctx, client, c, domain); err != nil { return err } } return nil } func (a *ACME) verify(ctx context.Context, client *acme.Client, c *certConfig, domain string) error { // Make an authorization request to the ACME server, and // verify that it returns a valid response with challenges. authz, err := client.Authorize(ctx, domain) if err != nil { return err } switch authz.Status { case acme.StatusValid: return nil // already authorized case acme.StatusInvalid: return fmt.Errorf("invalid authorization %q", authz.URI) } // 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(authz.Challenges, c) if chal == nil { return fmt.Errorf("unable to authorize %q", domain) } v, ok := a.validators[chal.Type] if !ok { return fmt.Errorf("challenge type '%s' is not available", chal.Type) } cleanup, err := v.Fulfill(ctx, client, domain, chal) if err != nil { return err } defer cleanup() // Tell the ACME server that we've accepted the challenge, and // then wait, possibly for some time, until there is an // authorization response (either successful or not) from the // server. if _, err = client.Accept(ctx, chal); err != nil { return err } _, err = client.WaitAuthorization(ctx, authz.URI) return err } // 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 }