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 <filippo@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Trust: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
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 @@
"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 @@
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 @@
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 @@
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 testManager(t *testing.T) *Manager {
+ man := &Manager{
+ Prompt: AcceptTOS,
+ Cache: newMemCache(t),
}
+ t.Cleanup(man.stopRenew)
+ return man
}
func TestGetCertificate(t *testing.T) {
- man := &Manager{Prompt: AcceptTOS}
- defer man.stopRenew()
- hello := clientHelloInfo("example.org", algECDSA)
- testGetCertificate(t, man, "example.org", hello)
-}
+ 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))
+ }
-func TestGetCertificate_trailingDot(t *testing.T) {
- man := &Manager{Prompt: AcceptTOS}
- defer man.stopRenew()
- hello := clientHelloInfo("example.org.", algECDSA)
- testGetCertificate(t, man, "example.org", hello)
-}
+ if tt.prepare != nil {
+ tt.prepare(t, man, s)
+ }
-func TestGetCertificate_unicodeIDN(t *testing.T) {
- man := &Manager{Prompt: AcceptTOS}
- defer man.stopRenew()
+ s.Start()
- hello := clientHelloInfo("éé.com", algECDSA)
- testGetCertificate(t, man, "xn--9caa.com", hello)
+ man.Client = &acme.Client{DirectoryURL: s.URL()}
- hello = clientHelloInfo("éÉ.com", algECDSA)
- testGetCertificate(t, man, "xn--9caa.com", hello)
-}
+ 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_mixedcase(t *testing.T) {
- man := &Manager{Prompt: AcceptTOS}
- defer man.stopRenew()
+ 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)
+ }
- hello := clientHelloInfo("example.org", algECDSA)
- testGetCertificate(t, man, "example.org", hello)
+ if san := leaf.DNSNames[0]; san != tt.domain {
+ t.Errorf("got SAN %q, expected %q", san, tt.domain)
+ }
- hello = clientHelloInfo("EXAMPLE.ORG", algECDSA)
- testGetCertificate(t, man, "example.org", hello)
-}
-
-func TestGetCertificate_ForceRSA(t *testing.T) {
- man := &Manager{
- Prompt: AcceptTOS,
- Cache: newMemCache(t),
- ForceRSA: true,
- }
- defer man.stopRenew()
- hello := clientHelloInfo(exampleDomain, algECDSA)
- testGetCertificate(t, man, exampleDomain, hello)
-
- // ForceRSA was deprecated and is now ignored.
- cert, err := man.cacheGet(context.Background(), exampleCertKey)
- if err != nil {
- t.Fatalf("man.cacheGet: %v", err)
- }
- if _, ok := cert.PrivateKey.(*ecdsa.PrivateKey); !ok {
- t.Errorf("cert.PrivateKey is %T; want *ecdsa.PrivateKey", cert.PrivateKey)
- }
-}
-
-func TestGetCertificate_nilPrompt(t *testing.T) {
- man := &Manager{}
- defer man.stopRenew()
- url, finish := startACMEServerStub(t, tokenCertFn(man, algECDSA), "example.org")
- defer finish()
- man.Client = &acme.Client{DirectoryURL: url}
- hello := clientHelloInfo("example.org", algECDSA)
- if _, err := man.GetCertificate(hello); err == nil {
- t.Error("got certificate for example.org; wanted error")
- }
-}
-
-func TestGetCertificate_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")
- }
-}
-
-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,
- }
-
- 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_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,
- }
-
- 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 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 @@
}
}
-// testGetCertificate_tokenCache tests the fallback of token certificate fetches
-// to cache when Manager.certTokens misses.
-// algorithmSupport refers to the CA when verifying the certificate token.
-func testGetCertificate_tokenCache(t *testing.T, tokenAlg algorithmSupport) {
- man1 := &Manager{
- Cache: newMemCache(t),
- Prompt: AcceptTOS,
- }
- defer man1.stopRenew()
- man2 := &Manager{
- Cache: man1.Cache,
- Prompt: AcceptTOS,
- }
- defer man2.stopRenew()
-
- // Send the verification request to a different Manager from the one that
- // initiated the authorization, when they share caches.
- url, finish := startACMEServerStub(t, tokenCertFn(man2, tokenAlg), "example.org")
- defer finish()
- man1.Client = &acme.Client{DirectoryURL: url}
- man2.Client = &acme.Client{DirectoryURL: url}
- hello := clientHelloInfo("example.org", algECDSA)
- if _, err := man1.GetCertificate(hello); err != nil {
- t.Error(err)
- }
- if _, err := man2.GetCertificate(hello); err != nil {
- t.Error(err)
- }
-}
-
-func TestGetCertificate_tokenCache(t *testing.T) {
- t.Run("ecdsaSupport=true", func(t *testing.T) {
- testGetCertificate_tokenCache(t, algECDSA)
- })
- t.Run("ecdsaSupport=false", func(t *testing.T) {
- testGetCertificate_tokenCache(t, algRSA)
- })
-}
-
-func TestGetCertificate_ecdsaVsRSA(t *testing.T) {
- cache := newMemCache(t)
- man := &Manager{Prompt: AcceptTOS, Cache: cache}
- defer man.stopRenew()
- url, finish := startACMEServerStub(t, tokenCertFn(man, algECDSA), "example.org")
- defer finish()
- man.Client = &acme.Client{DirectoryURL: url}
-
- cert, err := man.GetCertificate(clientHelloInfo("example.org", algECDSA))
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok {
- t.Error("an ECDSA client was served a non-ECDSA certificate")
- }
-
- cert, err = man.GetCertificate(clientHelloInfo("example.org", algRSA))
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := cert.Leaf.PublicKey.(*rsa.PublicKey); !ok {
- t.Error("a RSA client was served a non-RSA certificate")
- }
-
- if _, err := man.GetCertificate(clientHelloInfo("example.org", algECDSA)); err != nil {
- t.Error(err)
- }
- if _, err := man.GetCertificate(clientHelloInfo("example.org", algRSA)); err != nil {
- t.Error(err)
- }
- if numCerts := cache.numCerts(); numCerts != 2 {
- t.Errorf("found %d certificates in cache; want %d", numCerts, 2)
- }
-}
-
-func TestGetCertificate_wrongCacheKeyType(t *testing.T) {
- cache := newMemCache(t)
- man := &Manager{Prompt: AcceptTOS, Cache: cache}
- defer man.stopRenew()
- url, finish := startACMEServerStub(t, tokenCertFn(man, algECDSA), exampleDomain)
- defer finish()
- man.Client = &acme.Client{DirectoryURL: url}
-
- // Make an RSA cert and cache it without suffix.
- pk, err := rsa.GenerateKey(rand.Reader, 512)
- if err != nil {
- t.Fatal(err)
- }
- tmpl := &x509.Certificate{
- SerialNumber: big.NewInt(1),
- 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)
- }
-
- // The RSA cached cert should be silently ignored and replaced.
- cert, err := man.GetCertificate(clientHelloInfo(exampleDomain, algECDSA))
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok {
- t.Error("an ECDSA client was served a non-ECDSA certificate")
- }
- if numCerts := cache.numCerts(); numCerts != 1 {
- t.Errorf("found %d certificates in cache; want %d", numCerts, 1)
- }
-}
-
-type getCertificateFunc func(domain string) (*tls.Certificate, error)
-
-// startACMEServerStub runs an ACME server
-// The domain argument is the expected domain name of a certificate request.
-// TODO: Drop this in favour of x/crypto/acme/autocert/internal/acmetest.
-func startACMEServerStub(t *testing.T, tokenCert getCertificateFunc, domain string) (url string, finish func()) {
- verifyTokenCert := func() {
- tlscert, err := tokenCert(domain)
- if err != nil {
- t.Errorf("verifyTokenCert: tokenCert(%q): %v", domain, err)
- return
- }
- crt, err := x509.ParseCertificate(tlscert.Certificate[0])
- if err != nil {
- t.Errorf("verifyTokenCert: x509.ParseCertificate: %v", err)
- }
- if err := crt.VerifyHostname(domain); err != nil {
- t.Errorf("verifyTokenCert: %v", err)
- }
- // 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.Error("verifyTokenCert: no id-pe-acmeIdentifier extension found")
- }
-
- // ACME CA server stub
- var ca *httptest.Server
- ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Replay-Nonce", "nonce")
- if r.Method == "HEAD" {
- // a nonce request
- return
- }
-
- switch r.URL.Path {
- // discovery
- case "/":
- if err := discoTmpl.Execute(w, ca.URL); err != nil {
- t.Errorf("discoTmpl: %v", err)
- }
- // client key registration
- case "/new-reg":
- w.Write([]byte("{}"))
- // domain authorization
- case "/new-authz":
- w.Header().Set("Location", ca.URL+"/authz/1")
- w.WriteHeader(http.StatusCreated)
- if err := authzTmpl.Execute(w, ca.URL); err != nil {
- t.Errorf("authzTmpl: %v", err)
- }
- // accept tls-alpn-01 challenge
- case "/challenge/tls-alpn-01":
- verifyTokenCert()
- w.Write([]byte("{}"))
- // authorization status
- case "/authz/1":
- w.Write([]byte(`{"status": "valid"}`))
- // cert request
- case "/new-cert":
- var req struct {
- CSR string `json:"csr"`
- }
- decodePayload(&req, r.Body)
- b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
- csr, err := x509.ParseCertificateRequest(b)
- if err != nil {
- t.Errorf("new-cert: CSR: %v", err)
- }
- if csr.Subject.CommonName != domain {
- t.Errorf("CommonName in CSR = %q; want %q", csr.Subject.CommonName, domain)
- }
- der, err := dummyCert(csr.PublicKey, domain)
- if err != nil {
- t.Errorf("new-cert: dummyCert: %v", err)
- }
- chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
- w.Header().Set("Link", chainUp)
- w.WriteHeader(http.StatusCreated)
- w.Write(der)
- // CA chain cert
- case "/ca-cert":
- der, err := dummyCert(nil, "ca")
- if err != nil {
- t.Errorf("ca-cert: dummyCert: %v", err)
- }
- w.Write(der)
- default:
- t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
- }
- }))
- finish = func() {
- ca.Close()
-
- // make sure token cert was removed
- cancel := make(chan struct{})
- done := make(chan struct{})
- go func() {
- defer close(done)
- tick := time.NewTicker(100 * time.Millisecond)
- defer tick.Stop()
- for {
- if _, err := tokenCert(domain); err != nil {
- return
- }
- select {
- case <-tick.C:
- case <-cancel:
- return
- }
- }
- }()
- select {
- case <-done:
- case <-time.After(5 * time.Second):
- close(cancel)
- t.Error("token cert was not removed")
- <-done
- }
- }
- return ca.URL, finish
-}
-
-// tests man.GetCertificate flow using the provided hello argument.
-// The domain argument is the expected domain name of a certificate request.
-func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) *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)
- }
-
- // 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
+ 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")
}
- 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
+ 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")
- 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 @@
}
}
-// 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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
Challenges []challenge `json:"challenges"`
domain string
+ id int
}
type order struct {
@@ -175,7 +250,7 @@
}
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
}
}
-// 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 @@
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 @@
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 @@
}
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 @@
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)
}
- if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto {
- return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto)
+
+ 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 n := len(conn.ConnectionState().PeerCertificates); n != 1 {
- return fmt.Errorf("len(PeerCertificates) = %d; want 1", n)
+
+ if err := crt.VerifyHostname(a.domain); err != nil {
+ return fmt.Errorf("verifyALPNChallenge: VerifyHostname: %v", err)
}
- // TODO: verify conn.ConnectionState().PeerCertificates[0]
+ // 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
+ }
+ }
+ 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 !strings.HasPrefix(body, token) {
+ return fmt.Errorf("http token value = %q; want 'token-http-01.' prefix", body)
+ }
return nil
}
@@ -535,8 +730,8 @@
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 @@
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 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 @@
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 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 @@
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 @@
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 @@
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 @@
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)
}
}
}