Files
crypto/acme/autocert/autocert_test.go
T
Alex Vaghin 0e8c3a9068 acme/autocert: support ACME RFC 8555
The Manager now uses RFC 8555 implementation of Let's Encrypt by default.
Existing users need not do any manual upgrades. If you vendor
acme/autocert, it is enough to just rebuild your binaries at this CL.

If there's an account key stored in Manager's cache which has been used
with an earlier Let's Encrypt implementation (aka v1 or draft-02),
it will be automatically re-registered with the new endpoint.

One notable change is the CAServer from internal/acmetest was amended
to simulate a CA implementing RFC 8555, replacing draft-02.
Support for both RFC and draft-02 seemed too complicated and not worth
the benefits: the old pre-RFC bits will be removed from both acme and
acme/autocert packages at some point.

Fixes golang/go#21081

Change-Id: Id530758ac612b1c20f9df51c4d10f770e5f41ecf
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/199520
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
2019-10-11 19:05:13 +00:00

1234 lines
34 KiB
Go

// 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"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"io/ioutil"
"math/big"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"sync"
"testing"
"time"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert/internal/acmetest"
)
var (
exampleDomain = "example.org"
exampleCertKey = certKey{domain: exampleDomain}
exampleCertKeyRSA = certKey{domain: exampleDomain, isRSA: true}
)
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/tls-alpn-01",
"type": "tls-alpn-01",
"token": "token-alpn"
},
{
"uri": "{{.}}/challenge/dns-01",
"type": "dns-01",
"token": "token-dns-01"
},
{
"uri": "{{.}}/challenge/http-01",
"type": "http-01",
"token": "token-http-01"
}
]
}`))
type memCache struct {
t *testing.T
mu sync.Mutex
keyData map[string][]byte
}
func (m *memCache) Get(ctx context.Context, key string) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.keyData[key]
if !ok {
return nil, ErrCacheMiss
}
return v, nil
}
// filenameSafe returns whether all characters in s are printable ASCII
// and safe to use in a filename on most filesystems.
func filenameSafe(s string) bool {
for _, c := range s {
if c < 0x20 || c > 0x7E {
return false
}
switch c {
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
return false
}
}
return true
}
func (m *memCache) Put(ctx context.Context, key string, data []byte) error {
if !filenameSafe(key) {
m.t.Errorf("invalid characters in cache key %q", key)
}
m.mu.Lock()
defer m.mu.Unlock()
m.keyData[key] = data
return nil
}
func (m *memCache) Delete(ctx context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.keyData, key)
return nil
}
func newMemCache(t *testing.T) *memCache {
return &memCache{
t: t,
keyData: make(map[string][]byte),
}
}
func (m *memCache) numCerts() int {
m.mu.Lock()
defer m.mu.Unlock()
res := 0
for key := range m.keyData {
if strings.HasSuffix(key, "+token") ||
strings.HasSuffix(key, "+key") ||
strings.HasSuffix(key, "+http-01") {
continue
}
res++
}
return res
}
func dummyCert(pub interface{}, san ...string) ([]byte, error) {
return dateDummyCert(pub, time.Now(), time.Now().Add(90*24*time.Hour), san...)
}
func dateDummyCert(pub interface{}, start, end time.Time, san ...string) ([]byte, error) {
// use EC key to run faster on 386
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
t := &x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: start,
NotAfter: end,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment,
DNSNames: san,
}
if pub == nil {
pub = &key.PublicKey
}
return x509.CreateCertificate(rand.Reader, t, t, pub, key)
}
func decodePayload(v interface{}, r io.Reader) error {
var req struct{ Payload string }
if err := json.NewDecoder(r).Decode(&req); err != nil {
return err
}
payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
if err != nil {
return err
}
return json.Unmarshal(payload, v)
}
type algorithmSupport int
const (
algRSA algorithmSupport = iota
algECDSA
)
func clientHelloInfo(sni string, alg algorithmSupport) *tls.ClientHelloInfo {
hello := &tls.ClientHelloInfo{
ServerName: sni,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
}
if alg == algECDSA {
hello.CipherSuites = append(hello.CipherSuites, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305)
}
return hello
}
// tokenCertFn returns a function suitable for startACMEServerStub.
// The returned function simulates a TLS hello request from a CA
// during validation of a tls-alpn-01 challenge.
func tokenCertFn(man *Manager, alg algorithmSupport) getCertificateFunc {
return func(sni string) (*tls.Certificate, error) {
hello := clientHelloInfo(sni, alg)
hello.SupportedProtos = []string{acme.ALPNProto}
return man.GetCertificate(hello)
}
}
func TestGetCertificate(t *testing.T) {
man := &Manager{Prompt: AcceptTOS}
defer man.stopRenew()
hello := clientHelloInfo("example.org", algECDSA)
testGetCertificate(t, man, "example.org", hello)
}
func TestGetCertificate_trailingDot(t *testing.T) {
man := &Manager{Prompt: AcceptTOS}
defer man.stopRenew()
hello := clientHelloInfo("example.org.", algECDSA)
testGetCertificate(t, man, "example.org", hello)
}
func TestGetCertificate_unicodeIDN(t *testing.T) {
man := &Manager{Prompt: AcceptTOS}
defer man.stopRenew()
hello := clientHelloInfo("σσσ.com", algECDSA)
testGetCertificate(t, man, "xn--4xaaa.com", hello)
hello = clientHelloInfo("σςΣ.com", algECDSA)
testGetCertificate(t, man, "xn--4xaaa.com", hello)
}
func TestGetCertificate_mixedcase(t *testing.T) {
man := &Manager{Prompt: AcceptTOS}
defer man.stopRenew()
hello := clientHelloInfo("example.org", algECDSA)
testGetCertificate(t, man, "example.org", hello)
hello = clientHelloInfo("EXAMPLE.ORG", algECDSA)
testGetCertificate(t, man, "example.org", hello)
}
func TestGetCertificate_ForceRSA(t *testing.T) {
man := &Manager{
Prompt: AcceptTOS,
Cache: newMemCache(t),
ForceRSA: true,
}
defer man.stopRenew()
hello := clientHelloInfo(exampleDomain, algECDSA)
testGetCertificate(t, man, exampleDomain, hello)
// ForceRSA was deprecated and is now ignored.
cert, err := man.cacheGet(context.Background(), exampleCertKey)
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
if _, ok := cert.PrivateKey.(*ecdsa.PrivateKey); !ok {
t.Errorf("cert.PrivateKey is %T; want *ecdsa.PrivateKey", cert.PrivateKey)
}
}
func TestGetCertificate_nilPrompt(t *testing.T) {
man := &Manager{}
defer man.stopRenew()
url, finish := startACMEServerStub(t, tokenCertFn(man, algECDSA), "example.org")
defer finish()
man.Client = &acme.Client{DirectoryURL: url}
hello := clientHelloInfo("example.org", algECDSA)
if _, err := man.GetCertificate(hello); err == nil {
t.Error("got certificate for example.org; wanted error")
}
}
func TestGetCertificate_expiredCache(t *testing.T) {
// Make an expired cert and cache it.
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: exampleDomain},
NotAfter: time.Now(),
}
pub, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &pk.PublicKey, pk)
if err != nil {
t.Fatal(err)
}
tlscert := &tls.Certificate{
Certificate: [][]byte{pub},
PrivateKey: pk,
}
man := &Manager{Prompt: AcceptTOS, Cache: newMemCache(t)}
defer man.stopRenew()
if err := man.cachePut(context.Background(), exampleCertKey, tlscert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
// The expired cached cert should trigger a new cert issuance
// and return without an error.
hello := clientHelloInfo(exampleDomain, algECDSA)
testGetCertificate(t, man, exampleDomain, hello)
}
func TestGetCertificate_failedAttempt(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}))
defer ts.Close()
d := createCertRetryAfter
f := testDidRemoveState
defer func() {
createCertRetryAfter = d
testDidRemoveState = f
}()
createCertRetryAfter = 0
done := make(chan struct{})
testDidRemoveState = func(ck certKey) {
if ck != exampleCertKey {
t.Errorf("testDidRemoveState: domain = %v; want %v", ck, exampleCertKey)
}
close(done)
}
man := &Manager{
Prompt: AcceptTOS,
Client: &acme.Client{
DirectoryURL: ts.URL,
},
}
defer man.stopRenew()
hello := clientHelloInfo(exampleDomain, algECDSA)
if _, err := man.GetCertificate(hello); err == nil {
t.Error("GetCertificate: err is nil")
}
select {
case <-time.After(5 * time.Second):
t.Errorf("took too long to remove the %q state", exampleCertKey)
case <-done:
man.stateMu.Lock()
defer man.stateMu.Unlock()
if v, exist := man.state[exampleCertKey]; exist {
t.Errorf("state exists for %v: %+v", exampleCertKey, v)
}
}
}
// testGetCertificate_tokenCache tests the fallback of token certificate fetches
// to cache when Manager.certTokens misses.
// algorithmSupport refers to the CA when verifying the certificate token.
func testGetCertificate_tokenCache(t *testing.T, tokenAlg algorithmSupport) {
man1 := &Manager{
Cache: newMemCache(t),
Prompt: AcceptTOS,
}
defer man1.stopRenew()
man2 := &Manager{
Cache: man1.Cache,
Prompt: AcceptTOS,
}
defer man2.stopRenew()
// Send the verification request to a different Manager from the one that
// initiated the authorization, when they share caches.
url, finish := startACMEServerStub(t, tokenCertFn(man2, tokenAlg), "example.org")
defer finish()
man1.Client = &acme.Client{DirectoryURL: url}
man2.Client = &acme.Client{DirectoryURL: url}
hello := clientHelloInfo("example.org", algECDSA)
if _, err := man1.GetCertificate(hello); err != nil {
t.Error(err)
}
if _, err := man2.GetCertificate(hello); err != nil {
t.Error(err)
}
}
func TestGetCertificate_tokenCache(t *testing.T) {
t.Run("ecdsaSupport=true", func(t *testing.T) {
testGetCertificate_tokenCache(t, algECDSA)
})
t.Run("ecdsaSupport=false", func(t *testing.T) {
testGetCertificate_tokenCache(t, algRSA)
})
}
func TestGetCertificate_ecdsaVsRSA(t *testing.T) {
cache := newMemCache(t)
man := &Manager{Prompt: AcceptTOS, Cache: cache}
defer man.stopRenew()
url, finish := startACMEServerStub(t, tokenCertFn(man, algECDSA), "example.org")
defer finish()
man.Client = &acme.Client{DirectoryURL: url}
cert, err := man.GetCertificate(clientHelloInfo("example.org", algECDSA))
if err != nil {
t.Fatal(err)
}
if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok {
t.Error("an ECDSA client was served a non-ECDSA certificate")
}
cert, err = man.GetCertificate(clientHelloInfo("example.org", algRSA))
if err != nil {
t.Fatal(err)
}
if _, ok := cert.Leaf.PublicKey.(*rsa.PublicKey); !ok {
t.Error("a RSA client was served a non-RSA certificate")
}
if _, err := man.GetCertificate(clientHelloInfo("example.org", algECDSA)); err != nil {
t.Error(err)
}
if _, err := man.GetCertificate(clientHelloInfo("example.org", algRSA)); err != nil {
t.Error(err)
}
if numCerts := cache.numCerts(); numCerts != 2 {
t.Errorf("found %d certificates in cache; want %d", numCerts, 2)
}
}
func TestGetCertificate_wrongCacheKeyType(t *testing.T) {
cache := newMemCache(t)
man := &Manager{Prompt: AcceptTOS, Cache: cache}
defer man.stopRenew()
url, finish := startACMEServerStub(t, tokenCertFn(man, algECDSA), exampleDomain)
defer finish()
man.Client = &acme.Client{DirectoryURL: url}
// Make an RSA cert and cache it without suffix.
pk, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: exampleDomain},
NotAfter: time.Now().Add(90 * 24 * time.Hour),
}
pub, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &pk.PublicKey, pk)
if err != nil {
t.Fatal(err)
}
rsaCert := &tls.Certificate{
Certificate: [][]byte{pub},
PrivateKey: pk,
}
if err := man.cachePut(context.Background(), exampleCertKey, rsaCert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
// The RSA cached cert should be silently ignored and replaced.
cert, err := man.GetCertificate(clientHelloInfo(exampleDomain, algECDSA))
if err != nil {
t.Fatal(err)
}
if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok {
t.Error("an ECDSA client was served a non-ECDSA certificate")
}
if numCerts := cache.numCerts(); numCerts != 1 {
t.Errorf("found %d certificates in cache; want %d", numCerts, 1)
}
}
type getCertificateFunc func(domain string) (*tls.Certificate, error)
// startACMEServerStub runs an ACME server
// The domain argument is the expected domain name of a certificate request.
// TODO: Drop this in favour of x/crypto/acme/autocert/internal/acmetest.
func startACMEServerStub(t *testing.T, tokenCert getCertificateFunc, domain string) (url string, finish func()) {
verifyTokenCert := func() {
tlscert, err := tokenCert(domain)
if err != nil {
t.Errorf("verifyTokenCert: tokenCert(%q): %v", domain, err)
return
}
crt, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Errorf("verifyTokenCert: x509.ParseCertificate: %v", err)
}
if err := crt.VerifyHostname(domain); err != nil {
t.Errorf("verifyTokenCert: %v", err)
}
// TODO: Update OID to the latest value 1.3.6.1.5.5.7.1.31
// See https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1
oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
for _, x := range crt.Extensions {
if x.Id.Equal(oid) {
// No need to check the extension value here.
// This is done in acme package tests.
return
}
}
t.Error("verifyTokenCert: no id-pe-acmeIdentifier extension found")
}
// 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")
if r.Method == "HEAD" {
// a nonce request
return
}
switch r.URL.Path {
// discovery
case "/":
if err := discoTmpl.Execute(w, ca.URL); err != nil {
t.Errorf("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.Errorf("authzTmpl: %v", err)
}
// accept tls-alpn-01 challenge
case "/challenge/tls-alpn-01":
verifyTokenCert()
w.Write([]byte("{}"))
// authorization status
case "/authz/1":
w.Write([]byte(`{"status": "valid"}`))
// cert request
case "/new-cert":
var req struct {
CSR string `json:"csr"`
}
decodePayload(&req, r.Body)
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
csr, err := x509.ParseCertificateRequest(b)
if err != nil {
t.Errorf("new-cert: CSR: %v", err)
}
if csr.Subject.CommonName != domain {
t.Errorf("CommonName in CSR = %q; want %q", csr.Subject.CommonName, domain)
}
der, err := dummyCert(csr.PublicKey, domain)
if err != nil {
t.Errorf("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(nil, "ca")
if err != nil {
t.Errorf("ca-cert: dummyCert: %v", err)
}
w.Write(der)
default:
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
}
}))
finish = func() {
ca.Close()
// make sure token cert was removed
cancel := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
tick := time.NewTicker(100 * time.Millisecond)
defer tick.Stop()
for {
if _, err := tokenCert(domain); err != nil {
return
}
select {
case <-tick.C:
case <-cancel:
return
}
}
}()
select {
case <-done:
case <-time.After(5 * time.Second):
close(cancel)
t.Error("token cert was not removed")
<-done
}
}
return ca.URL, finish
}
// tests man.GetCertificate flow using the provided hello argument.
// The domain argument is the expected domain name of a certificate request.
func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) {
url, finish := startACMEServerStub(t, tokenCertFn(man, algECDSA), domain)
defer finish()
man.Client = &acme.Client{DirectoryURL: url}
// simulate tls.Config.GetCertificate
var tlscert *tls.Certificate
var err error
done := make(chan struct{})
go func() {
tlscert, err = man.GetCertificate(hello)
close(done)
}()
select {
case <-time.After(time.Minute):
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)
}
}
func TestVerifyHTTP01(t *testing.T) {
var (
http01 http.Handler
authzCount int // num. of created authorizations
didAcceptHTTP01 bool
)
verifyHTTPToken := func() {
r := httptest.NewRequest("GET", "/.well-known/acme-challenge/token-http-01", nil)
w := httptest.NewRecorder()
http01.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK)
}
if v := w.Body.String(); !strings.HasPrefix(v, "token-http-01.") {
t.Errorf("http token value = %q; want 'token-http-01.' prefix", v)
}
}
// ACME CA server stub, only the needed bits.
// TODO: Replace this with x/crypto/acme/autocert/internal/acmetest.
var ca *httptest.Server
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
if r.Method == "HEAD" {
// a nonce request
return
}
switch r.URL.Path {
// Discovery.
case "/":
if err := discoTmpl.Execute(w, ca.URL); err != nil {
t.Errorf("discoTmpl: %v", err)
}
// Client key registration.
case "/new-reg":
w.Write([]byte("{}"))
// New domain authorization.
case "/new-authz":
authzCount++
w.Header().Set("Location", fmt.Sprintf("%s/authz/%d", ca.URL, authzCount))
w.WriteHeader(http.StatusCreated)
if err := authzTmpl.Execute(w, ca.URL); err != nil {
t.Errorf("authzTmpl: %v", err)
}
// Reject tls-alpn-01.
case "/challenge/tls-alpn-01":
http.Error(w, "won't accept tls-sni-01", http.StatusBadRequest)
// Should not accept dns-01.
case "/challenge/dns-01":
t.Errorf("dns-01 challenge was accepted")
http.Error(w, "won't accept dns-01", http.StatusBadRequest)
// Accept http-01.
case "/challenge/http-01":
didAcceptHTTP01 = true
verifyHTTPToken()
w.Write([]byte("{}"))
// Authorization statuses.
case "/authz/1": // tls-alpn-01
w.Write([]byte(`{"status": "invalid"}`))
case "/authz/2": // http-01
w.Write([]byte(`{"status": "valid"}`))
default:
http.NotFound(w, r)
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
}
}))
defer ca.Close()
m := &Manager{
Client: &acme.Client{
DirectoryURL: ca.URL,
},
}
http01 = m.HTTPHandler(nil)
ctx := context.Background()
client, err := m.acmeClient(ctx)
if err != nil {
t.Fatalf("m.acmeClient: %v", err)
}
if err := m.verify(ctx, client, "example.org"); err != nil {
t.Errorf("m.verify: %v", err)
}
// Only tls-alpn-01 and http-01 must be accepted.
// The dns-01 challenge is unsupported.
if authzCount != 2 {
t.Errorf("authzCount = %d; want 2", authzCount)
}
if !didAcceptHTTP01 {
t.Error("did not accept http-01 challenge")
}
}
func TestRevokeFailedAuthz(t *testing.T) {
// Prefill authorization URIs expected to be revoked.
// The challenges are selected in a specific order,
// each tried within a newly created authorization.
// This means each authorization URI corresponds to a different challenge type.
revokedAuthz := map[string]bool{
"/authz/0": false, // tls-alpn-01
"/authz/1": false, // http-01
"/authz/2": false, // no viable challenge, but authz is created
}
var authzCount int // num. of created authorizations
var revokeCount int // num. of revoked authorizations
done := make(chan struct{}) // closed when revokeCount is 3
// ACME CA server stub, only the needed bits.
// TODO: Replace this with x/crypto/acme/autocert/internal/acmetest.
var ca *httptest.Server
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
if r.Method == "HEAD" {
// a nonce request
return
}
switch r.URL.Path {
// Discovery.
case "/":
if err := discoTmpl.Execute(w, ca.URL); err != nil {
t.Errorf("discoTmpl: %v", err)
}
// Client key registration.
case "/new-reg":
w.Write([]byte("{}"))
// New domain authorization.
case "/new-authz":
w.Header().Set("Location", fmt.Sprintf("%s/authz/%d", ca.URL, authzCount))
w.WriteHeader(http.StatusCreated)
if err := authzTmpl.Execute(w, ca.URL); err != nil {
t.Errorf("authzTmpl: %v", err)
}
authzCount++
// tls-alpn-01 challenge "accept" request.
case "/challenge/tls-alpn-01":
// Refuse.
http.Error(w, "won't accept tls-alpn-01 challenge", http.StatusBadRequest)
// http-01 challenge "accept" request.
case "/challenge/http-01":
// Refuse.
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"status":"invalid"}`))
// Authorization requests.
case "/authz/0", "/authz/1", "/authz/2":
// Revocation requests.
if r.Method == "POST" {
var req struct{ Status string }
if err := decodePayload(&req, r.Body); err != nil {
t.Errorf("%s: decodePayload: %v", r.URL, err)
}
switch req.Status {
case "deactivated":
revokedAuthz[r.URL.Path] = true
revokeCount++
if revokeCount >= 3 {
// Last authorization is revoked.
defer close(done)
}
default:
t.Errorf("%s: req.Status = %q; want 'deactivated'", r.URL, req.Status)
}
w.Write([]byte(`{"status": "invalid"}`))
return
}
// Authorization status requests.
w.Write([]byte(`{"status":"pending"}`))
default:
http.NotFound(w, r)
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
}
}))
defer ca.Close()
m := &Manager{
Client: &acme.Client{DirectoryURL: ca.URL},
}
m.HTTPHandler(nil) // enable http-01 challenge type
// Should fail and revoke 3 authorizations.
// The first 2 are tls-alpn-01 and http-01 challenges.
// The third time an authorization is created but no viable challenge is found.
// See revokedAuthz above for more explanation.
if _, err := m.createCert(context.Background(), exampleCertKey); err == nil {
t.Errorf("m.createCert returned nil error")
}
select {
case <-time.After(3 * time.Second):
t.Error("revocations took too long")
case <-done:
// revokeCount is at least 3.
}
for uri, ok := range revokedAuthz {
if !ok {
t.Errorf("%q authorization was not revoked", uri)
}
}
}
func TestHTTPHandlerDefaultFallback(t *testing.T) {
tt := []struct {
method, url string
wantCode int
wantLocation string
}{
{"GET", "http://example.org", 302, "https://example.org/"},
{"GET", "http://example.org/foo", 302, "https://example.org/foo"},
{"GET", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"},
{"GET", "http://example.org/?a=b", 302, "https://example.org/?a=b"},
{"GET", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"},
{"GET", "http://example.org:80/foo?a=b", 302, "https://example.org:443/foo?a=b"},
{"GET", "http://example.org:80/foo%20bar", 302, "https://example.org:443/foo%20bar"},
{"GET", "http://[2602:d1:xxxx::c60a]:1234", 302, "https://[2602:d1:xxxx::c60a]:443/"},
{"GET", "http://[2602:d1:xxxx::c60a]", 302, "https://[2602:d1:xxxx::c60a]/"},
{"GET", "http://[2602:d1:xxxx::c60a]/foo?a=b", 302, "https://[2602:d1:xxxx::c60a]/foo?a=b"},
{"HEAD", "http://example.org", 302, "https://example.org/"},
{"HEAD", "http://example.org/foo", 302, "https://example.org/foo"},
{"HEAD", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"},
{"HEAD", "http://example.org/?a=b", 302, "https://example.org/?a=b"},
{"HEAD", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"},
{"POST", "http://example.org", 400, ""},
{"PUT", "http://example.org", 400, ""},
{"GET", "http://example.org/.well-known/acme-challenge/x", 404, ""},
}
var m Manager
h := m.HTTPHandler(nil)
for i, test := range tt {
r := httptest.NewRequest(test.method, test.url, nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != test.wantCode {
t.Errorf("%d: w.Code = %d; want %d", i, w.Code, test.wantCode)
t.Errorf("%d: body: %s", i, w.Body.Bytes())
}
if v := w.Header().Get("Location"); v != test.wantLocation {
t.Errorf("%d: Location = %q; want %q", i, v, test.wantLocation)
}
}
}
func TestAccountKeyCache(t *testing.T) {
m := Manager{Cache: newMemCache(t)}
ctx := context.Background()
k1, err := m.accountKey(ctx)
if err != nil {
t.Fatal(err)
}
k2, err := m.accountKey(ctx)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(k1, k2) {
t.Errorf("account keys don't match: k1 = %#v; k2 = %#v", k1, k2)
}
}
func TestCache(t *testing.T) {
ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
cert, err := dummyCert(ecdsaKey.Public(), exampleDomain)
if err != nil {
t.Fatal(err)
}
ecdsaCert := &tls.Certificate{
Certificate: [][]byte{cert},
PrivateKey: ecdsaKey,
}
rsaKey, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal(err)
}
cert, err = dummyCert(rsaKey.Public(), exampleDomain)
if err != nil {
t.Fatal(err)
}
rsaCert := &tls.Certificate{
Certificate: [][]byte{cert},
PrivateKey: rsaKey,
}
man := &Manager{Cache: newMemCache(t)}
defer man.stopRenew()
ctx := context.Background()
if err := man.cachePut(ctx, exampleCertKey, ecdsaCert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
if err := man.cachePut(ctx, exampleCertKeyRSA, rsaCert); err != nil {
t.Fatalf("man.cachePut: %v", err)
}
res, err := man.cacheGet(ctx, exampleCertKey)
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
if res == nil || !bytes.Equal(res.Certificate[0], ecdsaCert.Certificate[0]) {
t.Errorf("man.cacheGet = %+v; want %+v", res, ecdsaCert)
}
res, err = man.cacheGet(ctx, exampleCertKeyRSA)
if err != nil {
t.Fatalf("man.cacheGet: %v", err)
}
if res == nil || !bytes.Equal(res.Certificate[0], rsaCert.Certificate[0]) {
t.Errorf("man.cacheGet = %+v; want %+v", res, rsaCert)
}
}
func TestHostWhitelist(t *testing.T) {
policy := HostWhitelist("example.com", "EXAMPLE.ORG", "*.example.net", "σςΣ.com")
tt := []struct {
host string
allow bool
}{
{"example.com", true},
{"example.org", true},
{"xn--4xaaa.com", true},
{"one.example.com", false},
{"two.example.org", false},
{"three.example.net", false},
{"dummy", false},
}
for i, test := range tt {
err := policy(nil, test.host)
if err != nil && test.allow {
t.Errorf("%d: policy(%q): %v; want nil", i, test.host, err)
}
if err == nil && !test.allow {
t.Errorf("%d: policy(%q): nil; want an error", i, test.host)
}
}
}
func TestValidCert(t *testing.T) {
key1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
key2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
key3, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal(err)
}
cert1, err := dummyCert(key1.Public(), "example.org")
if err != nil {
t.Fatal(err)
}
cert2, err := dummyCert(key2.Public(), "example.org")
if err != nil {
t.Fatal(err)
}
cert3, err := dummyCert(key3.Public(), "example.org")
if err != nil {
t.Fatal(err)
}
now := time.Now()
early, err := dateDummyCert(key1.Public(), now.Add(time.Hour), now.Add(2*time.Hour), "example.org")
if err != nil {
t.Fatal(err)
}
expired, err := dateDummyCert(key1.Public(), now.Add(-2*time.Hour), now.Add(-time.Hour), "example.org")
if err != nil {
t.Fatal(err)
}
tt := []struct {
ck certKey
key crypto.Signer
cert [][]byte
ok bool
}{
{certKey{domain: "example.org"}, key1, [][]byte{cert1}, true},
{certKey{domain: "example.org", isRSA: true}, key3, [][]byte{cert3}, true},
{certKey{domain: "example.org"}, key1, [][]byte{cert1, cert2, cert3}, true},
{certKey{domain: "example.org"}, key1, [][]byte{cert1, {1}}, false},
{certKey{domain: "example.org"}, key1, [][]byte{{1}}, false},
{certKey{domain: "example.org"}, key1, [][]byte{cert2}, false},
{certKey{domain: "example.org"}, key2, [][]byte{cert1}, false},
{certKey{domain: "example.org"}, key1, [][]byte{cert3}, false},
{certKey{domain: "example.org"}, key3, [][]byte{cert1}, false},
{certKey{domain: "example.net"}, key1, [][]byte{cert1}, false},
{certKey{domain: "example.org"}, key1, [][]byte{early}, false},
{certKey{domain: "example.org"}, key1, [][]byte{expired}, false},
{certKey{domain: "example.org", isRSA: true}, key1, [][]byte{cert1}, false},
{certKey{domain: "example.org"}, key3, [][]byte{cert3}, false},
}
for i, test := range tt {
leaf, err := validCert(test.ck, test.cert, test.key, now)
if err != nil && test.ok {
t.Errorf("%d: err = %v", i, err)
}
if err == nil && !test.ok {
t.Errorf("%d: err is nil", i)
}
if err == nil && test.ok && leaf == nil {
t.Errorf("%d: leaf is nil", i)
}
}
}
type cacheGetFunc func(ctx context.Context, key string) ([]byte, error)
func (f cacheGetFunc) Get(ctx context.Context, key string) ([]byte, error) {
return f(ctx, key)
}
func (f cacheGetFunc) Put(ctx context.Context, key string, data []byte) error {
return fmt.Errorf("unsupported Put of %q = %q", key, data)
}
func (f cacheGetFunc) Delete(ctx context.Context, key string) error {
return fmt.Errorf("unsupported Delete of %q", key)
}
func TestManagerGetCertificateBogusSNI(t *testing.T) {
m := Manager{
Prompt: AcceptTOS,
Cache: cacheGetFunc(func(ctx context.Context, key string) ([]byte, error) {
return nil, fmt.Errorf("cache.Get of %s", key)
}),
}
tests := []struct {
name string
wantErr string
}{
{"foo.com", "cache.Get of foo.com"},
{"foo.com.", "cache.Get of foo.com"},
{`a\b.com`, "acme/autocert: server name contains invalid character"},
{`a/b.com`, "acme/autocert: server name contains invalid character"},
{"", "acme/autocert: missing server name"},
{"foo", "acme/autocert: server name component count invalid"},
{".foo", "acme/autocert: server name component count invalid"},
{"foo.", "acme/autocert: server name component count invalid"},
{"fo.o", "cache.Get of fo.o"},
}
for _, tt := range tests {
_, err := m.GetCertificate(clientHelloInfo(tt.name, algECDSA))
got := fmt.Sprint(err)
if got != tt.wantErr {
t.Errorf("GetCertificate(SNI = %q) = %q; want %q", tt.name, got, tt.wantErr)
}
}
}
func TestCertRequest(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
// An extension from RFC7633. Any will do.
ext := pkix.Extension{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1},
Value: []byte("dummy"),
}
b, err := certRequest(key, "example.org", []pkix.Extension{ext}, "san.example.org")
if err != nil {
t.Fatalf("certRequest: %v", err)
}
r, err := x509.ParseCertificateRequest(b)
if err != nil {
t.Fatalf("ParseCertificateRequest: %v", err)
}
var found bool
for _, v := range r.Extensions {
if v.Id.Equal(ext.Id) {
found = true
break
}
}
if !found {
t.Errorf("want %v in Extensions: %v", ext, r.Extensions)
}
}
func TestSupportsECDSA(t *testing.T) {
tests := []struct {
CipherSuites []uint16
SignatureSchemes []tls.SignatureScheme
SupportedCurves []tls.CurveID
ecdsaOk bool
}{
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}, nil, nil, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, nil, nil, true},
// SignatureSchemes limits, not extends, CipherSuites
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, nil, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256,
}, nil, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, nil, true},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, []tls.CurveID{
tls.CurveP521,
}, false},
{[]uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}, []tls.SignatureScheme{
tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256,
}, []tls.CurveID{
tls.CurveP256,
tls.CurveP521,
}, true},
}
for i, tt := range tests {
result := supportsECDSA(&tls.ClientHelloInfo{
CipherSuites: tt.CipherSuites,
SignatureSchemes: tt.SignatureSchemes,
SupportedCurves: tt.SupportedCurves,
})
if result != tt.ecdsaOk {
t.Errorf("%d: supportsECDSA = %v; want %v", i, result, tt.ecdsaOk)
}
}
}
// TODO: add same end-to-end for http-01 challenge type.
func TestEndToEnd(t *testing.T) {
const domain = "example.org"
// ACME CA server
ca := acmetest.NewCAServer([]string{"tls-alpn-01"}, []string{domain})
defer ca.Close()
// User dummy server.
m := &Manager{
Prompt: AcceptTOS,
Client: &acme.Client{DirectoryURL: ca.URL},
}
us := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
us.TLS = &tls.Config{
NextProtos: []string{"http/1.1", acme.ALPNProto},
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := m.GetCertificate(hello)
if err != nil {
t.Errorf("m.GetCertificate: %v", err)
}
return cert, err
},
}
us.StartTLS()
defer us.Close()
// In TLS-ALPN challenge verification, CA connects to the domain:443 in question.
// Because the domain won't resolve in tests, we need to tell the CA
// where to dial to instead.
ca.Resolve(domain, strings.TrimPrefix(us.URL, "https://"))
// A client visiting user dummy server.
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: ca.Roots,
ServerName: domain,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(us.URL)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if v := string(b); v != "OK" {
t.Errorf("user server response: %q; want 'OK'", v)
}
}