Refactor the PKI revocation handler to prep for unified revocation (#18685)

* Rename revokeCert variable to identify serial number formatting

* Refactor out lease specific behavior out of revokeCert

 - Isolate the specific behavior regarding revoking lease specific
   certificates outside of the revokeCert function and into the only
   caller that leveraged used it.
 - This allows us to simplify revokeCert a little bit and keeps the
   function purely about revoking a certificate

* Within revokeCert short circuit the already revoked use-case

 - Make the function a little easier to process by exiting early
   if the certificate has already been revoked.

* Do not load certificates from storage multiple times during revocation

 - Isolate the loading of a certificate and parsing of a certificate
   into a single attempt, either when provided the certificate for BYOC
   revocation or strictly from storage for the other revocation types.

* With BYOC write certificate entry using dashes not the legacy colon char
This commit is contained in:
Steven Clark 2023-01-13 10:31:03 -05:00 committed by GitHub
parent e8aa9c6429
commit e0e957731b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 116 additions and 149 deletions

View file

@ -7,7 +7,6 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"fmt" "fmt"
"math/big" "math/big"
"strings"
"sync" "sync"
"time" "time"
@ -468,7 +467,7 @@ func fetchIssuerMapForRevocationChecking(sc *storageContext) (map[issuerID]*x509
} }
// Revokes a cert, and tries to be smart about error recovery // Revokes a cert, and tries to be smart about error recovery
func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Response, error) { func revokeCert(sc *storageContext, cert *x509.Certificate) (*logical.Response, error) {
// As this backend is self-contained and this function does not hook into // As this backend is self-contained and this function does not hook into
// third parties to manage users or resources, if the mount is tainted, // third parties to manage users or resources, if the mount is tainted,
// revocation doesn't matter anyways -- the CRL that would be written will // revocation doesn't matter anyways -- the CRL that would be written will
@ -478,6 +477,9 @@ func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Res
return nil, nil return nil, nil
} }
colonSerial := serialFromCert(cert)
hyphenSerial := normalizeSerial(colonSerial)
// Validate that no issuers match the serial number to be revoked. We need // Validate that no issuers match the serial number to be revoked. We need
// to gracefully degrade to the legacy cert bundle when it is required, as // to gracefully degrade to the legacy cert bundle when it is required, as
// secondary PR clusters might not have been upgraded, but still need to // secondary PR clusters might not have been upgraded, but still need to
@ -490,16 +492,13 @@ func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Res
// Ensure we don't revoke an issuer via this API; use /issuer/:issuer_ref/revoke // Ensure we don't revoke an issuer via this API; use /issuer/:issuer_ref/revoke
// instead. // instead.
for issuer, certificate := range issuerIDCertMap { for issuer, certificate := range issuerIDCertMap {
colonSerial := strings.ReplaceAll(strings.ToLower(serial), "-", ":")
if colonSerial == serialFromCert(certificate) { if colonSerial == serialFromCert(certificate) {
return logical.ErrorResponse(fmt.Sprintf("adding issuer (id: %v) to its own CRL is not allowed", issuer)), nil return logical.ErrorResponse(fmt.Sprintf("adding issuer (id: %v) to its own CRL is not allowed", issuer)), nil
} }
} }
alreadyRevoked := false
var revInfo revocationInfo var revInfo revocationInfo
revEntry, err := fetchCertBySerial(sc, revokedPath, colonSerial)
revEntry, err := fetchCertBySerial(sc, revokedPath, serial)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case errutil.UserError: case errutil.UserError:
@ -510,77 +509,51 @@ func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Res
} }
if revEntry != nil { if revEntry != nil {
// Set the revocation info to the existing values // Set the revocation info to the existing values
alreadyRevoked = true
err = revEntry.DecodeJSON(&revInfo) err = revEntry.DecodeJSON(&revInfo)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding existing revocation info") return nil, fmt.Errorf("error decoding existing revocation info")
} }
resp := &logical.Response{
Data: map[string]interface{}{
"revocation_time": revInfo.RevocationTime,
},
}
if !revInfo.RevocationTimeUTC.IsZero() {
resp.Data["revocation_time_rfc3339"] = revInfo.RevocationTimeUTC.Format(time.RFC3339Nano)
}
return resp, nil
} }
if !alreadyRevoked { // Add a little wiggle room because leases are stored with a second
certEntry, err := fetchCertBySerial(sc, "certs/", serial) // granularity
if err != nil { if cert.NotAfter.Before(time.Now().Add(2 * time.Second)) {
switch err.(type) { response := &logical.Response{}
case errutil.UserError: response.AddWarning(fmt.Sprintf("certificate with serial %s already expired; refusing to add to CRL", colonSerial))
return logical.ErrorResponse(err.Error()), nil return response, nil
default:
return nil, err
}
}
if certEntry == nil {
if fromLease {
// We can't write to revoked/ or update the CRL anyway because we don't have the cert,
// and there's no reason to expect this will work on a subsequent
// retry. Just give up and let the lease get deleted.
return nil, nil
}
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found", serial)), nil
}
cert, err := x509.ParseCertificate(certEntry.Value)
if err != nil {
return nil, fmt.Errorf("error parsing certificate: %w", err)
}
if cert == nil {
return nil, fmt.Errorf("got a nil certificate")
}
// Add a little wiggle room because leases are stored with a second
// granularity
if cert.NotAfter.Before(time.Now().Add(2 * time.Second)) {
response := &logical.Response{}
response.AddWarning(fmt.Sprintf("certificate with serial %s already expired; refusing to add to CRL", serial))
return response, nil
}
// Compatibility: Don't revoke CAs if they had leases. New CAs going
// forward aren't issued leases.
if cert.IsCA && fromLease {
return nil, nil
}
currTime := time.Now()
revInfo.CertificateBytes = certEntry.Value
revInfo.RevocationTime = currTime.Unix()
revInfo.RevocationTimeUTC = currTime.UTC()
// We may not find an issuer with this certificate; that's fine so
// ignore the return value.
associateRevokedCertWithIsssuer(&revInfo, cert, issuerIDCertMap)
revEntry, err = logical.StorageEntryJSON(revokedPath+normalizeSerial(serial), revInfo)
if err != nil {
return nil, fmt.Errorf("error creating revocation entry")
}
certsCounted := sc.Backend.certsCounted.Load()
err = sc.Storage.Put(sc.Context, revEntry)
if err != nil {
return nil, fmt.Errorf("error saving revoked certificate to new location")
}
sc.Backend.incrementTotalRevokedCertificatesCount(certsCounted, revEntry.Key)
} }
currTime := time.Now()
revInfo.CertificateBytes = cert.Raw
revInfo.RevocationTime = currTime.Unix()
revInfo.RevocationTimeUTC = currTime.UTC()
// We may not find an issuer with this certificate; that's fine so
// ignore the return value.
associateRevokedCertWithIsssuer(&revInfo, cert, issuerIDCertMap)
revEntry, err = logical.StorageEntryJSON(revokedPath+hyphenSerial, revInfo)
if err != nil {
return nil, fmt.Errorf("error creating revocation entry")
}
certsCounted := sc.Backend.certsCounted.Load()
err = sc.Storage.Put(sc.Context, revEntry)
if err != nil {
return nil, fmt.Errorf("error saving revoked certificate to new location")
}
sc.Backend.incrementTotalRevokedCertificatesCount(certsCounted, revEntry.Key)
// Fetch the config and see if we need to rebuild the CRL. If we have // Fetch the config and see if we need to rebuild the CRL. If we have
// auto building enabled, we will wait for the next rebuild period to // auto building enabled, we will wait for the next rebuild period to
// actually rebuild it. // actually rebuild it.
@ -603,7 +576,7 @@ func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Res
return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr) return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr)
} }
} }
} else if !alreadyRevoked { } else {
// Regardless of whether or not we've presently enabled Delta CRLs, // Regardless of whether or not we've presently enabled Delta CRLs,
// we should always write the Delta WAL in case it is enabled in the // we should always write the Delta WAL in case it is enabled in the
// future. We could trigger another full CRL rebuild instead (to avoid // future. We could trigger another full CRL rebuild instead (to avoid
@ -619,7 +592,7 @@ func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Res
// //
// Currently we don't store any data in the WAL entry. // Currently we don't store any data in the WAL entry.
var walInfo deltaWALInfo var walInfo deltaWALInfo
walEntry, err := logical.StorageEntryJSON(deltaWALPath+normalizeSerial(serial), walInfo) walEntry, err := logical.StorageEntryJSON(deltaWALPath+hyphenSerial, walInfo)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create delta CRL WAL entry") return nil, fmt.Errorf("unable to create delta CRL WAL entry")
} }
@ -631,7 +604,7 @@ func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Res
// In order for periodic delta rebuild to be mildly efficient, we // In order for periodic delta rebuild to be mildly efficient, we
// should write the last revoked delta WAL entry so we know if we // should write the last revoked delta WAL entry so we know if we
// have new revocations that we should rebuild the delta WAL for. // have new revocations that we should rebuild the delta WAL for.
lastRevSerial := lastWALInfo{Serial: serial} lastRevSerial := lastWALInfo{Serial: colonSerial}
lastWALEntry, err := logical.StorageEntryJSON(deltaWALLastRevokedSerial, lastRevSerial) lastWALEntry, err := logical.StorageEntryJSON(deltaWALLastRevokedSerial, lastRevSerial)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create last delta CRL WAL entry") return nil, fmt.Errorf("unable to create last delta CRL WAL entry")
@ -641,15 +614,12 @@ func revokeCert(sc *storageContext, serial string, fromLease bool) (*logical.Res
} }
} }
resp := &logical.Response{ return &logical.Response{
Data: map[string]interface{}{ Data: map[string]interface{}{
"revocation_time": revInfo.RevocationTime, "revocation_time": revInfo.RevocationTime,
"revocation_time_rfc3339": revInfo.RevocationTimeUTC.Format(time.RFC3339Nano),
}, },
} }, nil
if !revInfo.RevocationTimeUTC.IsZero() {
resp.Data["revocation_time_rfc3339"] = revInfo.RevocationTimeUTC.Format(time.RFC3339Nano)
}
return resp, nil
} }
func buildCRLs(sc *storageContext, forceNew bool) error { func buildCRLs(sc *storageContext, forceNew bool) error {

View file

@ -9,11 +9,9 @@ import (
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"strings"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/helper/errutil"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
) )
@ -138,7 +136,7 @@ func pathRotateDeltaCRL(b *backend) *framework.Path {
} }
} }
func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *logical.Request, certPem string) (string, bool, []byte, error) { func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *logical.Request, certPem string) (string, bool, *x509.Certificate, error) {
// This function handles just the verification of the certificate against // This function handles just the verification of the certificate against
// the global issuer set, checking whether or not it is importable. // the global issuer set, checking whether or not it is importable.
// //
@ -208,7 +206,7 @@ func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *log
// imported this certificate, likely when we issued it. We don't // imported this certificate, likely when we issued it. We don't
// need to re-verify the signature as we assume it was already // need to re-verify the signature as we assume it was already
// verified when it was imported. // verified when it was imported.
return serial, false, certEntry.Value, nil return serial, false, certReferenceStored, nil
} }
} }
@ -242,13 +240,13 @@ func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *log
} }
if foundMatchingIssuer { if foundMatchingIssuer {
return serial, true, certReference.Raw, nil return serial, true, certReference, nil
} }
return serial, false, nil, errutil.UserError{Err: "unable to verify signature on presented cert from any present issuer in this mount; certificates from previous CAs will need to have their issuing CA and key re-imported if revocation is necessary"} return serial, false, nil, errutil.UserError{Err: "unable to verify signature on presented cert from any present issuer in this mount; certificates from previous CAs will need to have their issuing CA and key re-imported if revocation is necessary"}
} }
func (b *backend) pathRevokeWriteHandleKey(ctx context.Context, req *logical.Request, cert []byte, keyPem string) error { func (b *backend) pathRevokeWriteHandleKey(req *logical.Request, certReference *x509.Certificate, keyPem string) error {
if keyPem == "" { if keyPem == "" {
// The only way to get here should be via the /revoke endpoint; // The only way to get here should be via the /revoke endpoint;
// validate the path one more time and return an error if necessary. // validate the path one more time and return an error if necessary.
@ -261,12 +259,6 @@ func (b *backend) pathRevokeWriteHandleKey(ctx context.Context, req *logical.Req
return nil return nil
} }
// Parse the certificate for reference.
certReference, err := x509.ParseCertificate(cert)
if err != nil {
return errutil.UserError{Err: fmt.Sprintf("certificate could not be parsed: %v", err)}
}
// Now parse the key's PEM block. // Now parse the key's PEM block.
pemBlock, _ := pem.Decode([]byte(keyPem)) pemBlock, _ := pem.Decode([]byte(keyPem))
if pemBlock == nil { if pemBlock == nil {
@ -347,16 +339,25 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
} }
} }
writeCert := false
var cert *x509.Certificate
var serial string var serial string
if haveSerial {
sc := b.makeStorageContext(ctx, req.Storage)
if haveCert {
var err error
serial, writeCert, cert, err = b.pathRevokeWriteHandleCertificate(ctx, req, rawCertificate.(string))
if err != nil {
return nil, err
}
} else {
// Easy case: this cert should be in storage already. // Easy case: this cert should be in storage already.
serial = rawSerial.(string) serial = rawSerial.(string)
if len(serial) == 0 { if len(serial) == 0 {
return logical.ErrorResponse("The serial number must be provided"), nil return logical.ErrorResponse("The serial number must be provided"), nil
} }
// Here, fetch the certificate from disk to validate we can revoke it.
sc := b.makeStorageContext(ctx, req.Storage)
certEntry, err := fetchCertBySerial(sc, "certs/", serial) certEntry, err := fetchCertBySerial(sc, "certs/", serial)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
@ -366,67 +367,43 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
return nil, err return nil, err
} }
} }
if certEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found or was already revoked", serial)), nil
}
// Now, if the user provided a key, we'll have to make sure the key if certEntry != nil {
// and stored certificate match. cert, err = x509.ParseCertificate(certEntry.Value)
if err := b.pathRevokeWriteHandleKey(ctx, req, certEntry.Value, keyPem); err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("error parsing certificate: %w", err)
}
} }
} else { }
// Otherwise, we've gotta parse the certificate from the request and
// then import it into cluster-local storage. Before writing the if cert == nil {
// certificate (and forwarding), we want to verify this certificate return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found or was already revoked", serial)), nil
// was actually signed by one of our present issuers. }
var err error
var writeCert bool // Before we write the certificate, we've gotta verify the request in
var certBytes []byte // the event of a PoP-based revocation scheme; we don't want to litter
serial, writeCert, certBytes, err = b.pathRevokeWriteHandleCertificate(ctx, req, rawCertificate.(string)) // storage with issued-but-not-revoked certificates.
if err := b.pathRevokeWriteHandleKey(req, cert, keyPem); err != nil {
return nil, err
}
// At this point, a forward operation will occur if we're on a standby
// node as we're now attempting to write the bytes of the cert out to
// disk.
if writeCert {
err := req.Storage.Put(ctx, &logical.StorageEntry{
Key: "certs/" + normalizeSerial(serial),
Value: cert.Raw,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Before we write the certificate, we've gotta verify the request in
// the event of a PoP-based revocation scheme; we don't want to litter
// storage with issued-but-not-revoked certificates.
if err := b.pathRevokeWriteHandleKey(ctx, req, certBytes, keyPem); err != nil {
return nil, err
}
// At this point, a forward operation will occur if we're on a standby
// node as we're now attempting to write the bytes of the cert out to
// disk.
if writeCert {
err = req.Storage.Put(ctx, &logical.StorageEntry{
Key: "certs/" + serial,
Value: certBytes,
})
if err != nil {
return nil, err
}
}
// Finally, we have a valid serial number to use for BYOC revocation!
} }
// Assumption: this check is cheap. Call this twice, in the cert-import
// case, to allow cert verification to get rejected on the standby node,
// but we still need it to protect the serial number case.
if b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) {
return nil, logical.ErrReadOnly
}
// We store and identify by lowercase colon-separated hex, but other
// utilities use dashes and/or uppercase, so normalize
serial = strings.ReplaceAll(strings.ToLower(serial), "-", ":")
b.revokeStorageLock.Lock() b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock() defer b.revokeStorageLock.Unlock()
sc := b.makeStorageContext(ctx, req.Storage) return revokeCert(sc, cert)
return revokeCert(sc, serial, false)
} }
func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {

View file

@ -2,6 +2,7 @@ package pki
import ( import (
"context" "context"
"crypto/x509"
"fmt" "fmt"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
@ -49,9 +50,28 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, _
defer b.revokeStorageLock.Unlock() defer b.revokeStorageLock.Unlock()
sc := b.makeStorageContext(ctx, req.Storage) sc := b.makeStorageContext(ctx, req.Storage)
resp, err := revokeCert(sc, serialInt.(string), true)
if resp == nil && err == nil { certEntry, err := fetchCertBySerial(sc, "certs/", serialInt.(string))
b.Logger().Warn("expired certificate revoke failed because not found in storage, treating as success", "serial", serialInt.(string)) if err != nil {
return nil, err
} }
return resp, err if certEntry == nil {
// We can't write to revoked/ or update the CRL anyway because we don't have the cert,
// and there's no reason to expect this will work on a subsequent
// retry. Just give up and let the lease get deleted.
b.Logger().Warn("expired certificate revoke failed because not found in storage, treating as success", "serial", serialInt.(string))
return nil, nil
}
cert, err := x509.ParseCertificate(certEntry.Value)
if err != nil {
return nil, fmt.Errorf("error parsing certificate: %w", err)
}
// Compatibility: Don't revoke CAs if they had leases. New CAs going forward aren't issued leases.
if cert.IsCA {
return nil, nil
}
return revokeCert(sc, cert)
} }