vault/builtin/logical/pki/path_manage_keys.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

321 lines
9.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package pki
import (
"bytes"
"context"
"encoding/pem"
"net/http"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/logical"
)
func pathGenerateKey(b *backend) *framework.Path {
return &framework.Path{
Pattern: "keys/generate/(internal|exported|kms)",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
OperationVerb: "generate",
OperationSuffix: "internal-key|exported-key|kms-key",
},
Fields: map[string]*framework.FieldSchema{
keyNameParam: {
Type: framework.TypeString,
Description: "Optional name to be used for this key",
},
keyTypeParam: {
Type: framework.TypeString,
Default: "rsa",
Description: `The type of key to use; defaults to RSA. "rsa"
"ec" and "ed25519" are the only valid values.`,
AllowedValues: []interface{}{"rsa", "ec", "ed25519"},
DisplayAttrs: &framework.DisplayAttributes{
Value: "rsa",
},
},
keyBitsParam: {
Type: framework.TypeInt,
Default: 0,
Description: `The number of bits to use. Allowed values are
0 (universal default); with rsa key_type: 2048 (default), 3072, or
4096; with ec key_type: 224, 256 (default), 384, or 521; ignored with
ed25519.`,
},
"managed_key_name": {
Type: framework.TypeString,
Description: `The name of the managed key to use when the exported
type is kms. When kms type is the key type, this field or managed_key_id
is required. Ignored for other types.`,
},
"managed_key_id": {
Type: framework.TypeString,
Description: `The name of the managed key to use when the exported
type is kms. When kms type is the key type, this field or managed_key_name
is required. Ignored for other types.`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathGenerateKeyHandler,
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"key_id": {
Type: framework.TypeString,
Description: `ID assigned to this key.`,
Required: true,
},
"key_name": {
Type: framework.TypeString,
Description: `Name assigned to this key.`,
Required: true,
},
"key_type": {
Type: framework.TypeString,
Description: `The type of key to use; defaults to RSA. "rsa"
"ec" and "ed25519" are the only valid values.`,
Required: true,
},
"private_key": {
Type: framework.TypeString,
Description: `The private key string`,
Required: false,
},
},
}},
},
ForwardPerformanceStandby: true,
ForwardPerformanceSecondary: true,
},
},
HelpSynopsis: pathGenerateKeyHelpSyn,
HelpDescription: pathGenerateKeyHelpDesc,
}
}
const (
pathGenerateKeyHelpSyn = `Generate a new private key used for signing.`
pathGenerateKeyHelpDesc = `This endpoint will generate a new key pair of the specified type (internal, exported, or kms).`
)
func (b *backend) pathGenerateKeyHandler(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 generate keys until migration has completed"), nil
}
sc := b.makeStorageContext(ctx, req.Storage)
keyName, err := getKeyName(sc, data)
if err != nil { // Fail Immediately if Key Name is in Use, etc...
return logical.ErrorResponse(err.Error()), nil
}
exportPrivateKey := false
var keyBundle certutil.KeyBundle
var actualPrivateKeyType certutil.PrivateKeyType
switch {
case strings.HasSuffix(req.Path, "/exported"):
exportPrivateKey = true
fallthrough
case strings.HasSuffix(req.Path, "/internal"):
keyType := data.Get(keyTypeParam).(string)
keyBits := data.Get(keyBitsParam).(int)
keyBits, _, err := certutil.ValidateDefaultOrValueKeyTypeSignatureLength(keyType, keyBits, 0)
if err != nil {
return logical.ErrorResponse("Validation for key_type, key_bits failed: %s", err.Error()), nil
}
// Internal key generation, stored in storage
keyBundle, err = certutil.CreateKeyBundle(keyType, keyBits, b.GetRandomReader())
if err != nil {
return nil, err
}
actualPrivateKeyType = keyBundle.PrivateKeyType
case strings.HasSuffix(req.Path, "/kms"):
keyId, err := getManagedKeyId(data)
if err != nil {
return nil, err
}
keyBundle, actualPrivateKeyType, err = createKmsKeyBundle(ctx, b, keyId)
if err != nil {
return nil, err
}
default:
return logical.ErrorResponse("Unknown type of key to generate"), nil
}
privateKeyPemString, err := keyBundle.ToPrivateKeyPemString()
if err != nil {
return nil, err
}
key, _, err := sc.importKey(privateKeyPemString, keyName, keyBundle.PrivateKeyType)
if err != nil {
return nil, err
}
responseData := map[string]interface{}{
keyIdParam: key.ID,
keyNameParam: key.Name,
keyTypeParam: string(actualPrivateKeyType),
}
if exportPrivateKey {
responseData["private_key"] = privateKeyPemString
}
return &logical.Response{
Data: responseData,
}, nil
}
func pathImportKey(b *backend) *framework.Path {
return &framework.Path{
Pattern: "keys/import",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
OperationVerb: "import",
OperationSuffix: "key",
},
Fields: map[string]*framework.FieldSchema{
keyNameParam: {
Type: framework.TypeString,
Description: "Optional name to be used for this key",
},
"pem_bundle": {
Type: framework.TypeString,
Description: `PEM-format, unencrypted secret key`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathImportKeyHandler,
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"key_id": {
Type: framework.TypeString,
Description: `ID assigned to this key.`,
Required: true,
},
"key_name": {
Type: framework.TypeString,
Description: `Name assigned to this key.`,
Required: true,
},
"key_type": {
Type: framework.TypeString,
Description: `The type of key to use; defaults to RSA. "rsa"
"ec" and "ed25519" are the only valid values.`,
Required: true,
},
},
}},
},
ForwardPerformanceStandby: true,
ForwardPerformanceSecondary: true,
},
},
HelpSynopsis: pathImportKeyHelpSyn,
HelpDescription: pathImportKeyHelpDesc,
}
}
const (
pathImportKeyHelpSyn = `Import the specified key.`
pathImportKeyHelpDesc = `This endpoint allows importing a specified issuer key from a pem bundle.
If key_name is set, that will be set on the key, assuming the key did not exist previously.`
)
func (b *backend) pathImportKeyHandler(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("Cannot import keys until migration has completed"), nil
}
sc := b.makeStorageContext(ctx, req.Storage)
pemBundle := data.Get("pem_bundle").(string)
keyName, err := getKeyName(sc, data)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
if len(pemBundle) < 64 {
// It is almost nearly impossible to store a complete key in
// less than 64 bytes. It is definitely impossible to do so when PEM
// encoding has been applied. Detect this and give a better warning
// than "provided PEM block contained no data" in this case. This is
// because the PEM headers contain 5*4 + 6 + 4 + 2 + 2 = 34 characters
// minimum (five dashes, "BEGIN" + space + at least one character
// identifier, "END" + space + at least one character identifier, and
// a pair of new lines). That would leave 30 bytes for Base64 data,
// meaning at most a 22-byte DER key. Even with a 128-bit key, 6 bytes
// is not sufficient for the required ASN.1 structure and OID encoding.
//
// However, < 64 bytes is probably a good length for a file path so
// suggest that is the case.
return logical.ErrorResponse("provided data for import was too short; perhaps a path was passed to the API rather than the contents of a PEM file"), nil
}
pemBytes := []byte(pemBundle)
var pemBlock *pem.Block
var keys []string
for len(bytes.TrimSpace(pemBytes)) > 0 {
pemBlock, pemBytes = pem.Decode(pemBytes)
if pemBlock == nil {
return logical.ErrorResponse("provided PEM block contained no data"), nil
}
pemBlockString := string(pem.EncodeToMemory(pemBlock))
keys = append(keys, pemBlockString)
}
if len(keys) != 1 {
return logical.ErrorResponse("only a single key can be present within the pem_bundle for importing"), nil
}
key, existed, err := importKeyFromBytes(sc, keys[0], keyName)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
resp := logical.Response{
Data: map[string]interface{}{
keyIdParam: key.ID,
keyNameParam: key.Name,
keyTypeParam: key.PrivateKeyType,
},
}
if existed {
resp.AddWarning("Key already imported, use key/ endpoint to update name.")
}
return &resp, nil
}