04bb7eef15
* Refine documentation for public_key Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Support additional key types in importing version This originally left off the custom support for Ed25519 and RSA-PSS formatted keys that we've added manually. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add support for Ed25519 keys Here, we prevent importing public-key only keys with derived Ed25519 keys. Notably, we still allow import of derived Ed25519 keys via private key method, though this is a touch weird: this private key must have been packaged in an Ed25519 format (and parseable through Go as such), even though it is (strictly) an HKDF key and isn't ever used for Ed25519. Outside of this, importing non-derived Ed25519 keys works as expected. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add public-key only export method to Transit This allows the existing endpoints to retain private-key only, including empty strings for versions which lack private keys. On the public-key endpoint, all versions will have key material returned. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Update tests for exporting via public-key interface Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add public-key export option to docs Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
351 lines
8.4 KiB
Go
351 lines
8.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package transit
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/helper/keysutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
const (
|
|
exportTypeEncryptionKey = "encryption-key"
|
|
exportTypeSigningKey = "signing-key"
|
|
exportTypeHMACKey = "hmac-key"
|
|
exportTypePublicKey = "public-key"
|
|
)
|
|
|
|
func (b *backend) pathExportKeys() *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "export/" + framework.GenericNameRegex("type") + "/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("version"),
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixTransit,
|
|
OperationVerb: "export",
|
|
OperationSuffix: "key|key-version",
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"type": {
|
|
Type: framework.TypeString,
|
|
Description: "Type of key to export (encryption-key, signing-key, hmac-key, public-key)",
|
|
},
|
|
"name": {
|
|
Type: framework.TypeString,
|
|
Description: "Name of the key",
|
|
},
|
|
"version": {
|
|
Type: framework.TypeString,
|
|
Description: "Version of the key",
|
|
},
|
|
},
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
logical.ReadOperation: b.pathPolicyExportRead,
|
|
},
|
|
|
|
HelpSynopsis: pathExportHelpSyn,
|
|
HelpDescription: pathExportHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
exportType := d.Get("type").(string)
|
|
name := d.Get("name").(string)
|
|
version := d.Get("version").(string)
|
|
|
|
switch exportType {
|
|
case exportTypeEncryptionKey:
|
|
case exportTypeSigningKey:
|
|
case exportTypeHMACKey:
|
|
case exportTypePublicKey:
|
|
default:
|
|
return logical.ErrorResponse(fmt.Sprintf("invalid export type: %s", exportType)), logical.ErrInvalidRequest
|
|
}
|
|
|
|
p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{
|
|
Storage: req.Storage,
|
|
Name: name,
|
|
}, b.GetRandomReader())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p == nil {
|
|
return nil, nil
|
|
}
|
|
if !b.System().CachingDisabled() {
|
|
p.Lock(false)
|
|
}
|
|
defer p.Unlock()
|
|
|
|
if !p.Exportable {
|
|
return logical.ErrorResponse("key is not exportable"), nil
|
|
}
|
|
|
|
switch exportType {
|
|
case exportTypeEncryptionKey:
|
|
if !p.Type.EncryptionSupported() {
|
|
return logical.ErrorResponse("encryption not supported for the key"), logical.ErrInvalidRequest
|
|
}
|
|
case exportTypeSigningKey:
|
|
if !p.Type.SigningSupported() {
|
|
return logical.ErrorResponse("signing not supported for the key"), logical.ErrInvalidRequest
|
|
}
|
|
}
|
|
|
|
retKeys := map[string]string{}
|
|
switch version {
|
|
case "":
|
|
for k, v := range p.Keys {
|
|
exportKey, err := getExportKey(p, &v, exportType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
retKeys[k] = exportKey
|
|
}
|
|
|
|
default:
|
|
var versionValue int
|
|
if version == "latest" {
|
|
versionValue = p.LatestVersion
|
|
} else {
|
|
version = strings.TrimPrefix(version, "v")
|
|
versionValue, err = strconv.Atoi(version)
|
|
if err != nil {
|
|
return logical.ErrorResponse("invalid key version"), logical.ErrInvalidRequest
|
|
}
|
|
}
|
|
|
|
if versionValue < p.MinDecryptionVersion {
|
|
return logical.ErrorResponse("version for export is below minimum decryption version"), logical.ErrInvalidRequest
|
|
}
|
|
key, ok := p.Keys[strconv.Itoa(versionValue)]
|
|
if !ok {
|
|
return logical.ErrorResponse("version does not exist or cannot be found"), logical.ErrInvalidRequest
|
|
}
|
|
|
|
exportKey, err := getExportKey(p, &key, exportType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
retKeys[strconv.Itoa(versionValue)] = exportKey
|
|
}
|
|
|
|
resp := &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"name": p.Name,
|
|
"type": p.Type.String(),
|
|
"keys": retKeys,
|
|
},
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType string) (string, error) {
|
|
if policy == nil {
|
|
return "", errors.New("nil policy provided")
|
|
}
|
|
|
|
switch exportType {
|
|
case exportTypeHMACKey:
|
|
return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.HMACKey)), nil
|
|
|
|
case exportTypeEncryptionKey:
|
|
switch policy.Type {
|
|
case keysutil.KeyType_AES128_GCM96, keysutil.KeyType_AES256_GCM96, keysutil.KeyType_ChaCha20_Poly1305:
|
|
return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil
|
|
|
|
case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096:
|
|
rsaKey, err := encodeRSAPrivateKey(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return rsaKey, nil
|
|
}
|
|
|
|
case exportTypeSigningKey:
|
|
switch policy.Type {
|
|
case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521:
|
|
var curve elliptic.Curve
|
|
switch policy.Type {
|
|
case keysutil.KeyType_ECDSA_P384:
|
|
curve = elliptic.P384()
|
|
case keysutil.KeyType_ECDSA_P521:
|
|
curve = elliptic.P521()
|
|
default:
|
|
curve = elliptic.P256()
|
|
}
|
|
ecKey, err := keyEntryToECPrivateKey(key, curve)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return ecKey, nil
|
|
|
|
case keysutil.KeyType_ED25519:
|
|
if len(key.Key) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil
|
|
|
|
case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096:
|
|
rsaKey, err := encodeRSAPrivateKey(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return rsaKey, nil
|
|
}
|
|
case exportTypePublicKey:
|
|
switch policy.Type {
|
|
case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521:
|
|
var curve elliptic.Curve
|
|
switch policy.Type {
|
|
case keysutil.KeyType_ECDSA_P384:
|
|
curve = elliptic.P384()
|
|
case keysutil.KeyType_ECDSA_P521:
|
|
curve = elliptic.P521()
|
|
default:
|
|
curve = elliptic.P256()
|
|
}
|
|
ecKey, err := keyEntryToECPublicKey(key, curve)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return ecKey, nil
|
|
|
|
case keysutil.KeyType_ED25519:
|
|
return strings.TrimSpace(key.FormattedPublicKey), nil
|
|
|
|
case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096:
|
|
rsaKey, err := encodeRSAPublicKey(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return rsaKey, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("unknown key type %v for export type %v", policy.Type, exportType)
|
|
}
|
|
|
|
func encodeRSAPrivateKey(key *keysutil.KeyEntry) (string, error) {
|
|
if key == nil {
|
|
return "", errors.New("nil KeyEntry provided")
|
|
}
|
|
|
|
if key.IsPrivateKeyMissing() {
|
|
return "", nil
|
|
}
|
|
|
|
// When encoding PKCS1, the PEM header should be `RSA PRIVATE KEY`. When Go
|
|
// has PKCS8 encoding support, we may want to change this.
|
|
blockType := "RSA PRIVATE KEY"
|
|
derBytes := x509.MarshalPKCS1PrivateKey(key.RSAKey)
|
|
pemBlock := pem.Block{
|
|
Type: blockType,
|
|
Bytes: derBytes,
|
|
}
|
|
|
|
pemBytes := pem.EncodeToMemory(&pemBlock)
|
|
return string(pemBytes), nil
|
|
}
|
|
|
|
func encodeRSAPublicKey(key *keysutil.KeyEntry) (string, error) {
|
|
if key == nil {
|
|
return "", errors.New("nil KeyEntry provided")
|
|
}
|
|
|
|
blockType := "RSA PUBLIC KEY"
|
|
derBytes, err := x509.MarshalPKIXPublicKey(key.RSAPublicKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pemBlock := pem.Block{
|
|
Type: blockType,
|
|
Bytes: derBytes,
|
|
}
|
|
|
|
pemBytes := pem.EncodeToMemory(&pemBlock)
|
|
return string(pemBytes), nil
|
|
}
|
|
|
|
func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) {
|
|
if k == nil {
|
|
return "", errors.New("nil KeyEntry provided")
|
|
}
|
|
|
|
if k.IsPrivateKeyMissing() {
|
|
return "", nil
|
|
}
|
|
|
|
pubKey := ecdsa.PublicKey{
|
|
Curve: curve,
|
|
X: k.EC_X,
|
|
Y: k.EC_Y,
|
|
}
|
|
|
|
blockType := "EC PRIVATE KEY"
|
|
privKey := &ecdsa.PrivateKey{
|
|
PublicKey: pubKey,
|
|
D: k.EC_D,
|
|
}
|
|
derBytes, err := x509.MarshalECPrivateKey(privKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pemBlock := pem.Block{
|
|
Type: blockType,
|
|
Bytes: derBytes,
|
|
}
|
|
|
|
return strings.TrimSpace(string(pem.EncodeToMemory(&pemBlock))), nil
|
|
}
|
|
|
|
func keyEntryToECPublicKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) {
|
|
if k == nil {
|
|
return "", errors.New("nil KeyEntry provided")
|
|
}
|
|
|
|
pubKey := ecdsa.PublicKey{
|
|
Curve: curve,
|
|
X: k.EC_X,
|
|
Y: k.EC_Y,
|
|
}
|
|
|
|
blockType := "PUBLIC KEY"
|
|
derBytes, err := x509.MarshalPKIXPublicKey(&pubKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pemBlock := pem.Block{
|
|
Type: blockType,
|
|
Bytes: derBytes,
|
|
}
|
|
|
|
return strings.TrimSpace(string(pem.EncodeToMemory(&pemBlock))), nil
|
|
}
|
|
|
|
const pathExportHelpSyn = `Export named encryption or signing key`
|
|
|
|
const pathExportHelpDesc = `
|
|
This path is used to export the named keys that are configured as
|
|
exportable.
|
|
`
|