diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index 5256bc3..980c045 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 6020e55..74d1397 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 acc486a..faffd20 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 294f902..d13d190 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 {