vault/builtin/logical/pki/path_acme_revoke.go
Steven Clark cbf6dc2c4f
PKI refactoring to start breaking apart monolith into sub-packages (#24406)
* PKI refactoring to start breaking apart monolith into sub-packages

 - This was broken down by commit within enterprise for ease of review
   but would be too difficult to bring back individual commits back
   to the CE repository. (they would be squashed anyways)
 - This change was created by exporting a patch of the enterprise PR
   and applying it to CE repository

* Fix TestBackend_OID_SANs to not be rely on map ordering
2023-12-07 09:22:53 -05:00

183 lines
6.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package pki
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"fmt"
"time"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
func pathAcmeRevoke(b *backend, baseUrl string, opts acmeWrapperOpts) *framework.Path {
return patternAcmeRevoke(b, baseUrl+"/revoke-cert", opts)
}
func patternAcmeRevoke(b *backend, pattern string, opts acmeWrapperOpts) *framework.Path {
fields := map[string]*framework.FieldSchema{}
addFieldsForACMEPath(fields, pattern)
addFieldsForACMERequest(fields)
return &framework.Path{
Pattern: pattern,
Fields: fields,
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.acmeParsedWrapper(opts, b.acmeRevocationHandler),
ForwardPerformanceSecondary: false,
ForwardPerformanceStandby: true,
},
},
HelpSynopsis: pathAcmeHelpSync,
HelpDescription: pathAcmeHelpDesc,
}
}
func (b *backend) acmeRevocationHandler(acmeCtx *acmeContext, _ *logical.Request, _ *framework.FieldData, userCtx *jwsCtx, data map[string]interface{}) (*logical.Response, error) {
var cert *x509.Certificate
rawCertificate, present := data["certificate"]
if present {
certBase64, ok := rawCertificate.(string)
if !ok {
return nil, fmt.Errorf("invalid type (%T; expected string) for field 'certificate': %w", rawCertificate, ErrMalformed)
}
certBytes, err := base64.RawURLEncoding.DecodeString(certBase64)
if err != nil {
return nil, fmt.Errorf("failed to base64 decode certificate: %v: %w", err, ErrMalformed)
}
cert, err = x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %v: %w", err, ErrMalformed)
}
} else {
return nil, fmt.Errorf("bad request was lacking required field 'certificate': %w", ErrMalformed)
}
rawReason, present := data["reason"]
if present {
reason, ok := rawReason.(float64)
if !ok {
return nil, fmt.Errorf("invalid type (%T; expected float64) for field 'reason': %w", rawReason, ErrMalformed)
}
if int(reason) != 0 {
return nil, fmt.Errorf("Vault does not support revocation reasons (got %v; expected omitted or 0/unspecified): %w", int(reason), ErrBadRevocationReason)
}
}
// If the certificate expired, there's no point in revoking it.
if cert.NotAfter.Before(time.Now()) {
return nil, fmt.Errorf("refusing to revoke expired certificate: %w", ErrMalformed)
}
// Fetch the CRL config as we need it to ultimately do the
// revocation. This should be cached and thus relatively fast.
config, err := b.CrlBuilder().getConfigWithUpdate(acmeCtx.sc)
if err != nil {
return nil, fmt.Errorf("unable to revoke certificate: failed reading revocation config: %v: %w", err, ErrServerInternal)
}
// Load our certificate from storage to ensure it exists and matches
// what was given to us.
serial := serialFromCert(cert)
certEntry, err := fetchCertBySerial(acmeCtx.sc, "certs/", serial)
if err != nil {
return nil, fmt.Errorf("unable to revoke certificate: err reading global cert entry: %v: %w", err, ErrServerInternal)
}
if certEntry == nil {
return nil, fmt.Errorf("unable to revoke certificate: no global cert entry found: %w", ErrServerInternal)
}
// Validate that the provided certificate matches the stored
// certificate. This completes the chain of:
//
// provided_auth -> provided_cert == stored cert.
//
// Allowing revocation to be safe.
//
// We use the non-subtle unsafe bytes equality check here as we have
// already fetched this certificate from storage, thus already leaking
// timing information that this cert exists. The user could thus simply
// fetch the cert from Vault matching this serial number via the unauthed
// pki/certs/:serial API endpoint.
if !bytes.Equal(certEntry.Value, cert.Raw) {
return nil, fmt.Errorf("unable to revoke certificate: supplied certificate does not match CA's stored value: %w", ErrMalformed)
}
// Check if it was already revoked; in this case, we do not need to
// revoke it again and want to respond with an appropriate error message.
revEntry, err := fetchCertBySerial(acmeCtx.sc, "revoked/", serial)
if err != nil {
return nil, fmt.Errorf("unable to revoke certificate: err reading revocation entry: %v: %w", err, ErrServerInternal)
}
if revEntry != nil {
return nil, fmt.Errorf("unable to revoke certificate: %w", ErrAlreadyRevoked)
}
// Finally, do the relevant permissions/authorization check as
// appropriate based on the type of revocation happening.
if !userCtx.Existing {
return b.acmeRevocationByPoP(acmeCtx, userCtx, cert, config)
}
return b.acmeRevocationByAccount(acmeCtx, userCtx, cert, config)
}
func (b *backend) acmeRevocationByPoP(acmeCtx *acmeContext, userCtx *jwsCtx, cert *x509.Certificate, config *crlConfig) (*logical.Response, error) {
// Since this account does not exist, ensure we've gotten a private key
// matching the certificate's public key. This private key isn't
// explicitly provided, but instead provided by proxy (public key,
// signature over message). That signature is validated by an earlier
// wrapper (VerifyJWS called by ParseRequestParams). What still remains
// is validating that this implicit private key (with given public key
// and valid JWS signature) matches the certificate's public key.
givenPublic, ok := userCtx.Key.Key.(crypto.PublicKey)
if !ok {
return nil, fmt.Errorf("unable to revoke certificate: unable to parse message header's JWS key of type (%T): %w", userCtx.Key.Key, ErrMalformed)
}
// Ensure that our PoP's implicit private key matches this certificate's
// public key.
if err := validatePublicKeyMatchesCert(givenPublic, cert); err != nil {
return nil, fmt.Errorf("unable to revoke certificate: unable to verify proof of possession of private key provided by proxy: %v: %w", err, ErrMalformed)
}
// Now it is safe to revoke.
b.GetRevokeStorageLock().Lock()
defer b.GetRevokeStorageLock().Unlock()
return revokeCert(acmeCtx.sc, config, cert)
}
func (b *backend) acmeRevocationByAccount(acmeCtx *acmeContext, userCtx *jwsCtx, cert *x509.Certificate, config *crlConfig) (*logical.Response, error) {
// Fetch the account; disallow revocations from non-valid-status accounts.
_, err := requireValidAcmeAccount(acmeCtx, userCtx)
if err != nil {
return nil, fmt.Errorf("failed to lookup account: %w", err)
}
// We only support certificates issued by this user, we don't support
// cross-account revocations.
serial := serialFromCert(cert)
acmeEntry, err := b.GetAcmeState().GetIssuedCert(acmeCtx, userCtx.Kid, serial)
if err != nil || acmeEntry == nil {
return nil, fmt.Errorf("unable to revoke certificate: %v: %w", err, ErrMalformed)
}
// Now it is safe to revoke.
b.GetRevokeStorageLock().Lock()
defer b.GetRevokeStorageLock().Unlock()
return revokeCert(acmeCtx.sc, config, cert)
}