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:
parent
294d1c31d7
commit
16c7a21a57
|
@ -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{}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
19
ra/ra.go
19
ra/ra.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue