Add proof possession revocation for PKI secrets engine (#16566)
* Allow Proof of Possession based revocation Revocation by proof of possession ensures that we have a private key matching the (provided or stored) certificate. This allows callers to revoke certificate they own (as proven by holding the corresponding private key), without having an admin create innumerable ACLs around the serial_number parameter for every issuance/user. We base this on Go TLS stack's verification of certificate<->key matching, but extend it where applicable to ensure curves match, the private key is indeed valid, and has the same structure as the corresponding public key from the certificate. This endpoint currently is authenticated, allowing operators to disable the endpoint if it isn't desirable to use, via ACL policies. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Clarify error message on ParseDERKey Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog entry Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Leave revoke-with-key authenticated After some discussion, given the potential for DoS (via submitting a lot of keys/certs to validate, including invalid pairs), it seems best to leave this as an authenticated endpoint. Presently in Vault, there's no way to have an authenticated-but-unauthorized path (i.e., one which bypasses ACL controls), so it is recommended (but not enforced) to make this endpoint generally available by permissive ACL policies. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add API documentation on PoP Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add acceptance tests for Proof of Possession Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Exercise negative cases in PoP tests Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
parent
d7da5da0f6
commit
1e6730573c
|
@ -121,6 +121,7 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||
pathIssue(&b),
|
||||
pathRotateCRL(&b),
|
||||
pathRevoke(&b),
|
||||
pathRevokeWithKey(&b),
|
||||
pathTidy(&b),
|
||||
pathTidyStatus(&b),
|
||||
|
||||
|
|
|
@ -420,6 +420,164 @@ func TestBYOC(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPoP(t *testing.T) {
|
||||
b, s := createBackendWithStorage(t)
|
||||
|
||||
// Create a root CA.
|
||||
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
|
||||
"common_name": "root example.com",
|
||||
"issuer_name": "root",
|
||||
"key_type": "ec",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEmpty(t, resp.Data["certificate"])
|
||||
oldRoot := resp.Data["certificate"].(string)
|
||||
|
||||
// Create a role for issuance.
|
||||
_, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{
|
||||
"allow_any_name": true,
|
||||
"enforce_hostnames": false,
|
||||
"key_type": "ec",
|
||||
"ttl": "75s",
|
||||
"no_store": "true",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Issue a leaf cert and ensure we can revoke it with the private key and
|
||||
// an explicit certificate.
|
||||
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
|
||||
"common_name": "testing1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEmpty(t, resp.Data["certificate"])
|
||||
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"certificate": resp.Data["certificate"],
|
||||
"private_key": resp.Data["private_key"],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Issue a second leaf, but hold onto it for now.
|
||||
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
|
||||
"common_name": "testing2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEmpty(t, resp.Data["certificate"])
|
||||
notStoredCert := resp.Data["certificate"].(string)
|
||||
notStoredKey := resp.Data["private_key"].(string)
|
||||
|
||||
// Update the role to make things stored and issue another cert.
|
||||
_, err = CBWrite(b, s, "roles/stored-testing", map[string]interface{}{
|
||||
"allow_any_name": true,
|
||||
"enforce_hostnames": false,
|
||||
"key_type": "ec",
|
||||
"ttl": "75s",
|
||||
"no_store": "false",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Issue a leaf and ensure we can revoke it via serial number and private key.
|
||||
resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{
|
||||
"common_name": "testing3",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEmpty(t, resp.Data["certificate"])
|
||||
require.NotEmpty(t, resp.Data["serial_number"])
|
||||
require.NotEmpty(t, resp.Data["private_key"])
|
||||
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"serial_number": resp.Data["serial_number"],
|
||||
"private_key": resp.Data["private_key"],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Issue a leaf cert and ensure we can revoke it after removing its root;
|
||||
// hold onto it for now.
|
||||
resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{
|
||||
"common_name": "testing4",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEmpty(t, resp.Data["certificate"])
|
||||
storedCert := resp.Data["certificate"].(string)
|
||||
storedKey := resp.Data["private_key"].(string)
|
||||
|
||||
// Delete the root and regenerate a new one.
|
||||
_, err = CBDelete(b, s, "issuer/default")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = CBList(b, s, "issuers")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(resp.Data), 0)
|
||||
|
||||
_, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{
|
||||
"common_name": "root2 example.com",
|
||||
"issuer_name": "root2",
|
||||
"key_type": "ec",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Issue a new leaf and revoke that one.
|
||||
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
|
||||
"common_name": "testing5",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEmpty(t, resp.Data["certificate"])
|
||||
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"certificate": resp.Data["certificate"],
|
||||
"private_key": resp.Data["private_key"],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now attempt to revoke the earlier leaves. The first should fail since
|
||||
// we deleted its issuer, but the stored one should succeed.
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"certificate": notStoredCert,
|
||||
"private_key": notStoredKey,
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// Incorrect combination (stored with not stored key) should fail.
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"certificate": storedCert,
|
||||
"private_key": notStoredKey,
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// Correct combination (stored with stored) should succeed.
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"certificate": storedCert,
|
||||
"private_key": storedKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Import the old root again and revoke the no stored leaf should work.
|
||||
_, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{
|
||||
"pem_bundle": oldRoot,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Incorrect combination (not stored with stored key) should fail.
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"certificate": notStoredCert,
|
||||
"private_key": storedKey,
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// Correct combination (not stored with not stored) should succeed.
|
||||
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
|
||||
"certificate": notStoredCert,
|
||||
"private_key": notStoredKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func requestCrlFromBackend(t *testing.T, s logical.Storage, b *backend) *logical.Response {
|
||||
crlReq := &logical.Request{
|
||||
Operation: logical.ReadOperation,
|
||||
|
|
|
@ -2,12 +2,17 @@ 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"
|
||||
|
@ -44,6 +49,42 @@ signed by an issuer in this mount.`,
|
|||
}
|
||||
}
|
||||
|
||||
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`,
|
||||
|
@ -63,7 +104,7 @@ func pathRotateCRL(b *backend) *framework.Path {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *logical.Request, data *framework.FieldData, certPem string) (string, []byte, error) {
|
||||
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.
|
||||
//
|
||||
|
@ -74,29 +115,29 @@ func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *log
|
|||
// 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 "", nil, errutil.UserError{Err: "unable to process BYOC revocation until CA issuer migration has completed"}
|
||||
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 "", 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"}
|
||||
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 "", nil, errutil.UserError{Err: "certificate contains no PEM data"}
|
||||
return "", false, nil, errutil.UserError{Err: "certificate contains no PEM data"}
|
||||
}
|
||||
|
||||
certReference, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
return "", nil, errutil.UserError{Err: fmt.Sprintf("certificate could not be parsed: %v", err)}
|
||||
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 "", nil, errutil.UserError{Err: fmt.Sprintf("invalid serial number on presented certificate")}
|
||||
return "", false, nil, errutil.UserError{Err: fmt.Sprintf("invalid serial number on presented certificate")}
|
||||
}
|
||||
|
||||
// We have two approaches here: we could start verifying against issuers
|
||||
|
@ -110,7 +151,7 @@ func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *log
|
|||
// and if it exists, compare the contents.
|
||||
certEntry, err := fetchCertBySerial(ctx, b, req, req.Path, serial)
|
||||
if err != nil {
|
||||
return serial, nil, err
|
||||
return serial, false, nil, err
|
||||
}
|
||||
|
||||
if certEntry != nil {
|
||||
|
@ -119,43 +160,43 @@ func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *log
|
|||
// from the raw data.
|
||||
certReferenceStored, err := x509.ParseCertificate(certEntry.Value)
|
||||
if err != nil {
|
||||
return serial, nil, err
|
||||
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, nil, fmt.Errorf("certificate with same serial but unequal value already present in this cluster's storage; refusing to revoke")
|
||||
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, nil, nil
|
||||
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 be the copy to write out.
|
||||
// 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, nil, err
|
||||
return serial, false, nil, err
|
||||
}
|
||||
|
||||
foundMatchingIssuer := false
|
||||
for _, issuerId := range issuers {
|
||||
issuer, err := sc.fetchIssuerById(issuerId)
|
||||
if err != nil {
|
||||
return serial, nil, err
|
||||
return serial, false, nil, err
|
||||
}
|
||||
|
||||
issuerCert, err := issuer.GetCertificate()
|
||||
if err != nil {
|
||||
return serial, nil, err
|
||||
return serial, false, nil, err
|
||||
}
|
||||
|
||||
if err := certReference.CheckSignatureFrom(issuerCert); err == nil {
|
||||
|
@ -167,10 +208,85 @@ func (b *backend) pathRevokeWriteHandleCertificate(ctx context.Context, req *log
|
|||
}
|
||||
|
||||
if foundMatchingIssuer {
|
||||
return serial, certReference.Raw, nil
|
||||
return serial, true, certReference.Raw, nil
|
||||
}
|
||||
|
||||
return serial, nil, errutil.UserError{Err: fmt.Sprintf("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")}
|
||||
return serial, false, nil, errutil.UserError{Err: fmt.Sprintf("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: %v", 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) {
|
||||
|
@ -183,26 +299,71 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
|
|||
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, certBytes, err = b.pathRevokeWriteHandleCertificate(ctx, req, data, rawCertificate.(string))
|
||||
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 certBytes != nil {
|
||||
if writeCert {
|
||||
err = req.Storage.Put(ctx, &logical.StorageEntry{
|
||||
Key: "certs/" + serial,
|
||||
Value: certBytes,
|
||||
|
@ -215,10 +376,6 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
|
|||
// Finally, we have a valid serial number to use for BYOC revocation!
|
||||
}
|
||||
|
||||
if len(serial) == 0 {
|
||||
return logical.ErrorResponse("The serial number must be provided"), nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -258,11 +415,15 @@ func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, _
|
|||
}
|
||||
|
||||
const pathRevokeHelpSyn = `
|
||||
Revoke a certificate by serial number.
|
||||
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 revoked using its serial number. A root token is required.
|
||||
This allows certificates to be revoke. A root token or corresponding
|
||||
private key is required.
|
||||
`
|
||||
|
||||
const pathRotateCRLHelpSyn = `
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
secrets/pki: Allow revocation via proving possession of certificate's private key
|
||||
```
|
|
@ -176,18 +176,21 @@ func ParsePKIJSON(input []byte) (*ParsedCertBundle, error) {
|
|||
}
|
||||
|
||||
func ParseDERKey(privateKeyBytes []byte) (signer crypto.Signer, format BlockType, err error) {
|
||||
if signer, err = x509.ParseECPrivateKey(privateKeyBytes); err == nil {
|
||||
var firstError error
|
||||
if signer, firstError = x509.ParseECPrivateKey(privateKeyBytes); firstError == nil {
|
||||
format = ECBlock
|
||||
return
|
||||
}
|
||||
|
||||
if signer, err = x509.ParsePKCS1PrivateKey(privateKeyBytes); err == nil {
|
||||
var secondError error
|
||||
if signer, secondError = x509.ParsePKCS1PrivateKey(privateKeyBytes); secondError == nil {
|
||||
format = PKCS1Block
|
||||
return
|
||||
}
|
||||
|
||||
var thirdError error
|
||||
var rawKey interface{}
|
||||
if rawKey, err = x509.ParsePKCS8PrivateKey(privateKeyBytes); err == nil {
|
||||
if rawKey, thirdError = x509.ParsePKCS8PrivateKey(privateKeyBytes); thirdError == nil {
|
||||
switch rawSigner := rawKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
signer = rawSigner
|
||||
|
@ -203,7 +206,7 @@ func ParseDERKey(privateKeyBytes []byte) (signer crypto.Signer, format BlockType
|
|||
return
|
||||
}
|
||||
|
||||
return nil, UnknownBlock, err
|
||||
return nil, UnknownBlock, fmt.Errorf("got errors attempting to parse DER private key:\n1. %v\n2. %v\n3. %v", firstError, secondError, thirdError)
|
||||
}
|
||||
|
||||
func ParsePEMKey(keyPem string) (crypto.Signer, BlockType, error) {
|
||||
|
|
|
@ -28,6 +28,7 @@ update your API calls accordingly.
|
|||
- [Sign Self-Issued](#sign-self-issued)
|
||||
- [Sign Verbatim](#sign-verbatim)
|
||||
- [Revoke Certificate](#revoke-certificate)
|
||||
- [Revoke Certificate with Private Key](#revoke-certificate-with-private-key)
|
||||
- [Accessing Authority Information](#accessing-authority-information)
|
||||
- [List Issuers](#list-issuers)
|
||||
- [Read Issuer Certificate](#read-issuer-certificate)
|
||||
|
@ -872,6 +873,74 @@ $ curl \
|
|||
}
|
||||
```
|
||||
|
||||
### Revoke Certificate with Private Key
|
||||
|
||||
This endpoint revokes a certificate using its private key as proof that the
|
||||
request is authorized by an appropriate individual (Proof of Possession).
|
||||
|
||||
This is an alternative option to the standard method of revoking using Vault
|
||||
lease IDs or revocation via serial number. A successful revocation will
|
||||
rotate the CRL.
|
||||
|
||||
It is not possible to revoke issuers using this path.
|
||||
|
||||
~> **Note**: This operation is **NOT** privileged, as it validates revocation
|
||||
has a private key corresponding to a certificate signed by Vault. However,
|
||||
to avoid third parties performing a denial-of-service (DOS) against Vault,
|
||||
we've made this endpoint authenticated. Thus it is strongly encouraged to
|
||||
generally allow all access to this path via ACLs.
|
||||
|
||||
| Method | Path |
|
||||
| :----- | :--------------------- |
|
||||
| `POST` | `/pki/revoke-with-key` |
|
||||
|
||||
#### Parameters
|
||||
|
||||
~> Note: either `serial_number` or `certificate` (but not both) must be
|
||||
specified on requests to this endpoint.
|
||||
|
||||
- `serial_number` `(string: <optional>)` - Specifies the serial number of the
|
||||
certificate to revoke, in hyphen-separated or colon-separated hexadecimal.
|
||||
|
||||
- `certificate` `(string: <optional>)` - Specifies the certificate to revoke,
|
||||
in PEM format. This certificate must have been signed by one of the issuers
|
||||
in this mount in order to be accepted for revocation.
|
||||
|
||||
- `private_key` `(string: <required>)` - Specifies the private key (in PEM
|
||||
format) corresponding to the certificate issued by Vault that is attempted
|
||||
to be revoked. This endpoint must be called several times (with each unique
|
||||
certificate/serial number) if this private key is used in multiple
|
||||
certificates as Vault does not maintain such a mapping.
|
||||
|
||||
#### Sample Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"serial_number": "39:dd:2e...",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\n..."
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Request
|
||||
|
||||
```shell-session
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/pki/revoke-with-key
|
||||
```
|
||||
|
||||
#### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"revocation_time": 1433269787
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessing Authority Information
|
||||
|
|
|
@ -423,6 +423,7 @@ For these personas, we suggest the following ACLs, in condensed, tabular form:
|
|||
| `/issuer/:issuer_ref/(json¦der¦pem)` | Read | Yes | Yes | Yes | Yes | Yes |
|
||||
| `/issuer/:issuer_ref/crl(/der¦/pem)?` | Read | Yes | Yes | Yes | Yes | Yes |
|
||||
| `/certs` | List | Yes | Yes | Yes | Yes | |
|
||||
| `/revoke-with-key` | Write | Yes | Yes | Yes | Yes | |
|
||||
| `/roles` | List | Yes | Yes | Yes | Yes | |
|
||||
| `/roles/:role` | Read | Yes | Yes | Yes | Yes | |
|
||||
| `/(issue¦sign)/:role` | Write | Yes | Yes | Yes | Yes | |
|
||||
|
|
Loading…
Reference in New Issue