From 0e8c3a9068a2fb325459490d2ce7baa4ffe2d00f Mon Sep 17 00:00:00 2001 From: Alex Vaghin Date: Thu, 26 Sep 2019 16:26:22 +0200 Subject: [PATCH] acme/autocert: support ACME RFC 8555 The Manager now uses RFC 8555 implementation of Let's Encrypt by default. Existing users need not do any manual upgrades. If you vendor acme/autocert, it is enough to just rebuild your binaries at this CL. If there's an account key stored in Manager's cache which has been used with an earlier Let's Encrypt implementation (aka v1 or draft-02), it will be automatically re-registered with the new endpoint. One notable change is the CAServer from internal/acmetest was amended to simulate a CA implementing RFC 8555, replacing draft-02. Support for both RFC and draft-02 seemed too complicated and not worth the benefits: the old pre-RFC bits will be removed from both acme and acme/autocert packages at some point. Fixes golang/go#21081 Change-Id: Id530758ac612b1c20f9df51c4d10f770e5f41ecf Reviewed-on: https://go-review.googlesource.com/c/crypto/+/199520 Reviewed-by: Brad Fitzpatrick --- acme/autocert/autocert.go | 233 ++++++++++++++----- acme/autocert/autocert_test.go | 16 +- acme/autocert/internal/acmetest/ca.go | 322 ++++++++++++++++++-------- acme/autocert/renewal_test.go | 3 + 4 files changed, 412 insertions(+), 162 deletions(-) diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index 5256bc3105..980c0451ac 100644 --- a/acme/autocert/autocert.go +++ b/acme/autocert/autocert.go @@ -35,6 +35,9 @@ import ( "golang.org/x/net/idna" ) +// DefaultACMEDirectory is the default ACME Directory URL used when the Manager's Client is nil. +const DefaultACMEDirectory = "https://acme-v02.api.letsencrypt.org/directory" + // createCertRetryAfter is how much time to wait before removing a failed state // entry due to an unsuccessful createCert call. // This is a variable instead of a const for testing. @@ -135,9 +138,10 @@ type Manager struct { // Client is used to perform low-level operations, such as account registration // and requesting new certificates. // - // If Client is nil, a zero-value acme.Client is used with acme.LetsEncryptURL - // as directory endpoint. If the Client.Key is nil, a new ECDSA P-256 key is - // generated and, if Cache is not nil, stored in cache. + // If Client is nil, a zero-value acme.Client is used with DefaultACMEDirectory + // as the directory endpoint. + // If the Client.Key is nil, a new ECDSA P-256 key is generated and, + // if Cache is not nil, stored in cache. // // Mutating the field after the first call of GetCertificate method will have no effect. Client *acme.Client @@ -640,71 +644,64 @@ func (m *Manager) certState(ck certKey) (*certState, error) { // authorizedCert starts the domain ownership verification process and requests a new cert upon success. // The key argument is the certificate private key. func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, ck certKey) (der [][]byte, leaf *x509.Certificate, err error) { - client, err := m.acmeClient(ctx) - if err != nil { - return nil, nil, err - } - - if err := m.verify(ctx, client, ck.domain); err != nil { - return nil, nil, err - } csr, err := certRequest(key, ck.domain, m.ExtraExtensions) if err != nil { return nil, nil, err } - der, _, err = client.CreateCert(ctx, csr, 0, true) + + client, err := m.acmeClient(ctx) if err != nil { return nil, nil, err } - leaf, err = validCert(ck, der, key, m.now()) + dir, err := client.Discover(ctx) if err != nil { return nil, nil, err } - return der, leaf, nil -} -// revokePendingAuthz revokes all authorizations idenfied by the elements of uri slice. -// It ignores revocation errors. -func (m *Manager) revokePendingAuthz(ctx context.Context, uri []string) { - client, err := m.acmeClient(ctx) - if err != nil { - return + var chain [][]byte + switch { + // Pre-RFC legacy CA. + case dir.OrderURL == "": + if err := m.verify(ctx, client, ck.domain); err != nil { + return nil, nil, err + } + der, _, err := client.CreateCert(ctx, csr, 0, true) + if err != nil { + return nil, nil, err + } + chain = der + // RFC 8555 compliant CA. + default: + o, err := m.verifyRFC(ctx, client, ck.domain) + if err != nil { + return nil, nil, err + } + der, _, err := client.CreateOrderCert(ctx, o.FinalizeURL, csr, true) + if err != nil { + return nil, nil, err + } + chain = der } - for _, u := range uri { - client.RevokeAuthorization(ctx, u) + leaf, err = validCert(ck, chain, key, m.now()) + if err != nil { + return nil, nil, err } + return chain, leaf, nil } -// verify runs the identifier (domain) authorization flow +// verify runs the identifier (domain) pre-authorization flow for legacy CAs // using each applicable ACME challenge type. func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string) error { - // The list of challenge types we'll try to fulfill - // in this specific order. - challengeTypes := []string{"tls-alpn-01"} - m.tokensMu.RLock() - if m.tryHTTP01 { - challengeTypes = append(challengeTypes, "http-01") - } - m.tokensMu.RUnlock() - - // Keep track of pending authzs and revoke the ones that did not validate. - pendingAuthzs := make(map[string]bool) + // Remove all hanging authorizations to reduce rate limit quotas + // after we're done. + var authzURLs []string defer func() { - var uri []string - for k, pending := range pendingAuthzs { - if pending { - uri = append(uri, k) - } - } - if len(uri) > 0 { - // Use "detached" background context. - // The revocations need not happen in the current verification flow. - go m.revokePendingAuthz(context.Background(), uri) - } + go m.deactivatePendingAuthz(authzURLs) }() // errs accumulates challenge failure errors, printed if all fail errs := make(map[*acme.Challenge]error) + challengeTypes := m.supportedChallengeTypes() var nextTyp int // challengeType index of the next challenge type to try for { // Start domain authorization and get the challenge. @@ -712,6 +709,7 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string if err != nil { return err } + authzURLs = append(authzURLs, authz.URI) // No point in accepting challenges if the authorization status // is in a final state. switch authz.Status { @@ -721,8 +719,6 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string return fmt.Errorf("acme/autocert: invalid authorization %q", authz.URI) } - pendingAuthzs[authz.URI] = true - // Pick the next preferred challenge. var chal *acme.Challenge for chal == nil && nextTyp < len(challengeTypes) { @@ -752,11 +748,126 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string errs[chal] = err continue } - delete(pendingAuthzs, authz.URI) return nil } } +// verifyRFC runs the identifier (domain) order-based authorization flow for RFC compliant CAs +// using each applicable ACME challenge type. +func (m *Manager) verifyRFC(ctx context.Context, client *acme.Client, domain string) (*acme.Order, error) { + // Try each supported challenge type starting with a new order each time. + // The nextTyp index of the next challenge type to try is shared across + // all order authorizations: if we've tried a challenge type once and it didn't work, + // it will most likely not work on another order's authorization either. + challengeTypes := m.supportedChallengeTypes() + nextTyp := 0 // challengeTypes index +AuthorizeOrderLoop: + for { + o, err := client.AuthorizeOrder(ctx, acme.DomainIDs(domain)) + if err != nil { + return nil, err + } + // Remove all hanging authorizations to reduce rate limit quotas + // after we're done. + defer func() { + go m.deactivatePendingAuthz(o.AuthzURLs) + }() + + // Check if there's actually anything we need to do. + switch o.Status { + case acme.StatusReady: + // Already authorized. + return o, nil + case acme.StatusPending: + // Continue normal Order-based flow. + default: + return nil, fmt.Errorf("acme/autocert: invalid new order status %q; order URL: %q", o.Status, o.URI) + } + + // Satisfy all pending authorizations. + for _, zurl := range o.AuthzURLs { + z, err := client.GetAuthorization(ctx, zurl) + if err != nil { + return nil, err + } + if z.Status != acme.StatusPending { + // We are interested only in pending authorizations. + continue + } + // Pick the next preferred challenge. + var chal *acme.Challenge + for chal == nil && nextTyp < len(challengeTypes) { + chal = pickChallenge(challengeTypes[nextTyp], z.Challenges) + nextTyp++ + } + if chal == nil { + return nil, fmt.Errorf("acme/autocert: unable to satisfy %q for domain %q: no viable challenge type found", z.URI, domain) + } + // Respond to the challenge and wait for validation result. + cleanup, err := m.fulfill(ctx, client, chal, domain) + if err != nil { + continue AuthorizeOrderLoop + } + defer cleanup() + if _, err := client.Accept(ctx, chal); err != nil { + continue AuthorizeOrderLoop + } + if _, err := client.WaitAuthorization(ctx, z.URI); err != nil { + continue AuthorizeOrderLoop + } + } + + // All authorizations are satisfied. + // Wait for the CA to update the order status. + o, err = client.WaitOrder(ctx, o.URI) + if err != nil { + continue AuthorizeOrderLoop + } + return o, nil + } +} + +func pickChallenge(typ string, chal []*acme.Challenge) *acme.Challenge { + for _, c := range chal { + if c.Type == typ { + return c + } + } + return nil +} + +func (m *Manager) supportedChallengeTypes() []string { + m.tokensMu.RLock() + defer m.tokensMu.RUnlock() + typ := []string{"tls-alpn-01"} + if m.tryHTTP01 { + typ = append(typ, "http-01") + } + return typ +} + +// deactivatePendingAuthz relinquishes all authorizations identified by the elements +// of the provided uri slice which are in "pending" state. +// It ignores revocation errors. +// +// deactivatePendingAuthz takes no context argument and instead runs with its own +// "detached" context because deactivations are done in a goroutine separate from +// that of the main issuance or renewal flow. +func (m *Manager) deactivatePendingAuthz(uri []string) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + client, err := m.acmeClient(ctx) + if err != nil { + return + } + for _, u := range uri { + z, err := client.GetAuthorization(ctx, u) + if err == nil && z.Status == acme.StatusPending { + client.RevokeAuthorization(ctx, u) + } + } +} + // fulfill provisions a response to the challenge chal. // The cleanup is non-nil only if provisioning succeeded. func (m *Manager) fulfill(ctx context.Context, client *acme.Client, chal *acme.Challenge, domain string) (cleanup func(), err error) { @@ -780,15 +891,6 @@ func (m *Manager) fulfill(ctx context.Context, client *acme.Client, chal *acme.C return nil, fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type) } -func pickChallenge(typ string, chal []*acme.Challenge) *acme.Challenge { - for _, c := range chal { - if c.Type == typ { - return c - } - } - return nil -} - // putCertToken stores the token certificate with the specified name // in both m.certTokens map and m.Cache. func (m *Manager) putCertToken(ctx context.Context, name string, cert *tls.Certificate) { @@ -949,7 +1051,7 @@ func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) { client := m.Client if client == nil { - client = &acme.Client{DirectoryURL: acme.LetsEncryptURL} + client = &acme.Client{DirectoryURL: DefaultACMEDirectory} } if client.Key == nil { var err error @@ -967,14 +1069,23 @@ func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) { } a := &acme.Account{Contact: contact} _, err := client.Register(ctx, a, m.Prompt) - if ae, ok := err.(*acme.Error); err == nil || ok && ae.StatusCode == http.StatusConflict { - // conflict indicates the key is already registered + if err == nil || isAccountAlreadyExist(err) { m.client = client err = nil } return m.client, err } +// isAccountAlreadyExist reports whether the err, as returned from acme.Client.Register, +// indicates the account has already been registered. +func isAccountAlreadyExist(err error) bool { + if err == acme.ErrAccountAlreadyExists { + return true + } + ae, ok := err.(*acme.Error) + return ok && ae.StatusCode == http.StatusConflict +} + func (m *Manager) hostPolicy() HostPolicy { if m.HostPolicy != nil { return m.HostPolicy diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go index 6020e55ae6..74d1397500 100644 --- a/acme/autocert/autocert_test.go +++ b/acme/autocert/autocert_test.go @@ -372,6 +372,7 @@ func testGetCertificate_tokenCache(t *testing.T, tokenAlg algorithmSupport) { 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) @@ -400,7 +401,7 @@ func TestGetCertificate_ecdsaVsRSA(t *testing.T) { cert, err := man.GetCertificate(clientHelloInfo("example.org", algECDSA)) if err != nil { - t.Error(err) + t.Fatal(err) } if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok { t.Error("an ECDSA client was served a non-ECDSA certificate") @@ -408,7 +409,7 @@ func TestGetCertificate_ecdsaVsRSA(t *testing.T) { cert, err = man.GetCertificate(clientHelloInfo("example.org", algRSA)) if err != nil { - t.Error(err) + t.Fatal(err) } if _, ok := cert.Leaf.PublicKey.(*rsa.PublicKey); !ok { t.Error("a RSA client was served a non-RSA certificate") @@ -458,7 +459,7 @@ func TestGetCertificate_wrongCacheKeyType(t *testing.T) { // The RSA cached cert should be silently ignored and replaced. cert, err := man.GetCertificate(clientHelloInfo(exampleDomain, algECDSA)) if err != nil { - t.Error(err) + t.Fatal(err) } if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok { t.Error("an ECDSA client was served a non-ECDSA certificate") @@ -778,8 +779,9 @@ func TestRevokeFailedAuthz(t *testing.T) { http.Error(w, "won't accept tls-alpn-01 challenge", http.StatusBadRequest) // http-01 challenge "accept" request. case "/challenge/http-01": - // Accept but the authorization will be "expired". - w.Write([]byte("{}")) + // Refuse. + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"status":"invalid"}`)) // Authorization requests. case "/authz/0", "/authz/1", "/authz/2": // Revocation requests. @@ -803,8 +805,7 @@ func TestRevokeFailedAuthz(t *testing.T) { return } // Authorization status requests. - // Simulate abandoned authorization, deleted by the CA. - w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"status":"pending"}`)) default: http.NotFound(w, r) t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) @@ -1219,7 +1220,6 @@ func TestEndToEnd(t *testing.T) { client := &http.Client{Transport: tr} res, err := client.Get(us.URL) if err != nil { - t.Logf("CA errors: %v", ca.Errors()) t.Fatal(err) } defer res.Body.Close() diff --git a/acme/autocert/internal/acmetest/ca.go b/acme/autocert/internal/acmetest/ca.go index acc486af5c..faffd20bc9 100644 --- a/acme/autocert/internal/acmetest/ca.go +++ b/acme/autocert/internal/acmetest/ca.go @@ -17,15 +17,21 @@ import ( "crypto/x509/pkix" "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "io" + "log" "math/big" "net/http" "net/http/httptest" + "path" "sort" + "strconv" "strings" "sync" "time" + + "golang.org/x/crypto/acme" ) // CAServer is a simple test server which implements ACME spec bits needed for testing. @@ -45,6 +51,7 @@ type CAServer struct { 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 } @@ -83,7 +90,7 @@ func NewCAServer(challengeTypes []string, domainsWhitelist []string) *CAServer { NotAfter: time.Now().Add(365 * 24 * time.Hour), KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true, - IsCA: true, + IsCA: true, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) if err != nil { @@ -110,11 +117,24 @@ func (ca *CAServer) Close() { ca.server.Close() } -// Errors returns all client errors. -func (ca *CAServer) Errors() []error { +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) { ca.mu.Lock() defer ca.mu.Unlock() - return ca.errors + addr, ok := ca.domainAddr[domain] + if !ok { + return "", fmt.Errorf("CAServer: no addr resolution for %q", domain) + } + return addr, nil +} + +func (ca *CAServer) httpErrorf(w http.ResponseWriter, code int, format string, a ...interface{}) { + s := fmt.Sprintf(format, a...) + log.Println(s) + http.Error(w, s, code) } // Resolve adds a domain to address resolution for the ca to dial to @@ -126,9 +146,10 @@ func (ca *CAServer) Resolve(domain, addr string) { } type discovery struct { - NewReg string `json:"new-reg"` - NewAuthz string `json:"new-authz"` - NewCert string `json:"new-cert"` + NewNonce string `json:"newNonce"` + NewReg string `json:"newAccount"` + NewOrder string `json:"newOrder"` + NewAuthz string `json:"newAuthz"` } type challenge struct { @@ -141,98 +162,117 @@ type authorization struct { Status string `json:"status"` Challenges []challenge `json:"challenges"` - id int domain string } +type order struct { + Status string `json:"status"` + AuthzURLs []string `json:"authorizations"` + FinalizeURL string `json:"finalize"` // CSR submit URL + CertURL string `json:"certificate"` // already issued cert + + leaf []byte // issued cert in DER format +} + func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s", r.Method, r.URL) w.Header().Set("Replay-Nonce", "nonce") - if r.Method == "HEAD" { - // a nonce request - return - } - // TODO: Verify nonce header for all POST requests. switch { default: - err := fmt.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) - ca.addError(err) - http.Error(w, err.Error(), http.StatusBadRequest) + ca.httpErrorf(w, http.StatusBadRequest, "unrecognized r.URL.Path: %s", r.URL.Path) // Discovery request. case r.URL.Path == "/": resp := &discovery{ + NewNonce: ca.serverURL("/new-nonce"), NewReg: ca.serverURL("/new-reg"), + NewOrder: ca.serverURL("/new-order"), NewAuthz: ca.serverURL("/new-authz"), - NewCert: ca.serverURL("/new-cert"), } if err := json.NewEncoder(w).Encode(resp); err != nil { panic(fmt.Sprintf("discovery response: %v", err)) } + // Nonce requests. + case r.URL.Path == "/new-nonce": + // Nonce values are always set. Nothing else to do. + return + // Client key registration request. case r.URL.Path == "/new-reg": // TODO: Check the user account key against a ca.accountKeys? + w.Header().Set("Location", ca.serverURL("/accounts/1")) + w.WriteHeader(http.StatusCreated) w.Write([]byte("{}")) - // Domain authorization request. + // New order request. + case r.URL.Path == "/new-order": + var req struct { + Identifiers []struct{ Value string } + } + if err := decodePayload(&req, r.Body); err != nil { + ca.httpErrorf(w, http.StatusBadRequest, err.Error()) + return + } + ca.mu.Lock() + defer ca.mu.Unlock() + 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)) + } + orderID := len(ca.orders) + ca.orders = append(ca.orders, o) + w.Header().Set("Location", ca.serverURL("/orders/%d", orderID)) + w.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(w).Encode(o); err != nil { + panic(err) + } + + // Existing order status requests. + case strings.HasPrefix(r.URL.Path, "/orders/"): + ca.mu.Lock() + defer ca.mu.Unlock() + o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/orders/")) + if err != nil { + ca.httpErrorf(w, http.StatusBadRequest, err.Error()) + return + } + if err := json.NewEncoder(w).Encode(o); err != nil { + 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.addError(err) - http.Error(w, err.Error(), http.StatusBadRequest) + ca.httpErrorf(w, http.StatusBadRequest, err.Error()) return } ca.mu.Lock() defer ca.mu.Unlock() - authz, ok := ca.authorizations[req.Identifier.Value] - if !ok { - authz = &authorization{ - domain: req.Identifier.Value, - Status: "pending", - } - 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), - }) - } - ca.authorizations[authz.domain] = authz - } - w.Header().Set("Location", ca.serverURL("/authz/%s", authz.domain)) + 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(authz); err != nil { + if err := json.NewEncoder(w).Encode(z); err != nil { panic(fmt.Sprintf("new authz response: %v", err)) } // Accept tls-alpn-01 challenge type requests. - // TODO: Add http-01 and dns-01 handlers. case strings.HasPrefix(r.URL.Path, "/challenge/tls-alpn-01/"): domain := strings.TrimPrefix(r.URL.Path, "/challenge/tls-alpn-01/") ca.mu.Lock() - defer ca.mu.Unlock() - if _, ok := ca.authorizations[domain]; !ok { - err := fmt.Errorf("challenge accept: no authz for %q", domain) - ca.addError(err) - http.Error(w, err.Error(), http.StatusNotFound) + _, exist := ca.authorizations[domain] + ca.mu.Unlock() + if !exist { + ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: no authz for %q", domain) return } - go func(domain string) { - err := ca.verifyALPNChallenge(domain) - ca.mu.Lock() - defer ca.mu.Unlock() - authz := ca.authorizations[domain] - if err != nil { - authz.Status = "invalid" - return - } - authz.Status = "valid" - - }(domain) + go ca.validateChallenge("tls-alpn-01", domain) w.Write([]byte("{}")) // Get authorization status requests. @@ -242,15 +282,28 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { defer ca.mu.Unlock() authz, ok := ca.authorizations[domain] if !ok { - http.Error(w, fmt.Sprintf("no authz for %q", domain), http.StatusNotFound) + ca.httpErrorf(w, http.StatusNotFound, "no authz for %q", domain) return } if err := json.NewEncoder(w).Encode(authz); err != nil { panic(fmt.Sprintf("get authz for %q response: %v", domain, err)) } - // Cert issuance request. - case r.URL.Path == "/new-cert": + // Certificate issuance request. + case strings.HasPrefix(r.URL.Path, "/new-cert/"): + ca.mu.Lock() + defer ca.mu.Unlock() + orderID := strings.TrimPrefix(r.URL.Path, "/new-cert/") + o, err := ca.storedOrder(orderID) + if err != nil { + ca.httpErrorf(w, http.StatusBadRequest, err.Error()) + return + } + if o.Status != acme.StatusReady { + ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status) + return + } + // Validate CSR request. var req struct { CSR string `json:"csr"` } @@ -258,47 +311,52 @@ func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { b, _ := base64.RawURLEncoding.DecodeString(req.CSR) csr, err := x509.ParseCertificateRequest(b) if err != nil { - ca.addError(err) - http.Error(w, err.Error(), http.StatusBadRequest) + ca.httpErrorf(w, http.StatusBadRequest, err.Error()) return } names := unique(append(csr.DNSNames, csr.Subject.CommonName)) if err := ca.matchWhitelist(names); err != nil { - ca.addError(err) - http.Error(w, err.Error(), http.StatusUnauthorized) + ca.httpErrorf(w, http.StatusUnauthorized, err.Error()) return } if err := ca.authorized(names); err != nil { - ca.addError(err) - http.Error(w, err.Error(), http.StatusUnauthorized) + ca.httpErrorf(w, http.StatusUnauthorized, err.Error()) return } + // Issue the certificate. der, err := ca.leafCert(csr) if err != nil { - err = fmt.Errorf("new-cert response: ca.leafCert: %v", err) - ca.addError(err) - http.Error(w, err.Error(), http.StatusBadRequest) + ca.httpErrorf(w, http.StatusBadRequest, "new-cert response: ca.leafCert: %v", err) + return + } + o.leaf = der + o.CertURL = ca.serverURL("/issued-cert/%s", orderID) + o.Status = acme.StatusValid + if err := json.NewEncoder(w).Encode(o); err != nil { + panic(err) } - w.Header().Set("Link", fmt.Sprintf("<%s>; rel=up", ca.serverURL("/ca-cert"))) - w.WriteHeader(http.StatusCreated) - w.Write(der) - // CA chain cert request. - case r.URL.Path == "/ca-cert": - w.Write(ca.rootCert) + // Already issued cert download requests. + case strings.HasPrefix(r.URL.Path, "/issued-cert/"): + ca.mu.Lock() + defer ca.mu.Unlock() + o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/issued-cert/")) + if err != nil { + ca.httpErrorf(w, http.StatusBadRequest, err.Error()) + return + } + if o.Status != acme.StatusValid { + ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status) + return + } + w.Header().Set("Content-Type", "application/pem-certificate-chain") + pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: o.leaf}) + pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: ca.rootCert}) } } -func (ca *CAServer) addError(err error) { - ca.mu.Lock() - defer ca.mu.Unlock() - ca.errors = append(ca.errors, err) -} - -func (ca *CAServer) serverURL(format string, arg ...interface{}) string { - return ca.server.URL + fmt.Sprintf(format, arg...) -} - +// 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 @@ -316,13 +374,50 @@ func (ca *CAServer) matchWhitelist(dnsNames []string) error { 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) { + idx, err := strconv.Atoi(i) + if err != nil { + return nil, fmt.Errorf("storedOrder: %v", err) + } + if idx < 0 { + return nil, fmt.Errorf("storedOrder: invalid order index %d", idx) + } + if idx > len(ca.orders)-1 { + return nil, fmt.Errorf("storedOrder: no such order %d", idx) + } + return ca.orders[idx], nil +} + +// authz returns an existing 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] + if !ok { + authz = &authorization{ + 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), + }) + } + ca.authorizations[authz.domain] = 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 { - ca.mu.Lock() - defer ca.mu.Unlock() var noauthz []string for _, name := range dnsNames { authz, ok := ca.authorizations[name] - if !ok || authz.Status != "valid" { + if !ok || authz.Status != acme.StatusValid { noauthz = append(noauthz, name) } } @@ -332,9 +427,9 @@ func (ca *CAServer) authorized(dnsNames []string) error { 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) { - ca.mu.Lock() - defer ca.mu.Unlock() ca.certCount++ // next leaf cert serial number leaf := &x509.Certificate{ SerialNumber: big.NewInt(int64(ca.certCount)), @@ -352,14 +447,55 @@ func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err erro return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey) } -func (ca *CAServer) addr(domain string) (string, error) { +// TODO: Only tls-alpn-01 is currently supported: implement http-01 and dns-01. +func (ca *CAServer) validateChallenge(typ, identifier string) { + var err error + switch typ { + case "tls-alpn-01": + err = ca.verifyALPNChallenge(identifier) + default: + panic(fmt.Sprintf("validation of %q is not implemented", typ)) + } ca.mu.Lock() defer ca.mu.Unlock() - addr, ok := ca.domainAddr[domain] - if !ok { - return "", fmt.Errorf("CAServer: no addr resolution for %q", domain) + authz := ca.authorizations[identifier] + if err != nil { + authz.Status = "invalid" + } else { + authz.Status = "valid" + } + log.Printf("validated %q for %q; authz status is now: %s", typ, identifier, authz.Status) + // Update all pending orders. + // An order becomes "ready" if all authorizations are "valid". + // An order becomes "invalid" if any authorization is "invalid". + // Status changes: https://tools.ietf.org/html/rfc8555#section-7.1.6 +OrdersLoop: + for i, o := range ca.orders { + if o.Status != acme.StatusPending { + continue + } + 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) + continue OrdersLoop + } + if z.Status == acme.StatusInvalid { + o.Status = acme.StatusInvalid + log.Printf("order %d is now invalid", i) + continue OrdersLoop + } + if z.Status == acme.StatusValid { + countValid++ + } + } + 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) + } } - return addr, nil } func (ca *CAServer) verifyALPNChallenge(domain string) error { diff --git a/acme/autocert/renewal_test.go b/acme/autocert/renewal_test.go index 294f902643..d13d19041d 100644 --- a/acme/autocert/renewal_test.go +++ b/acme/autocert/renewal_test.go @@ -71,6 +71,9 @@ func TestRenewFromCache(t *testing.T) { 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 {