acme/autocert: new high-level package for automatic cert management
Package autocert provides automatic access to certificates from Let's Encrypt and any other ACME-based CA. It is heavily based on the ideas from rsc.io/letsencrypt. Change-Id: I62021452a918cd49093162f3d6c74e9d7f452fb8 Reviewed-on: https://go-review.googlesource.com/23970 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package autocert provides automatic access to certificates from Let's Encrypt
|
||||
// and any other ACME-based CA.
|
||||
//
|
||||
// This package is a work in progress and makes no API stability promises.
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/internal/acme"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// AcceptTOS always returns true to indicate the acceptance of a CA Terms of Service
|
||||
// during account registration.
|
||||
func AcceptTOS(tosURL string) bool { return true }
|
||||
|
||||
// Manager is a stateful certificate manager built on top of acme.Client.
|
||||
// It obtains and refreshes certificates automatically,
|
||||
// as well as providing them to a TLS server via tls.Config.
|
||||
//
|
||||
// A simple usage example:
|
||||
//
|
||||
// m := autocert.Manager{Prompt: autocert.AcceptTOS}
|
||||
// s := &http.Server{
|
||||
// Addr: ":https",
|
||||
// TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
|
||||
// }
|
||||
// s.ListenAndServeTLS("", "")
|
||||
//
|
||||
// To preserve issued certificates and improve overall performance,
|
||||
// use a cache implementation of Cache. For instance, DirCache.
|
||||
type Manager struct {
|
||||
// Prompt specifies a callback function to conditionally accept a CA's Terms of Service (TOS).
|
||||
// The registration may require the caller to agree to the CA's TOS.
|
||||
// If so, Manager calls Prompt with a TOS URL provided by the CA. Prompt should report
|
||||
// whether the caller agrees to the terms.
|
||||
//
|
||||
// To always accept the terms, the callers can use AcceptTOS.
|
||||
Prompt func(tosURL string) bool
|
||||
|
||||
// Cache optionally stores and retrieves previously-obtained certificates.
|
||||
// If nil, certs will only be cached for the lifetime of the Manager.
|
||||
//
|
||||
// Manager passes the Cache certificates data encoded in PEM, with private/public
|
||||
// parts combined in a single Cache.Put call, private key first.
|
||||
Cache Cache
|
||||
|
||||
// DNSNames restricts Manager to work with only the specified domain names.
|
||||
// If the field is nil or empty, any domain name is allowed.
|
||||
// The elements of DNSNames must be sorted in lexical order.
|
||||
// Only exact matches are supported, no regexp or wildcard.
|
||||
DNSNames []string
|
||||
|
||||
// Client is used to perform low-level operations, such as account registration
|
||||
// and requesting new certificates.
|
||||
// If Client is nil, a zero-value acme.Client is used with acme.LetsEncryptURL
|
||||
// directory endpoint and a newly-generated 2048-bit RSA key.
|
||||
//
|
||||
// Mutating the field after the first call of GetCertificate method will have no effect.
|
||||
Client *acme.Client
|
||||
|
||||
// Email optionally specifies a contact email address.
|
||||
// This is used by CAs, such as Let's Encrypt, to notify about problems
|
||||
// with issued certificates.
|
||||
//
|
||||
// If the Client's account key is already registered, Email is not used.
|
||||
Email string
|
||||
|
||||
clientMu sync.Mutex
|
||||
client *acme.Client // initialized by acmeClient method
|
||||
|
||||
stateMu sync.Mutex
|
||||
state map[string]*certState // keyed by domain name
|
||||
|
||||
// tokenCert is keyed by token domain name, which matches server name
|
||||
// of ClientHello. Keys always have ".acme.invalid" suffix.
|
||||
tokenCertMu sync.RWMutex
|
||||
tokenCert map[string]*tls.Certificate
|
||||
}
|
||||
|
||||
// GetCertificate implements the tls.Config.GetCertificate hook.
|
||||
// It provides a TLS certificate for hello.ServerName host, including answering
|
||||
// *.acme.invalid (TLS-SNI) challenges. All other fields of hello are ignored.
|
||||
//
|
||||
// A simple usage can be shown as follows:
|
||||
//
|
||||
// s := &http.Server{
|
||||
// Addr: ":https",
|
||||
// TLSConfig: &tls.Config{
|
||||
// GetCertificate: m.GetCertificate,
|
||||
// },
|
||||
// }
|
||||
// s.ListenAndServeTLS("", "")
|
||||
//
|
||||
// If m.DNSNames is not empty and none of its elements match hello.ServerName exactly,
|
||||
// GetCertificate returns an error.
|
||||
func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
name := hello.ServerName
|
||||
if name == "" {
|
||||
return nil, errors.New("acme/autocert: missing server name")
|
||||
}
|
||||
|
||||
// check whether this is a token cert requested for TLS-SNI challenge
|
||||
if strings.HasSuffix(name, ".acme.invalid") {
|
||||
m.tokenCertMu.RLock()
|
||||
defer m.tokenCertMu.RUnlock()
|
||||
if cert := m.tokenCert[name]; cert != nil {
|
||||
return cert, nil
|
||||
}
|
||||
if cert, err := m.cacheGet(name); err == nil {
|
||||
return cert, nil
|
||||
}
|
||||
// TODO: cache error results?
|
||||
return nil, fmt.Errorf("acme/autocert: no token cert for %q", name)
|
||||
}
|
||||
|
||||
// check against allowed set of host names
|
||||
if len(m.DNSNames) > 0 {
|
||||
i := sort.SearchStrings(m.DNSNames, name)
|
||||
if i >= len(m.DNSNames) || m.DNSNames[i] != name {
|
||||
return nil, fmt.Errorf("acme/autocert: %q is not allowed", name)
|
||||
}
|
||||
}
|
||||
|
||||
// regular domain
|
||||
cert, err := m.cert(name)
|
||||
if err == nil {
|
||||
return cert, nil
|
||||
}
|
||||
if err != ErrCacheMiss {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// first-time
|
||||
ctx := context.Background() // TODO: use a deadline?
|
||||
cert, err = m.createCert(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.cachePut(name, cert)
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// cert returns an existing certificate either from m.state or cache.
|
||||
// If a certificate is found in cache but not in m.state, the latter will be filled
|
||||
// with the cached value.
|
||||
func (m *Manager) cert(name string) (*tls.Certificate, error) {
|
||||
m.stateMu.Lock()
|
||||
s, ok := m.state[name]
|
||||
if ok {
|
||||
m.stateMu.Unlock()
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return s.tlscert()
|
||||
}
|
||||
defer m.stateMu.Unlock()
|
||||
cert, err := m.cacheGet(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer, ok := cert.PrivateKey.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.New("acme/autocert: private key cannot sign")
|
||||
}
|
||||
if m.state == nil {
|
||||
m.state = make(map[string]*certState)
|
||||
}
|
||||
m.state[name] = &certState{
|
||||
key: signer,
|
||||
cert: cert.Certificate,
|
||||
leaf: cert.Leaf,
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// cacheGet always returns a valid certificate, or an error otherwise.
|
||||
func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
|
||||
if m.Cache == nil {
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
// TODO: might want to define a cache timeout on m
|
||||
ctx := context.Background()
|
||||
data, err := m.Cache.Get(ctx, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// private
|
||||
priv, pub := pem.Decode(data)
|
||||
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
|
||||
return nil, errors.New("acme/autocert: no private key found in cache")
|
||||
}
|
||||
privKey, err := parsePrivateKey(priv.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// public
|
||||
var pubDER []byte
|
||||
for len(pub) > 0 {
|
||||
var b *pem.Block
|
||||
b, pub = pem.Decode(pub)
|
||||
if b == nil {
|
||||
break
|
||||
}
|
||||
pubDER = append(pubDER, b.Bytes...)
|
||||
}
|
||||
if len(pub) > 0 {
|
||||
return nil, errors.New("acme/autocert: invalid public key")
|
||||
}
|
||||
|
||||
// parse public part(s) and verify the leaf is not expired
|
||||
// and corresponds to the private key
|
||||
x509Cert, err := x509.ParseCertificates(pubDER)
|
||||
if len(x509Cert) == 0 {
|
||||
return nil, errors.New("acme/autocert: no public key found in cache")
|
||||
}
|
||||
leaf := x509Cert[0]
|
||||
now := time.Now()
|
||||
if now.Before(leaf.NotBefore) {
|
||||
return nil, errors.New("acme/autocert: certificate is not valid yet")
|
||||
}
|
||||
if now.After(leaf.NotAfter) {
|
||||
return nil, errors.New("acme/autocert: expired certificate")
|
||||
}
|
||||
if !domainMatch(leaf, domain) {
|
||||
return nil, errors.New("acme/autocert: certificate does not match domain name")
|
||||
}
|
||||
switch pub := leaf.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
prv, ok := privKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("acme/autocert: private key type does not match public key type")
|
||||
}
|
||||
if pub.N.Cmp(prv.N) != 0 {
|
||||
return nil, errors.New("acme/autocert: private key does not match public key")
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
prv, ok := privKey.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("acme/autocert: private key type does not match public key type")
|
||||
}
|
||||
if pub.X.Cmp(prv.X) != 0 || pub.Y.Cmp(prv.Y) != 0 {
|
||||
return nil, errors.New("acme/autocert: private key does not match public key")
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("acme/autocert: unknown public key algorithm")
|
||||
}
|
||||
|
||||
tlscert := &tls.Certificate{
|
||||
Certificate: make([][]byte, len(x509Cert)),
|
||||
PrivateKey: privKey,
|
||||
Leaf: leaf,
|
||||
}
|
||||
for i, crt := range x509Cert {
|
||||
tlscert.Certificate[i] = crt.Raw
|
||||
}
|
||||
return tlscert, nil
|
||||
}
|
||||
|
||||
func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error {
|
||||
if m.Cache == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// contains PEM-encoded data
|
||||
var buf bytes.Buffer
|
||||
|
||||
// private
|
||||
switch key := tlscert.PrivateKey.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
b, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pb := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
|
||||
if err := pem.Encode(&buf, pb); err != nil {
|
||||
return err
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
b := x509.MarshalPKCS1PrivateKey(key)
|
||||
pb := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: b}
|
||||
if err := pem.Encode(&buf, pb); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("acme/autocert: unknown private key type")
|
||||
}
|
||||
|
||||
// public
|
||||
for _, b := range tlscert.Certificate {
|
||||
pb := &pem.Block{Type: "CERTIFICATE", Bytes: b}
|
||||
if err := pem.Encode(&buf, pb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: might want to define a cache timeout on m
|
||||
ctx := context.Background()
|
||||
return m.Cache.Put(ctx, domain, buf.Bytes())
|
||||
}
|
||||
|
||||
// createCert starts domain ownership verification and returns a certificate for that domain
|
||||
// upon success.
|
||||
//
|
||||
// If the domain is already being verified, it waits for the existing verification to complete.
|
||||
// Either way, createCert blocks for the duration of the whole process.
|
||||
func (m *Manager) createCert(ctx context.Context, domain string) (*tls.Certificate, error) {
|
||||
state, ok, err := m.certState(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// state may exist if another goroutine is already working on it
|
||||
// in which case just wait for it to finish
|
||||
if ok {
|
||||
state.RLock()
|
||||
defer state.RUnlock()
|
||||
return state.tlscert()
|
||||
}
|
||||
|
||||
// We are the first.
|
||||
// Unblock the readers when domain ownership is verified
|
||||
// and the we got the cert or the process failed.
|
||||
defer state.Unlock()
|
||||
// TODO: make m.verify retry or retry m.verify calls here
|
||||
if err := m.verify(ctx, domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := m.acmeClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csr, err := certRequest(state.key, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, _, err := client.CreateCert(ctx, csr, 0, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.cert = der
|
||||
return state.tlscert()
|
||||
}
|
||||
|
||||
// verify starts a new identifier (domain) authorization flow.
|
||||
// It prepares a challenge response and then blocks until the authorization
|
||||
// is marked as "completed" by the CA (either succeeded or failed).
|
||||
//
|
||||
// verify returns nil iff the verification was successful.
|
||||
func (m *Manager) verify(ctx context.Context, domain string) error {
|
||||
client, err := m.acmeClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start domain authorization and get the challenge
|
||||
authz, err := client.Authorize(ctx, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// pick a challenge: prefer tls-sni-02 over tls-sni-01
|
||||
// TODO: consider authz.Combinations
|
||||
var chal *acme.Challenge
|
||||
for _, c := range authz.Challenges {
|
||||
if c.Type == "tls-sni-02" {
|
||||
chal = c
|
||||
break
|
||||
}
|
||||
if c.Type == "tls-sni-01" {
|
||||
chal = c
|
||||
}
|
||||
}
|
||||
if chal == nil {
|
||||
return errors.New("acme/autocert: no supported challenge type found")
|
||||
}
|
||||
|
||||
// create a token cert for the challenge response
|
||||
var (
|
||||
cert tls.Certificate
|
||||
name string
|
||||
)
|
||||
switch chal.Type {
|
||||
case "tls-sni-01":
|
||||
cert, name, err = client.TLSSNI01ChallengeCert(chal.Token)
|
||||
case "tls-sni-02":
|
||||
cert, name, err = client.TLSSNI02ChallengeCert(chal.Token)
|
||||
default:
|
||||
err = fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.putTokenCert(name, &cert)
|
||||
defer func() {
|
||||
// verification has ended at this point
|
||||
// don't need token cert anymore
|
||||
go m.deleteTokenCert(name)
|
||||
}()
|
||||
|
||||
// ready to fulfill the challenge
|
||||
if _, err := client.Accept(ctx, chal); err != nil {
|
||||
return err
|
||||
}
|
||||
// wait for the CA to validate
|
||||
for {
|
||||
a, err := client.GetAuthz(ctx, authz.URI)
|
||||
if err == nil {
|
||||
if a.Status == acme.StatusValid {
|
||||
break
|
||||
}
|
||||
if a.Status == acme.StatusInvalid {
|
||||
return fmt.Errorf("acme/autocert: validation for domain %q failed", domain)
|
||||
}
|
||||
}
|
||||
// still pending
|
||||
d := time.Second
|
||||
if ae, ok := err.(*acme.Error); ok {
|
||||
d = retryAfter(ae.Header.Get("retry-after"))
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(d):
|
||||
// retry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// certState returns existing state or creates a new one locked for read/write.
|
||||
// The boolean return value indicates whether the state was found in m.state.
|
||||
func (m *Manager) certState(domain string) (*certState, bool, error) {
|
||||
m.stateMu.Lock()
|
||||
defer m.stateMu.Unlock()
|
||||
if m.state == nil {
|
||||
m.state = make(map[string]*certState)
|
||||
}
|
||||
// existing state
|
||||
if state, ok := m.state[domain]; ok {
|
||||
return state, true, nil
|
||||
}
|
||||
// new locked state
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
state := &certState{key: key}
|
||||
state.Lock()
|
||||
m.state[domain] = state
|
||||
return state, false, nil
|
||||
}
|
||||
|
||||
// putTokenCert stores the cert under the named key in both m.tokenCert map
|
||||
// and m.Cache.
|
||||
func (m *Manager) putTokenCert(name string, cert *tls.Certificate) {
|
||||
m.tokenCertMu.Lock()
|
||||
defer m.tokenCertMu.Unlock()
|
||||
if m.tokenCert == nil {
|
||||
m.tokenCert = make(map[string]*tls.Certificate)
|
||||
}
|
||||
m.tokenCert[name] = cert
|
||||
m.cachePut(name, cert)
|
||||
}
|
||||
|
||||
// deleteTokenCert removes the token certificate for the specified domain name
|
||||
// from both m.tokenCert map and m.Cache.
|
||||
func (m *Manager) deleteTokenCert(name string) {
|
||||
m.tokenCertMu.Lock()
|
||||
defer m.tokenCertMu.Unlock()
|
||||
delete(m.tokenCert, name)
|
||||
if m.Cache != nil {
|
||||
m.Cache.Delete(context.Background(), name)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) {
|
||||
m.clientMu.Lock()
|
||||
defer m.clientMu.Unlock()
|
||||
if m.client != nil {
|
||||
return m.client, nil
|
||||
}
|
||||
|
||||
client := m.Client
|
||||
if client == nil {
|
||||
client = &acme.Client{DirectoryURL: acme.LetsEncryptURL}
|
||||
}
|
||||
if client.Key == nil {
|
||||
var err error
|
||||
client.Key, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var contact []string
|
||||
if m.Email != "" {
|
||||
contact = []string{"mailto:" + m.Email}
|
||||
}
|
||||
a := &acme.Account{Contact: contact}
|
||||
_, err := client.Register(ctx, a, m.Prompt)
|
||||
if ae, ok := err.(*acme.Error); err == nil || ok && ae.StatusCode == http.StatusConflict {
|
||||
// conflict indicates the key is already registered
|
||||
m.client = client
|
||||
err = nil
|
||||
}
|
||||
return m.client, err
|
||||
}
|
||||
|
||||
// certState is ready when its mutex is unlocked for reading.
|
||||
type certState struct {
|
||||
sync.RWMutex
|
||||
key crypto.Signer
|
||||
cert [][]byte // DER encoding
|
||||
leaf *x509.Certificate // parsed cert[0]; may be nil
|
||||
}
|
||||
|
||||
// tlscert creates a tls.Certificate from s.key and s.cert.
|
||||
// Callers should wrap it in s.RLock() and s.RUnlock().
|
||||
func (s *certState) tlscert() (*tls.Certificate, error) {
|
||||
if s.key == nil {
|
||||
return nil, errors.New("acme/autocert: missing signer")
|
||||
}
|
||||
if len(s.cert) == 0 {
|
||||
return nil, errors.New("acme/autocert: missing certificate")
|
||||
}
|
||||
// TODO: compare pub.N with key.N or pub.{X,Y} for ECDSA?
|
||||
return &tls.Certificate{
|
||||
PrivateKey: s.key,
|
||||
Certificate: s.cert,
|
||||
Leaf: s.leaf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// certRequest creates a certificate request for the given common name cn
|
||||
// and optional SANs.
|
||||
func certRequest(key crypto.Signer, cn string, san ...string) ([]byte, error) {
|
||||
req := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: cn},
|
||||
DNSNames: san,
|
||||
}
|
||||
return x509.CreateCertificateRequest(rand.Reader, req, key)
|
||||
}
|
||||
|
||||
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
|
||||
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
|
||||
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
||||
//
|
||||
// Copied from crypto/tls/tls.go.
|
||||
func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey, *ecdsa.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.New("acme/autocert: found unknown private key type in PKCS#8 wrapping")
|
||||
}
|
||||
}
|
||||
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("acme/autocert: failed to parse private key")
|
||||
}
|
||||
|
||||
// domainMatch matches cert against the specified domain name.
|
||||
// It doesn't support wildcard.
|
||||
func domainMatch(cert *x509.Certificate, name string) bool {
|
||||
if cert.Subject.CommonName == name {
|
||||
return true
|
||||
}
|
||||
sort.Strings(cert.DNSNames)
|
||||
i := sort.SearchStrings(cert.DNSNames, name)
|
||||
return i < len(cert.DNSNames) && cert.DNSNames[i] == name
|
||||
}
|
||||
|
||||
func retryAfter(v string) time.Duration {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return time.Duration(i) * time.Second
|
||||
}
|
||||
if t, err := http.ParseTime(v); err == nil {
|
||||
return t.Sub(time.Now())
|
||||
}
|
||||
return time.Second
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme/internal/acme"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var discoTmpl = template.Must(template.New("disco").Parse(`{
|
||||
"new-reg": "{{.}}/new-reg",
|
||||
"new-authz": "{{.}}/new-authz",
|
||||
"new-cert": "{{.}}/new-cert"
|
||||
}`))
|
||||
|
||||
var authzTmpl = template.Must(template.New("authz").Parse(`{
|
||||
"status": "pending",
|
||||
"challenges": [
|
||||
{
|
||||
"uri": "{{.}}/challenge/1",
|
||||
"type": "tls-sni-01",
|
||||
"token": "token-01"
|
||||
},
|
||||
{
|
||||
"uri": "{{.}}/challenge/2",
|
||||
"type": "tls-sni-02",
|
||||
"token": "token-02"
|
||||
}
|
||||
]
|
||||
}`))
|
||||
|
||||
func dummyCert(san ...string) ([]byte, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
||||
DNSNames: san,
|
||||
}
|
||||
return x509.CreateCertificate(rand.Reader, t, t, &key.PublicKey, key)
|
||||
}
|
||||
|
||||
func TestGetCertificate(t *testing.T) {
|
||||
const domain = "example.org"
|
||||
man := &Manager{Prompt: AcceptTOS}
|
||||
|
||||
// echo token-02 | shasum -a 256
|
||||
// then divide result in 2 parts separated by dot
|
||||
tokenCertName := "4e8eb87631187e9ff2153b56b13a4dec.13a35d002e485d60ff37354b32f665d9.token.acme.invalid"
|
||||
verifyTokenCert := func() {
|
||||
hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
|
||||
_, err := man.GetCertificate(hello)
|
||||
if err != nil {
|
||||
t.Errorf("verifyTokenCert: GetCertificate(%q): %v", tokenCertName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ACME CA server stub
|
||||
var ca *httptest.Server
|
||||
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("replay-nonce", "nonce")
|
||||
switch r.URL.Path {
|
||||
// discovery
|
||||
case "/":
|
||||
if err := discoTmpl.Execute(w, ca.URL); err != nil {
|
||||
t.Fatalf("discoTmpl: %v", err)
|
||||
}
|
||||
// client key registration
|
||||
case "/new-reg":
|
||||
w.Write([]byte("{}"))
|
||||
// domain authorization
|
||||
case "/new-authz":
|
||||
w.Header().Set("location", ca.URL+"/authz/1")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := authzTmpl.Execute(w, ca.URL); err != nil {
|
||||
t.Fatalf("authzTmpl: %v", err)
|
||||
}
|
||||
// accept tls-sni-02 challenge
|
||||
case "/challenge/2":
|
||||
verifyTokenCert()
|
||||
w.Write([]byte("{}"))
|
||||
// authorization status
|
||||
case "/authz/1":
|
||||
w.Write([]byte(`{"status": "valid"}`))
|
||||
// cert request
|
||||
case "/new-cert":
|
||||
der, err := dummyCert(domain)
|
||||
if err != nil {
|
||||
t.Fatalf("new-cert: dummyCert: %v", err)
|
||||
}
|
||||
chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
|
||||
w.Header().Set("link", chainUp)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(der)
|
||||
// CA chain cert
|
||||
case "/ca-cert":
|
||||
der, err := dummyCert("ca")
|
||||
if err != nil {
|
||||
t.Fatalf("ca-cert: dummyCert: %v", err)
|
||||
}
|
||||
w.Write(der)
|
||||
default:
|
||||
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer ca.Close()
|
||||
man.Client = &acme.Client{DirectoryURL: ca.URL}
|
||||
|
||||
// simulate tls.Config.GetCertificate
|
||||
var (
|
||||
tlscert *tls.Certificate
|
||||
err error
|
||||
done = make(chan struct{})
|
||||
)
|
||||
go func() {
|
||||
hello := &tls.ClientHelloInfo{ServerName: domain}
|
||||
tlscert, err = man.GetCertificate(hello)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("man.GetCertificate took too long to return")
|
||||
case <-done:
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("man.GetCertificate: %v", err)
|
||||
}
|
||||
|
||||
// verify the tlscert is the same we responded with from the CA stub
|
||||
if len(tlscert.Certificate) == 0 {
|
||||
t.Fatal("len(tlscert.Certificate) is 0")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(tlscert.Certificate[0])
|
||||
if err != nil {
|
||||
t.Fatalf("x509.ParseCertificate: %v", err)
|
||||
}
|
||||
if len(cert.DNSNames) == 0 || cert.DNSNames[0] != domain {
|
||||
t.Errorf("cert.DNSNames = %v; want %q", cert.DNSNames, domain)
|
||||
}
|
||||
|
||||
// make sure token cert was removed
|
||||
done = make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
|
||||
if _, err := man.GetCertificate(hello); err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Error("token cert was not removed")
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
|
||||
type memCache map[string][]byte
|
||||
|
||||
func (m memCache) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (m memCache) Put(ctx context.Context, key string, data []byte) error {
|
||||
m[key] = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m memCache) Delete(ctx context.Context, key string) error {
|
||||
delete(m, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "example.org"},
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
}
|
||||
pub, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &privKey.PublicKey, privKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tlscert := &tls.Certificate{
|
||||
Certificate: [][]byte{pub},
|
||||
PrivateKey: privKey,
|
||||
}
|
||||
|
||||
cache := make(memCache)
|
||||
man := Manager{Cache: cache}
|
||||
if err := man.cachePut("example.org", tlscert); err != nil {
|
||||
t.Fatalf("man.cachePut: %v", err)
|
||||
}
|
||||
res, err := man.cacheGet("example.org")
|
||||
if err != nil {
|
||||
t.Fatalf("man.cacheGet: %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Fatal("res is nil")
|
||||
}
|
||||
|
||||
priv := x509.MarshalPKCS1PrivateKey(privKey)
|
||||
dummy, err := dummyCert("dummy")
|
||||
if err != nil {
|
||||
t.Fatalf("dummyCert: %v", err)
|
||||
}
|
||||
tt := []struct {
|
||||
key string
|
||||
prv, pub []byte
|
||||
}{
|
||||
{"dummy", priv, dummy},
|
||||
{"bad1", priv, []byte{1}},
|
||||
{"bad2", []byte{1}, pub},
|
||||
}
|
||||
for i, test := range tt {
|
||||
var buf bytes.Buffer
|
||||
pb := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: test.prv}
|
||||
if err := pem.Encode(&buf, pb); err != nil {
|
||||
t.Errorf("%d: pem.Encode: %v", i, err)
|
||||
}
|
||||
pb = &pem.Block{Type: "CERTIFICATE", Bytes: test.pub}
|
||||
if err := pem.Encode(&buf, pb); err != nil {
|
||||
t.Errorf("%d: pem.Encode: %v", i, err)
|
||||
}
|
||||
|
||||
cache.Put(nil, test.key, buf.Bytes())
|
||||
if _, err := man.cacheGet(test.key); err == nil {
|
||||
t.Errorf("%d: err is nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSNames(t *testing.T) {
|
||||
man := Manager{
|
||||
DNSNames: []string{"example.com"},
|
||||
// prevent network round-trips, just in case
|
||||
Client: &acme.Client{DirectoryURL: "dummy"},
|
||||
}
|
||||
hello := &tls.ClientHelloInfo{ServerName: "example.org"}
|
||||
_, err := man.GetCertificate(hello)
|
||||
if err == nil || !strings.Contains(err.Error(), "not allowed") {
|
||||
t.Errorf("err = %v; want 'not allowed'", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ErrCacheMiss is returned when a certificate is not found in cache.
|
||||
var ErrCacheMiss = errors.New("acme/autocert: certificate cache miss")
|
||||
|
||||
// Cache is used by Manager to store and retrieve previously obtained certificates
|
||||
// as opaque data.
|
||||
//
|
||||
// The key argument of the methods refers to a domain name but need not be an FQDN.
|
||||
// Cache implementations should not rely on the key naming pattern.
|
||||
type Cache interface {
|
||||
// Get returns a certificate data for the specified key.
|
||||
// If there's no such key, Get returns ErrCacheMiss.
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
|
||||
// Put stores the data in the cache under the specified key.
|
||||
// Inderlying implementations may use any data storage format,
|
||||
// as long as the reverse operation, Get, results in the original data.
|
||||
Put(ctx context.Context, key string, data []byte) error
|
||||
|
||||
// Delete removes a certificate data from the cache under the specified key.
|
||||
// If there's no such key in the cache, Delete returns nil.
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
// DirCache implements Cache using a directory on the local filesystem.
|
||||
// If the directory does not exist, it will be created with 0700 permissions.
|
||||
type DirCache string
|
||||
|
||||
// Get reads a certificate data from the specified file name.
|
||||
func (d DirCache) Get(ctx context.Context, name string) ([]byte, error) {
|
||||
name = filepath.Join(string(d), name)
|
||||
var (
|
||||
data []byte
|
||||
err error
|
||||
done = make(chan struct{})
|
||||
)
|
||||
go func() {
|
||||
data, err = ioutil.ReadFile(name)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-done:
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrCacheMiss
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
// Put writes the certificate data to the specified file name.
|
||||
// The file will be created with 0600 permissions.
|
||||
func (d DirCache) Put(ctx context.Context, name string, data []byte) error {
|
||||
if err := os.MkdirAll(string(d), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
var err error
|
||||
go func() {
|
||||
defer close(done)
|
||||
var tmp string
|
||||
if tmp, err = d.writeTempFile(name, data); err != nil {
|
||||
return
|
||||
}
|
||||
// prevent overwriting the file if the context was cancelled
|
||||
if ctx.Err() != nil {
|
||||
return // no need to set err
|
||||
}
|
||||
name = filepath.Join(string(d), name)
|
||||
err = os.Rename(tmp, name)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-done:
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete removes the specified file name.
|
||||
func (d DirCache) Delete(ctx context.Context, name string) error {
|
||||
name = filepath.Join(string(d), name)
|
||||
var (
|
||||
err error
|
||||
done = make(chan struct{})
|
||||
)
|
||||
go func() {
|
||||
err = os.Remove(name)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-done:
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeTempFile writes b to a temporary file, closes the file and returns its path.
|
||||
func (d DirCache) writeTempFile(prefix string, b []byte) (string, error) {
|
||||
// TempFile uses 0600 permissions
|
||||
f, err := ioutil.TempFile(string(d), prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := f.Write(b); err != nil {
|
||||
f.Close()
|
||||
return "", err
|
||||
}
|
||||
return f.Name(), f.Close()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// make sure DirCache satisfies Cache interface
|
||||
var _ Cache = DirCache("/")
|
||||
|
||||
func TestDirCache(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "autocert")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir = filepath.Join(dir, "certs") // a nonexistent dir
|
||||
cache := DirCache(dir)
|
||||
ctx := context.Background()
|
||||
|
||||
// test cache miss
|
||||
if _, err := cache.Get(ctx, "nonexistent"); err != ErrCacheMiss {
|
||||
t.Errorf("get: %v; want ErrCacheMiss", err)
|
||||
}
|
||||
|
||||
// test put/get
|
||||
b1 := []byte{1}
|
||||
if err := cache.Put(ctx, "dummy", b1); err != nil {
|
||||
t.Fatalf("put: %v", err)
|
||||
}
|
||||
b2, err := cache.Get(ctx, "dummy")
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(b1, b2) {
|
||||
t.Errorf("b1 = %v; want %v", b1, b2)
|
||||
}
|
||||
name := filepath.Join(dir, "dummy")
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// test delete
|
||||
if err := cache.Delete(ctx, "dummy"); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
if _, err := cache.Get(ctx, "dummy"); err != ErrCacheMiss {
|
||||
t.Errorf("get: %v; want ErrCacheMiss", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user