package acmeserver import ( "bytes" "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "errors" "fmt" "io/ioutil" "log" "net/http" "path/filepath" "strings" "sync" "golang.org/x/crypto/acme" ) // ACME handles the certificate creation/renewal workflow using acme // http-01 challenges. It serves validation tokens under the // /.well-known/acme-challenge path when used as an HTTP handler. type ACME struct { email string accountKeyPath string client *acme.Client mx sync.Mutex httpTokens map[string][]byte } // 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") } return &ACME{ email: config.Email, accountKeyPath: filepath.Join(config.Dir, "account.key"), httpTokens: make(map[string][]byte), }, nil } func (a *ACME) accountKey(ctx context.Context) (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 } func (a *ACME) acmeClient(ctx context.Context) (*acme.Client, error) { if a.client != nil { return a.client, nil } client := &acme.Client{ DirectoryURL: acme.LetsEncryptURL, } key, err := a.accountKey(ctx) if err != nil { return nil, err } client.Key = key ac := &acme.Account{ Contact: []string{"mailto:" + a.email}, } _, 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, domains []string) (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, domains); err != nil { return nil, nil, err } csr, err := certRequest(key, domains) 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(domains, der, key) if err != nil { return nil, nil, err } return der, leaf, nil } func (a *ACME) verifyAll(ctx context.Context, client *acme.Client, domains []string) error { for _, domain := range domains { if err := a.verify(ctx, client, domain); err != nil { return err } } return nil } func (a *ACME) verify(ctx context.Context, client *acme.Client, domain string) error { 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) } chal := pickChallenge("http-01", authz.Challenges) if chal == nil { return fmt.Errorf("unable to authorize %q", domain) } cleanup, err := a.fulfill(ctx, client, chal) if err != nil { return err } defer cleanup() if _, err = client.Accept(ctx, chal); err != nil { return err } _, err = client.WaitAuthorization(ctx, authz.URI) return err } func (a *ACME) fulfill(ctx context.Context, client *acme.Client, chal *acme.Challenge) (cleanup func(), err error) { if chal.Type != "http-01" { return nil, errors.New("unsupported challenge type") } resp, err := client.HTTP01ChallengeResponse(chal.Token) if err != nil { return nil, err } p := client.HTTP01ChallengePath(chal.Token) a.putHTTPToken(ctx, p, resp) return func() { go a.deleteHTTPToken(p) }, nil } func (a *ACME) httpToken(ctx context.Context, tokenPath string) ([]byte, error) { a.mx.Lock() defer a.mx.Unlock() if v, ok := a.httpTokens[tokenPath]; ok { return v, nil } return nil, errors.New("token not found") } func (a *ACME) putHTTPToken(ctx context.Context, tokenPath, val string) { a.mx.Lock() a.httpTokens[tokenPath] = []byte(val) a.mx.Unlock() } func (a *ACME) deleteHTTPToken(tokenPath string) { a.mx.Lock() delete(a.httpTokens, tokenPath) a.mx.Unlock() } func (a *ACME) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { http.NotFound(w, r) return } a.mx.Lock() defer a.mx.Unlock() data, err := a.httpToken(r.Context(), r.URL.Path) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } w.Write(data) }