Add support for revoke by serial number to update the unified CRL (#18786)

This commit is contained in:
Steven Clark 2023-01-23 10:22:10 -05:00 committed by GitHub
parent 0eedcd979b
commit f3ce351e01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 212 additions and 27 deletions

View File

@ -118,6 +118,7 @@ func Backend(conf *logical.BackendConfig) *backend {
WriteForwardedStorage: []string{
crossRevocationPath,
unifiedRevocationWritePathPrefix,
},
},
@ -143,6 +144,7 @@ func Backend(conf *logical.BackendConfig) *backend {
pathRevokeWithKey(&b),
pathListCertsRevoked(&b),
pathListCertsRevocationQueue(&b),
pathListUnifiedRevoked(&b),
pathTidy(&b),
pathTidyCancel(&b),
pathTidyStatus(&b),

View File

@ -543,7 +543,10 @@ func (cb *crlBuilder) processRevocationQueue(sc *storageContext) error {
removalQueue := cb.removalQueue.Iterate()
sc.Backend.Logger().Debug(fmt.Sprintf("gathered %v revocations and %v confirmation entries", len(revQueue), len(removalQueue)))
crlConfig, err := cb.getConfigWithUpdate(sc)
if err != nil {
return err
}
for _, req := range revQueue {
sc.Backend.Logger().Debug(fmt.Sprintf("handling revocation request: %v", req))
rPath := crossRevocationPrefix + req.Cluster + "/" + req.Serial
@ -558,7 +561,7 @@ func (cb *crlBuilder) processRevocationQueue(sc *storageContext) error {
continue
}
resp, err := tryRevokeCertBySerial(sc, req.Serial)
resp, err := tryRevokeCertBySerial(sc, crlConfig, req.Serial)
sc.Backend.Logger().Debug(fmt.Sprintf("checked local revocation entry: %v / %v", resp, err))
if err == nil && resp != nil && !resp.IsError() && resp.Data != nil && resp.Data["state"].(string) == "revoked" {
if isNotPerfPrimary {
@ -674,7 +677,7 @@ func fetchIssuerMapForRevocationChecking(sc *storageContext) (map[issuerID]*x509
// Revoke a certificate from a given serial number if it is present in local
// storage.
func tryRevokeCertBySerial(sc *storageContext, serial string) (*logical.Response, error) {
func tryRevokeCertBySerial(sc *storageContext, config *crlConfig, serial string) (*logical.Response, error) {
certEntry, err := fetchCertBySerial(sc, "certs/", serial)
if err != nil {
switch err.(type) {
@ -694,11 +697,11 @@ func tryRevokeCertBySerial(sc *storageContext, serial string) (*logical.Response
return nil, fmt.Errorf("error parsing certificate: %w", err)
}
return revokeCert(sc, cert)
return revokeCert(sc, config, cert)
}
// Revokes a cert, and tries to be smart about error recovery
func revokeCert(sc *storageContext, cert *x509.Certificate) (*logical.Response, error) {
func revokeCert(sc *storageContext, config *crlConfig, cert *x509.Certificate) (*logical.Response, error) {
// 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,
// revocation doesn't matter anyways -- the CRL that would be written will
@ -786,12 +789,23 @@ func revokeCert(sc *storageContext, cert *x509.Certificate) (*logical.Response,
}
sc.Backend.incrementTotalRevokedCertificatesCount(certsCounted, revEntry.Key)
// 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
// actually rebuild it.
config, err := sc.Backend.crlBuilder.getConfigWithUpdate(sc)
if err != nil {
return nil, fmt.Errorf("error building CRL: while updating config: %w", err)
// If this flag is enabled after the fact, existing local entries will be published to
// the unified storage space through a periodic function.
if config.UnifiedCRL {
entry := &unifiedRevocationEntry{
SerialNumber: colonSerial,
CertExpiration: cert.NotAfter,
RevocationTimeUTC: revInfo.RevocationTimeUTC,
CertificateIssuer: revInfo.CertificateIssuer,
}
ignoreErr := writeUnifiedRevocationEntry(sc, entry)
if ignoreErr != nil {
// Just log the error if we fail to write across clusters, a separate background
// thread will reattempt it later on as we have the local write done.
sc.Backend.Logger().Debug("Failed to write unified revocation entry",
"serial_number", colonSerial, "error", ignoreErr)
}
}
if !config.AutoRebuild {

View File

@ -25,6 +25,7 @@ type crlConfig struct {
EnableDelta bool `json:"enable_delta"`
DeltaRebuildInterval string `json:"delta_rebuild_interval"`
UseGlobalQueue bool `json:"cross_cluster_revocation"`
UnifiedCRL bool `json:"unified_crl"`
}
// Implicit default values for the config if it does not exist.
@ -39,6 +40,7 @@ var defaultCrlConfig = crlConfig{
EnableDelta: false,
DeltaRebuildInterval: "15m",
UseGlobalQueue: false,
UnifiedCRL: false,
}
func pathConfigCRL(b *backend) *framework.Path {
@ -88,6 +90,13 @@ the NextUpdate field); defaults to 12 hours`,
Description: `Whether to enable a global, cross-cluster revocation queue.
Must be used with auto_rebuild=true.`,
},
"unified_crl": {
Type: framework.TypeBool,
Description: `If set to true enables global replication of revocation entries,
also enabling unified versions of OCSP and CRLs if their respective features are enabled.
disable for CRLs and ocsp_disable for OCSP.`,
Default: "false",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
@ -125,6 +134,7 @@ func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, _ *fram
"enable_delta": config.EnableDelta,
"delta_rebuild_interval": config.DeltaRebuildInterval,
"cross_cluster_revocation": config.UseGlobalQueue,
"unified_crl": config.UnifiedCRL,
},
}, nil
}
@ -195,6 +205,10 @@ func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *fra
config.UseGlobalQueue = useGlobalQueue.(bool)
}
if unifiedCrlRaw, ok := d.GetOk("unified_crl"); ok {
config.UnifiedCRL = unifiedCrlRaw.(bool)
}
expiry, _ := time.ParseDuration(config.Expiry)
if config.AutoRebuild {
gracePeriod, _ := time.ParseDuration(config.AutoRebuildGracePeriod)
@ -260,6 +274,8 @@ func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *fra
"auto_rebuild_grace_period": config.AutoRebuildGracePeriod,
"enable_delta": config.EnableDelta,
"delta_rebuild_interval": config.DeltaRebuildInterval,
"cross_cluster_revocation": config.UseGlobalQueue,
"unified_crl": config.UnifiedCRL,
},
}, nil
}

View File

@ -152,6 +152,21 @@ func pathRotateDeltaCRL(b *backend) *framework.Path {
}
}
func pathListUnifiedRevoked(b *backend) *framework.Path {
return &framework.Path{
Pattern: "certs/unified-revoked/?$",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: b.pathListUnifiedRevokedCertsHandler,
},
},
HelpSynopsis: pathListUnifiedRevokedHelpSyn,
HelpDescription: pathListUnifiedRevokedHelpDesc,
}
}
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
// the global issuer set, checking whether or not it is importable.
@ -331,14 +346,14 @@ func (b *backend) pathRevokeWriteHandleKey(req *logical.Request, certReference *
return nil
}
func (b *backend) maybeRevokeCrossCluster(ctx context.Context, sc *storageContext, serial string) (*logical.Response, error) {
config, err := b.crlBuilder.getConfigWithUpdate(sc)
if err != nil {
return nil, err
func (b *backend) maybeRevokeCrossCluster(sc *storageContext, config *crlConfig, serial string, havePrivateKey bool) (*logical.Response, error) {
if !config.UseGlobalQueue {
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found.", serial)), nil
}
if !config.UseGlobalQueue {
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found or was already revoked", serial)), nil
if havePrivateKey {
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found, "+
"and cross-cluster revocation not supported with key revocation.", serial)), nil
}
// Here, we have to use the global revocation queue as the cert
@ -355,7 +370,7 @@ func (b *backend) maybeRevokeCrossCluster(ctx context.Context, sc *storageContex
return nil, fmt.Errorf("failed to create storage entry for cross-cluster revocation request: %w", err)
}
if err := sc.Storage.Put(ctx, reqEntry); err != nil {
if err := sc.Storage.Put(sc.Context, reqEntry); err != nil {
return nil, fmt.Errorf("error persisting cross-cluster revocation request: %w\nThis may occur when the active node of the primary performance replication cluster is unavailable.", err)
}
@ -397,8 +412,12 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
var cert *x509.Certificate
var serial string
config, err := sc.Backend.crlBuilder.getConfigWithUpdate(sc)
if err != nil {
return nil, fmt.Errorf("error revoking serial: %s: failed reading config: %w", serial, err)
}
if haveCert {
var err error
serial, writeCert, cert, err = b.pathRevokeWriteHandleCertificate(ctx, req, rawCertificate.(string))
if err != nil {
return nil, err
@ -425,14 +444,29 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
if err != nil {
return nil, fmt.Errorf("error parsing certificate: %w", err)
}
} else if keyPem == "" {
// Cross-cluster revocation can only happen without PoP currently.
return b.maybeRevokeCrossCluster(ctx, sc, serial)
}
}
if cert == nil {
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found or was already revoked", serial)), nil
if config.UnifiedCRL {
// Saving grace if we aren't able to load the certificate locally/or were given it,
// if we have a unified revocation entry already return its revocation times,
// otherwise we fail with a certificate not found message.
unifiedRev, err := getUnifiedRevocationBySerial(sc, normalizeSerial(serial))
if err != nil {
return nil, err
}
if unifiedRev != nil {
return &logical.Response{
Data: map[string]interface{}{
"revocation_time": unifiedRev.RevocationTimeUTC.Unix(),
"revocation_time_rfc3339": unifiedRev.RevocationTimeUTC.Format(time.RFC3339Nano),
},
}, nil
}
}
return b.maybeRevokeCrossCluster(sc, config, serial, keyPem != "")
}
// Before we write the certificate, we've gotta verify the request in
@ -458,7 +492,7 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
return revokeCert(sc, cert)
return revokeCert(sc, config, cert)
}
func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
@ -583,6 +617,22 @@ func (b *backend) pathListRevocationQueueHandler(ctx context.Context, request *l
return logical.ListResponseWithInfo(responseKeys, responseInfo), nil
}
func (b *backend) pathListUnifiedRevokedCertsHandler(ctx context.Context, request *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
sc := b.makeStorageContext(ctx, request.Storage)
revokedCerts, err := listUnifiedRevokedCerts(sc)
if err != nil {
return nil, err
}
// Normalize serial back to a format people are expecting.
for i, serial := range revokedCerts {
revokedCerts[i] = denormalizeSerial(serial)
}
return logical.ListResponse(revokedCerts), nil
}
const pathRevokeHelpSyn = `
Revoke a certificate by serial number or with explicit certificate.
@ -619,6 +669,14 @@ const pathListRevokedHelpDesc = `
Returns a list of serial numbers for revoked certificates in the local cluster.
`
const pathListUnifiedRevokedHelpSyn = `
List all revoked serial numbers within this cluster's unified storage area.
`
const pathListUnifiedRevokedHelpDesc = `
Returns a list of serial numbers for revoked certificates within this cluster's unified storage.
`
const pathListRevocationQueueHelpSyn = `
List all pending, cross-cluster revocations known to the local cluster.
`

View File

@ -50,8 +50,9 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, _
defer b.revokeStorageLock.Unlock()
sc := b.makeStorageContext(ctx, req.Storage)
serial := serialInt.(string)
certEntry, err := fetchCertBySerial(sc, "certs/", serialInt.(string))
certEntry, err := fetchCertBySerial(sc, "certs/", serial)
if err != nil {
return nil, err
}
@ -59,7 +60,7 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, _
// 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))
b.Logger().Warn("expired certificate revoke failed because not found in storage, treating as success", "serial", serial)
return nil, nil
}
@ -73,5 +74,10 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, _
return nil, nil
}
return revokeCert(sc, cert)
config, err := sc.Backend.crlBuilder.getConfigWithUpdate(sc)
if err != nil {
return nil, fmt.Errorf("error revoking serial: %s: failed reading config: %w", serial, err)
}
return revokeCert(sc, config, cert)
}

View File

@ -0,0 +1,89 @@
package pki
import (
"fmt"
"time"
"github.com/hashicorp/vault/sdk/logical"
)
const (
unifiedRevocationReadPathPrefix = "unified-revocation/"
unifiedRevocationWritePathPrefix = unifiedRevocationReadPathPrefix + "{{clusterId}}/"
)
type unifiedRevocationEntry struct {
SerialNumber string `json:"-"`
CertExpiration time.Time `json:"certificate_expiration_utc"`
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
CertificateIssuer issuerID `json:"issuer_id"`
}
func getUnifiedRevocationBySerial(sc *storageContext, serial string) (*unifiedRevocationEntry, error) {
clusterPaths, err := lookupClusterPaths(sc)
if err != nil {
return nil, err
}
for _, path := range clusterPaths {
serialPath := path + serial
entryRaw, err := sc.Storage.Get(sc.Context, serialPath)
if err != nil {
return nil, err
}
if entryRaw != nil {
var revEntry unifiedRevocationEntry
if err := entryRaw.DecodeJSON(&revEntry); err != nil {
return nil, fmt.Errorf("failed json decoding of unified entry at path %s: %w", serialPath, err)
}
revEntry.SerialNumber = serial
return &revEntry, nil
}
}
return nil, nil
}
func writeUnifiedRevocationEntry(sc *storageContext, ure *unifiedRevocationEntry) error {
json, err := logical.StorageEntryJSON(unifiedRevocationWritePathPrefix+normalizeSerial(ure.SerialNumber), ure)
if err != nil {
return err
}
return sc.Storage.Put(sc.Context, json)
}
func listUnifiedRevokedCerts(sc *storageContext) ([]string, error) {
allSerials := []string{}
clusterPaths, err := lookupClusterPaths(sc)
if err != nil {
return nil, err
}
for _, path := range clusterPaths {
clusterSerials, err := sc.Storage.List(sc.Context, path)
if err != nil {
return nil, fmt.Errorf("failed listing revoked certs for path %s: %w", path, err)
}
allSerials = append(allSerials, clusterSerials...)
}
return allSerials, nil
}
func lookupClusterPaths(sc *storageContext) ([]string, error) {
fullPaths := []string{}
clusterPaths, err := sc.Storage.List(sc.Context, unifiedRevocationReadPathPrefix)
if err != nil {
return fullPaths, err
}
for _, clusterId := range clusterPaths {
fullPaths = append(fullPaths, unifiedRevocationReadPathPrefix+clusterId)
}
return fullPaths, nil
}