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>
This commit is contained in:
Filippo Valsorda
2022-01-28 20:06:15 +00:00
parent aa10faf2a1
commit 198e4374d7
3 changed files with 761 additions and 1023 deletions
File diff suppressed because it is too large Load Diff
+355 -160
View File
@@ -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)
}
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)
}
// 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)
}
// 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 {
+30 -117
View File
@@ -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)
}
}
}