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>
1234 lines
34 KiB
Go
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)
|
|
}
|
|
}
|