RA: Multi-issuer support for OCSP purging (#5160)

The RA is responsible for contacting Akamai to purge cached OCSP
responses when a certificate is revoked and fresh OCSP responses need to
be served ASAP. In order to do so, it needs to construct the same OCSP
URLs that clients would construct, and that Akamai would cache. In order
to do that, it needs access to the issuing certificate to compute a hash
across its Subject Info and Public Key.

Currently, the RA holds a single issuer certificate in memory, and uses
that cert to compute all OCSP URLs, on the assumption that all certs
we're being asked to revoke were issued by the same issuer.

In order to support issuance from multiple intermediates at the same
time (e.g. RSA and ECDSA), and to support rollover between different
issuers of the same type (we may need to revoke certs issued by two
different issuers for the 90 days in which their end-entity certs
overlap), this commit changes the configuration to provide a list of
issuer certificates instead.

In order to support efficient lookup of issuer certs, this change also
introduces a new concept, the Chain ID. The Chain ID is a truncated hash
across the raw bytes of either the Issuer Info or the Subject Info of a
given cert. As such, it can be used to confirm issuer/subject
relationships between certificates. In the future, this may be a
replacement for our current IssuerID (a truncated hash over the whole
issuer certificate), but for now it is used to map revoked certs to
their issuers inside the RA.

Part of #5120
This commit is contained in:
Aaron Gable 2020-11-06 13:58:32 -08:00 committed by GitHub
parent 294d1c31d7
commit 16c7a21a57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 73 additions and 11 deletions

View File

@ -66,7 +66,8 @@ func TestRevokeBatch(t *testing.T) {
0,
nil,
nil,
&issuance.Certificate{Certificate: &x509.Certificate{}})
[]*issuance.Certificate{{Certificate: &x509.Certificate{}}},
)
ra.SA = ssa
ra.CA = &mockCA{}

View File

