| // Copyright 2016 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package autocert |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto" |
| "crypto/ecdsa" |
| "crypto/elliptic" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/tls" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/asn1" |
| "fmt" |
| "io" |
| "math/big" |
| "net/http" |
| "net/http/httptest" |
| "reflect" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| |
| "golang.org/x/crypto/acme" |
| "golang.org/x/crypto/acme/autocert/internal/acmetest" |
| ) |
| |
| var ( |
| exampleDomain = "example.org" |
| exampleCertKey = certKey{domain: exampleDomain} |
| exampleCertKeyRSA = certKey{domain: exampleDomain, isRSA: true} |
| ) |
| |
| type memCache struct { |
| t *testing.T |
| mu sync.Mutex |
| keyData map[string][]byte |
| } |
| |
| func (m *memCache) Get(ctx context.Context, key string) ([]byte, error) { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| |
| v, ok := m.keyData[key] |
| if !ok { |
| return nil, ErrCacheMiss |
| } |
| return v, nil |
| } |
| |
| // filenameSafe returns whether all characters in s are printable ASCII |
| // and safe to use in a filename on most filesystems. |
| func filenameSafe(s string) bool { |
| for _, c := range s { |
| if c < 0x20 || c > 0x7E { |
| return false |
| } |
| switch c { |
| case '\\', '/', ':', '*', '?', '"', '<', '>', '|': |
| return false |
| } |
| } |
| return true |
| } |
| |
| func (m *memCache) Put(ctx context.Context, key string, data []byte) error { |
| if !filenameSafe(key) { |
| m.t.Errorf("invalid characters in cache key %q", key) |
| } |
| |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| |
| m.keyData[key] = data |
| return nil |
| } |
| |
| func (m *memCache) Delete(ctx context.Context, key string) error { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| |
| delete(m.keyData, key) |
| return nil |
| } |
| |
| func newMemCache(t *testing.T) *memCache { |
| return &memCache{ |
| t: t, |
| keyData: make(map[string][]byte), |
| } |
| } |
| |
| func (m *memCache) numCerts() int { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| |
| res := 0 |
| for key := range m.keyData { |
| if strings.HasSuffix(key, "+token") || |
| strings.HasSuffix(key, "+key") || |
| strings.HasSuffix(key, "+http-01") { |
| continue |
| } |
| res++ |
| } |
| return res |
| } |
| |
| func dummyCert(pub interface{}, san ...string) ([]byte, error) { |
| return dateDummyCert(pub, time.Now(), time.Now().Add(90*24*time.Hour), san...) |
| } |
| |
| func dateDummyCert(pub interface{}, start, end time.Time, san ...string) ([]byte, error) { |
| // use EC key to run faster on 386 |
| key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| return nil, err |
| } |
| t := &x509.Certificate{ |
| SerialNumber: randomSerial(), |
| NotBefore: start, |
| NotAfter: end, |
| BasicConstraintsValid: true, |
| KeyUsage: x509.KeyUsageKeyEncipherment, |
| DNSNames: san, |
| } |
| if pub == nil { |
| pub = &key.PublicKey |
| } |
| return x509.CreateCertificate(rand.Reader, t, t, pub, key) |
| } |
| |
| func randomSerial() *big.Int { |
| serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 32)) |
| if err != nil { |
| panic(err) |
| } |
| return serial |
| } |
| |
| type algorithmSupport int |
| |
| const ( |
| algRSA algorithmSupport = iota |
| algECDSA |
| ) |
| |
| func clientHelloInfo(sni string, alg algorithmSupport) *tls.ClientHelloInfo { |
| hello := &tls.ClientHelloInfo{ |
| ServerName: sni, |
| CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}, |
| } |
| if alg == algECDSA { |
| hello.CipherSuites = append(hello.CipherSuites, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305) |
| } |
| return hello |
| } |
| |
| func testManager(t *testing.T) *Manager { |
| man := &Manager{ |
| Prompt: AcceptTOS, |
| Cache: newMemCache(t), |
| } |
| t.Cleanup(man.stopRenew) |
| return man |
| } |
| |
| func TestGetCertificate(t *testing.T) { |
| tests := []struct { |
| name string |
| hello *tls.ClientHelloInfo |
| domain string |
| expectError string |
| prepare func(t *testing.T, man *Manager, s *acmetest.CAServer) |
| verify func(t *testing.T, man *Manager, leaf *x509.Certificate) |
| disableALPN bool |
| disableHTTP bool |
| }{ |
| { |
| name: "ALPN", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| disableHTTP: true, |
| }, |
| { |
| name: "HTTP", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| disableALPN: true, |
| }, |
| { |
| name: "nilPrompt", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| man.Prompt = nil |
| }, |
| expectError: "Manager.Prompt not set", |
| }, |
| { |
| name: "trailingDot", |
| hello: clientHelloInfo("example.org.", algECDSA), |
| domain: "example.org", |
| }, |
| { |
| name: "unicodeIDN", |
| hello: clientHelloInfo("éé.com", algECDSA), |
| domain: "xn--9caa.com", |
| }, |
| { |
| name: "unicodeIDN/mixedCase", |
| hello: clientHelloInfo("éÉ.com", algECDSA), |
| domain: "xn--9caa.com", |
| }, |
| { |
| name: "upperCase", |
| hello: clientHelloInfo("EXAMPLE.ORG", algECDSA), |
| domain: "example.org", |
| }, |
| { |
| name: "goodCache", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| // Make a valid cert and cache it. |
| c := s.Start().LeafCert(exampleDomain, "ECDSA", |
| // Use a time before the Let's Encrypt revocation cutoff to also test |
| // that non-Let's Encrypt certificates are not renewed. |
| time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), |
| time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), |
| ) |
| if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| }, |
| // Break the server to check that the cache is used. |
| disableALPN: true, disableHTTP: true, |
| }, |
| { |
| name: "expiredCache", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| // Make an expired cert and cache it. |
| c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now().Add(-10*time.Minute), time.Now().Add(-5*time.Minute)) |
| if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| }, |
| }, |
| { |
| name: "forceRSA", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| man.ForceRSA = true |
| }, |
| verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { |
| if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok { |
| t.Errorf("leaf.PublicKey is %T; want *ecdsa.PublicKey", leaf.PublicKey) |
| } |
| }, |
| }, |
| { |
| name: "goodLetsEncrypt", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| // Make a valid certificate issued after the TLS-ALPN-01 |
| // revocation window and cache it. |
| s.IssuerName(pkix.Name{Country: []string{"US"}, |
| Organization: []string{"Let's Encrypt"}, CommonName: "R3"}) |
| c := s.Start().LeafCert(exampleDomain, "ECDSA", |
| time.Date(2022, time.January, 26, 12, 0, 0, 0, time.UTC), |
| time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), |
| ) |
| if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| }, |
| // Break the server to check that the cache is used. |
| disableALPN: true, disableHTTP: true, |
| }, |
| { |
| name: "revokedLetsEncrypt", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| // Make a certificate issued during the TLS-ALPN-01 |
| // revocation window and cache it. |
| s.IssuerName(pkix.Name{Country: []string{"US"}, |
| Organization: []string{"Let's Encrypt"}, CommonName: "R3"}) |
| c := s.Start().LeafCert(exampleDomain, "ECDSA", |
| time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), |
| time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), |
| ) |
| if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| }, |
| verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { |
| if leaf.NotBefore.Before(time.Now().Add(-10 * time.Minute)) { |
| t.Error("certificate was not reissued") |
| } |
| }, |
| }, |
| { |
| // TestGetCertificate/tokenCache tests the fallback of token |
| // certificate fetches to cache when Manager.certTokens misses. |
| name: "tokenCacheALPN", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| // Make a separate manager with a shared cache, simulating |
| // separate nodes that serve requests for the same domain. |
| man2 := testManager(t) |
| man2.Cache = man.Cache |
| // Redirect the verification request to man2, although the |
| // client request will hit man, testing that they can complete a |
| // verification by communicating through the cache. |
| s.ResolveGetCertificate("example.org", man2.GetCertificate) |
| }, |
| // Drop the default verification paths. |
| disableALPN: true, |
| }, |
| { |
| name: "tokenCacheHTTP", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| man2 := testManager(t) |
| man2.Cache = man.Cache |
| s.ResolveHandler("example.org", man2.HTTPHandler(nil)) |
| }, |
| disableHTTP: true, |
| }, |
| { |
| name: "ecdsa", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { |
| if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok { |
| t.Error("an ECDSA client was served a non-ECDSA certificate") |
| } |
| }, |
| }, |
| { |
| name: "rsa", |
| hello: clientHelloInfo("example.org", algRSA), |
| domain: "example.org", |
| verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { |
| if _, ok := leaf.PublicKey.(*rsa.PublicKey); !ok { |
| t.Error("an RSA client was served a non-RSA certificate") |
| } |
| }, |
| }, |
| { |
| name: "wrongCacheKeyType", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| // Make an RSA cert and cache it without suffix. |
| c := s.Start().LeafCert(exampleDomain, "RSA", time.Now(), time.Now().Add(90*24*time.Hour)) |
| if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| }, |
| verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { |
| // The RSA cached cert should be silently ignored and replaced. |
| if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok { |
| t.Error("an ECDSA client was served a non-ECDSA certificate") |
| } |
| if numCerts := man.Cache.(*memCache).numCerts(); numCerts != 1 { |
| t.Errorf("found %d certificates in cache; want %d", numCerts, 1) |
| } |
| }, |
| }, |
| { |
| name: "almostExpiredCache", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| man.RenewBefore = 24 * time.Hour |
| // Cache an almost expired cert. |
| c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now(), time.Now().Add(10*time.Minute)) |
| if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| }, |
| }, |
| { |
| name: "provideExternalAuth", |
| hello: clientHelloInfo("example.org", algECDSA), |
| domain: "example.org", |
| prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { |
| s.ExternalAccountRequired() |
| |
| man.ExternalAccountBinding = &acme.ExternalAccountBinding{ |
| KID: "test-key", |
| Key: make([]byte, 32), |
| } |
| }, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| man := testManager(t) |
| s := acmetest.NewCAServer(t) |
| if !tt.disableALPN { |
| s.ResolveGetCertificate(tt.domain, man.GetCertificate) |
| } |
| if !tt.disableHTTP { |
| s.ResolveHandler(tt.domain, man.HTTPHandler(nil)) |
| } |
| |
| if tt.prepare != nil { |
| tt.prepare(t, man, s) |
| } |
| |
| s.Start() |
| |
| man.Client = &acme.Client{DirectoryURL: s.URL()} |
| |
| var tlscert *tls.Certificate |
| var err error |
| done := make(chan struct{}) |
| go func() { |
| tlscert, err = man.GetCertificate(tt.hello) |
| close(done) |
| }() |
| select { |
| case <-time.After(time.Minute): |
| t.Fatal("man.GetCertificate took too long to return") |
| case <-done: |
| } |
| if tt.expectError != "" { |
| if err == nil { |
| t.Fatal("expected error, got certificate") |
| } |
| if !strings.Contains(err.Error(), tt.expectError) { |
| t.Errorf("got %q, expected %q", err, tt.expectError) |
| } |
| return |
| } |
| if err != nil { |
| t.Fatalf("man.GetCertificate: %v", err) |
| } |
| |
| leaf, err := x509.ParseCertificate(tlscert.Certificate[0]) |
| if err != nil { |
| t.Fatal(err) |
| } |
| opts := x509.VerifyOptions{ |
| DNSName: tt.domain, |
| Intermediates: x509.NewCertPool(), |
| Roots: s.Roots(), |
| } |
| for _, cert := range tlscert.Certificate[1:] { |
| c, err := x509.ParseCertificate(cert) |
| if err != nil { |
| t.Fatal(err) |
| } |
| opts.Intermediates.AddCert(c) |
| } |
| if _, err := leaf.Verify(opts); err != nil { |
| t.Error(err) |
| } |
| |
| if san := leaf.DNSNames[0]; san != tt.domain { |
| t.Errorf("got SAN %q, expected %q", san, tt.domain) |
| } |
| |
| if tt.verify != nil { |
| tt.verify(t, man, leaf) |
| } |
| }) |
| } |
| } |
| |
| func TestGetCertificate_failedAttempt(t *testing.T) { |
| ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.WriteHeader(http.StatusBadRequest) |
| })) |
| defer ts.Close() |
| |
| d := createCertRetryAfter |
| f := testDidRemoveState |
| defer func() { |
| createCertRetryAfter = d |
| testDidRemoveState = f |
| }() |
| createCertRetryAfter = 0 |
| done := make(chan struct{}) |
| testDidRemoveState = func(ck certKey) { |
| if ck != exampleCertKey { |
| t.Errorf("testDidRemoveState: domain = %v; want %v", ck, exampleCertKey) |
| } |
| close(done) |
| } |
| |
| man := &Manager{ |
| Prompt: AcceptTOS, |
| Client: &acme.Client{ |
| DirectoryURL: ts.URL, |
| }, |
| } |
| defer man.stopRenew() |
| hello := clientHelloInfo(exampleDomain, algECDSA) |
| if _, err := man.GetCertificate(hello); err == nil { |
| t.Error("GetCertificate: err is nil") |
| } |
| select { |
| case <-time.After(5 * time.Second): |
| t.Errorf("took too long to remove the %q state", exampleCertKey) |
| case <-done: |
| man.stateMu.Lock() |
| defer man.stateMu.Unlock() |
| if v, exist := man.state[exampleCertKey]; exist { |
| t.Errorf("state exists for %v: %+v", exampleCertKey, v) |
| } |
| } |
| } |
| |
| func TestRevokeFailedAuthz(t *testing.T) { |
| ca := acmetest.NewCAServer(t) |
| // Make the authz unfulfillable on the client side, so it will be left |
| // pending at the end of the verification attempt. |
| ca.ChallengeTypes("fake-01", "fake-02") |
| ca.Start() |
| |
| m := testManager(t) |
| m.Client = &acme.Client{DirectoryURL: ca.URL()} |
| |
| _, err := m.GetCertificate(clientHelloInfo("example.org", algECDSA)) |
| if err == nil { |
| t.Fatal("expected GetCertificate to fail") |
| } |
| |
| start := time.Now() |
| for time.Since(start) < 3*time.Second { |
| authz, err := m.Client.GetAuthorization(context.Background(), ca.URL()+"/authz/0") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if authz.Status == acme.StatusDeactivated { |
| return |
| } |
| time.Sleep(50 * time.Millisecond) |
| } |
| t.Error("revocations took too long") |
| |
| } |
| |
| func TestHTTPHandlerDefaultFallback(t *testing.T) { |
| tt := []struct { |
| method, url string |
| wantCode int |
| wantLocation string |
| }{ |
| {"GET", "http://example.org", 302, "https://example.org/"}, |
| {"GET", "http://example.org/foo", 302, "https://example.org/foo"}, |
| {"GET", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"}, |
| {"GET", "http://example.org/?a=b", 302, "https://example.org/?a=b"}, |
| {"GET", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"}, |
| {"GET", "http://example.org:80/foo?a=b", 302, "https://example.org:443/foo?a=b"}, |
| {"GET", "http://example.org:80/foo%20bar", 302, "https://example.org:443/foo%20bar"}, |
| {"GET", "http://[2602:d1:xxxx::c60a]:1234", 302, "https://[2602:d1:xxxx::c60a]:443/"}, |
| {"GET", "http://[2602:d1:xxxx::c60a]", 302, "https://[2602:d1:xxxx::c60a]/"}, |
| {"GET", "http://[2602:d1:xxxx::c60a]/foo?a=b", 302, "https://[2602:d1:xxxx::c60a]/foo?a=b"}, |
| {"HEAD", "http://example.org", 302, "https://example.org/"}, |
| {"HEAD", "http://example.org/foo", 302, "https://example.org/foo"}, |
| {"HEAD", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"}, |
| {"HEAD", "http://example.org/?a=b", 302, "https://example.org/?a=b"}, |
| {"HEAD", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"}, |
| {"POST", "http://example.org", 400, ""}, |
| {"PUT", "http://example.org", 400, ""}, |
| {"GET", "http://example.org/.well-known/acme-challenge/x", 404, ""}, |
| } |
| var m Manager |
| h := m.HTTPHandler(nil) |
| for i, test := range tt { |
| r := httptest.NewRequest(test.method, test.url, nil) |
| w := httptest.NewRecorder() |
| h.ServeHTTP(w, r) |
| if w.Code != test.wantCode { |
| t.Errorf("%d: w.Code = %d; want %d", i, w.Code, test.wantCode) |
| t.Errorf("%d: body: %s", i, w.Body.Bytes()) |
| } |
| if v := w.Header().Get("Location"); v != test.wantLocation { |
| t.Errorf("%d: Location = %q; want %q", i, v, test.wantLocation) |
| } |
| } |
| } |
| |
| func TestAccountKeyCache(t *testing.T) { |
| m := Manager{Cache: newMemCache(t)} |
| ctx := context.Background() |
| k1, err := m.accountKey(ctx) |
| if err != nil { |
| t.Fatal(err) |
| } |
| k2, err := m.accountKey(ctx) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !reflect.DeepEqual(k1, k2) { |
| t.Errorf("account keys don't match: k1 = %#v; k2 = %#v", k1, k2) |
| } |
| } |
| |
| func TestCache(t *testing.T) { |
| ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| cert, err := dummyCert(ecdsaKey.Public(), exampleDomain) |
| if err != nil { |
| t.Fatal(err) |
| } |
| ecdsaCert := &tls.Certificate{ |
| Certificate: [][]byte{cert}, |
| PrivateKey: ecdsaKey, |
| } |
| |
| rsaKey, err := rsa.GenerateKey(rand.Reader, 512) |
| if err != nil { |
| t.Fatal(err) |
| } |
| cert, err = dummyCert(rsaKey.Public(), exampleDomain) |
| if err != nil { |
| t.Fatal(err) |
| } |
| rsaCert := &tls.Certificate{ |
| Certificate: [][]byte{cert}, |
| PrivateKey: rsaKey, |
| } |
| |
| man := &Manager{Cache: newMemCache(t)} |
| defer man.stopRenew() |
| ctx := context.Background() |
| |
| if err := man.cachePut(ctx, exampleCertKey, ecdsaCert); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| if err := man.cachePut(ctx, exampleCertKeyRSA, rsaCert); err != nil { |
| t.Fatalf("man.cachePut: %v", err) |
| } |
| |
| res, err := man.cacheGet(ctx, exampleCertKey) |
| if err != nil { |
| t.Fatalf("man.cacheGet: %v", err) |
| } |
| if res == nil || !bytes.Equal(res.Certificate[0], ecdsaCert.Certificate[0]) { |
| t.Errorf("man.cacheGet = %+v; want %+v", res, ecdsaCert) |
| } |
| |
| res, err = man.cacheGet(ctx, exampleCertKeyRSA) |
| if err != nil { |
| t.Fatalf("man.cacheGet: %v", err) |
| } |
| if res == nil || !bytes.Equal(res.Certificate[0], rsaCert.Certificate[0]) { |
| t.Errorf("man.cacheGet = %+v; want %+v", res, rsaCert) |
| } |
| } |
| |
| func TestHostWhitelist(t *testing.T) { |
| policy := HostWhitelist("example.com", "EXAMPLE.ORG", "*.example.net", "éÉ.com") |
| tt := []struct { |
| host string |
| allow bool |
| }{ |
| {"example.com", true}, |
| {"example.org", true}, |
| {"xn--9caa.com", true}, // éé.com |
| {"one.example.com", false}, |
| {"two.example.org", false}, |
| {"three.example.net", false}, |
| {"dummy", false}, |
| } |
| for i, test := range tt { |
| err := policy(nil, test.host) |
| if err != nil && test.allow { |
| t.Errorf("%d: policy(%q): %v; want nil", i, test.host, err) |
| } |
| if err == nil && !test.allow { |
| t.Errorf("%d: policy(%q): nil; want an error", i, test.host) |
| } |
| } |
| } |
| |
| func TestValidCert(t *testing.T) { |
| key1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| key2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| key3, err := rsa.GenerateKey(rand.Reader, 512) |
| if err != nil { |
| t.Fatal(err) |
| } |
| cert1, err := dummyCert(key1.Public(), "example.org") |
| if err != nil { |
| t.Fatal(err) |
| } |
| cert2, err := dummyCert(key2.Public(), "example.org") |
| if err != nil { |
| t.Fatal(err) |
| } |
| cert3, err := dummyCert(key3.Public(), "example.org") |
| if err != nil { |
| t.Fatal(err) |
| } |
| now := time.Now() |
| early, err := dateDummyCert(key1.Public(), now.Add(time.Hour), now.Add(2*time.Hour), "example.org") |
| if err != nil { |
| t.Fatal(err) |
| } |
| expired, err := dateDummyCert(key1.Public(), now.Add(-2*time.Hour), now.Add(-time.Hour), "example.org") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| tt := []struct { |
| ck certKey |
| key crypto.Signer |
| cert [][]byte |
| ok bool |
| }{ |
| {certKey{domain: "example.org"}, key1, [][]byte{cert1}, true}, |
| {certKey{domain: "example.org", isRSA: true}, key3, [][]byte{cert3}, true}, |
| {certKey{domain: "example.org"}, key1, [][]byte{cert1, cert2, cert3}, true}, |
| {certKey{domain: "example.org"}, key1, [][]byte{cert1, {1}}, false}, |
| {certKey{domain: "example.org"}, key1, [][]byte{{1}}, false}, |
| {certKey{domain: "example.org"}, key1, [][]byte{cert2}, false}, |
| {certKey{domain: "example.org"}, key2, [][]byte{cert1}, false}, |
| {certKey{domain: "example.org"}, key1, [][]byte{cert3}, false}, |
| {certKey{domain: "example.org"}, key3, [][]byte{cert1}, false}, |
| {certKey{domain: "example.net"}, key1, [][]byte{cert1}, false}, |
| {certKey{domain: "example.org"}, key1, [][]byte{early}, false}, |
| {certKey{domain: "example.org"}, key1, [][]byte{expired}, false}, |
| {certKey{domain: "example.org", isRSA: true}, key1, [][]byte{cert1}, false}, |
| {certKey{domain: "example.org"}, key3, [][]byte{cert3}, false}, |
| } |
| for i, test := range tt { |
| leaf, err := validCert(test.ck, test.cert, test.key, now) |
| if err != nil && test.ok { |
| t.Errorf("%d: err = %v", i, err) |
| } |
| if err == nil && !test.ok { |
| t.Errorf("%d: err is nil", i) |
| } |
| if err == nil && test.ok && leaf == nil { |
| t.Errorf("%d: leaf is nil", i) |
| } |
| } |
| } |
| |
| type cacheGetFunc func(ctx context.Context, key string) ([]byte, error) |
| |
| func (f cacheGetFunc) Get(ctx context.Context, key string) ([]byte, error) { |
| return f(ctx, key) |
| } |
| |
| func (f cacheGetFunc) Put(ctx context.Context, key string, data []byte) error { |
| return fmt.Errorf("unsupported Put of %q = %q", key, data) |
| } |
| |
| func (f cacheGetFunc) Delete(ctx context.Context, key string) error { |
| return fmt.Errorf("unsupported Delete of %q", key) |
| } |
| |
| func TestManagerGetCertificateBogusSNI(t *testing.T) { |
| m := Manager{ |
| Prompt: AcceptTOS, |
| Cache: cacheGetFunc(func(ctx context.Context, key string) ([]byte, error) { |
| return nil, fmt.Errorf("cache.Get of %s", key) |
| }), |
| } |
| tests := []struct { |
| name string |
| wantErr string |
| }{ |
| {"foo.com", "cache.Get of foo.com"}, |
| {"foo.com.", "cache.Get of foo.com"}, |
| {`a\b.com`, "acme/autocert: server name contains invalid character"}, |
| {`a/b.com`, "acme/autocert: server name contains invalid character"}, |
| {"", "acme/autocert: missing server name"}, |
| {"foo", "acme/autocert: server name component count invalid"}, |
| {".foo", "acme/autocert: server name component count invalid"}, |
| {"foo.", "acme/autocert: server name component count invalid"}, |
| {"fo.o", "cache.Get of fo.o"}, |
| } |
| for _, tt := range tests { |
| _, err := m.GetCertificate(clientHelloInfo(tt.name, algECDSA)) |
| got := fmt.Sprint(err) |
| if got != tt.wantErr { |
| t.Errorf("GetCertificate(SNI = %q) = %q; want %q", tt.name, got, tt.wantErr) |
| } |
| } |
| } |
| |
| func TestCertRequest(t *testing.T) { |
| key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // An extension from RFC7633. Any will do. |
| ext := pkix.Extension{ |
| Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1}, |
| Value: []byte("dummy"), |
| } |
| b, err := certRequest(key, "example.org", []pkix.Extension{ext}) |
| if err != nil { |
| t.Fatalf("certRequest: %v", err) |
| } |
| r, err := x509.ParseCertificateRequest(b) |
| if err != nil { |
| t.Fatalf("ParseCertificateRequest: %v", err) |
| } |
| var found bool |
| for _, v := range r.Extensions { |
| if v.Id.Equal(ext.Id) { |
| found = true |
| break |
| } |
| } |
| if !found { |
| t.Errorf("want %v in Extensions: %v", ext, r.Extensions) |
| } |
| } |
| |
| func TestSupportsECDSA(t *testing.T) { |
| tests := []struct { |
| CipherSuites []uint16 |
| SignatureSchemes []tls.SignatureScheme |
| SupportedCurves []tls.CurveID |
| ecdsaOk bool |
| }{ |
| {[]uint16{ |
| tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |
| }, nil, nil, false}, |
| {[]uint16{ |
| tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |
| }, nil, nil, true}, |
| |
| // SignatureSchemes limits, not extends, CipherSuites |
| {[]uint16{ |
| tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |
| }, []tls.SignatureScheme{ |
| tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256, |
| }, nil, false}, |
| {[]uint16{ |
| tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |
| }, []tls.SignatureScheme{ |
| tls.PKCS1WithSHA256, |
| }, nil, false}, |
| {[]uint16{ |
| tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |
| }, []tls.SignatureScheme{ |
| tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256, |
| }, nil, true}, |
| |
| {[]uint16{ |
| tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |
| }, []tls.SignatureScheme{ |
| tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256, |
| }, []tls.CurveID{ |
| tls.CurveP521, |
| }, false}, |
| {[]uint16{ |
| tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |
| }, []tls.SignatureScheme{ |
| tls.PKCS1WithSHA256, tls.ECDSAWithP256AndSHA256, |
| }, []tls.CurveID{ |
| tls.CurveP256, |
| tls.CurveP521, |
| }, true}, |
| } |
| for i, tt := range tests { |
| result := supportsECDSA(&tls.ClientHelloInfo{ |
| CipherSuites: tt.CipherSuites, |
| SignatureSchemes: tt.SignatureSchemes, |
| SupportedCurves: tt.SupportedCurves, |
| }) |
| if result != tt.ecdsaOk { |
| t.Errorf("%d: supportsECDSA = %v; want %v", i, result, tt.ecdsaOk) |
| } |
| } |
| } |
| |
| func TestEndToEndALPN(t *testing.T) { |
| const domain = "example.org" |
| |
| // ACME CA server |
| ca := acmetest.NewCAServer(t).Start() |
| |
| // User HTTPS server. |
| m := &Manager{ |
| Prompt: AcceptTOS, |
| Client: &acme.Client{DirectoryURL: ca.URL()}, |
| } |
| us := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Write([]byte("OK")) |
| })) |
| us.TLS = &tls.Config{ |
| NextProtos: []string{"http/1.1", acme.ALPNProto}, |
| GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { |
| cert, err := m.GetCertificate(hello) |
| if err != nil { |
| t.Errorf("m.GetCertificate: %v", err) |
| } |
| return cert, err |
| }, |
| } |
| us.StartTLS() |
| defer us.Close() |
| // In TLS-ALPN challenge verification, CA connects to the domain:443 in question. |
| // Because the domain won't resolve in tests, we need to tell the CA |
| // where to dial to instead. |
| ca.Resolve(domain, strings.TrimPrefix(us.URL, "https://")) |
| |
| // A client visiting user's HTTPS server. |
| tr := &http.Transport{ |
| TLSClientConfig: &tls.Config{ |
| RootCAs: ca.Roots(), |
| ServerName: domain, |
| }, |
| } |
| client := &http.Client{Transport: tr} |
| res, err := client.Get(us.URL) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer res.Body.Close() |
| b, err := io.ReadAll(res.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if v := string(b); v != "OK" { |
| t.Errorf("user server response: %q; want 'OK'", v) |
| } |
| } |
| |
| func TestEndToEndHTTP(t *testing.T) { |
| const domain = "example.org" |
| |
| // ACME CA server. |
| ca := acmetest.NewCAServer(t).ChallengeTypes("http-01").Start() |
| |
| // User HTTP server for the ACME challenge. |
| m := testManager(t) |
| m.Client = &acme.Client{DirectoryURL: ca.URL()} |
| s := httptest.NewServer(m.HTTPHandler(nil)) |
| defer s.Close() |
| |
| // User HTTPS server. |
| ss := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Write([]byte("OK")) |
| })) |
| ss.TLS = &tls.Config{ |
| NextProtos: []string{"http/1.1", acme.ALPNProto}, |
| GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { |
| cert, err := m.GetCertificate(hello) |
| if err != nil { |
| t.Errorf("m.GetCertificate: %v", err) |
| } |
| return cert, err |
| }, |
| } |
| ss.StartTLS() |
| defer ss.Close() |
| |
| // Redirect the CA requests to the HTTP server. |
| ca.Resolve(domain, strings.TrimPrefix(s.URL, "http://")) |
| |
| // A client visiting user's HTTPS server. |
| tr := &http.Transport{ |
| TLSClientConfig: &tls.Config{ |
| RootCAs: ca.Roots(), |
| ServerName: domain, |
| }, |
| } |
| client := &http.Client{Transport: tr} |
| res, err := client.Get(ss.URL) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer res.Body.Close() |
| b, err := io.ReadAll(res.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if v := string(b); v != "OK" { |
| t.Errorf("user server response: %q; want 'OK'", v) |
| } |
| } |