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 || err == acme.ErrAccountAlreadyExists || (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
}
o, err := a.verifyAll(ctx, client, c)
if err != nil {
der, _, err = client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
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...))
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)
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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
// 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