Skip to content

Commit 76bc7bb

Browse files
desdeel2d0mFiloSottile
authored andcommitted
acme/autocert: revoke dangling pending authzs
We now keep track of pending authorization requests during verify() and defer the asynchronous revocation of the ones that failed. This should help avoid letsencrypt's "too many currently pending authorizations" error. Fixes golang/go#23426 Change-Id: Ibffb10f59733962d45e43b67fc42a2ec7c5faf51 Reviewed-on: https://go-review.googlesource.com/100078 Run-TryBot: Filippo Valsorda <filippo@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Leo Antunes <costela@gmail.com> Reviewed-by: Filippo Valsorda <filippo@golang.org>
1 parent 4a5907b commit 76bc7bb

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

acme/autocert/autocert.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,20 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain
542542
return der, leaf, nil
543543
}
544544

545+
// revokePending revokes all provided authorizations (passed as a map of URIs)
546+
func (m *Manager) revokePending(ctx context.Context, pendingAuthzURIs map[string]bool) {
547+
f := func(ctx context.Context) {
548+
for uri := range pendingAuthzURIs {
549+
m.Client.RevokeAuthorization(ctx, uri)
550+
}
551+
}
552+
if waitForRevocations {
553+
f(ctx)
554+
} else {
555+
go f(context.Background())
556+
}
557+
}
558+
545559
// verify runs the identifier (domain) authorization flow
546560
// using each applicable ACME challenge type.
547561
func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string) error {
@@ -554,6 +568,10 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string
554568
}
555569
m.tokensMu.RUnlock()
556570

571+
// we keep track of pending authzs and revoke the ones that did not validate.
572+
pendingAuthzs := make(map[string]bool)
573+
defer m.revokePending(ctx, pendingAuthzs)
574+
557575
var nextTyp int // challengeType index of the next challenge type to try
558576
for {
559577
// Start domain authorization and get the challenge.
@@ -570,6 +588,8 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string
570588
return fmt.Errorf("acme/autocert: invalid authorization %q", authz.URI)
571589
}
572590

591+
pendingAuthzs[authz.URI] = true
592+
573593
// Pick the next preferred challenge.
574594
var chal *acme.Challenge
575595
for chal == nil && nextTyp < len(challengeTypes) {
@@ -590,6 +610,7 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string
590610

591611
// A challenge is fulfilled and accepted: wait for the CA to validate.
592612
if _, err := client.WaitAuthorization(ctx, authz.URI); err == nil {
613+
delete(pendingAuthzs, authz.URI)
593614
return nil
594615
}
595616
}
@@ -959,4 +980,7 @@ var (
959980

960981
// Called when a state is removed.
961982
testDidRemoveState = func(domain string) {}
983+
984+
// make testing of revokePending synchronous
985+
waitForRevocations = false
962986
)

acme/autocert/autocert_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import (
1919
"fmt"
2020
"html/template"
2121
"io"
22+
"io/ioutil"
2223
"math/big"
2324
"net/http"
2425
"net/http/httptest"
2526
"reflect"
27+
"strconv"
2628
"strings"
2729
"sync"
2830
"testing"
@@ -529,6 +531,103 @@ func TestVerifyHTTP01(t *testing.T) {
529531
}
530532
}
531533

534+
func TestRevokeFailedAuth(t *testing.T) {
535+
var (
536+
authzCount int // num. of created authorizations
537+
revoked [3]bool // there will be three different authorization attempts; see below
538+
)
539+
540+
// make cleanup revocations synchronous
541+
waitForRevocations = true
542+
543+
// ACME CA server stub, only the needed bits.
544+
// TODO: Merge this with startACMEServerStub, making it a configurable CA for testing.
545+
var ca *httptest.Server
546+
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
547+
w.Header().Set("Replay-Nonce", "nonce")
548+
if r.Method == "HEAD" {
549+
// a nonce request
550+
return
551+
}
552+
553+
switch r.URL.Path {
554+
// Discovery.
555+
case "/":
556+
if err := discoTmpl.Execute(w, ca.URL); err != nil {
557+
t.Errorf("discoTmpl: %v", err)
558+
}
559+
// Client key registration.
560+
case "/new-reg":
561+
w.Write([]byte("{}"))
562+
// New domain authorization.
563+
case "/new-authz":
564+
w.Header().Set("Location", fmt.Sprintf("%s/authz/%d", ca.URL, authzCount))
565+
w.WriteHeader(http.StatusCreated)
566+
if err := authzTmpl.Execute(w, ca.URL); err != nil {
567+
t.Errorf("authzTmpl: %v", err)
568+
}
569+
authzCount++
570+
// force error on Accept()
571+
case "/challenge/2":
572+
http.Error(w, "won't accept tls-sni-02 challenge", http.StatusBadRequest)
573+
// accept; authorization will be expired (404; see /authz/1 below)
574+
case "/challenge/1":
575+
w.Write([]byte("{}"))
576+
// Authorization statuses.
577+
case "/authz/0", "/authz/1", "/authz/2":
578+
if r.Method == "POST" {
579+
body, err := ioutil.ReadAll(r.Body)
580+
if err != nil {
581+
t.Errorf("could not read request body")
582+
}
583+
if reflect.DeepEqual(body, []byte(`{"status": "deactivated"}`)) {
584+
t.Errorf("unexpected POST to authz: '%s'", body)
585+
}
586+
i, err := strconv.ParseInt(r.URL.Path[len(r.URL.Path)-1:], 10, 64)
587+
if err != nil {
588+
t.Errorf("could not convert authz ID to int")
589+
}
590+
revoked[i] = true
591+
w.Write([]byte(`{"status": "invalid"}`))
592+
} else {
593+
switch r.URL.Path {
594+
case "/authz/0":
595+
// ensure we get a spurious validation if error forcing did not work (see above)
596+
w.Write([]byte(`{"status": "valid"}`))
597+
case "/authz/1":
598+
// force error during WaitAuthorization()
599+
w.WriteHeader(http.StatusNotFound)
600+
}
601+
}
602+
default:
603+
http.NotFound(w, r)
604+
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
605+
}
606+
}))
607+
defer ca.Close()
608+
609+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
610+
if err != nil {
611+
t.Fatal(err)
612+
}
613+
m := &Manager{
614+
Client: &acme.Client{
615+
Key: key,
616+
DirectoryURL: ca.URL,
617+
},
618+
}
619+
620+
// should fail and revoke tsl-sni-02, tls-sni-01 and the third time after not finding any remaining challenges
621+
if err := m.verify(context.Background(), m.Client, "example.org"); err == nil {
622+
t.Errorf("m.verify should have failed!")
623+
}
624+
for i := range revoked {
625+
if !revoked[i] {
626+
t.Errorf("authorization attempt %d not revoked after error", i)
627+
}
628+
}
629+
}
630+
532631
func TestHTTPHandlerDefaultFallback(t *testing.T) {
533632
tt := []struct {
534633
method, url string

0 commit comments

Comments
 (0)