Skip to content

Commit 27e08f4

Browse files
authored
Fix re-revocation representations on CRL (#8096)
For explicitly sharded certificates, CRL status is read from the `revokedCertificates` table. This table gets written at revocation time. At re-revocation time (for key compromise), it only gets written by the SA if the caller passes a nonzero ShardIdx to UpdateRevokedCertificate. The RA was never passing a nonzero ShardIdx to UpdateRevokedCertificate.
1 parent 0fe66b6 commit 27e08f4

File tree

3 files changed

+113
-39
lines changed

3 files changed

+113
-39
lines changed

ra/ra.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1754,12 +1754,27 @@ func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx contex
17541754
return berrors.AlreadyRevokedError("unable to re-revoke serial %q which is already revoked for keyCompromise", serialString)
17551755
}
17561756

1757+
cert, err := ra.SA.GetCertificate(ctx, &sapb.Serial{Serial: serialString})
1758+
if err != nil {
1759+
return berrors.NotFoundError("unable to confirm that serial %q was ever issued: %s", serialString, err)
1760+
}
1761+
x509Cert, err := x509.ParseCertificate(cert.Der)
1762+
if err != nil {
1763+
return err
1764+
}
1765+
1766+
shardIdx, err := crlShard(x509Cert)
1767+
if err != nil {
1768+
return err
1769+
}
1770+
17571771
_, err = ra.SA.UpdateRevokedCertificate(ctx, &sapb.RevokeCertificateRequest{
17581772
Serial: serialString,
17591773
Reason: int64(ocsp.KeyCompromise),
17601774
Date: timestamppb.New(ra.clk.Now()),
17611775
Backdate: status.RevokedDate,
17621776
IssuerID: int64(issuerID),
1777+
ShardIdx: shardIdx,
17631778
})
17641779
if err != nil {
17651780
return err

ra/ra_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3616,6 +3616,35 @@ func (msar *mockSARevocation) GetCertificateStatus(_ context.Context, req *sapb.
36163616
return nil, berrors.UnknownSerialError()
36173617
}
36183618

3619+
func (msar *mockSARevocation) GetCertificate(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) {
3620+
var serialBytes [16]byte
3621+
_, _ = rand.Read(serialBytes[:])
3622+
serial := big.NewInt(0).SetBytes(serialBytes[:])
3623+
3624+
key, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
3625+
if err != nil {
3626+
return nil, err
3627+
}
3628+
3629+
template := &x509.Certificate{
3630+
SerialNumber: serial,
3631+
DNSNames: []string{"revokememaybe.example.com"},
3632+
NotBefore: time.Now(),
3633+
NotAfter: time.Now().Add(6 * 24 * time.Hour),
3634+
IssuingCertificateURL: []string{"http://localhost:4001/acme/issuer-cert/1234"},
3635+
CRLDistributionPoints: []string{"http://example.com/123.crl"},
3636+
}
3637+
3638+
testCertDER, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
3639+
if err != nil {
3640+
return nil, err
3641+
}
3642+
3643+
return &corepb.Certificate{
3644+
Der: testCertDER,
3645+
}, nil
3646+
}
3647+
36193648
func (msar *mockSARevocation) RevokeCertificate(_ context.Context, req *sapb.RevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) {
36203649
if _, present := msar.revoked[req.Serial]; present {
36213650
return nil, berrors.AlreadyRevokedError("already revoked")

test/integration/revocation_test.go

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -89,45 +89,69 @@ func getAllCRLs(t *testing.T) map[string][]*x509.RevocationList {
8989
// 10 is the number of shards configured in test/config*/crl-updater.json
9090
for i := range 10 {
9191
crlURL := fmt.Sprintf("%s%d.crl", issuer.CRLURLBase, i+1)
92-
resp, err := http.Get(crlURL)
93-
if err != nil {
94-
t.Fatalf("getting CRL from %s: %s", crlURL, err)
95-
}
96-
if resp.StatusCode != http.StatusOK {
97-
t.Fatalf("fetching %s: status code %d", crlURL, resp.StatusCode)
98-
}
99-
body, err := io.ReadAll(resp.Body)
100-
if err != nil {
101-
t.Fatalf("reading CRL from %s: %s", crlURL, err)
102-
}
103-
resp.Body.Close()
92+
list := getCRL(t, crlURL, issuerCert)
10493

105-
list, err := x509.ParseRevocationList(body)
106-
if err != nil {
107-
t.Fatalf("parsing CRL from %s: %s (bytes: %x)", crlURL, err, body)
108-
}
94+
ret[issuerSKID] = append(ret[issuerSKID], list)
95+
}
96+
}
97+
return ret
98+
}
10999

110-
err = list.CheckSignatureFrom(issuerCert)
111-
if err != nil {
112-
t.Errorf("checking CRL signature on %s from %s: %s",
113-
crlURL, issuerCert.Subject, err)
114-
}
100+
// getCRL fetches a CRL, parses it, and checks the signature.
101+
func getCRL(t *testing.T, crlURL string, issuerCert *x509.Certificate) *x509.RevocationList {
102+
resp, err := http.Get(crlURL)
103+
if err != nil {
104+
t.Fatalf("getting CRL from %s: %s", crlURL, err)
105+
}
106+
if resp.StatusCode != http.StatusOK {
107+
t.Fatalf("fetching %s: status code %d", crlURL, resp.StatusCode)
108+
}
109+
body, err := io.ReadAll(resp.Body)
110+
if err != nil {
111+
t.Fatalf("reading CRL from %s: %s", crlURL, err)
112+
}
113+
resp.Body.Close()
115114

116-
idpURIs, err := idp.GetIDPURIs(list.Extensions)
117-
if err != nil {
118-
t.Fatalf("getting IDP URIs: %s", err)
119-
}
120-
if len(idpURIs) != 1 {
121-
t.Errorf("CRL at %s: expected 1 IDP URI, got %s", crlURL, idpURIs)
122-
}
123-
if idpURIs[0] != crlURL {
124-
t.Errorf("fetched CRL from %s, got IDP of %s (should be same)", crlURL, idpURIs[0])
125-
}
115+
list, err := x509.ParseRevocationList(body)
116+
if err != nil {
117+
t.Fatalf("parsing CRL from %s: %s (bytes: %x)", crlURL, err, body)
118+
}
126119

127-
ret[issuerSKID] = append(ret[issuerSKID], list)
120+
err = list.CheckSignatureFrom(issuerCert)
121+
if err != nil {
122+
t.Errorf("checking CRL signature on %s from %s: %s",
123+
crlURL, issuerCert.Subject, err)
124+
}
125+
126+
idpURIs, err := idp.GetIDPURIs(list.Extensions)
127+
if err != nil {
128+
t.Fatalf("getting IDP URIs: %s", err)
129+
}
130+
if len(idpURIs) != 1 {
131+
t.Errorf("CRL at %s: expected 1 IDP URI, got %s", crlURL, idpURIs)
132+
}
133+
if idpURIs[0] != crlURL {
134+
t.Errorf("fetched CRL from %s, got IDP of %s (should be same)", crlURL, idpURIs[0])
135+
}
136+
return list
137+
}
138+
139+
func fetchAndCheckRevoked(t *testing.T, cert, issuer *x509.Certificate, expectedReason int) {
140+
t.Helper()
141+
if len(cert.CRLDistributionPoints) != 1 {
142+
t.Errorf("expected certificate to have one CRLDistributionPoints field")
143+
}
144+
crlURL := cert.CRLDistributionPoints[0]
145+
list := getCRL(t, crlURL, issuer)
146+
for _, entry := range list.RevokedCertificateEntries {
147+
if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
148+
if entry.ReasonCode != expectedReason {
149+
t.Errorf("serial %x found on CRL %s with reason %d, want %d", entry.SerialNumber, crlURL, entry.ReasonCode, expectedReason)
150+
}
151+
return
128152
}
129153
}
130-
return ret
154+
t.Errorf("serial %x not found on CRL %s, expected it to be revoked with reason %d", cert.SerialNumber, crlURL, expectedReason)
131155
}
132156

133157
func checkRevoked(t *testing.T, revocations map[string][]*x509.RevocationList, cert *x509.Certificate, expectedReason int) {
@@ -421,6 +445,7 @@ func TestReRevocation(t *testing.T) {
421445
res, err := authAndIssue(issueClient, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "")
422446
test.AssertNotError(t, err, "authAndIssue failed")
423447
cert := res.certs[0]
448+
issuer := res.certs[1]
424449

425450
// Initially, the cert should have a Good OCSP response.
426451
ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good)
@@ -457,11 +482,10 @@ func TestReRevocation(t *testing.T) {
457482
)
458483
test.AssertNotError(t, err, "initial revocation should have succeeded")
459484

460-
// Check the OCSP response for the certificate again. It should now be
485+
// Check the CRL for the certificate again. It should now be
461486
// revoked.
462-
ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(tc.reason1)
463-
_, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
464-
test.AssertNotError(t, err, "requesting OCSP for revoked cert")
487+
runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json"))
488+
fetchAndCheckRevoked(t, cert, issuer, tc.reason1)
465489

466490
// Set up the account and key that we'll use to *re*-revoke the cert.
467491
switch tc.method2 {
@@ -498,7 +522,10 @@ func TestReRevocation(t *testing.T) {
498522
// revoked, with the same reason.
499523
ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(tc.reason1)
500524
_, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
501-
test.AssertNotError(t, err, "requesting OCSP for revoked cert")
525+
// Check the CRL for the certificate again. It should still be
526+
// revoked, with the same reason.
527+
runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json"))
528+
fetchAndCheckRevoked(t, cert, issuer, tc.reason1)
502529

503530
case false:
504531
test.AssertNotError(t, err, "second revocation should have succeeded")
@@ -507,7 +534,10 @@ func TestReRevocation(t *testing.T) {
507534
// revoked with reason keyCompromise.
508535
ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(tc.reason2)
509536
_, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig)
510-
test.AssertNotError(t, err, "requesting OCSP for revoked cert")
537+
// Check the CRL for the certificate again. It should now be
538+
// revoked with reason keyCompromise.
539+
runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json"))
540+
fetchAndCheckRevoked(t, cert, issuer, tc.reason2)
511541
}
512542
})
513543
}

0 commit comments

Comments
 (0)