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