Newer
Older
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.
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
}
directoryURL: directoryURL,
handler: h,
validators: v,
defaultChallengeType: defaultChalType,
// 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{
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 != nil {
return nil, nil, err
}
der, _, err = client.CreateCert(ctx, csr, 0, true)
if err != nil {
return nil, nil, err
}
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 {
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)
// 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
}
// Handler returns the http.Handler that is used to satisfy the
// http-01 validation requests.
func (a *ACME) Handler() http.Handler {
return a.handler