123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233 |
- // 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)
- }
- }
|