f9541a1c96
* pki: add subject key identifier to read key response This will be helpful for the Terraform Vault Provider to detect migration of pre-1.11 exported keys (from CA generation) into post-1.11 Vault. * add changelog * Update builtin/logical/pki/path_fetch_keys.go Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com> * check for managed key first * Validate the SKID matches on root CAs Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Validate SKID matches on int CAs Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix formatting of tests Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
401 lines
11 KiB
Go
401 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package pki
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/helper/errutil"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func pathListKeys(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "keys/?$",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
OperationSuffix: "keys",
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ListOperation: &framework.PathOperation{
|
|
Callback: b.pathListKeysHandler,
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"keys": {
|
|
Type: framework.TypeStringSlice,
|
|
Description: `A list of keys`,
|
|
Required: true,
|
|
},
|
|
"key_info": {
|
|
Type: framework.TypeMap,
|
|
Description: `Key info with issuer name`,
|
|
Required: false,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
ForwardPerformanceStandby: false,
|
|
ForwardPerformanceSecondary: false,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathListKeysHelpSyn,
|
|
HelpDescription: pathListKeysHelpDesc,
|
|
}
|
|
}
|
|
|
|
const (
|
|
pathListKeysHelpSyn = `Fetch a list of all issuer keys`
|
|
pathListKeysHelpDesc = `This endpoint allows listing of known backing keys, returning
|
|
their identifier and their name (if set).`
|
|
)
|
|
|
|
func (b *backend) pathListKeysHandler(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Can not list keys until migration has completed"), nil
|
|
}
|
|
|
|
var responseKeys []string
|
|
responseInfo := make(map[string]interface{})
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
entries, err := sc.listKeys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config, err := sc.getKeysConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, identifier := range entries {
|
|
key, err := sc.fetchKeyById(identifier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
responseKeys = append(responseKeys, string(identifier))
|
|
responseInfo[string(identifier)] = map[string]interface{}{
|
|
keyNameParam: key.Name,
|
|
"is_default": identifier == config.DefaultKeyId,
|
|
}
|
|
|
|
}
|
|
return logical.ListResponseWithInfo(responseKeys, responseInfo), nil
|
|
}
|
|
|
|
func pathKey(b *backend) *framework.Path {
|
|
pattern := "key/" + framework.GenericNameRegex(keyRefParam)
|
|
|
|
displayAttrs := &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixPKI,
|
|
OperationSuffix: "key",
|
|
}
|
|
|
|
return buildPathKey(b, pattern, displayAttrs)
|
|
}
|
|
|
|
func buildPathKey(b *backend, pattern string, displayAttrs *framework.DisplayAttributes) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: pattern,
|
|
DisplayAttrs: displayAttrs,
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
keyRefParam: {
|
|
Type: framework.TypeString,
|
|
Description: `Reference to key; either "default" for the configured default key, an identifier of a key, or the name assigned to the key.`,
|
|
Default: defaultRef,
|
|
},
|
|
keyNameParam: {
|
|
Type: framework.TypeString,
|
|
Description: `Human-readable name for this key.`,
|
|
},
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: b.pathGetKeyHandler,
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusOK: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"key_id": {
|
|
Type: framework.TypeString,
|
|
Description: `Key Id`,
|
|
Required: true,
|
|
},
|
|
"key_name": {
|
|
Type: framework.TypeString,
|
|
Description: `Key Name`,
|
|
Required: true,
|
|
},
|
|
"key_type": {
|
|
Type: framework.TypeString,
|
|
Description: `Key Type`,
|
|
Required: true,
|
|
},
|
|
"subject_key_id": {
|
|
Type: framework.TypeString,
|
|
Description: `RFC 5280 Subject Key Identifier of the public counterpart`,
|
|
Required: false,
|
|
},
|
|
"managed_key_id": {
|
|
Type: framework.TypeString,
|
|
Description: `Managed Key Id`,
|
|
Required: false,
|
|
},
|
|
"managed_key_name": {
|
|
Type: framework.TypeString,
|
|
Description: `Managed Key Name`,
|
|
Required: false,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
ForwardPerformanceStandby: false,
|
|
ForwardPerformanceSecondary: false,
|
|
},
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathUpdateKeyHandler,
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusNoContent: {{
|
|
Description: "OK",
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"key_id": {
|
|
Type: framework.TypeString,
|
|
Description: `Key Id`,
|
|
Required: true,
|
|
},
|
|
"key_name": {
|
|
Type: framework.TypeString,
|
|
Description: `Key Name`,
|
|
Required: true,
|
|
},
|
|
"key_type": {
|
|
Type: framework.TypeString,
|
|
Description: `Key Type`,
|
|
Required: true,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
ForwardPerformanceStandby: true,
|
|
ForwardPerformanceSecondary: true,
|
|
},
|
|
logical.DeleteOperation: &framework.PathOperation{
|
|
Callback: b.pathDeleteKeyHandler,
|
|
Responses: map[int][]framework.Response{
|
|
http.StatusNoContent: {{
|
|
Description: "No Content",
|
|
}},
|
|
},
|
|
ForwardPerformanceStandby: true,
|
|
ForwardPerformanceSecondary: true,
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathKeysHelpSyn,
|
|
HelpDescription: pathKeysHelpDesc,
|
|
}
|
|
}
|
|
|
|
const (
|
|
pathKeysHelpSyn = `Fetch a single issuer key`
|
|
pathKeysHelpDesc = `This allows fetching information associated with the underlying key.
|
|
|
|
:ref can be either the literal value "default", in which case /config/keys
|
|
will be consulted for the present default key, an identifier of a key,
|
|
or its assigned name value.
|
|
|
|
Writing to /key/:ref allows updating of the name field associated with
|
|
the certificate.
|
|
`
|
|
)
|
|
|
|
func (b *backend) pathGetKeyHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Can not get keys until migration has completed"), nil
|
|
}
|
|
|
|
keyRef := data.Get(keyRefParam).(string)
|
|
if len(keyRef) == 0 {
|
|
return logical.ErrorResponse("missing key reference"), nil
|
|
}
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
keyId, err := sc.resolveKeyReference(keyRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if keyId == "" {
|
|
return logical.ErrorResponse("unable to resolve key id for reference" + keyRef), nil
|
|
}
|
|
|
|
key, err := sc.fetchKeyById(keyId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
respData := map[string]interface{}{
|
|
keyIdParam: key.ID,
|
|
keyNameParam: key.Name,
|
|
keyTypeParam: string(key.PrivateKeyType),
|
|
}
|
|
|
|
var pkForSkid crypto.PublicKey
|
|
if key.isManagedPrivateKey() {
|
|
managedKeyUUID, err := key.getManagedKeyUUID()
|
|
if err != nil {
|
|
return nil, errutil.InternalError{Err: fmt.Sprintf("failed extracting managed key uuid from key id %s (%s): %v", key.ID, key.Name, err)}
|
|
}
|
|
|
|
keyInfo, err := getManagedKeyInfo(ctx, b, managedKeyUUID)
|
|
if err != nil {
|
|
return nil, errutil.InternalError{Err: fmt.Sprintf("failed fetching managed key info from key id %s (%s): %v", key.ID, key.Name, err)}
|
|
}
|
|
|
|
pkForSkid, err = getManagedKeyPublicKey(sc.Context, sc.Backend, managedKeyUUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// To remain consistent across the api responses (mainly generate root/intermediate calls), return the actual
|
|
// type of key, not that it is a managed key.
|
|
respData[keyTypeParam] = string(keyInfo.keyType)
|
|
respData[managedKeyIdArg] = string(keyInfo.uuid)
|
|
respData[managedKeyNameArg] = string(keyInfo.name)
|
|
} else {
|
|
pkForSkid, err = getPublicKeyFromBytes([]byte(key.PrivateKey))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
skid, err := certutil.GetSubjectKeyID(pkForSkid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
respData[skidParam] = certutil.GetHexFormatted([]byte(skid), ":")
|
|
|
|
return &logical.Response{Data: respData}, nil
|
|
}
|
|
|
|
func (b *backend) pathUpdateKeyHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
// Since we're planning on updating keys here, grab the lock so we've
|
|
// got a consistent view.
|
|
b.issuersLock.Lock()
|
|
defer b.issuersLock.Unlock()
|
|
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Can not update keys until migration has completed"), nil
|
|
}
|
|
|
|
keyRef := data.Get(keyRefParam).(string)
|
|
if len(keyRef) == 0 {
|
|
return logical.ErrorResponse("missing key reference"), nil
|
|
}
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
keyId, err := sc.resolveKeyReference(keyRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if keyId == "" {
|
|
return logical.ErrorResponse("unable to resolve key id for reference" + keyRef), nil
|
|
}
|
|
|
|
key, err := sc.fetchKeyById(keyId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newName := data.Get(keyNameParam).(string)
|
|
if len(newName) > 0 && !nameMatcher.MatchString(newName) {
|
|
return logical.ErrorResponse("new key name outside of valid character limits"), nil
|
|
}
|
|
|
|
if newName != key.Name {
|
|
key.Name = newName
|
|
|
|
err := sc.writeKey(*key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
resp := &logical.Response{
|
|
Data: map[string]interface{}{
|
|
keyIdParam: key.ID,
|
|
keyNameParam: key.Name,
|
|
keyTypeParam: key.PrivateKeyType,
|
|
},
|
|
}
|
|
|
|
if len(newName) == 0 {
|
|
resp.AddWarning("Name successfully deleted, you will now need to reference this key by it's Id: " + string(key.ID))
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (b *backend) pathDeleteKeyHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
// Since we're planning on updating issuers here, grab the lock so we've
|
|
// got a consistent view.
|
|
b.issuersLock.Lock()
|
|
defer b.issuersLock.Unlock()
|
|
|
|
if b.useLegacyBundleCaStorage() {
|
|
return logical.ErrorResponse("Can not delete keys until migration has completed"), nil
|
|
}
|
|
|
|
keyRef := data.Get(keyRefParam).(string)
|
|
if len(keyRef) == 0 {
|
|
return logical.ErrorResponse("missing key reference"), nil
|
|
}
|
|
|
|
sc := b.makeStorageContext(ctx, req.Storage)
|
|
keyId, err := sc.resolveKeyReference(keyRef)
|
|
if err != nil {
|
|
if keyId == KeyRefNotFound {
|
|
// We failed to lookup the key, we should ignore any error here and reply as if it was deleted.
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
keyInUse, issuerId, err := sc.isKeyInUse(keyId.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if keyInUse {
|
|
return logical.ErrorResponse(fmt.Sprintf("Failed to Delete Key. Key in Use by Issuer: %s", issuerId)), nil
|
|
}
|
|
|
|
wasDefault, err := sc.deleteKey(keyId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response *logical.Response
|
|
if wasDefault {
|
|
msg := fmt.Sprintf("Deleted key %v (via key_ref %v); this was configured as the default key. Operations without an explicit key will not work until a new default is configured.", string(keyId), keyRef)
|
|
b.Logger().Error(msg)
|
|
response = &logical.Response{}
|
|
response.AddWarning(msg)
|
|
}
|
|
|
|
return response, nil
|
|
}
|