Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ type Config struct {
// during certificate issuance. This flag must be set to true in the
// RA and VA services for full functionality.
DNSPersist01Enabled bool

// RevokeAuthzsUponRevokeCert controls whether the RA will call for
// revocation of Authorizations for identifiers in a certificate that is
// successfully revoked by a requester that is DIFFERENT than the one that
// was originally granted the certificate. In this scenario, the new
// requester has demonstrated control over the requisite set of identifiers,
// so we can avoid the possibility of Authz re-use by the original
// requester via Authz revocation.
RevokeAuthzsUponRevokeCert bool
}

var fMu = new(sync.RWMutex)
Expand Down
33 changes: 33 additions & 0 deletions ra/ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,21 @@ func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx contex
return nil
}

func (ra *RegistrationAuthorityImpl) revokeAuthorizations(ctx context.Context, cert *x509.Certificate, smeta *sapb.SerialMetadata) {
if features.Get().RevokeAuthzsUponRevokeCert {
idents := identifier.FromCert(cert)
for _, ident := range idents {
_, err := ra.SA.RevokeAuthorizationFor(ctx, &sapb.RevokeAuthorizationForRequest{
RegistrationID: smeta.RegistrationID,
Identifier: ident.ToProto(),
})
if err != nil {
ra.log.Infof("Authz revocation failed: %s", err)
}
}
}
}

// RevokeCertByApplicant revokes the certificate in question. It allows any
// revocation reason from (0, 1, 3, 4, 5, 9), because Subscribers are allowed to
// request any revocation reason for their own certificates. However, if the
Expand Down Expand Up @@ -1720,6 +1735,10 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context,
Requester: req.RegID,
}

// By default, do not revoke Authorizations held for the revoked-cert
// identifiers.
requestAuthzRevocation := false

// Below this point, do not re-declare `err` (i.e. type `err :=`) in a
// nested scope. Doing so will create a new `err` variable that is not
// captured by this closure.
Expand Down Expand Up @@ -1772,13 +1791,27 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context,
// domain names in the certificate". Override the reason code to match.
reasonCode = revocation.CessationOfOperation
logEvent.Reason = reasonCode

// We have confirmed that the requester RegistrationID is NOT the same
// as the original subscriber. Requester has demonstrated control over
// the set of identifiers sufficient for certificate revocation. Given
// BOTH, enable this boolean to signal that authorizations held by the
// original subscriber RegID should be revoked after certificate
// revocation.
requestAuthzRevocation = true
}

err = ra.revokeCertificate(ctx, cert, reasonCode)
if err != nil {
return nil, err
}

// Asynchronously request to revoke authorizations held by the RegID from
// cert metadata, confirmed above to be different than requester ID.
if requestAuthzRevocation {
go ra.revokeAuthorizations(ctx, cert, metadata)
}

return &emptypb.Empty{}, nil
}

Expand Down
55 changes: 55 additions & 0 deletions ra/ra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3670,6 +3670,61 @@ func TestRevokeCertByApplicant_Controller(t *testing.T) {
test.AssertEquals(t, mockSA.revoked[core.SerialToString(cert.SerialNumber)].RevokedReason, int64(revocation.CessationOfOperation))
}

// mockSARecordAuthzRevocation is a mock sapb.StorageAuthorityClient that simply
// maps identifier strings to RegistrationIDs for received RevokeAuthorizationFor
// requests.
type mockSARecordAuthzRevocation struct {
sapb.StorageAuthorityClient
recv map[string]int64
}

func (msa *mockSARecordAuthzRevocation) RevokeAuthorizationFor(ctx context.Context, req *sapb.RevokeAuthorizationForRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) {
msa.recv[req.Identifier.Value] = req.RegistrationID
return &emptypb.Empty{}, nil
}

func TestRevokeAuthorizations_FeatureDisabled(t *testing.T) {
_, _, ra, _, clk, _, cleanUp := initAuthorities(t)
defer cleanUp()

features.Set(features.Config{RevokeAuthzsUponRevokeCert: false})
defer features.Reset()

mockSA := mockSARecordAuthzRevocation{}
mockSA.recv = make(map[string]int64)
ra.SA = &mockSA

_, cert := test.ThrowAwayCert(t, clk)

meta := &sapb.SerialMetadata{RegistrationID: 333}

ra.revokeAuthorizations(context.Background(), cert, meta)
// mockSA should not have received ANY requests
test.AssertEquals(t, len(mockSA.recv), 0)
}
func TestRevokeAuthorizations_FeatureEnabled(t *testing.T) {
_, _, ra, _, clk, _, cleanUp := initAuthorities(t)
defer cleanUp()

features.Set(features.Config{RevokeAuthzsUponRevokeCert: true})
defer features.Reset()

mockSA := mockSARecordAuthzRevocation{}
mockSA.recv = make(map[string]int64)
ra.SA = &mockSA

_, cert := test.ThrowAwayCert(t, clk)
idents := identifier.FromCert(cert)

meta := &sapb.SerialMetadata{RegistrationID: 333}

ra.revokeAuthorizations(context.Background(), cert, meta)
// mockSA should have received requests for each of the certificate identifiers
for _, ident := range idents {
test.AssertEquals(t, mockSA.recv[ident.Value], meta.RegistrationID)
}
}

func TestRevokeCertByKey(t *testing.T) {
_, _, ra, _, clk, _, cleanUp := initAuthorities(t)
defer cleanUp()
Expand Down
Loading