Previously Manager.createCert() read state.locked without holding any lock, while the owner goroutine wrote state.locked = false under the state's write lock. Concurrent goroutines for the same domain raced on that field. We fix the issue by removing the certState.locked field and instead having Manager.certState() return an additional owner bool computed under the m.stateMu lock. This allows the Manager.createCert() caller to know whether it must block on the read lock and return the state handled by another goroutine, or instead do the work itself and release the write lock. A regression unit test is included that fails under -race without the fix, and passes with the fix in-place. Fixes golang/go#80119 Change-Id: I0f4c5c98f7e6bcf1ab71a3c0707bb0c7ce73415b Reviewed-on: https://go-review.googlesource.com/c/crypto/+/793840 Reviewed-by: Roland Shoemaker <roland@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Carlos Amedee <carlos@golang.org> Auto-Submit: Daniel McCarney <daniel@binaryparadox.net>
1024 lines
28 KiB
Go
1024 lines
28 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"
|
|
"fmt"
|
|
"io"
|
|
"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}
|
|
)
|
|
|
|
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: randomSerial(),
|
|
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 randomSerial() *big.Int {
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 32))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return serial
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func testManager(t *testing.T) *Manager {
|
|
man := &Manager{
|
|
Prompt: AcceptTOS,
|
|
Cache: newMemCache(t),
|
|
}
|
|
t.Cleanup(man.stopRenew)
|
|
return man
|
|
}
|
|
|
|
func TestGetCertificate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hello *tls.ClientHelloInfo
|
|
domain string
|
|
expectError string
|
|
prepare func(t *testing.T, man *Manager, s *acmetest.CAServer)
|
|
verify func(t *testing.T, man *Manager, leaf *x509.Certificate)
|
|
disableALPN bool
|
|
disableHTTP bool
|
|
}{
|
|
{
|
|
name: "ALPN",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
disableHTTP: true,
|
|
},
|
|
{
|
|
name: "HTTP",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
disableALPN: true,
|
|
},
|
|
{
|
|
name: "nilPrompt",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
man.Prompt = nil
|
|
},
|
|
expectError: "missing Manager.Prompt",
|
|
},
|
|
{
|
|
name: "trailingDot",
|
|
hello: clientHelloInfo("example.org.", algECDSA),
|
|
domain: "example.org",
|
|
},
|
|
{
|
|
name: "unicodeIDN",
|
|
hello: clientHelloInfo("éé.com", algECDSA),
|
|
domain: "xn--9caa.com",
|
|
},
|
|
{
|
|
name: "unicodeIDN/mixedCase",
|
|
hello: clientHelloInfo("éÉ.com", algECDSA),
|
|
domain: "xn--9caa.com",
|
|
},
|
|
{
|
|
name: "upperCase",
|
|
hello: clientHelloInfo("EXAMPLE.ORG", algECDSA),
|
|
domain: "example.org",
|
|
},
|
|
{
|
|
name: "goodCache",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
// Make a valid cert and cache it.
|
|
c := s.Start().LeafCert(exampleDomain, "ECDSA",
|
|
// Use a time before the Let's Encrypt revocation cutoff to also test
|
|
// that non-Let's Encrypt certificates are not renewed.
|
|
time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC),
|
|
)
|
|
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
|
|
t.Fatalf("man.cachePut: %v", err)
|
|
}
|
|
},
|
|
// Break the server to check that the cache is used.
|
|
disableALPN: true, disableHTTP: true,
|
|
},
|
|
{
|
|
name: "expiredCache",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
// Make an expired cert and cache it.
|
|
c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now().Add(-10*time.Minute), time.Now().Add(-5*time.Minute))
|
|
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
|
|
t.Fatalf("man.cachePut: %v", err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "forceRSA",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
man.ForceRSA = true
|
|
},
|
|
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
|
|
if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok {
|
|
t.Errorf("leaf.PublicKey is %T; want *ecdsa.PublicKey", leaf.PublicKey)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "goodLetsEncrypt",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
// Make a valid certificate issued after the TLS-ALPN-01
|
|
// revocation window and cache it.
|
|
s.IssuerName(pkix.Name{Country: []string{"US"},
|
|
Organization: []string{"Let's Encrypt"}, CommonName: "R3"})
|
|
c := s.Start().LeafCert(exampleDomain, "ECDSA",
|
|
time.Date(2022, time.January, 26, 12, 0, 0, 0, time.UTC),
|
|
time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC),
|
|
)
|
|
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
|
|
t.Fatalf("man.cachePut: %v", err)
|
|
}
|
|
},
|
|
// Break the server to check that the cache is used.
|
|
disableALPN: true, disableHTTP: true,
|
|
},
|
|
{
|
|
name: "revokedLetsEncrypt",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
// Make a certificate issued during the TLS-ALPN-01
|
|
// revocation window and cache it.
|
|
s.IssuerName(pkix.Name{Country: []string{"US"},
|
|
Organization: []string{"Let's Encrypt"}, CommonName: "R3"})
|
|
c := s.Start().LeafCert(exampleDomain, "ECDSA",
|
|
time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC),
|
|
)
|
|
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
|
|
t.Fatalf("man.cachePut: %v", err)
|
|
}
|
|
},
|
|
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
|
|
if leaf.NotBefore.Before(time.Now().Add(-10 * time.Minute)) {
|
|
t.Error("certificate was not reissued")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
// TestGetCertificate/tokenCache tests the fallback of token
|
|
// certificate fetches to cache when Manager.certTokens misses.
|
|
name: "tokenCacheALPN",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
// Make a separate manager with a shared cache, simulating
|
|
// separate nodes that serve requests for the same domain.
|
|
man2 := testManager(t)
|
|
man2.Cache = man.Cache
|
|
// Redirect the verification request to man2, although the
|
|
// client request will hit man, testing that they can complete a
|
|
// verification by communicating through the cache.
|
|
s.ResolveGetCertificate("example.org", man2.GetCertificate)
|
|
},
|
|
// Drop the default verification paths.
|
|
disableALPN: true,
|
|
},
|
|
{
|
|
name: "tokenCacheHTTP",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
man2 := testManager(t)
|
|
man2.Cache = man.Cache
|
|
s.ResolveHandler("example.org", man2.HTTPHandler(nil))
|
|
},
|
|
disableHTTP: true,
|
|
},
|
|
{
|
|
name: "ecdsa",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
|
|
if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok {
|
|
t.Error("an ECDSA client was served a non-ECDSA certificate")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "rsa",
|
|
hello: clientHelloInfo("example.org", algRSA),
|
|
domain: "example.org",
|
|
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
|
|
if _, ok := leaf.PublicKey.(*rsa.PublicKey); !ok {
|
|
t.Error("an RSA client was served a non-RSA certificate")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "wrongCacheKeyType",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
// Make an RSA cert and cache it without suffix.
|
|
c := s.Start().LeafCert(exampleDomain, "RSA", time.Now(), time.Now().Add(90*24*time.Hour))
|
|
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
|
|
t.Fatalf("man.cachePut: %v", err)
|
|
}
|
|
},
|
|
verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) {
|
|
// The RSA cached cert should be silently ignored and replaced.
|
|
if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok {
|
|
t.Error("an ECDSA client was served a non-ECDSA certificate")
|
|
}
|
|
if numCerts := man.Cache.(*memCache).numCerts(); numCerts != 1 {
|
|
t.Errorf("found %d certificates in cache; want %d", numCerts, 1)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "almostExpiredCache",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
man.RenewBefore = 24 * time.Hour
|
|
// Cache an almost expired cert.
|
|
c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now(), time.Now().Add(10*time.Minute))
|
|
if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil {
|
|
t.Fatalf("man.cachePut: %v", err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "provideExternalAuth",
|
|
hello: clientHelloInfo("example.org", algECDSA),
|
|
domain: "example.org",
|
|
prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) {
|
|
s.ExternalAccountRequired()
|
|
|
|
man.ExternalAccountBinding = &acme.ExternalAccountBinding{
|
|
KID: "test-key",
|
|
Key: make([]byte, 32),
|
|
}
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
man := testManager(t)
|
|
s := acmetest.NewCAServer(t)
|
|
if !tt.disableALPN {
|
|
s.ResolveGetCertificate(tt.domain, man.GetCertificate)
|
|
}
|
|
if !tt.disableHTTP {
|
|
s.ResolveHandler(tt.domain, man.HTTPHandler(nil))
|
|
}
|
|
|
|
if tt.prepare != nil {
|
|
tt.prepare(t, man, s)
|
|
}
|
|
|
|
s.Start()
|
|
|
|
man.Client = &acme.Client{DirectoryURL: s.URL()}
|
|
|
|
tlscert, err := man.GetCertificate(tt.hello)
|
|
if tt.expectError != "" {
|
|
if err == nil {
|
|
t.Fatal("expected error, got certificate")
|
|
}
|
|
if !strings.Contains(err.Error(), tt.expectError) {
|
|
t.Errorf("got %q, expected %q", err, tt.expectError)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("man.GetCertificate: %v", err)
|
|
}
|
|
|
|
leaf, err := x509.ParseCertificate(tlscert.Certificate[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts := x509.VerifyOptions{
|
|
DNSName: tt.domain,
|
|
Intermediates: x509.NewCertPool(),
|
|
Roots: s.Roots(),
|
|
}
|
|
for _, cert := range tlscert.Certificate[1:] {
|
|
c, err := x509.ParseCertificate(cert)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts.Intermediates.AddCert(c)
|
|
}
|
|
if _, err := leaf.Verify(opts); err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
if san := leaf.DNSNames[0]; san != tt.domain {
|
|
t.Errorf("got SAN %q, expected %q", san, tt.domain)
|
|
}
|
|
|
|
if tt.verify != nil {
|
|
tt.verify(t, man, leaf)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
<-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_concurrent guards against a data race on certState
|
|
// fields between the goroutine performing the ACME work for a domain and
|
|
// the other goroutines waiting on the result. See golang/go#80119.
|
|
func TestGetCertificate_concurrent(t *testing.T) {
|
|
// An unreachable directory URL makes the owner goroutine fail
|
|
// quickly inside createCert, so its writes overlap with the
|
|
// non-owner goroutines reaching the same code path.
|
|
m := &Manager{
|
|
Prompt: AcceptTOS,
|
|
Client: &acme.Client{DirectoryURL: "http://127.0.0.1:1/"},
|
|
}
|
|
defer m.stopRenew()
|
|
|
|
start := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 64; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
m.GetCertificate(clientHelloInfo(exampleDomain, algECDSA))
|
|
}()
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestRevokeFailedAuthz(t *testing.T) {
|
|
ca := acmetest.NewCAServer(t)
|
|
// Make the authz unfulfillable on the client side, so it will be left
|
|
// pending at the end of the verification attempt.
|
|
ca.ChallengeTypes("fake-01", "fake-02")
|
|
ca.Start()
|
|
|
|
m := testManager(t)
|
|
m.Client = &acme.Client{DirectoryURL: ca.URL()}
|
|
|
|
_, err := m.GetCertificate(clientHelloInfo("example.org", algECDSA))
|
|
if err == nil {
|
|
t.Fatal("expected GetCertificate to fail")
|
|
}
|
|
|
|
logTicker := time.NewTicker(3 * time.Second)
|
|
defer logTicker.Stop()
|
|
for {
|
|
authz, err := m.Client.GetAuthorization(context.Background(), ca.URL()+"/authz/0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if authz.Status == acme.StatusDeactivated {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-logTicker.C:
|
|
t.Logf("still waiting on revocations")
|
|
default:
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
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:abcd::c60a]:1234", 302, "https://[2602:d1:abcd::c60a]:443/"},
|
|
{"GET", "http://[2602:d1:abcd::c60a]", 302, "https://[2602:d1:abcd::c60a]/"},
|
|
{"GET", "http://[2602:d1:abcd::c60a]/foo?a=b", 302, "https://[2602:d1:abcd::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, 1024)
|
|
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--9caa.com", true}, // éé.com
|
|
{"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, 1024)
|
|
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})
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEndToEndALPN(t *testing.T) {
|
|
const domain = "example.org"
|
|
|
|
// ACME CA server
|
|
ca := acmetest.NewCAServer(t).Start()
|
|
|
|
// User HTTPS 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's HTTPS 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 := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v := string(b); v != "OK" {
|
|
t.Errorf("user server response: %q; want 'OK'", v)
|
|
}
|
|
}
|
|
|
|
func TestEndToEndHTTP(t *testing.T) {
|
|
const domain = "example.org"
|
|
|
|
// ACME CA server.
|
|
ca := acmetest.NewCAServer(t).ChallengeTypes("http-01").Start()
|
|
|
|
// User HTTP server for the ACME challenge.
|
|
m := testManager(t)
|
|
m.Client = &acme.Client{DirectoryURL: ca.URL()}
|
|
s := httptest.NewServer(m.HTTPHandler(nil))
|
|
defer s.Close()
|
|
|
|
// User HTTPS server.
|
|
ss := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("OK"))
|
|
}))
|
|
ss.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
|
|
},
|
|
}
|
|
ss.StartTLS()
|
|
defer ss.Close()
|
|
|
|
// Redirect the CA requests to the HTTP server.
|
|
ca.Resolve(domain, strings.TrimPrefix(s.URL, "http://"))
|
|
|
|
// A client visiting user's HTTPS server.
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: ca.Roots(),
|
|
ServerName: domain,
|
|
},
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
res, err := client.Get(ss.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer res.Body.Close()
|
|
b, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v := string(b); v != "OK" {
|
|
t.Errorf("user server response: %q; want 'OK'", v)
|
|
}
|
|
}
|