Skip to content
Snippets Groups Projects
acme.go 6.27 KiB
Newer Older
ale's avatar
ale committed
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"
)

ale's avatar
ale committed
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.
ale's avatar
ale committed
type ACME struct {
	email          string
	accountKeyPath string
ale's avatar
ale committed
	directoryURL   string
ale's avatar
ale committed
	client         *acme.Client

ale's avatar
ale committed
	handler              http.Handler
	validators           map[string]validator
	defaultChallengeType string
ale's avatar
ale committed
}

// 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")
	}

ale's avatar
ale committed
	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
	}

ale's avatar
ale committed
	return &ACME{
ale's avatar
ale committed
		email:                config.Email,
		accountKeyPath:       filepath.Join(config.Dir, "account.key"),
		directoryURL:         directoryURL,
		handler:              h,
		validators:           v,
		defaultChallengeType: defaultChalType,
ale's avatar
ale committed
	}, nil
}

ale's avatar
ale committed
// Return the account key, possibly initializing it if it does not exist.
func (a *ACME) accountKey() (crypto.Signer, error) {
ale's avatar
ale committed
	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
}

ale's avatar
ale committed
// 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.
ale's avatar
ale committed
func (a *ACME) acmeClient(ctx context.Context) (*acme.Client, error) {
	if a.client != nil {
		return a.client, nil
	}

	client := &acme.Client{
ale's avatar
ale committed
		DirectoryURL: a.directoryURL,
ale's avatar
ale committed
	}
ale's avatar
ale committed
	key, err := a.accountKey()
ale's avatar
ale committed
	if err != nil {
		return nil, err
	}
	client.Key = key
	ac := &acme.Account{
		Contact: []string{"mailto:" + a.email},
	}
ale's avatar
ale committed

	// Register the account (accept TOS) if necessary. If the
	// account is already registered we get a StatusConflict,
	// which we can ignore.
ale's avatar
ale committed
	_, 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.
ale's avatar
ale committed
func (a *ACME) GetCertificate(ctx context.Context, key crypto.Signer, c *certConfig) (der [][]byte, leaf *x509.Certificate, err error) {
ale's avatar
ale committed
	client, err := a.acmeClient(ctx)
	if err != nil {
		return nil, nil, err
	}

ale's avatar
ale committed
	if err = a.verifyAll(ctx, client, c); err != nil {
ale's avatar
ale committed
		return nil, nil, err
	}

ale's avatar
ale committed
	csr, err := certRequest(key, c.Names)
ale's avatar
ale committed
	if err != nil {
		return nil, nil, err
	}
	der, _, err = client.CreateCert(ctx, csr, 0, true)
	if err != nil {
		return nil, nil, err
	}
ale's avatar
ale committed
	leaf, err = validCert(c.Names, der, key)
ale's avatar
ale committed
	if err != nil {
		return nil, nil, err
	}
	return der, leaf, nil
}

ale's avatar
ale committed
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 {
ale's avatar
ale committed
			return err
		}
	}
	return nil
}

ale's avatar
ale committed
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.
ale's avatar
ale committed
	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)
	}

ale's avatar
ale committed
	// 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)
ale's avatar
ale committed
	if chal == nil {
		return fmt.Errorf("unable to authorize %q", domain)
	}
ale's avatar
ale committed
	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)
ale's avatar
ale committed
	if err != nil {
		return err
	}
	defer cleanup()

ale's avatar
ale committed
	// 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.
ale's avatar
ale committed
	if _, err = client.Accept(ctx, chal); err != nil {
		return err
	}
	_, err = client.WaitAuthorization(ctx, authz.URI)
	return err
}

ale's avatar
ale committed
// 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
		}
ale's avatar
ale committed
	}
ale's avatar
ale committed
	return nil
ale's avatar
ale committed
}

ale's avatar
ale committed
// Handler returns the http.Handler that is used to satisfy the
// http-01 validation requests.
func (a *ACME) Handler() http.Handler {
	return a.handler
ale's avatar
ale committed
}