open-vault/builtin/logical/transit/path_export.go
Alexander Scheel 04bb7eef15
Update transit public keys for Ed25519 support (#20727)
* 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>
2023-05-24 11:26:35 -04:00

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.
`