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:
Alex Vaghin
2016-08-16 18:52:56 +00:00
parent 1ce41b6ca7
commit f0e11a3ccc
4 changed files with 1073 additions and 0 deletions
+607
View File
@@ -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
}
+278
View File
@@ -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)
}
}
+130
View File
@@ -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()
}
+58
View File
@@ -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)
}
}