e7d57bfe90
Also remove one duplicate error masked by return. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
535 lines
18 KiB
Go
535 lines
18 KiB
Go
package pki
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/subtle"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"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/logical"
|
|
)
|
|
|
|
func pathListCertsRevoked(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "certs/revoked/?$",
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ListOperation: &framework.PathOperation{
|
|
Callback: b.pathListRevokedCertsHandler,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathListRevokedHelpSyn,
|
|
HelpDescription: pathListRevokedHelpDesc,
|
|
}
|
|
}
|
|
|
|
func pathRevoke(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: `revoke`,
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"serial_number": {
|
|
Type: framework.TypeString,
|
|
Description: `Certificate serial number, in colon- or
|
|
hyphen-separated octal`,
|
|
},
|
|
"certificate": {
|
|
Type: framework.TypeString,
|
|
Description: `Certificate to revoke in PEM format; must be
|
|
signed by an issuer in this mount.`,
|
|
},
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.metricsWrap("revoke", noRole, b.pathRevokeWrite),
|
|
// This should never be forwarded. See backend.go for more information.
|
|
// If this needs to write, the entire request will be forwarded to the
|
|
// active node of the current performance cluster, but we don't want to
|
|
// forward invalid revoke requests there.
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathRevokeHelpSyn,
|
|
HelpDescription: pathRevokeHelpDesc,
|
|
}
|
|
}
|
|
|
|
func pathRevokeWithKey(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: `revoke-with-key`,
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"serial_number": {
|
|
Type: framework.TypeString,
|
|
Description: `Certificate serial number, in colon- or
|
|
hyphen-separated octal`,
|
|
},
|
|
"certificate": {
|
|
Type: framework.TypeString,
|
|
Description: `Certificate to revoke in PEM format; must be
|
|
signed by an issuer in this mount.`,
|
|
},
|
|
"private_key": {
|
|
Type: framework.TypeString,
|
|
Description: `Key to use to verify revocation permission; must
|
|
be in PEM format.`,
|
|
},
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.metricsWrap("revoke", noRole, b.pathRevokeWrite),
|
|
// This should never be forwarded. See backend.go for more information.
|
|
// If this needs to write, the entire request will be forwarded to the
|
|
// active node of the current performance cluster, but we don't want to
|
|
// forward invalid revoke requests there.
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathRevokeHelpSyn,
|
|
HelpDescription: pathRevokeHelpDesc,
|
|
}
|
|
}
|
|
|
|
func pathRotateCRL(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: `crl/rotate`,
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: b.pathRotateCRLRead,
|
|
// See backend.go; we will read a lot of data prior to calling write,
|
|
// so this request should be forwarded when it is first seen, not
|
|
// when it is ready to write.
|
|
ForwardPerformanceStandby: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathRotateCRLHelpSyn,
|
|
HelpDescription: pathRotateCRLHelpDesc,
|
|
}
|
|
}
|
|
|
|
func pathRotateDeltaCRL(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: `crl/rotate-delta`,
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: b.pathRotateDeltaCRLRead,
|
|
// See backend.go; we will read a lot of data prior to calling write,
|
|
// so this request should be forwarded when it is first seen, not
|
|
// when it is ready to write.
|
|
ForwardPerformanceStandby: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathRotateDeltaCRLHelpSyn,
|
|
HelpDescription: pathRotateDeltaCRLHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *logical.Request, certPem string) (string, bool, []byte, error) {
|
|
// This function handles just the verification of the certificate against
|
|
// the global issuer set, checking whether or not it is importable.
|
|
//
|
|
// We return the parsed serial number, an optionally-nil byte array to
|
|
// write out to disk, and an error if one occurred.
|
|
if b.useLegacyBundleCaStorage() {
|
|
// We require listing all issuers from the 1.11 method. If we're
|
|
// still using the legacy CA bundle but with the newer certificate
|
|
// attribute, we err and require the operator to upgrade and migrate
|
|
// prior to servicing new requests.
|
|
return "", false, nil, errutil.UserError{Err: "unable to process BYOC revocation until CA issuer migration has completed"}
|
|
}
|
|
|
|
// First start by parsing the certificate.
|
|
if len(certPem) < 75 {
|
|
// See note in pathImportIssuers about this check.
|
|
return "", false, nil, errutil.UserError{Err: "provided certificate data was too short; perhaps a path was passed to the API rather than the contents of a PEM file"}
|
|
}
|
|
|
|
pemBlock, _ := pem.Decode([]byte(certPem))
|
|
if pemBlock == nil {
|
|
return "", false, nil, errutil.UserError{Err: "certificate contains no PEM data"}
|
|
}
|
|
|
|
certReference, err := x509.ParseCertificate(pemBlock.Bytes)
|
|
if err != nil {
|
|
return "", false, nil, errutil.UserError{Err: fmt.Sprintf("certificate could not be parsed: %v", err)}
|
|
}
|
|
|
|
// Ensure we have a well-formed serial number before continuing.
|
|
serial := serialFromCert(certReference)
|
|
if len(serial) == 0 {
|
|
return "", false, nil, errutil.UserError{Err: "invalid serial number on presented certificate"}
|
|
}
|
|
|
|
// We have two approaches here: we could start verifying against issuers
|
|
// (which involves fetching and parsing them), or we could see if, by
|
|
// some chance we've already imported it (cheap). The latter tells us
|
|
// if we happen to have a serial number collision (which shouldn't
|
|
// happen in practice) versus an already-imported cert (which might
|
|
// happen and its fine to handle safely).
|
|
//
|
|
// Start with the latter since its cheaper. Fetch the cert (by serial)
|
|
// and if it exists, compare the contents.
|
|
certEntry, err := fetchCertBySerial(ctx, b, req, req.Path, serial)
|
|
if err != nil {
|
|
return serial, false, nil, err
|
|
}
|
|
|
|
if certEntry != nil {
|
|
// As seen with importing issuers, it is best to parse the certificate
|
|
// and compare parsed values, rather than attempting to infer equality
|
|
// from the raw data.
|
|
certReferenceStored, err := x509.ParseCertificate(certEntry.Value)
|
|
if err != nil {
|
|
return serial, false, nil, err
|
|
}
|
|
|
|
if !areCertificatesEqual(certReference, certReferenceStored) {
|
|
// Here we refuse the import with an error because the two certs
|
|
// are unequal but we would've otherwise overwritten the existing
|
|
// copy.
|
|
return serial, false, nil, fmt.Errorf("certificate with same serial but unequal value already present in this cluster's storage; refusing to revoke")
|
|
} else {
|
|
// Otherwise, we can return without an error as we've already
|
|
// imported this certificate, likely when we issued it. We don't
|
|
// need to re-verify the signature as we assume it was already
|
|
// verified when it was imported.
|
|
return serial, false, certEntry.Value, nil
|
|
}
|
|
}
|
|
|
|
// Otherwise, we must not have a stored copy. From here on out, the second
|
|
// parameter (except in error cases) should cause the cert to write out.
|
|
//
|
|
// Fetch and iterate through each issuer.
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
issuers, err := sc.listIssuers()
|
|
if err != nil {
|
|
return serial, false, nil, err
|
|
}
|
|
|
|
foundMatchingIssuer := false
|
|
for _, issuerId := range issuers {
|
|
issuer, err := sc.fetchIssuerById(issuerId)
|
|
if err != nil {
|
|
return serial, false, nil, err
|
|
}
|
|
|
|
issuerCert, err := issuer.GetCertificate()
|
|
if err != nil {
|
|
return serial, false, nil, err
|
|
}
|
|
|
|
if err := certReference.CheckSignatureFrom(issuerCert); err == nil {
|
|
// If the signature was valid, we found our match and can safely
|
|
// exit.
|
|
foundMatchingIssuer = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if foundMatchingIssuer {
|
|
return serial, true, certReference.Raw, 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"}
|
|
}
|
|
|
|
func (b *backend) pathRevokeWriteHandleKey(ctx context.Context, req *logical.Request, cert []byte, keyPem string) error {
|
|
if keyPem == "" {
|
|
// The only way to get here should be via the /revoke endpoint;
|
|
// validate the path one more time and return an error if necessary.
|
|
if req.Path != "revoke" {
|
|
return fmt.Errorf("must have private key to revoke via the /revoke-with-key path")
|
|
}
|
|
|
|
// Otherwise, we don't need to validate the key and thus can return
|
|
// with success.
|
|
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.
|
|
pemBlock, _ := pem.Decode([]byte(keyPem))
|
|
if pemBlock == nil {
|
|
return errutil.UserError{Err: "provided key PEM block contained no data or failed to parse"}
|
|
}
|
|
|
|
// Parse the inner DER key.
|
|
signer, _, err := certutil.ParseDERKey(pemBlock.Bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse provided private key: %w", err)
|
|
}
|
|
|
|
// Finally, verify if the cert and key match. This code has been
|
|
// cribbed from the Go TLS config code, with minor modifications.
|
|
//
|
|
// In particular, we validate against the derived public key
|
|
// components and ensure we validate exponent and curve information
|
|
// as well.
|
|
//
|
|
//
|
|
// See: https://github.com/golang/go/blob/c6a2dada0df8c2d75cf3ae599d7caed77d416fa2/src/crypto/tls/tls.go#L304-L331
|
|
switch certPub := certReference.PublicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
privPub, ok := signer.Public().(*rsa.PublicKey)
|
|
if !ok {
|
|
return errutil.UserError{Err: "provided private key type does not match certificate's public key type"}
|
|
}
|
|
if err := signer.(*rsa.PrivateKey).Validate(); err != nil {
|
|
return err
|
|
}
|
|
if certPub.N.Cmp(privPub.N) != 0 || certPub.E != privPub.E {
|
|
return errutil.UserError{Err: "provided private key does not match certificate's public key"}
|
|
}
|
|
case *ecdsa.PublicKey:
|
|
privPub, ok := signer.Public().(*ecdsa.PublicKey)
|
|
if !ok {
|
|
return errutil.UserError{Err: "provided private key type does not match certificate's public key type"}
|
|
}
|
|
if certPub.X.Cmp(privPub.X) != 0 || certPub.Y.Cmp(privPub.Y) != 0 || certPub.Params().Name != privPub.Params().Name {
|
|
return errutil.UserError{Err: "provided private key does not match certificate's public key"}
|
|
}
|
|
case ed25519.PublicKey:
|
|
privPub, ok := signer.Public().(ed25519.PublicKey)
|
|
if !ok {
|
|
return errutil.UserError{Err: "provided private key type does not match certificate's public key type"}
|
|
}
|
|
if subtle.ConstantTimeCompare(privPub, certPub) == 0 {
|
|
return errutil.UserError{Err: "provided private key does not match certificate's public key"}
|
|
}
|
|
default:
|
|
return errutil.UserError{Err: "certificate has an unknown public key algorithm; unable to validate provided private key; ask an admin to revoke this certificate instead"}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, data *framework.FieldData, _ *roleEntry) (*logical.Response, error) {
|
|
rawSerial, haveSerial := data.GetOk("serial_number")
|
|
rawCertificate, haveCert := data.GetOk("certificate")
|
|
|
|
if !haveSerial && !haveCert {
|
|
return logical.ErrorResponse("The serial number or certificate to revoke must be provided."), nil
|
|
} else if haveSerial && haveCert {
|
|
return logical.ErrorResponse("Must provide either the certificate or the serial to revoke; not both."), nil
|
|
}
|
|
|
|
var keyPem string
|
|
if req.Path == "revoke-with-key" {
|
|
rawKey, haveKey := data.GetOk("private_key")
|
|
if !haveKey {
|
|
return logical.ErrorResponse("Must have private key to revoke via the /revoke-with-key path."), nil
|
|
}
|
|
|
|
keyPem = rawKey.(string)
|
|
if len(keyPem) < 64 {
|
|
// See note in pathImportKeyHandler...
|
|
return logical.ErrorResponse("Provided data for private_key was too short; perhaps a path was passed to the API rather than the contents of a PEM file?"), nil
|
|
}
|
|
}
|
|
|
|
var serial string
|
|
if haveSerial {
|
|
// Easy case: this cert should be in storage already.
|
|
serial = rawSerial.(string)
|
|
if len(serial) == 0 {
|
|
return logical.ErrorResponse("The serial number must be provided"), nil
|
|
}
|
|
|
|
// Here, fetch the certificate from disk to validate we can revoke it.
|
|
certEntry, err := fetchCertBySerial(ctx, b, req, req.Path, serial)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case errutil.UserError:
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
default:
|
|
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
|
|
// and stored certificate match.
|
|
if err := b.pathRevokeWriteHandleKey(ctx, req, certEntry.Value, keyPem); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Otherwise, we've gotta parse the certificate from the request and
|
|
// then import it into cluster-local storage. Before writing the
|
|
// certificate (and forwarding), we want to verify this certificate
|
|
// was actually signed by one of our present issuers.
|
|
var err error
|
|
var writeCert bool
|
|
var certBytes []byte
|
|
serial, writeCert, certBytes, err = b.pathRevokeWriteHandleCertificate(ctx, req, rawCertificate.(string))
|
|
if err != nil {
|
|
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()
|
|
defer b.revokeStorageLock.Unlock()
|
|
|
|
return revokeCert(ctx, b, req, serial, false)
|
|
}
|
|
|
|
func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
|
b.revokeStorageLock.RLock()
|
|
defer b.revokeStorageLock.RUnlock()
|
|
|
|
crlErr := b.crlBuilder.rebuild(ctx, b, req, false)
|
|
if crlErr != nil {
|
|
switch crlErr.(type) {
|
|
case errutil.UserError:
|
|
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
|
|
default:
|
|
return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr)
|
|
}
|
|
}
|
|
|
|
return &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"success": true,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (b *backend) pathRotateDeltaCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
|
|
cfg, err := b.crlBuilder.getConfigWithUpdate(sc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error fetching CRL configuration: %w", err)
|
|
}
|
|
|
|
isEnabled := cfg.EnableDelta
|
|
|
|
crlErr := b.crlBuilder.rebuildDeltaCRLsIfForced(sc, true)
|
|
if crlErr != nil {
|
|
switch crlErr.(type) {
|
|
case errutil.UserError:
|
|
return logical.ErrorResponse(fmt.Sprintf("Error during delta CRL building: %s", crlErr)), nil
|
|
default:
|
|
return nil, fmt.Errorf("error encountered during delta CRL building: %w", crlErr)
|
|
}
|
|
}
|
|
|
|
resp := &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"success": true,
|
|
},
|
|
}
|
|
|
|
if !isEnabled {
|
|
resp.AddWarning("requested rebuild of delta CRL when delta CRL is not enabled; this is a no-op")
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (b *backend) pathListRevokedCertsHandler(ctx context.Context, request *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
|
sc := b.makeStorageContext(ctx, request.Storage)
|
|
|
|
revokedCerts, err := sc.listRevokedCerts()
|
|
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.
|
|
|
|
When calling /revoke-with-key, the private key corresponding to the
|
|
certificate must be provided to authenticate the request.
|
|
`
|
|
|
|
const pathRevokeHelpDesc = `
|
|
This allows certificates to be revoke. A root token or corresponding
|
|
private key is required.
|
|
`
|
|
|
|
const pathRotateCRLHelpSyn = `
|
|
Force a rebuild of the CRL.
|
|
`
|
|
|
|
const pathRotateCRLHelpDesc = `
|
|
Force a rebuild of the CRL. This can be used to remove expired certificates from it if no certificates have been revoked. A root token is required.
|
|
`
|
|
|
|
const pathRotateDeltaCRLHelpSyn = `
|
|
Force a rebuild of the delta CRL.
|
|
`
|
|
|
|
const pathRotateDeltaCRLHelpDesc = `
|
|
Force a rebuild of the delta CRL. This can be used to force an update of the otherwise periodically-rebuilt delta CRLs.
|
|
`
|
|
|
|
const pathListRevokedHelpSyn = `
|
|
List all revoked serial numbers within the local cluster
|
|
`
|
|
|
|
const pathListRevokedHelpDesc = `
|
|
Returns a list of serial numbers for revoked certificates in the local cluster.
|
|
`
|