Skip to content

Commit 57b49e0

Browse files
x1ddosAlex Vaghin
authored and
Alex Vaghin
committed
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 <[email protected]>
1 parent c7f4797 commit 57b49e0

File tree

4 files changed

+412
-162
lines changed

4 files changed

+412
-162
lines changed

acme/autocert/autocert.go

Lines changed: 172 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import (
3535
"golang.org/x/net/idna"
3636
)
3737

38+
// DefaultACMEDirectory is the default ACME Directory URL used when the Manager's Client is nil.
39+
const DefaultACMEDirectory = "https://acme-v02.api.letsencrypt.org/directory"
40+
3841
// createCertRetryAfter is how much time to wait before removing a failed state
3942
// entry due to an unsuccessful createCert call.
4043
// This is a variable instead of a const for testing.
@@ -135,9 +138,10 @@ type Manager struct {
135138
// Client is used to perform low-level operations, such as account registration
136139
// and requesting new certificates.
137140
//
138-
// If Client is nil, a zero-value acme.Client is used with acme.LetsEncryptURL
139-
// as directory endpoint. If the Client.Key is nil, a new ECDSA P-256 key is
140-
// generated and, if Cache is not nil, stored in cache.
141+
// If Client is nil, a zero-value acme.Client is used with DefaultACMEDirectory
142+
// as the directory endpoint.
143+
// If the Client.Key is nil, a new ECDSA P-256 key is generated and,
144+
// if Cache is not nil, stored in cache.
141145
//
142146
// Mutating the field after the first call of GetCertificate method will have no effect.
143147
Client *acme.Client
@@ -640,78 +644,72 @@ func (m *Manager) certState(ck certKey) (*certState, error) {
640644
// authorizedCert starts the domain ownership verification process and requests a new cert upon success.
641645
// The key argument is the certificate private key.
642646
func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, ck certKey) (der [][]byte, leaf *x509.Certificate, err error) {
643-
client, err := m.acmeClient(ctx)
644-
if err != nil {
645-
return nil, nil, err
646-
}
647-
648-
if err := m.verify(ctx, client, ck.domain); err != nil {
649-
return nil, nil, err
650-
}
651647
csr, err := certRequest(key, ck.domain, m.ExtraExtensions)
652648
if err != nil {
653649
return nil, nil, err
654650
}
655-
der, _, err = client.CreateCert(ctx, csr, 0, true)
651+
652+
client, err := m.acmeClient(ctx)
656653
if err != nil {
657654
return nil, nil, err
658655
}
659-
leaf, err = validCert(ck, der, key, m.now())
656+
dir, err := client.Discover(ctx)
660657
if err != nil {
661658
return nil, nil, err
662659
}
663-
return der, leaf, nil
664-
}
665660

666-
// revokePendingAuthz revokes all authorizations idenfied by the elements of uri slice.
667-
// It ignores revocation errors.
668-
func (m *Manager) revokePendingAuthz(ctx context.Context, uri []string) {
669-
client, err := m.acmeClient(ctx)
670-
if err != nil {
671-
return
661+
var chain [][]byte
662+
switch {
663+
// Pre-RFC legacy CA.
664+
case dir.OrderURL == "":
665+
if err := m.verify(ctx, client, ck.domain); err != nil {
666+
return nil, nil, err
667+
}
668+
der, _, err := client.CreateCert(ctx, csr, 0, true)
669+
if err != nil {
670+
return nil, nil, err
671+
}
672+
chain = der
673+
// RFC 8555 compliant CA.
674+
default:
675+
o, err := m.verifyRFC(ctx, client, ck.domain)
676+
if err != nil {
677+
return nil, nil, err
678+
}
679+
der, _, err := client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
680+
if err != nil {
681+
return nil, nil, err
682+
}
683+
chain = der
672684
}
673-
for _, u := range uri {
674-
client.RevokeAuthorization(ctx, u)
685+
leaf, err = validCert(ck, chain, key, m.now())
686+
if err != nil {
687+
return nil, nil, err
675688
}
689+
return chain, leaf, nil
676690
}
677691

678-
// verify runs the identifier (domain) authorization flow
692+
// verify runs the identifier (domain) pre-authorization flow for legacy CAs
679693
// using each applicable ACME challenge type.
680694
func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string) error {
681-
// The list of challenge types we'll try to fulfill
682-
// in this specific order.
683-
challengeTypes := []string{"tls-alpn-01"}
684-
m.tokensMu.RLock()
685-
if m.tryHTTP01 {
686-
challengeTypes = append(challengeTypes, "http-01")
687-
}
688-
m.tokensMu.RUnlock()
689-
690-
// Keep track of pending authzs and revoke the ones that did not validate.
691-
pendingAuthzs := make(map[string]bool)
695+
// Remove all hanging authorizations to reduce rate limit quotas
696+
// after we're done.
697+
var authzURLs []string
692698
defer func() {
693-
var uri []string
694-
for k, pending := range pendingAuthzs {
695-
if pending {
696-
uri = append(uri, k)
697-
}
698-
}
699-
if len(uri) > 0 {
700-
// Use "detached" background context.
701-
// The revocations need not happen in the current verification flow.
702-
go m.revokePendingAuthz(context.Background(), uri)
703-
}
699+
go m.deactivatePendingAuthz(authzURLs)
704700
}()
705701

706702
// errs accumulates challenge failure errors, printed if all fail
707703
errs := make(map[*acme.Challenge]error)
704+
challengeTypes := m.supportedChallengeTypes()
708705
var nextTyp int // challengeType index of the next challenge type to try
709706
for {
710707
// Start domain authorization and get the challenge.
711708
authz, err := client.Authorize(ctx, domain)
712709
if err != nil {
713710
return err
714711
}
712+
authzURLs = append(authzURLs, authz.URI)
715713
// No point in accepting challenges if the authorization status
716714
// is in a final state.
717715
switch authz.Status {
@@ -721,8 +719,6 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string
721719
return fmt.Errorf("acme/autocert: invalid authorization %q", authz.URI)
722720
}
723721

724-
pendingAuthzs[authz.URI] = true
725-
726722
// Pick the next preferred challenge.
727723
var chal *acme.Challenge
728724
for chal == nil && nextTyp < len(challengeTypes) {
@@ -752,11 +748,126 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string
752748
errs[chal] = err
753749
continue
754750
}
755-
delete(pendingAuthzs, authz.URI)
756751
return nil
757752
}
758753
}
759754

755+
// verifyRFC runs the identifier (domain) order-based authorization flow for RFC compliant CAs
756+
// using each applicable ACME challenge type.
757+
func (m *Manager) verifyRFC(ctx context.Context, client *acme.Client, domain string) (*acme.Order, error) {
758+
// Try each supported challenge type starting with a new order each time.
759+
// The nextTyp index of the next challenge type to try is shared across
760+
// all order authorizations: if we've tried a challenge type once and it didn't work,
761+
// it will most likely not work on another order's authorization either.
762+
challengeTypes := m.supportedChallengeTypes()
763+
nextTyp := 0 // challengeTypes index
764+
AuthorizeOrderLoop:
765+
for {
766+
o, err := client.AuthorizeOrder(ctx, acme.DomainIDs(domain))
767+
if err != nil {
768+
return nil, err
769+
}
770+
// Remove all hanging authorizations to reduce rate limit quotas
771+
// after we're done.
772+
defer func() {
773+
go m.deactivatePendingAuthz(o.AuthzURLs)
774+
}()
775+
776+
// Check if there's actually anything we need to do.
777+
switch o.Status {
778+
case acme.StatusReady:
779+
// Already authorized.
780+
return o, nil
781+
case acme.StatusPending:
782+
// Continue normal Order-based flow.
783+
default:
784+
return nil, fmt.Errorf("acme/autocert: invalid new order status %q; order URL: %q", o.Status, o.URI)
785+
}
786+
787+
// Satisfy all pending authorizations.
788+
for _, zurl := range o.AuthzURLs {
789+
z, err := client.GetAuthorization(ctx, zurl)
790+
if err != nil {
791+
return nil, err
792+
}
793+
if z.Status != acme.StatusPending {
794+
// We are interested only in pending authorizations.
795+
continue
796+
}
797+
// Pick the next preferred challenge.
798+
var chal *acme.Challenge
799+
for chal == nil && nextTyp < len(challengeTypes) {
800+
chal = pickChallenge(challengeTypes[nextTyp], z.Challenges)
801+
nextTyp++
802+
}
803+
if chal == nil {
804+
return nil, fmt.Errorf("acme/autocert: unable to satisfy %q for domain %q: no viable challenge type found", z.URI, domain)
805+
}
806+
// Respond to the challenge and wait for validation result.
807+
cleanup, err := m.fulfill(ctx, client, chal, domain)
808+
if err != nil {
809+
continue AuthorizeOrderLoop
810+
}
811+
defer cleanup()
812+
if _, err := client.Accept(ctx, chal); err != nil {
813+
continue AuthorizeOrderLoop
814+
}
815+
if _, err := client.WaitAuthorization(ctx, z.URI); err != nil {
816+
continue AuthorizeOrderLoop
817+
}
818+
}
819+
820+
// All authorizations are satisfied.
821+
// Wait for the CA to update the order status.
822+
o, err = client.WaitOrder(ctx, o.URI)
823+
if err != nil {
824+
continue AuthorizeOrderLoop
825+
}
826+
return o, nil
827+
}
828+
}
829+
830+
func pickChallenge(typ string, chal []*acme.Challenge) *acme.Challenge {
831+
for _, c := range chal {
832+
if c.Type == typ {
833+
return c
834+
}
835+
}
836+
return nil
837+
}
838+
839+
func (m *Manager) supportedChallengeTypes() []string {
840+
m.tokensMu.RLock()
841+
defer m.tokensMu.RUnlock()
842+
typ := []string{"tls-alpn-01"}
843+
if m.tryHTTP01 {
844+
typ = append(typ, "http-01")
845+
}
846+
return typ
847+
}
848+
849+
// deactivatePendingAuthz relinquishes all authorizations identified by the elements
850+
// of the provided uri slice which are in "pending" state.
851+
// It ignores revocation errors.
852+
//
853+
// deactivatePendingAuthz takes no context argument and instead runs with its own
854+
// "detached" context because deactivations are done in a goroutine separate from
855+
// that of the main issuance or renewal flow.
856+
func (m *Manager) deactivatePendingAuthz(uri []string) {
857+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
858+
defer cancel()
859+
client, err := m.acmeClient(ctx)
860+
if err != nil {
861+
return
862+
}
863+
for _, u := range uri {
864+
z, err := client.GetAuthorization(ctx, u)
865+
if err == nil && z.Status == acme.StatusPending {
866+
client.RevokeAuthorization(ctx, u)
867+
}
868+
}
869+
}
870+
760871
// fulfill provisions a response to the challenge chal.
761872
// The cleanup is non-nil only if provisioning succeeded.
762873
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
780891
return nil, fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type)
781892
}
782893

