open-vault/builtin/logical/pki/path_acme_revoke.go

183 lines
6.8 KiB
Go
Raw Permalink Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
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) []*framework.Path {
return buildAcmeFrameworkPaths(b, patternAcmeRevoke, "/revoke-cert")
}
func patternAcmeRevoke(b *backend, pattern string) *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(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.revokeStorageLock.Lock()
defer b.revokeStorageLock.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.acmeState.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.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
return revokeCert(acmeCtx.sc, config, cert)
}