Support External Account Binding (EAB) tokens to the Manager as defined in RFC 8555, Section 7.3.4. If the ExternalAccountBinding field is set on Manager, pass it into the acme Account during registration. Fixes golang/go#48809 Change-Id: I64c38b05ab577acbde9f526638cc8104d15ff055 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/354189 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Trust: Brad Fitzpatrick <bradfitz@golang.org> Reviewed-by: Filippo Valsorda <filippo@golang.org> Run-TryBot: Filippo Valsorda <filippo@golang.org> Auto-Submit: Filippo Valsorda <filippo@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org>
1006 lines
28 KiB
Go
1006 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/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}
|
|
)
|
|
|
|
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: "Manager.Prompt not set",
|
|
},
|
|
{
|
|
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: "expiredCache",
|
|
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()}
|
|
|
|
var tlscert *tls.Certificate
|
|
var err error
|
|
done := make(chan struct{})
|
|
go func() {
|
|
tlscert, err = man.GetCertificate(tt.hello)
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-time.After(time.Minute):
|
|
t.Fatal("man.GetCertificate took too long to return")
|
|
case <-done:
|
|
}
|
|
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")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
start := time.Now()
|
|
for time.Since(start) < 3*time.Second {
|
|
authz, err := m.Client.GetAuthorization(context.Background(), ca.URL()+"/authz/0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if authz.Status == acme.StatusDeactivated {
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
t.Error("revocations took too long")
|
|
|
|
}
|
|
|
|
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--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, 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})
|
|
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 := 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)
|
|
}
|
|
}
|
|
|
|
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 := 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)
|
|
}
|
|
}
|