783-
func pickChallenge(typ string, chal []*acme.Challenge) *acme.Challenge {
784-
for _, c := range chal {
785-
if c.Type == typ {
786-
return c
787-
}
788-
}
789-
return nil
790-
}
791-
792894
// putCertToken stores the token certificate with the specified name
793895
// in both m.certTokens map and m.Cache.
794896
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) {
9491051

9501052
client := m.Client
9511053
if client == nil {
952-
client = &acme.Client{DirectoryURL: acme.LetsEncryptURL}
1054+
client = &acme.Client{DirectoryURL: DefaultACMEDirectory}
9531055
}
9541056
if client.Key == nil {
9551057
var err error
@@ -967,14 +1069,23 @@ func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) {
9671069
}
9681070
a := &acme.Account{Contact: contact}
9691071
_, err := client.Register(ctx, a, m.Prompt)
970-
if ae, ok := err.(*acme.Error); err == nil || ok && ae.StatusCode == http.StatusConflict {
971-
// conflict indicates the key is already registered
1072+
if err == nil || isAccountAlreadyExist(err) {
9721073
m.client = client
9731074
err = nil
9741075
}
9751076
return m.client, err
9761077
}
9771078

1079+
// isAccountAlreadyExist reports whether the err, as returned from acme.Client.Register,
1080+
// indicates the account has already been registered.
1081+
func isAccountAlreadyExist(err error) bool {
1082+
if err == acme.ErrAccountAlreadyExists {
1083+
return true
1084+
}
1085+
ae, ok := err.(*acme.Error)
1086+
return ok && ae.StatusCode == http.StatusConflict
1087+
}
1088+
9781089
func (m *Manager) hostPolicy() HostPolicy {
9791090
if m.HostPolicy != nil {
9801091
return m.HostPolicy

acme/autocert/autocert_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ func testGetCertificate_tokenCache(t *testing.T, tokenAlg algorithmSupport) {
372372
url, finish := startACMEServerStub(t, tokenCertFn(man2, tokenAlg), "example.org")
373373
defer finish()
374374
man1.Client = &acme.Client{DirectoryURL: url}
375+
man2.Client = &acme.Client{DirectoryURL: url}
375376
hello := clientHelloInfo("example.org", algECDSA)
376377
if _, err := man1.GetCertificate(hello); err != nil {
377378
t.Error(err)
@@ -400,15 +401,15 @@ func TestGetCertificate_ecdsaVsRSA(t *testing.T) {
400401

401402
cert, err := man.GetCertificate(clientHelloInfo("example.org", algECDSA))
402403
if err != nil {
403-
t.Error(err)
404+
t.Fatal(err)
404405
}
405406
if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok {
406407
t.Error("an ECDSA client was served a non-ECDSA certificate")
407408
}
408409

409410
cert, err = man.GetCertificate(clientHelloInfo("example.org", algRSA))
410411
if err != nil {
411-
t.Error(err)
412+
t.Fatal(err)
412413
}
413414
if _, ok := cert.Leaf.PublicKey.(*rsa.PublicKey); !ok {
414415
t.Error("a RSA client was served a non-RSA certificate")
@@ -458,7 +459,7 @@ func TestGetCertificate_wrongCacheKeyType(t *testing.T) {
458459
// The RSA cached cert should be silently ignored and replaced.
459460
cert, err := man.GetCertificate(clientHelloInfo(exampleDomain, algECDSA))
460461
if err != nil {
461-
t.Error(err)
462+
t.Fatal(err)
462463
}
463464
if _, ok := cert.Leaf.PublicKey.(*ecdsa.PublicKey); !ok {
464465
t.Error("an ECDSA client was served a non-ECDSA certificate")
@@ -778,8 +779,9 @@ func TestRevokeFailedAuthz(t *testing.T) {
778779
http.Error(w, "won't accept tls-alpn-01 challenge", http.StatusBadRequest)
779780
// http-01 challenge "accept" request.
780781
case "/challenge/http-01":
781-
// Accept but the authorization will be "expired".
782-
w.Write([]byte("{}"))
782+
// Refuse.
783+
w.WriteHeader(http.StatusBadRequest)
784+
w.Write([]byte(`{"status":"invalid"}`))
783785
// Authorization requests.
784786
case "/authz/0", "/authz/1", "/authz/2":
785787
// Revocation requests.
@@ -803,8 +805,7 @@ func TestRevokeFailedAuthz(t *testing.T) {
803805
return
804806
}
805807
// Authorization status requests.
806-
// Simulate abandoned authorization, deleted by the CA.
807-
w.WriteHeader(http.StatusNotFound)
808+
w.Write([]byte(`{"status":"pending"}`))
808809
default:
809810
http.NotFound(w, r)
810811
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
@@ -1219,7 +1220,6 @@ func TestEndToEnd(t *testing.T) {
12191220
client := &http.Client{Transport: tr}
12201221
res, err := client.Get(us.URL)
12211222
if err != nil {
1222-
t.Logf("CA errors: %v", ca.Errors())
12231223
t.Fatal(err)
12241224
}
12251225
defer res.Body.Close()

0 commit comments

Comments
 (0)