@ -86,7 +86,12 @@ type config struct {
// IssuerCertPath is the path to the intermediate used to issue certificates.
// It is used to generate OCSP URLs to purge at revocation time.
// TODO(#5162): DEPRECATED. Remove this field entirely.
IssuerCertPath string
// IssuerCerts are paths to all intermediate certificates which may have
// been used to issue certificates in the last 90 days. These are used to
// generate OCSP URLs to purge during revocation.
IssuerCerts []string
Features map[string]bool
}
@ -161,8 +166,15 @@ func main() {
cmd.FailOnError(err, "Unable to create a Akamai Purger client")
apc := akamaipb.NewAkamaiPurgerClient(apConn)
issuerCert, err := issuance.LoadCertificate(c.RA.IssuerCertPath)
cmd.FailOnError(err, "Failed to load issuer certificate")
issuerCertPaths := c.RA.IssuerCerts
if len(issuerCertPaths) == 0 {
issuerCertPaths = []string{c.RA.IssuerCertPath}
}
issuerCerts := make([]*issuance.Certificate, len(issuerCertPaths))
for i, issuerCertPath := range issuerCertPaths {
issuerCerts[i], err = issuance.LoadCertificate(issuerCertPath)
cmd.FailOnError(err, "Failed to load issuer certificate")
}
// Boulder's components assume that there will always be CT logs configured.
// Issuing a certificate without SCTs embedded is a miss-issuance event in the
@ -225,7 +237,7 @@ func main() {
c.RA.OrderLifetime.Duration,
ctp,
apc,
issuerCert,
issuerCerts,
)
policyErr := rai.SetRateLimitPoliciesFile(c.RA.RateLimitPoliciesFilename)

View File

@ -344,11 +344,44 @@ type IssuerID int64
// ID provides a stable ID for an issuer's certificate. This is used for
// identifying which issuer issued a certificate in the certificateStatus table.
// This value is computed as a truncated hash over the whole certificate,
// meaning it is highly unique but not computable from end-entity certs.
func (ic *Certificate) ID() IssuerID {
h := sha256.Sum256(ic.Raw)
return IssuerID(big.NewInt(0).SetBytes(h[:4]).Int64())
}
// IssuerNameID is a statistically-unique small ID which can be computed from
// both CA and end-entity certs to link them together into a validation chain.
// It is computed as a truncated hash over the issuer Subject Name bytes, or
// over the end-entity's Issuer Name bytes, which are required to be equal.
type IssuerNameID int64
// NameID computes the IssuerNameID from an issuer certificate, i.e. it
// computes a truncated hash over the issuer's Subject Name raw bytes. Useful
// for storing as a lookup key in contexts that don't expect hash collisions.
func (ic *Certificate) NameID() IssuerNameID {
return truncatedHash(ic.RawSubject)
}
// GetIssuerNameID computes the IssuerNameID from an end-entity certificate,
// i.e. it computes a truncated hash over its Issuer Name raw bytes.
// Useful for performing lookups in contexts that don't expect hash collisions.
func GetIssuerNameID(ee *x509.Certificate) IssuerNameID {
return truncatedHash(ee.RawIssuer)
}
// truncatedHash computes a truncated SHA1 hash across arbitrary bytes. Uses
// SHA1 because that is the algorithm most commonly used in OCSP requests.
// PURPOSEFULLY NOT EXPORTED. Exists only to ensure that the implementations of
// Certificate.NameID() and GetIssuerNameID() never diverge. Use those instead.
func truncatedHash(name []byte) IssuerNameID {
h := crypto.SHA1.New()
h.Write(name)
s := h.Sum(nil)
return IssuerNameID(big.NewInt(0).SetBytes(s[:7]).Int64())
}
// Issuer is capable of issuing new certificates
// TODO(#5086): make Cert and Signer private when they're no longer needed by ca.internalIssuer
type Issuer struct {

View File

@ -77,8 +77,8 @@ type RegistrationAuthorityImpl struct {
reuseValidAuthz bool
orderLifetime time.Duration
issuer *issuance.Certificate
purger akamaipb.AkamaiPurgerClient
issuers map[issuance.IssuerNameID]*issuance.Certificate
purger akamaipb.AkamaiPurgerClient
ctpolicy *ctpolicy.CTPolicy
@ -108,7 +108,7 @@ func NewRegistrationAuthorityImpl(
orderLifetime time.Duration,
ctp *ctpolicy.CTPolicy,
purger akamaipb.AkamaiPurgerClient,
issuer *issuance.Certificate,
issuers []*issuance.Certificate,
) *RegistrationAuthorityImpl {
ctpolicyResults := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
@ -169,6 +169,11 @@ func NewRegistrationAuthorityImpl(
}, []string{"reason"})
stats.MustRegister(revocationReasonCounter)
issuersByID := make(map[issuance.IssuerNameID]*issuance.Certificate)
for _, issuer := range issuers {
issuersByID[issuer.NameID()] = issuer
}
ra := &RegistrationAuthorityImpl{
clk: clk,
log: logger,
@ -185,7 +190,7 @@ func NewRegistrationAuthorityImpl(
ctpolicy: ctp,
ctpolicyResults: ctpolicyResults,
purger: purger,
issuer: issuer,
issuers: issuersByID,
namesPerCert: namesPerCert,
rateLimitCounter: rateLimitCounter,
newRegCounter: newRegCounter,
@ -1711,7 +1716,11 @@ func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, cert
return err
}
}
purgeURLs, err := akamai.GeneratePurgeURLs(&cert, ra.issuer.Certificate)
issuer, ok := ra.issuers[issuance.GetIssuerNameID(&cert)]
if !ok {
return fmt.Errorf("unable to identify issuer of revoked certificate: %v", cert)
}
purgeURLs, err := akamai.GeneratePurgeURLs(&cert, issuer.Certificate)
if err != nil {
return err
}

View File

@ -3873,7 +3873,10 @@ func TestRevocationAddBlockedKey(t *testing.T) {
test.AssertNotError(t, err, "x509.CreateCertificate failed")
cert, err := x509.ParseCertificate(der)
test.AssertNotError(t, err, "x509.ParseCertificate failed")
ra.issuer = &issuance.Certificate{Certificate: cert}
ic := issuance.Certificate{Certificate: cert}
ra.issuers = map[issuance.IssuerNameID]*issuance.Certificate{
ic.NameID(): &ic,
}
err = ra.RevokeCertificateWithReg(context.Background(), *cert, ocsp.Unspecified, 0)
test.AssertNotError(t, err, "RevokeCertificateWithReg failed")

View File

@ -11,7 +11,11 @@
"weakKeyFile": "test/example-weak-keys.json",
"blockedKeyFile": "test/example-blocked-keys.yaml",
"orderLifetime": "168h",
"issuerCertPath": "/tmp/intermediate-cert-rsa-a.pem",
"issuerCerts": [
"/tmp/intermediate-cert-rsa-a.pem",
"/tmp/intermediate-cert-rsa-b.pem",
"/tmp/intermediate-cert-ecdsa-a.pem"
],
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/ra.boulder/cert.pem",