From 198e4374d7ed0909af956c010c19ee6bae6a1bc9 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Fri, 28 Jan 2022 13:06:27 -0500 Subject: acme/autocert: replace all ACME server stubs with acmetest Change-Id: Ie5520f33674471b4a018feb9d0efaf6696ea38a2 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/381715 Run-TryBot: Filippo Valsorda Reviewed-by: Roland Shoemaker Trust: Filippo Valsorda TryBot-Result: Gopher Robot --- acme/autocert/autocert_test.go | 1116 +++++++++++---------------------- acme/autocert/internal/acmetest/ca.go | 515 ++++++++++----- acme/autocert/renewal_test.go | 147 +---- 3 files changed, 758 insertions(+), 1020 deletions(-) diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go index e4663e9..4ae408f 100644 --- a/acme/autocert/autocert_test.go +++ b/acme/autocert/autocert_test.go @@ -16,11 +16,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" - "encoding/base64" - "encoding/json" "fmt" - "html/template" - "io" "io/ioutil" "math/big" "net/http" @@ -41,33 +37,6 @@ var ( 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 @@ -175,18 +144,6 @@ func randomSerial() *big.Int { return serial } -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 ( @@ -205,235 +162,311 @@ func clientHelloInfo(sni string, alg algorithmSupport) *tls.ClientHelloInfo { 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--9caa.com", hello) - - hello = clientHelloInfo("éÉ.com", algECDSA) - testGetCertificate(t, man, "xn--9caa.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) { +func testManager(t *testing.T) *Manager { 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_goodCache(t *testing.T) { - // Make a valid cert and cache it. - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - serial := randomSerial() - tmpl := &x509.Certificate{ - SerialNumber: serial, - DNSNames: []string{exampleDomain}, - // Use a time before the Let's Encrypt revocation cutoff to also test - // that non-Let's Encrypt certificates are not renewed. - NotBefore: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), - } - 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) - } - - hello := clientHelloInfo(exampleDomain, algECDSA) - gotCert := testGetCertificate(t, man, exampleDomain, hello) - if gotCert.SerialNumber.Cmp(serial) != 0 { - t.Error("good certificate was replaced") - } -} - -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) - } - serial := randomSerial() - tmpl := &x509.Certificate{ - SerialNumber: serial, - DNSNames: []string{exampleDomain}, - NotBefore: time.Now().Add(-1 * time.Minute), - 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) - gotCert := testGetCertificate(t, man, exampleDomain, hello) - if gotCert.SerialNumber.Cmp(serial) == 0 { - t.Error("expired certificate was not replaced") + Prompt: AcceptTOS, + Cache: newMemCache(t), } + t.Cleanup(man.stopRenew) + return man } -func TestGetCertificate_goodLetsEncrypt(t *testing.T) { - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - issuer := &x509.Certificate{ - Subject: pkix.Name{Country: []string{"US"}, - Organization: []string{"Let's Encrypt"}, CommonName: "R3"}, - } - serial := randomSerial() - tmpl := &x509.Certificate{ - SerialNumber: serial, - DNSNames: []string{exampleDomain}, - NotBefore: time.Date(2022, time.January, 26, 12, 0, 0, 0, time.UTC), - NotAfter: time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), - } - pub, err := x509.CreateCertificate(rand.Reader, tmpl, issuer, &pk.PublicKey, pk) - if err != nil { - t.Fatal(err) - } - tlscert := &tls.Certificate{ - Certificate: [][]byte{pub}, - PrivateKey: pk, +func TestGetCertificate(t *testing.T) { + tests := []struct { + name string + hello *tls.ClientHelloInfo + domain string + expectError string + prepare func(t *testing.T, man *Manager, s *acmetest.CAServer) + verify func(t *testing.T, man *Manager, leaf *x509.Certificate) + disableALPN bool + disableHTTP bool + }{ + { + name: "ALPN", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + disableHTTP: true, + }, + { + name: "HTTP", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + disableALPN: true, + }, + { + name: "nilPrompt", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + man.Prompt = nil + }, + expectError: "Manager.Prompt not set", + }, + { + name: "trailingDot", + hello: clientHelloInfo("example.org.", algECDSA), + domain: "example.org", + }, + { + name: "unicodeIDN", + hello: clientHelloInfo("éé.com", algECDSA), + domain: "xn--9caa.com", + }, + { + name: "unicodeIDN/mixedCase", + hello: clientHelloInfo("éÉ.com", algECDSA), + domain: "xn--9caa.com", + }, + { + name: "upperCase", + hello: clientHelloInfo("EXAMPLE.ORG", algECDSA), + domain: "example.org", + }, + { + name: "goodCache", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + // Make a valid cert and cache it. + c := s.Start().LeafCert(exampleDomain, "ECDSA", + // Use a time before the Let's Encrypt revocation cutoff to also test + // that non-Let's Encrypt certificates are not renewed. + time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), + time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), + ) + if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { + t.Fatalf("man.cachePut: %v", err) + } + }, + // Break the server to check that the cache is used. + disableALPN: true, disableHTTP: true, + }, + { + name: "expiredCache", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + // Make an expired cert and cache it. + c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now().Add(-10*time.Minute), time.Now().Add(-5*time.Minute)) + if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { + t.Fatalf("man.cachePut: %v", err) + } + }, + }, + { + name: "forceRSA", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + man.ForceRSA = true + }, + verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { + if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok { + t.Errorf("leaf.PublicKey is %T; want *ecdsa.PublicKey", leaf.PublicKey) + } + }, + }, + { + name: "goodLetsEncrypt", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + // Make a valid certificate issued after the TLS-ALPN-01 + // revocation window and cache it. + s.IssuerName(pkix.Name{Country: []string{"US"}, + Organization: []string{"Let's Encrypt"}, CommonName: "R3"}) + c := s.Start().LeafCert(exampleDomain, "ECDSA", + time.Date(2022, time.January, 26, 12, 0, 0, 0, time.UTC), + time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), + ) + if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { + t.Fatalf("man.cachePut: %v", err) + } + }, + // Break the server to check that the cache is used. + disableALPN: true, disableHTTP: true, + }, + { + name: "revokedLetsEncrypt", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + // Make a certificate issued during the TLS-ALPN-01 + // revocation window and cache it. + s.IssuerName(pkix.Name{Country: []string{"US"}, + Organization: []string{"Let's Encrypt"}, CommonName: "R3"}) + c := s.Start().LeafCert(exampleDomain, "ECDSA", + time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), + time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), + ) + if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { + t.Fatalf("man.cachePut: %v", err) + } + }, + verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { + if leaf.NotBefore.Before(time.Now().Add(-10 * time.Minute)) { + t.Error("certificate was not reissued") + } + }, + }, + { + // TestGetCertificate/tokenCache tests the fallback of token + // certificate fetches to cache when Manager.certTokens misses. + name: "tokenCacheALPN", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + // Make a separate manager with a shared cache, simulating + // separate nodes that serve requests for the same domain. + man2 := testManager(t) + man2.Cache = man.Cache + // Redirect the verification request to man2, although the + // client request will hit man, testing that they can complete a + // verification by communicating through the cache. + s.ResolveGetCertificate("example.org", man2.GetCertificate) + }, + // Drop the default verification paths. + disableALPN: true, + }, + { + name: "tokenCacheHTTP", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + man2 := testManager(t) + man2.Cache = man.Cache + s.ResolveHandler("example.org", man2.HTTPHandler(nil)) + }, + disableHTTP: true, + }, + { + name: "ecdsa", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { + if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok { + t.Error("an ECDSA client was served a non-ECDSA certificate") + } + }, + }, + { + name: "rsa", + hello: clientHelloInfo("example.org", algRSA), + domain: "example.org", + verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { + if _, ok := leaf.PublicKey.(*rsa.PublicKey); !ok { + t.Error("an RSA client was served a non-RSA certificate") + } + }, + }, + { + name: "wrongCacheKeyType", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + // Make an RSA cert and cache it without suffix. + c := s.Start().LeafCert(exampleDomain, "RSA", time.Now(), time.Now().Add(90*24*time.Hour)) + if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { + t.Fatalf("man.cachePut: %v", err) + } + }, + verify: func(t *testing.T, man *Manager, leaf *x509.Certificate) { + // The RSA cached cert should be silently ignored and replaced. + if _, ok := leaf.PublicKey.(*ecdsa.PublicKey); !ok { + t.Error("an ECDSA client was served a non-ECDSA certificate") + } + if numCerts := man.Cache.(*memCache).numCerts(); numCerts != 1 { + t.Errorf("found %d certificates in cache; want %d", numCerts, 1) + } + }, + }, + { + name: "expiredCache", + hello: clientHelloInfo("example.org", algECDSA), + domain: "example.org", + prepare: func(t *testing.T, man *Manager, s *acmetest.CAServer) { + man.RenewBefore = 24 * time.Hour + // Cache an almost expired cert. + c := s.Start().LeafCert(exampleDomain, "ECDSA", time.Now(), time.Now().Add(10*time.Minute)) + if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { + t.Fatalf("man.cachePut: %v", err) + } + }, + }, } + 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)) + } - 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) - } + if tt.prepare != nil { + tt.prepare(t, man, s) + } - hello := clientHelloInfo(exampleDomain, algECDSA) - gotCert := testGetCertificate(t, man, exampleDomain, hello) - if gotCert.SerialNumber.Cmp(serial) != 0 { - t.Error("good certificate was replaced") - } -} + 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) + } -func TestGetCertificate_revokedLetsEncrypt(t *testing.T) { - // Make a presumably revoked Let's Encrypt cert and cache it. - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - issuer := &x509.Certificate{ - Subject: pkix.Name{Country: []string{"US"}, - Organization: []string{"Let's Encrypt"}, CommonName: "R3"}, - } - serial := randomSerial() - tmpl := &x509.Certificate{ - SerialNumber: serial, - DNSNames: []string{exampleDomain}, - NotBefore: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), - NotAfter: time.Date(2122, time.January, 1, 0, 0, 0, 0, time.UTC), - } - pub, err := x509.CreateCertificate(rand.Reader, tmpl, issuer, &pk.PublicKey, pk) - if err != nil { - t.Fatal(err) - } - tlscert := &tls.Certificate{ - Certificate: [][]byte{pub}, - PrivateKey: pk, - } + 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) + } - 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) - } + if san := leaf.DNSNames[0]; san != tt.domain { + t.Errorf("got SAN %q, expected %q", san, tt.domain) + } - // The presumably revoked cached cert should trigger a new cert issuance - // and return without an error. - hello := clientHelloInfo(exampleDomain, algECDSA) - gotCert := testGetCertificate(t, man, exampleDomain, hello) - if gotCert.SerialNumber.Cmp(serial) == 0 { - t.Error("certificate was not replaced") + if tt.verify != nil { + tt.verify(t, man, leaf) + } + }) } } @@ -481,489 +514,34 @@ func TestGetCertificate_failedAttempt(t *testing.T) { } } -// 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 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() -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} + m := testManager(t) + m.Client = &acme.Client{DirectoryURL: ca.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), - DNSNames: []string{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) + _, err := m.GetCertificate(clientHelloInfo("example.org", algECDSA)) + if err == nil { + t.Fatal("expected GetCertificate to fail") } - // 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) + 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.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) - } - // 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, 31} - 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.Fatal(err) } - 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 + if authz.Status == acme.StatusDeactivated { 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) *x509.Certificate { - 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) + time.Sleep(50 * time.Millisecond) } + t.Error("revocations took too long") - // 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) - } - - return cert -} - -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) { @@ -1306,18 +884,16 @@ func TestSupportsECDSA(t *testing.T) { } } -// TODO: add same end-to-end for http-01 challenge type. -func TestEndToEnd(t *testing.T) { +func TestEndToEndALPN(t *testing.T) { const domain = "example.org" // ACME CA server - ca := acmetest.NewCAServer([]string{"tls-alpn-01"}, []string{domain}) - defer ca.Close() + ca := acmetest.NewCAServer(t).Start() - // User dummy server. + // User HTTPS server. m := &Manager{ Prompt: AcceptTOS, - Client: &acme.Client{DirectoryURL: ca.URL}, + Client: &acme.Client{DirectoryURL: ca.URL()}, } us := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) @@ -1339,10 +915,10 @@ func TestEndToEnd(t *testing.T) { // where to dial to instead. ca.Resolve(domain, strings.TrimPrefix(us.URL, "https://")) - // A client visiting user dummy server. + // A client visiting user's HTTPS server. tr := &http.Transport{ TLSClientConfig: &tls.Config{ - RootCAs: ca.Roots, + RootCAs: ca.Roots(), ServerName: domain, }, } @@ -1360,3 +936,57 @@ func TestEndToEnd(t *testing.T) { t.Errorf("user server response: %q; want 'OK'", v) } } + +func TestEndToEndHTTP(t *testing.T) { + const domain = "example.org" + + // ACME CA server. + ca := acmetest.NewCAServer(t).ChallengeTypes("http-01").Start() + + // User HTTP server for the ACME challenge. + m := testManager(t) + m.Client = &acme.Client{DirectoryURL: ca.URL()} + s := httptest.NewServer(m.HTTPHandler(nil)) + defer s.Close() + + // User HTTPS server. + ss := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + })) + ss.TLS = &tls.Config{ + NextProtos: []string{"http/1.1", acme.ALPNProto}, + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + cert, err := m.GetCertificate(hello) + if err != nil { + t.Errorf("m.GetCertificate: %v", err) + } + return cert, err + }, + } + ss.StartTLS() + defer ss.Close() + + // Redirect the CA requests to the HTTP server. + ca.Resolve(domain, strings.TrimPrefix(s.URL, "http://")) + + // A client visiting user's HTTPS server. + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: ca.Roots(), + ServerName: domain, + }, + } + client := &http.Client{Transport: tr} + res, err := client.Get(ss.URL) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + b, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if v := string(b); v != "OK" { + t.Errorf("user server response: %q; want 'OK'", v) + } +} diff --git a/acme/autocert/internal/acmetest/ca.go b/acme/autocert/internal/acmetest/ca.go index faffd20..bc0984f 100644 --- a/acme/autocert/internal/acmetest/ca.go +++ b/acme/autocert/internal/acmetest/ca.go @@ -8,27 +8,30 @@ package acmetest import ( + "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" - "log" "math/big" + "net" "net/http" "net/http/httptest" "path" - "sort" "strconv" "strings" "sync" + "testing" "time" "golang.org/x/crypto/acme" @@ -36,56 +39,64 @@ import ( // CAServer is a simple test server which implements ACME spec bits needed for testing. type CAServer struct { - URL string // server URL after it has been started - Roots *x509.CertPool // CA root certificates; initialized in NewCAServer - rootKey crypto.Signer rootCert []byte // DER encoding rootTemplate *x509.Certificate - server *httptest.Server - challengeTypes []string // supported challenge types - domainsWhitelist []string // only these domains are valid for issuing, unless empty + t *testing.T + server *httptest.Server + issuer pkix.Name + challengeTypes []string + url string + roots *x509.CertPool mu sync.Mutex - certCount int // number of issued certs - domainAddr map[string]string // domain name to addr:port resolution - authorizations map[string]*authorization // keyed by domain name - orders []*order // index is used as order ID - errors []error // encountered client errors + certCount int // number of issued certs + acctRegistered bool // set once an account has been registered + domainAddr map[string]string // domain name to addr:port resolution + domainGetCert map[string]getCertificateFunc // domain name to GetCertificate function + domainHandler map[string]http.Handler // domain name to Handle function + validAuthz map[string]*authorization // valid authz, keyed by domain name + authorizations []*authorization // all authz, index is used as ID + orders []*order // index is used as order ID + errors []error // encountered client errors } -// NewCAServer creates a new ACME test server and starts serving requests. -// The returned CAServer issues certs signed with the CA roots -// available in the Roots field. -// -// The challengeTypes argument defines the supported ACME challenge types -// sent to a client in a response for a domain authorization. -// If domainsWhitelist is non-empty, the certs will be issued only for the specified -// list of domains. Otherwise, any domain name is allowed. -func NewCAServer(challengeTypes []string, domainsWhitelist []string) *CAServer { - var whitelist []string - for _, name := range domainsWhitelist { - whitelist = append(whitelist, name) - } - sort.Strings(whitelist) - ca := &CAServer{ - challengeTypes: challengeTypes, - domainsWhitelist: whitelist, - domainAddr: make(map[string]string), - authorizations: make(map[string]*authorization), +type getCertificateFunc func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) + +// NewCAServer creates a new ACME test server. The returned CAServer issues +// certs signed with the CA roots available in the Roots field. +func NewCAServer(t *testing.T) *CAServer { + ca := &CAServer{t: t, + challengeTypes: []string{"fake-01", "tls-alpn-01", "http-01"}, + domainAddr: make(map[string]string), + domainGetCert: make(map[string]getCertificateFunc), + domainHandler: make(map[string]http.Handler), + validAuthz: make(map[string]*authorization), + } + + ca.server = httptest.NewUnstartedServer(http.HandlerFunc(ca.handle)) + + r, err := rand.Int(rand.Reader, big.NewInt(1000000)) + if err != nil { + panic(fmt.Sprintf("rand.Int: %v", err)) + } + ca.issuer = pkix.Name{ + Organization: []string{"Test Acme Co"}, + CommonName: "Root CA " + r.String(), } + return ca +} + +func (ca *CAServer) generateRoot() { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err)) } tmpl := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - Organization: []string{"Test Acme Co"}, - CommonName: "Root CA", - }, + SerialNumber: big.NewInt(1), + Subject: ca.issuer, NotBefore: time.Now(), NotAfter: time.Now().Add(365 * 24 * time.Hour), KeyUsage: x509.KeyUsageCertSign, @@ -100,40 +111,87 @@ func NewCAServer(challengeTypes []string, domainsWhitelist []string) *CAServer { if err != nil { panic(fmt.Sprintf("x509.ParseCertificate: %v", err)) } - ca.Roots = x509.NewCertPool() - ca.Roots.AddCert(cert) + ca.roots = x509.NewCertPool() + ca.roots.AddCert(cert) ca.rootKey = key ca.rootCert = der ca.rootTemplate = tmpl +} - ca.server = httptest.NewServer(http.HandlerFunc(ca.handle)) - ca.URL = ca.server.URL +// IssuerName sets the name of the issuing CA. +func (ca *CAServer) IssuerName(name pkix.Name) *CAServer { + if ca.url != "" { + panic("IssuerName must be called before Start") + } + ca.issuer = name return ca } -// Close shuts down the server and blocks until all outstanding -// requests on this server have completed. -func (ca *CAServer) Close() { - ca.server.Close() +// ChallengeTypes sets the supported challenge types. +func (ca *CAServer) ChallengeTypes(types ...string) *CAServer { + if ca.url != "" { + panic("ChallengeTypes must be called before Start") + } + ca.challengeTypes = types + return ca +} + +// URL returns the server address, after Start has been called. +func (ca *CAServer) URL() string { + if ca.url == "" { + panic("URL called before Start") + } + return ca.url +} + +// Roots returns a pool cointaining the CA root. +func (ca *CAServer) Roots() *x509.CertPool { + if ca.url == "" { + panic("Roots called before Start") + } + return ca.roots +} + +// Start starts serving requests. The server address becomes available in the +// URL field. +func (ca *CAServer) Start() *CAServer { + if ca.url == "" { + ca.generateRoot() + ca.server.Start() + ca.t.Cleanup(ca.server.Close) + ca.url = ca.server.URL + } + return ca } func (ca *CAServer) serverURL(format string, arg ...interface{}) string { return ca.server.URL + fmt.Sprintf(format, arg...) } -func (ca *CAServer) addr(domain string) (string, error) { +func (ca *CAServer) addr(domain string) (string, bool) { ca.mu.Lock() defer ca.mu.Unlock() addr, ok := ca.domainAddr[domain] - if !ok { - return "", fmt.Errorf("CAServer: no addr resolution for %q", domain) - } - return addr, nil + return addr, ok +} + +func (ca *CAServer) getCert(domain string) (getCertificateFunc, bool) { + ca.mu.Lock() + defer ca.mu.Unlock() + f, ok := ca.domainGetCert[domain] + return f, ok +} + +func (ca *CAServer) getHandler(domain string) (http.Handler, bool) { + ca.mu.Lock() + defer ca.mu.Unlock() + h, ok := ca.domainHandler[domain] + return h, ok } func (ca *CAServer) httpErrorf(w http.ResponseWriter, code int, format string, a ...interface{}) { s := fmt.Sprintf(format, a...) - log.Println(s) + ca.t.Errorf(format, a...) http.Error(w, s, code) } @@ -145,6 +203,22 @@ func (ca *CAServer) Resolve(domain, addr string) { ca.domainAddr[domain] = addr } +// ResolveGetCertificate redirects TLS connections for domain to f when +// validating challenges for the domain authorization. +func (ca *CAServer) ResolveGetCertificate(domain string, f getCertificateFunc) { + ca.mu.Lock() + defer ca.mu.Unlock() + ca.domainGetCert[domain] = f +} + +// ResolveHandler redirects HTTP requests for domain to f when +// validating challenges for the domain authorization. +func (ca *CAServer) ResolveHandler(domain string, h http.Handler) { + ca.mu.Lock() + defer ca.mu.Unlock() + ca.domainHandler[domain] = h +} + type discovery struct { NewNonce string `json:"newNonce"` NewReg string `json:"newAccount"` @@ -163,6 +237,7 @@ type authorization struct { Challenges []challenge `json:"challenges"` domain string + id int } type order struct { @@ -175,7 +250,7 @@ type order struct { } func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s", r.Method, r.URL) + ca.t.Logf("%s %s", r.Method, r.URL) w.Header().Set("Replay-Nonce", "nonce") // TODO: Verify nonce header for all POST requests. @@ -189,7 +264,6 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { NewNonce: ca.serverURL("/new-nonce"), NewReg: ca.serverURL("/new-reg"), NewOrder: ca.serverURL("/new-order"), - NewAuthz: ca.serverURL("/new-authz"), } if err := json.NewEncoder(w).Encode(resp); err != nil { panic(fmt.Sprintf("discovery response: %v", err)) @@ -202,6 +276,13 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { // Client key registration request. case r.URL.Path == "/new-reg": + ca.mu.Lock() + defer ca.mu.Unlock() + if ca.acctRegistered { + ca.httpErrorf(w, http.StatusServiceUnavailable, "multiple accounts are not implemented") + return + } + ca.acctRegistered = true // TODO: Check the user account key against a ca.accountKeys? w.Header().Set("Location", ca.serverURL("/accounts/1")) w.WriteHeader(http.StatusCreated) @@ -221,7 +302,7 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { o := &order{Status: acme.StatusPending} for _, id := range req.Identifiers { z := ca.authz(id.Value) - o.AuthzURLs = append(o.AuthzURLs, ca.serverURL("/authz/%s", z.domain)) + o.AuthzURLs = append(o.AuthzURLs, ca.serverURL("/authz/%d", z.id)) } orderID := len(ca.orders) ca.orders = append(ca.orders, o) @@ -244,49 +325,49 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { panic(err) } - // Identifier authorization request. - case r.URL.Path == "/new-authz": - var req struct { - Identifier struct{ Value string } - } - if err := decodePayload(&req, r.Body); err != nil { - ca.httpErrorf(w, http.StatusBadRequest, err.Error()) - return - } + // Accept challenge requests. + case strings.HasPrefix(r.URL.Path, "/challenge/"): + parts := strings.Split(r.URL.Path, "/") + typ, id := parts[len(parts)-2], parts[len(parts)-1] ca.mu.Lock() - defer ca.mu.Unlock() - z := ca.authz(req.Identifier.Value) - w.Header().Set("Location", ca.serverURL("/authz/%s", z.domain)) - w.WriteHeader(http.StatusCreated) - if err := json.NewEncoder(w).Encode(z); err != nil { - panic(fmt.Sprintf("new authz response: %v", err)) + supported := false + for _, suppTyp := range ca.challengeTypes { + if suppTyp == typ { + supported = true + } } - - // Accept tls-alpn-01 challenge type requests. - case strings.HasPrefix(r.URL.Path, "/challenge/tls-alpn-01/"): - domain := strings.TrimPrefix(r.URL.Path, "/challenge/tls-alpn-01/") - ca.mu.Lock() - _, exist := ca.authorizations[domain] + a, err := ca.storedAuthz(id) ca.mu.Unlock() - if !exist { - ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: no authz for %q", domain) + if !supported { + ca.httpErrorf(w, http.StatusBadRequest, "unsupported challenge: %v", typ) return } - go ca.validateChallenge("tls-alpn-01", domain) + if err != nil { + ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: %v", err) + return + } + go ca.validateChallenge(a, typ) w.Write([]byte("{}")) // Get authorization status requests. case strings.HasPrefix(r.URL.Path, "/authz/"): - domain := strings.TrimPrefix(r.URL.Path, "/authz/") + var req struct{ Status string } + decodePayload(&req, r.Body) + deactivate := req.Status == "deactivated" ca.mu.Lock() defer ca.mu.Unlock() - authz, ok := ca.authorizations[domain] - if !ok { - ca.httpErrorf(w, http.StatusNotFound, "no authz for %q", domain) + authz, err := ca.storedAuthz(strings.TrimPrefix(r.URL.Path, "/authz/")) + if err != nil { + ca.httpErrorf(w, http.StatusNotFound, "%v", err) return } + if deactivate { + // Note we don't invalidate authorized orders as we should. + authz.Status = "deactivated" + ca.t.Logf("authz %d is now %s", authz.id, authz.Status) + } if err := json.NewEncoder(w).Encode(authz); err != nil { - panic(fmt.Sprintf("get authz for %q response: %v", domain, err)) + panic(fmt.Sprintf("encoding authz %d: %v", authz.id, err)) } // Certificate issuance request. @@ -314,15 +395,6 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { ca.httpErrorf(w, http.StatusBadRequest, err.Error()) return } - names := unique(append(csr.DNSNames, csr.Subject.CommonName)) - if err := ca.matchWhitelist(names); err != nil { - ca.httpErrorf(w, http.StatusUnauthorized, err.Error()) - return - } - if err := ca.authorized(names); err != nil { - ca.httpErrorf(w, http.StatusUnauthorized, err.Error()) - return - } // Issue the certificate. der, err := ca.leafCert(csr) if err != nil { @@ -355,25 +427,6 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { } } -// matchWhitelist reports whether all dnsNames are whitelisted. -// The whitelist is provided in NewCAServer. -func (ca *CAServer) matchWhitelist(dnsNames []string) error { - if len(ca.domainsWhitelist) == 0 { - return nil - } - var nomatch []string - for _, name := range dnsNames { - i := sort.SearchStrings(ca.domainsWhitelist, name) - if i == len(ca.domainsWhitelist) || ca.domainsWhitelist[i] != name { - nomatch = append(nomatch, name) - } - } - if len(nomatch) > 0 { - return fmt.Errorf("matchWhitelist: some domains don't match: %q", nomatch) - } - return nil -} - // storedOrder retrieves a previously created order at index i. // It requires ca.mu to be locked. func (ca *CAServer) storedOrder(i string) (*order, error) { @@ -390,43 +443,45 @@ func (ca *CAServer) storedOrder(i string) (*order, error) { return ca.orders[idx], nil } -// authz returns an existing authorization for the identifier or creates a new one. +// storedAuthz retrieves a previously created authz at index i. // It requires ca.mu to be locked. +func (ca *CAServer) storedAuthz(i string) (*authorization, error) { + idx, err := strconv.Atoi(i) + if err != nil { + return nil, fmt.Errorf("storedAuthz: %v", err) + } + if idx < 0 { + return nil, fmt.Errorf("storedAuthz: invalid authz index %d", idx) + } + if idx > len(ca.authorizations)-1 { + return nil, fmt.Errorf("storedAuthz: no such authz %d", idx) + } + return ca.authorizations[idx], nil +} + +// authz returns an existing valid authorization for the identifier or creates a +// new one. It requires ca.mu to be locked. func (ca *CAServer) authz(identifier string) *authorization { - authz, ok := ca.authorizations[identifier] + authz, ok := ca.validAuthz[identifier] if !ok { + authzId := len(ca.authorizations) authz = &authorization{ + id: authzId, domain: identifier, Status: acme.StatusPending, } for _, typ := range ca.challengeTypes { authz.Challenges = append(authz.Challenges, challenge{ Type: typ, - URI: ca.serverURL("/challenge/%s/%s", typ, authz.domain), - Token: challengeToken(authz.domain, typ), + URI: ca.serverURL("/challenge/%s/%d", typ, authzId), + Token: challengeToken(authz.domain, typ, authzId), }) } - ca.authorizations[authz.domain] = authz + ca.authorizations = append(ca.authorizations, authz) } return authz } -// authorized reports whether all authorizations for dnsNames have been satisfied. -// It requires ca.mu to be locked. -func (ca *CAServer) authorized(dnsNames []string) error { - var noauthz []string - for _, name := range dnsNames { - authz, ok := ca.authorizations[name] - if !ok || authz.Status != acme.StatusValid { - noauthz = append(noauthz, name) - } - } - if len(noauthz) > 0 { - return fmt.Errorf("CAServer: no authz for %q", noauthz) - } - return nil -} - // leafCert issues a new certificate. // It requires ca.mu to be locked. func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) { @@ -447,24 +502,72 @@ func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err erro return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey) } -// TODO: Only tls-alpn-01 is currently supported: implement http-01 and dns-01. -func (ca *CAServer) validateChallenge(typ, identifier string) { +// LeafCert issues a leaf certificate. +func (ca *CAServer) LeafCert(name, keyType string, notBefore, notAfter time.Time) *tls.Certificate { + if ca.url == "" { + panic("LeafCert called before Start") + } + + ca.mu.Lock() + defer ca.mu.Unlock() + var pk crypto.Signer + switch keyType { + case "RSA": + var err error + pk, err = rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + ca.t.Fatal(err) + } + case "ECDSA": + var err error + pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + ca.t.Fatal(err) + } + default: + panic("LeafCert: unknown key type") + } + ca.certCount++ // next leaf cert serial number + leaf := &x509.Certificate{ + SerialNumber: big.NewInt(int64(ca.certCount)), + Subject: pkix.Name{Organization: []string{"Test Acme Co"}}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{name}, + BasicConstraintsValid: true, + } + der, err := x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, pk.Public(), ca.rootKey) + if err != nil { + ca.t.Fatal(err) + } + return &tls.Certificate{ + Certificate: [][]byte{der}, + PrivateKey: pk, + } +} + +func (ca *CAServer) validateChallenge(authz *authorization, typ string) { var err error switch typ { case "tls-alpn-01": - err = ca.verifyALPNChallenge(identifier) + err = ca.verifyALPNChallenge(authz) + case "http-01": + err = ca.verifyHTTPChallenge(authz) default: panic(fmt.Sprintf("validation of %q is not implemented", typ)) } ca.mu.Lock() defer ca.mu.Unlock() - authz := ca.authorizations[identifier] if err != nil { authz.Status = "invalid" } else { authz.Status = "valid" + ca.validAuthz[authz.domain] = authz } - log.Printf("validated %q for %q; authz status is now: %s", typ, identifier, authz.Status) + ca.t.Logf("validated %q for %q, err: %v", typ, authz.domain, err) + ca.t.Logf("authz %d is now %s", authz.id, authz.Status) // Update all pending orders. // An order becomes "ready" if all authorizations are "valid". // An order becomes "invalid" if any authorization is "invalid". @@ -476,14 +579,14 @@ OrdersLoop: } var countValid int for _, zurl := range o.AuthzURLs { - z, ok := ca.authorizations[path.Base(zurl)] - if !ok { - log.Printf("no authz %q for order %d", zurl, i) + z, err := ca.storedAuthz(path.Base(zurl)) + if err != nil { + ca.t.Logf("no authz %q for order %d", zurl, i) continue OrdersLoop } if z.Status == acme.StatusInvalid { o.Status = acme.StatusInvalid - log.Printf("order %d is now invalid", i) + ca.t.Logf("order %d is now invalid", i) continue OrdersLoop } if z.Status == acme.StatusValid { @@ -493,33 +596,125 @@ OrdersLoop: if countValid == len(o.AuthzURLs) { o.Status = acme.StatusReady o.FinalizeURL = ca.serverURL("/new-cert/%d", i) - log.Printf("order %d is now ready", i) + ca.t.Logf("order %d is now ready", i) } } } -func (ca *CAServer) verifyALPNChallenge(domain string) error { +func (ca *CAServer) verifyALPNChallenge(a *authorization) error { const acmeALPNProto = "acme-tls/1" - addr, err := ca.addr(domain) - if err != nil { - return err + addr, haveAddr := ca.addr(a.domain) + getCert, haveGetCert := ca.getCert(a.domain) + if !haveAddr && !haveGetCert { + return fmt.Errorf("no resolution information for %q", a.domain) } - conn, err := tls.Dial("tcp", addr, &tls.Config{ - ServerName: domain, - InsecureSkipVerify: true, - NextProtos: []string{acmeALPNProto}, - }) - if err != nil { - return err + if haveAddr && haveGetCert { + return fmt.Errorf("overlapping resolution information for %q", a.domain) + } + + var crt *x509.Certificate + switch { + case haveAddr: + conn, err := tls.Dial("tcp", addr, &tls.Config{ + ServerName: a.domain, + InsecureSkipVerify: true, + NextProtos: []string{acmeALPNProto}, + MinVersion: tls.VersionTLS12, + }) + if err != nil { + return err + } + if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto { + return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto) + } + if n := len(conn.ConnectionState().PeerCertificates); n != 1 { + return fmt.Errorf("len(PeerCertificates) = %d; want 1", n) + } + crt = conn.ConnectionState().PeerCertificates[0] + case haveGetCert: + hello := &tls.ClientHelloInfo{ + ServerName: a.domain, + // TODO: support selecting ECDSA. + CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}, + SupportedProtos: []string{acme.ALPNProto}, + SupportedVersions: []uint16{tls.VersionTLS12}, + } + c, err := getCert(hello) + if err != nil { + return err + } + crt, err = x509.ParseCertificate(c.Certificate[0]) + if err != nil { + return err + } + } + + if err := crt.VerifyHostname(a.domain); err != nil { + return fmt.Errorf("verifyALPNChallenge: VerifyHostname: %v", err) + } + // See RFC 8737, Section 6.1. + oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} + for _, x := range crt.Extensions { + if x.Id.Equal(oid) { + // TODO: check the token. + return nil + } } - if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto { - return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto) + return fmt.Errorf("verifyTokenCert: no id-pe-acmeIdentifier extension found") +} + +func (ca *CAServer) verifyHTTPChallenge(a *authorization) error { + addr, haveAddr := ca.addr(a.domain) + handler, haveHandler := ca.getHandler(a.domain) + if !haveAddr && !haveHandler { + return fmt.Errorf("no resolution information for %q", a.domain) + } + if haveAddr && haveHandler { + return fmt.Errorf("overlapping resolution information for %q", a.domain) + } + + token := challengeToken(a.domain, "http-01", a.id) + path := "/.well-known/acme-challenge/" + token + + var body string + switch { + case haveAddr: + t := &http.Transport{ + DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, network, addr) + }, + } + req, err := http.NewRequest("GET", "http://"+a.domain+path, nil) + if err != nil { + return err + } + res, err := t.RoundTrip(req) + if err != nil { + return err + } + if res.StatusCode != http.StatusOK { + return fmt.Errorf("http token: w.Code = %d; want %d", res.StatusCode, http.StatusOK) + } + b, err := io.ReadAll(res.Body) + if err != nil { + return err + } + body = string(b) + case haveHandler: + r := httptest.NewRequest("GET", path, nil) + r.Host = a.domain + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + if w.Code != http.StatusOK { + return fmt.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK) + } + body = w.Body.String() } - if n := len(conn.ConnectionState().PeerCertificates); n != 1 { - return fmt.Errorf("len(PeerCertificates) = %d; want 1", n) + + if !strings.HasPrefix(body, token) { + return fmt.Errorf("http token value = %q; want 'token-http-01.' prefix", body) } - // TODO: verify conn.ConnectionState().PeerCertificates[0] return nil } @@ -535,8 +730,8 @@ func decodePayload(v interface{}, r io.Reader) error { return json.Unmarshal(payload, v) } -func challengeToken(domain, challType string) string { - return fmt.Sprintf("token-%s-%s", domain, challType) +func challengeToken(domain, challType string, authzID int) string { + return fmt.Sprintf("token-%s-%s-%d", domain, challType, authzID) } func unique(a []string) []string { diff --git a/acme/autocert/renewal_test.go b/acme/autocert/renewal_test.go index d13d190..94bbc60 100644 --- a/acme/autocert/renewal_test.go +++ b/acme/autocert/renewal_test.go @@ -6,19 +6,13 @@ package autocert import ( "context" + "crypto" "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "encoding/base64" - "fmt" - "net/http" - "net/http/httptest" "testing" "time" "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert/internal/acmetest" ) func TestRenewalNext(t *testing.T) { @@ -48,86 +42,20 @@ func TestRenewalNext(t *testing.T) { } func TestRenewFromCache(t *testing.T) { - // 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 - } + man := testManager(t) + man.RenewBefore = 24 * time.Hour - switch r.URL.Path { - // discovery - case "/": - if err := discoTmpl.Execute(w, ca.URL); err != nil { - t.Fatalf("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) - w.Write([]byte(`{"status": "valid"}`)) - // authorization status request done by Manager's revokePendingAuthz. - 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.Fatalf("new-cert: CSR: %v", err) - } - der, err := dummyCert(csr.PublicKey, exampleDomain) - if err != nil { - t.Fatalf("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.Fatalf("ca-cert: dummyCert: %v", err) - } - w.Write(der) - default: - t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) - } - })) - defer ca.Close() + ca := acmetest.NewCAServer(t).Start() + ca.ResolveGetCertificate(exampleDomain, man.GetCertificate) - man := &Manager{ - Prompt: AcceptTOS, - Cache: newMemCache(t), - RenewBefore: 24 * time.Hour, - Client: &acme.Client{ - DirectoryURL: ca.URL, - }, + man.Client = &acme.Client{ + DirectoryURL: ca.URL(), } - defer man.stopRenew() // cache an almost expired cert - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } now := time.Now() - cert, err := dateDummyCert(key.Public(), now.Add(-2*time.Hour), now.Add(time.Minute), exampleDomain) - if err != nil { - t.Fatal(err) - } - tlscert := &tls.Certificate{PrivateKey: key, Certificate: [][]byte{cert}} - if err := man.cachePut(context.Background(), exampleCertKey, tlscert); err != nil { + c := ca.LeafCert(exampleDomain, "ECDSA", now.Add(-2*time.Hour), now.Add(time.Minute)) + if err := man.cachePut(context.Background(), exampleCertKey, c); err != nil { t.Fatal(err) } @@ -142,7 +70,7 @@ func TestRenewFromCache(t *testing.T) { t.Errorf("testDidRenewLoop: %v", err) } // Next should be about 90 days: - // dummyCert creates 90days expiry + account for man.RenewBefore. + // CaServer creates 90days expiry + account for man.RenewBefore. // Previous expiration was within 1 min. future := 88 * 24 * time.Hour if next < future { @@ -190,45 +118,30 @@ func TestRenewFromCache(t *testing.T) { } func TestRenewFromCacheAlreadyRenewed(t *testing.T) { - man := &Manager{ - Prompt: AcceptTOS, - Cache: newMemCache(t), - RenewBefore: 24 * time.Hour, - Client: &acme.Client{ - DirectoryURL: "invalid", - }, + ca := acmetest.NewCAServer(t).Start() + man := testManager(t) + man.RenewBefore = 24 * time.Hour + man.Client = &acme.Client{ + DirectoryURL: "invalid", } - defer man.stopRenew() // cache a recently renewed cert with a different private key - newKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } now := time.Now() - newCert, err := dateDummyCert(newKey.Public(), now.Add(-2*time.Hour), now.Add(time.Hour*24*90), exampleDomain) - if err != nil { + newCert := ca.LeafCert(exampleDomain, "ECDSA", now.Add(-2*time.Hour), now.Add(time.Hour*24*90)) + if err := man.cachePut(context.Background(), exampleCertKey, newCert); err != nil { t.Fatal(err) } - newLeaf, err := validCert(exampleCertKey, [][]byte{newCert}, newKey, now) + newLeaf, err := validCert(exampleCertKey, newCert.Certificate, newCert.PrivateKey.(crypto.Signer), now) if err != nil { t.Fatal(err) } - newTLSCert := &tls.Certificate{PrivateKey: newKey, Certificate: [][]byte{newCert}, Leaf: newLeaf} - if err := man.cachePut(context.Background(), exampleCertKey, newTLSCert); err != nil { - t.Fatal(err) - } // set internal state to an almost expired cert - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + oldCert := ca.LeafCert(exampleDomain, "ECDSA", now.Add(-2*time.Hour), now.Add(time.Minute)) if err != nil { t.Fatal(err) } - oldCert, err := dateDummyCert(key.Public(), now.Add(-2*time.Hour), now.Add(time.Minute), exampleDomain) - if err != nil { - t.Fatal(err) - } - oldLeaf, err := validCert(exampleCertKey, [][]byte{oldCert}, key, now) + oldLeaf, err := validCert(exampleCertKey, oldCert.Certificate, oldCert.PrivateKey.(crypto.Signer), now) if err != nil { t.Fatal(err) } @@ -237,14 +150,14 @@ func TestRenewFromCacheAlreadyRenewed(t *testing.T) { man.state = make(map[certKey]*certState) } s := &certState{ - key: key, - cert: [][]byte{oldCert}, + key: oldCert.PrivateKey.(crypto.Signer), + cert: oldCert.Certificate, leaf: oldLeaf, } man.state[exampleCertKey] = s man.stateMu.Unlock() - // veriy the renewal accepted the newer cached cert + // verify the renewal accepted the newer cached cert defer func() { testDidRenewLoop = func(next time.Duration, err error) {} }() @@ -278,8 +191,8 @@ func TestRenewFromCacheAlreadyRenewed(t *testing.T) { t.Fatalf("m.state[%q] is nil", exampleCertKey) } stateKey := s.key.Public().(*ecdsa.PublicKey) - if stateKey.X.Cmp(newKey.X) != 0 || stateKey.Y.Cmp(newKey.Y) != 0 { - t.Fatalf("state key was not updated from cache x: %v y: %v; want x: %v y: %v", stateKey.X, stateKey.Y, newKey.X, newKey.Y) + if !stateKey.Equal(newLeaf.PublicKey) { + t.Fatal("state key was not updated from cache") } tlscert, err = s.tlscert() if err != nil { @@ -295,8 +208,8 @@ func TestRenewFromCacheAlreadyRenewed(t *testing.T) { t.Fatalf("m.renewal[%q] is nil", exampleCertKey) } renewalKey := r.key.Public().(*ecdsa.PublicKey) - if renewalKey.X.Cmp(newKey.X) != 0 || renewalKey.Y.Cmp(newKey.Y) != 0 { - t.Fatalf("renewal private key was not updated from cache x: %v y: %v; want x: %v y: %v", renewalKey.X, renewalKey.Y, newKey.X, newKey.Y) + if !renewalKey.Equal(newLeaf.PublicKey) { + t.Fatal("renewal private key was not updated from cache") } } @@ -325,8 +238,8 @@ func TestRenewFromCacheAlreadyRenewed(t *testing.T) { if err != nil { t.Fatal(err) } - if !newTLSCert.Leaf.NotAfter.Equal(tlscert.Leaf.NotAfter) { - t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newTLSCert.Leaf.NotAfter) + if !newLeaf.NotAfter.Equal(tlscert.Leaf.NotAfter) { + t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newLeaf.NotAfter) } } } -- cgit v1.3