Skip to content

Commit 72ffb11

Browse files
committed
Add test for Authz revocation. Count "revoked" authz in cert-checker.
1 parent 0d82ade commit 72ffb11

5 files changed

Lines changed: 100 additions & 5 deletions

File tree

cmd/cert-checker/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,8 @@ var expectedExtensionContent = map[string][]byte{
322322

323323
// checkValidations checks the database for matching authorizations that were
324324
// likely valid at the time the certificate was issued. Authorizations with
325-
// status = "deactivated" are counted for this, so long as their validatedAt
326-
// is before the issuance and expiration is after.
325+
// status = "deactivated" and "revoked" are counted for this, so long as their
326+
// validatedAt is before the issuance and expiration is after.
327327
func (c *certChecker) checkValidations(ctx context.Context, cert *corepb.Certificate, idents identifier.ACMEIdentifiers) error {
328328
authzs, err := sa.SelectAuthzsMatchingIssuance(ctx, c.dbMap, cert.RegistrationID, cert.Issued.AsTime(), idents)
329329
if err != nil {

ra/ra.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1600,6 +1600,9 @@ func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx contex
16001600
}
16011601

16021602
func (ra *RegistrationAuthorityImpl) revokeAuthorizations(ctx context.Context, cert *x509.Certificate, smeta *sapb.SerialMetadata) {
1603+
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
1604+
defer cancel()
1605+
16031606
if features.Get().RevokeAuthzsUponRevokeCert {
16041607
idents := identifier.FromCert(cert)
16051608
for _, ident := range idents {
@@ -1608,7 +1611,9 @@ func (ra *RegistrationAuthorityImpl) revokeAuthorizations(ctx context.Context, c
16081611
Identifier: ident.ToProto(),
16091612
})
16101613
if err != nil {
1611-
ra.log.Infof("Authz revocation failed: %s", err)
1614+
ra.log.Error(ctx, "Authz revocation failed", err, blog.Idents(ident), blog.Acct(smeta.RegistrationID))
1615+
} else {
1616+
ra.log.Info(ctx, "Authz revocation succeeded", blog.Idents(ident), blog.Acct(smeta.RegistrationID))
16121617
}
16131618
}
16141619
}

ra/ra_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3681,6 +3681,7 @@ func TestRevokeAuthorizations_FeatureDisabled(t *testing.T) {
36813681
// mockSA should not have received ANY requests
36823682
test.AssertEquals(t, len(mockSA.recv), 0)
36833683
}
3684+
36843685
func TestRevokeAuthorizations_FeatureEnabled(t *testing.T) {
36853686
_, _, ra, _, clk, _, cleanUp := initAuthorities(t)
36863687
defer cleanUp()

sa/model.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ func SelectAuthzsMatchingIssuance(
541541
identConditions, identArgs := buildIdentifierQueryConditions(idents)
542542
query := fmt.Sprintf(`SELECT %s FROM authz2 WHERE
543543
registrationID = ? AND
544-
status IN (?, ?) AND
544+
status IN (?, ?, ?) AND
545545
expires >= ? AND
546546
attemptedAt <= ? AND
547547
(%s)`,
@@ -550,7 +550,7 @@ func SelectAuthzsMatchingIssuance(
550550
var args []any
551551
args = append(args,
552552
regID,
553-
statusToUint[core.StatusValid], statusToUint[core.StatusDeactivated],
553+
statusToUint[core.StatusValid], statusToUint[core.StatusDeactivated], statusToUint[core.StatusRevoked],
554554
issued.Add(-1*time.Second), // leeway for clock skew
555555
issued.Add(1*time.Second), // leeway for clock skew
556556
)

test/integration/revocation_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"os"
1818
"os/exec"
1919
"path"
20+
"slices"
2021
"strings"
2122
"sync"
2223
"testing"
@@ -699,3 +700,91 @@ func TestBadKeyRevokerByAccount(t *testing.T) {
699700
checkUnrevoked(t, allCRLs, bundle.certs[0])
700701
}
701702
}
703+
704+
// waitAndCheckRevoked uses an acme client to poll some(a slice of)
705+
// authorizations for a desired status. It is willing to repeatedly fetch the
706+
// authorizations up to four times, and wait up to 5 seconds, before reporting
707+
// failure.
708+
func waitAndCheckAuthzStatus(t *testing.T, aClient *client, authzs []string, wantStatus core.AcmeStatus) {
709+
t.Helper()
710+
711+
for try := range 4 {
712+
time.Sleep(core.RetryBackoff(try, time.Second, 2*time.Second, 1.5))
713+
714+
var allStatus []string
715+
716+
for authz := range authzs {
717+
respAuth, err := aClient.FetchAuthorization(aClient.Account, authzs[authz])
718+
t.Logf("Authorization fetched: %v", respAuth)
719+
test.AssertNotError(t, err, "FetchAuthorization Failed")
720+
721+
allStatus = append(allStatus, respAuth.Status)
722+
}
723+
724+
slices.Sort(allStatus)
725+
uniqStatus := slices.Compact(allStatus)
726+
727+
if len(uniqStatus) == 1 && uniqStatus[0] == string(wantStatus) {
728+
// Success, all authorizations match our desired result
729+
return
730+
}
731+
}
732+
733+
t.Errorf("exhausted authz polling attempts, status values still not as desired")
734+
}
735+
736+
func TestRevokeAuthzUponRevokeCert(t *testing.T) {
737+
if !strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") {
738+
t.Skip("RevokeAuthzUponRevokeCert is only configured in config-next")
739+
}
740+
741+
// Two acme clients, both alike in dignity
742+
clientRed, err := makeClient("mailto:example@letsencrypt.org")
743+
test.AssertNotError(t, err, "creating acme client")
744+
clientBlue, err := makeClient("mailto:example@letsencrypt.org")
745+
test.AssertNotError(t, err, "creating acme client")
746+
747+
// With different keys
748+
certKeyRed, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
749+
test.AssertNotError(t, err, "failed to generate cert key")
750+
certKeyBlue, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
751+
test.AssertNotError(t, err, "failed to generate cert key")
752+
753+
// Share one set of domains for BOTH client certs
754+
redVsBlueIdents := []acme.Identifier{
755+
{Type: "dns", Value: random_domain()},
756+
{Type: "dns", Value: random_domain()},
757+
{Type: "dns", Value: random_domain()},
758+
}
759+
760+
// Both clients issue certs for the shared-control set of domains
761+
res, err := authAndIssue(clientRed, certKeyRed, redVsBlueIdents, true, "")
762+
test.AssertNotError(t, err, "authAndIssue failed")
763+
certRed := res.certs[0]
764+
authzRed := res.Order.Authorizations
765+
766+
res, err = authAndIssue(clientBlue, certKeyBlue, redVsBlueIdents, true, "")
767+
test.AssertNotError(t, err, "authAndIssue failed")
768+
certBlue := res.certs[0]
769+
authzBlue := res.Order.Authorizations
770+
771+
// Red client revokes the Blue client cert with reason "Unspecified"
772+
err = clientRed.RevokeCertificate(clientRed.Account, certBlue, clientRed.PrivateKey, int(revocation.Unspecified))
773+
test.AssertNotError(t, err, "failed to revoke certificate")
774+
775+
// Authorizations for shared-control domains held by Blue client should be revoked
776+
t.Logf("Blue cert revoked by Red client, poll authz for Idents: %v", redVsBlueIdents)
777+
waitAndCheckAuthzStatus(t, clientBlue, authzBlue, core.StatusRevoked)
778+
779+
// Red client revokes its own certificate with reason "Unspecified"
780+
err = clientRed.RevokeCertificate(clientRed.Account, certRed, clientRed.PrivateKey, int(revocation.Unspecified))
781+
test.AssertNotError(t, err, "failed to revoke certificate")
782+
783+
// Authorizations for single-client domains should not be revoked
784+
t.Logf("Red cert revoked by Red client, poll authz for Idents: %v", redVsBlueIdents)
785+
// re-using waitAndCheckAuthzStatus() here is basically only telling us that
786+
// the authorizations did not _immediately_ change from valid to something
787+
// else. Another function might exhaust more wall clock time to test beyond
788+
// the context timeout.
789+
waitAndCheckAuthzStatus(t, clientRed, authzRed, core.StatusValid)
790+
}

0 commit comments

Comments
 (0)