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>
diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go
new file mode 100644
index 0000000..09fb065
--- /dev/null
+++ b/acme/autocert/autocert.go
@@ -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
+}
diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go
new file mode 100644
index 0000000..6059feb
--- /dev/null
+++ b/acme/autocert/autocert_test.go
@@ -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)
+ }
+}
diff --git a/acme/autocert/cache.go b/acme/autocert/cache.go
new file mode 100644
index 0000000..1c67f6c
--- /dev/null
+++ b/acme/autocert/cache.go
@@ -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()
+}
diff --git a/acme/autocert/cache_test.go b/acme/autocert/cache_test.go
new file mode 100644
index 0000000..ad6d4a4
--- /dev/null
+++ b/acme/autocert/cache_test.go
@@ -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)
+ }
+}