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) + } +}