183 lines
6.8 KiB
Go
183 lines
6.8 KiB
Go
// 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)
|
|
}
|