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>
This commit is contained in:
Alexander Scheel 2023-05-24 11:26:35 -04:00 committed by GitHub
parent 8698650394
commit 04bb7eef15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 218 additions and 66 deletions

View File

@ -6,6 +6,7 @@ package transit
import (
"context"
"crypto"
"crypto/ed25519"
cryptoRand "crypto/rand"
"crypto/x509"
"encoding/base64"
@ -2018,6 +2019,7 @@ func TestTransitPKICSR(t *testing.T) {
func TestTransit_ReadPublicKeyImported(t *testing.T) {
testTransit_ReadPublicKeyImported(t, "rsa-2048")
testTransit_ReadPublicKeyImported(t, "ecdsa-p256")
testTransit_ReadPublicKeyImported(t, "ed25519")
}
func testTransit_ReadPublicKeyImported(t *testing.T, keyType string) {
@ -2066,6 +2068,7 @@ func testTransit_ReadPublicKeyImported(t *testing.T, keyType string) {
func TestTransit_SignWithImportedPublicKey(t *testing.T) {
testTransit_SignWithImportedPublicKey(t, "rsa-2048")
testTransit_SignWithImportedPublicKey(t, "ecdsa-p256")
testTransit_SignWithImportedPublicKey(t, "ed25519")
}
func testTransit_SignWithImportedPublicKey(t *testing.T, keyType string) {
@ -2210,6 +2213,7 @@ func TestTransit_VerifyWithImportedPublicKey(t *testing.T) {
func TestTransit_ExportPublicKeyImported(t *testing.T) {
testTransit_ExportPublicKeyImported(t, "rsa-2048")
testTransit_ExportPublicKeyImported(t, "ecdsa-p256")
testTransit_ExportPublicKeyImported(t, "ed25519")
}
func testTransit_ExportPublicKeyImported(t *testing.T, keyType string) {
@ -2227,6 +2231,8 @@ func testTransit_ExportPublicKeyImported(t *testing.T, keyType string) {
t.Fatalf("failed to extract the public key: %s", err)
}
t.Logf("generated key: %v", string(publicKeyBytes))
// Import key
importReq := &logical.Request{
Storage: s,
@ -2243,10 +2249,12 @@ func testTransit_ExportPublicKeyImported(t *testing.T, keyType string) {
t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importResp)
}
t.Logf("importing key: %v", importResp)
// Export key
exportReq := &logical.Request{
Operation: logical.ReadOperation,
Path: fmt.Sprintf("export/signing-key/%s/latest", keyID),
Path: fmt.Sprintf("export/public-key/%s/latest", keyID),
Storage: s,
}
@ -2255,16 +2263,36 @@ func testTransit_ExportPublicKeyImported(t *testing.T, keyType string) {
t.Fatalf("failed to export key. err: %v\nresp: %#v", err, exportResp)
}
t.Logf("exporting key: %v", exportResp)
responseKeys, exist := exportResp.Data["keys"]
if !exist {
t.Fatal("expected response data to hold a 'keys' field")
}
exportedKeyBytes := responseKeys.(map[string]string)["1"]
if keyType != "ed25519" {
exportedKeyBlock, _ := pem.Decode([]byte(exportedKeyBytes))
publicKeyBlock, _ := pem.Decode(publicKeyBytes)
if !reflect.DeepEqual(publicKeyBlock.Bytes, exportedKeyBlock.Bytes) {
t.Fatal("exported key bytes should have matched with imported key")
t.Fatalf("exported key bytes should have matched with imported key for key type: %v\nexported: %v\nimported: %v", keyType, exportedKeyBlock.Bytes, publicKeyBlock.Bytes)
}
} else {
exportedKey, err := base64.StdEncoding.DecodeString(exportedKeyBytes)
if err != nil {
t.Fatalf("error decoding exported key bytes (%v) to base64 for key type %v: %v", exportedKeyBytes, keyType, err)
}
publicKeyBlock, _ := pem.Decode(publicKeyBytes)
publicKeyParsed, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes)
if err != nil {
t.Fatalf("error decoding source key bytes (%v) from PKIX marshaling for key type %v: %v", publicKeyBlock.Bytes, keyType, err)
}
if !reflect.DeepEqual([]byte(publicKeyParsed.(ed25519.PublicKey)), exportedKey) {
t.Fatalf("exported key bytes should have matched with imported key for key type: %v\nexported: %v\nimported: %v", keyType, exportedKey, publicKeyParsed)
}
}
}

View File

@ -24,6 +24,7 @@ const (
exportTypeEncryptionKey = "encryption-key"
exportTypeSigningKey = "signing-key"
exportTypeHMACKey = "hmac-key"
exportTypePublicKey = "public-key"
)
func (b *backend) pathExportKeys() *framework.Path {
@ -39,7 +40,7 @@ func (b *backend) pathExportKeys() *framework.Path {
Fields: map[string]*framework.FieldSchema{
"type": {
Type: framework.TypeString,
Description: "Type of key to export (encryption-key, signing-key, hmac-key)",
Description: "Type of key to export (encryption-key, signing-key, hmac-key, public-key)",
},
"name": {
Type: framework.TypeString,
@ -69,6 +70,7 @@ func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request
case exportTypeEncryptionKey:
case exportTypeSigningKey:
case exportTypeHMACKey:
case exportTypePublicKey:
default:
return logical.ErrorResponse(fmt.Sprintf("invalid export type: %s", exportType)), logical.ErrInvalidRequest
}
@ -194,6 +196,10 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st
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:
@ -203,26 +209,70 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st
}
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()
}
return "", fmt.Errorf("unknown key type %v", policy.Type)
}
func encodeRSAPrivateKey(key *keysutil.KeyEntry) (string, error) {
// When encoding PKCS1, the PEM header should be `RSA PRIVATE KEY`. When Go
// has PKCS8 encoding support, we may want to change this.
var blockType string
var derBytes []byte
var err error
if !key.IsPrivateKeyMissing() {
blockType = "RSA PRIVATE KEY"
derBytes = x509.MarshalPKCS1PrivateKey(key.RSAKey)
} else {
blockType = "PUBLIC KEY"
derBytes, err = x509.MarshalPKIXPublicKey(key.RSAPublicKey)
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{
@ -239,38 +289,49 @@ func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string,
return "", errors.New("nil KeyEntry provided")
}
if k.IsPrivateKeyMissing() {
return "", nil
}
pubKey := ecdsa.PublicKey{
Curve: curve,
X: k.EC_X,
Y: k.EC_Y,
}
var blockType string
var derBytes []byte
var err error
if !k.IsPrivateKeyMissing() {
blockType = "EC PRIVATE KEY"
blockType := "EC PRIVATE KEY"
privKey := &ecdsa.PrivateKey{
PublicKey: pubKey,
D: k.EC_D,
}
derBytes, err = x509.MarshalECPrivateKey(privKey)
if err != nil {
return "", err
}
if derBytes == nil {
return "", errors.New("no data returned when marshalling to private key")
}
} else {
blockType = "PUBLIC KEY"
derBytes, err = x509.MarshalPKIXPublicKey(&pubKey)
derBytes, err := x509.MarshalECPrivateKey(privKey)
if err != nil {
return "", err
}
if derBytes == nil {
return "", errors.New("no data returned when marshalling to public key")
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{

View File

@ -457,8 +457,8 @@ func TestTransit_Import(t *testing.T) {
},
}
_, err = b.HandleRequest(context.Background(), req)
if err == nil {
t.Fatalf("invalid public_key import incorrectly succeeeded")
if err != nil {
t.Fatalf("failed to import ed25519 key: %v", err)
}
})

View File

@ -169,7 +169,7 @@ func (kt KeyType) AssociatedDataSupported() bool {
func (kt KeyType) ImportPublicKeySupported() bool {
switch kt {
case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096, KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521:
case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096, KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521, KeyType_ED25519:
return true
}
return false
@ -1361,20 +1361,30 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o
return ecdsa.Verify(key, input, ecdsaSig.R, ecdsaSig.S), nil
case KeyType_ED25519:
var key ed25519.PrivateKey
var pub ed25519.PublicKey
if p.Derived {
// Derive the key that should be used
var err error
key, err = p.GetKey(context, ver, 32)
key, err := p.GetKey(context, ver, 32)
if err != nil {
return false, errutil.InternalError{Err: fmt.Sprintf("error deriving key: %v", err)}
}
pub = ed25519.PrivateKey(key).Public().(ed25519.PublicKey)
} else {
key = ed25519.PrivateKey(p.Keys[strconv.Itoa(ver)].Key)
keyEntry, err := p.safeGetKeyEntry(ver)
if err != nil {
return false, err
}
return ed25519.Verify(key.Public().(ed25519.PublicKey), input, sigBytes), nil
raw, err := base64.StdEncoding.DecodeString(keyEntry.FormattedPublicKey)
if err != nil {
return false, err
}
pub = ed25519.PublicKey(raw)
}
return ed25519.Verify(pub, input, sigBytes), nil
case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096:
keyEntry, err := p.safeGetKeyEntry(ver)
@ -1445,6 +1455,10 @@ func (p *Policy) ImportPublicOrPrivate(ctx context.Context, storage logical.Stor
entry.HMACKey = hmacKey
}
if p.Type == KeyType_ED25519 && p.Derived && !isPrivateKey {
return fmt.Errorf("unable to import only public key for derived Ed25519 key: imported key should not be an Ed25519 key pair but is instead an HKDF key")
}
if (p.Type == KeyType_AES128_GCM96 && len(key) != 16) ||
((p.Type == KeyType_AES256_GCM96 || p.Type == KeyType_ChaCha20_Poly1305) && len(key) != 32) ||
(p.Type == KeyType_HMAC && (len(key) < HmacMinKeySize || len(key) > HmacMaxKeySize)) {
@ -1615,13 +1629,19 @@ func (p *Policy) RotateInMemory(randReader io.Reader) (retErr error) {
entry.FormattedPublicKey = string(pemBytes)
case KeyType_ED25519:
// Go uses a 64-byte private key for Ed25519 keys (private+public, each
// 32-bytes long). When we do Key derivation, we still generate a 32-byte
// random value (and compute the corresponding Ed25519 public key), but
// use this entire 64-byte key as if it was an HKDF key. The corresponding
// underlying public key is never returned (which is probably good, because
// doing so would leak half of our HKDF key...), but means we cannot import
// derived-enabled Ed25519 public key components.
pub, pri, err := ed25519.GenerateKey(randReader)
if err != nil {
return err
}
entry.Key = pri
entry.FormattedPublicKey = base64.StdEncoding.EncodeToString(pub)
case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096:
bitSize := 2048
if p.Type == KeyType_RSA3072 {
@ -2090,8 +2110,26 @@ func (p *Policy) ImportPrivateKeyForVersion(ctx context.Context, storage logical
// Parse key
parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key)
if err != nil {
if strings.Contains(err.Error(), "unknown elliptic curve") {
var edErr error
parsedPrivateKey, edErr = ParsePKCS8Ed25519PrivateKey(key)
if edErr != nil {
return fmt.Errorf("error parsing asymmetric key:\n - assuming contents are an ed25519 private key: %s\n - original error: %v", edErr, err)
}
// Parsing as Ed25519-in-PKCS8-ECPrivateKey succeeded!
} else if strings.Contains(err.Error(), oidSignatureRSAPSS.String()) {
var rsaErr error
parsedPrivateKey, rsaErr = ParsePKCS8RSAPSSPrivateKey(key)
if rsaErr != nil {
return fmt.Errorf("error parsing asymmetric key:\n - assuming contents are an RSA/PSS private key: %v\n - original error: %w", rsaErr, err)
}
// Parsing as RSA-PSS in PKCS8 succeeded!
} else {
return fmt.Errorf("error parsing asymmetric key: %s", err)
}
}
switch parsedPrivateKey.(type) {
case *ecdsa.PrivateKey:
@ -2177,15 +2215,20 @@ func (ke *KeyEntry) parseFromKey(PolKeyType KeyType, parsedKey any) error {
return fmt.Errorf("error PEM-encoding public key")
}
ke.FormattedPublicKey = string(pemBytes)
case ed25519.PrivateKey:
case ed25519.PrivateKey, ed25519.PublicKey:
if PolKeyType != KeyType_ED25519 {
return fmt.Errorf("invalid key type: expected %s, got %T", PolKeyType, parsedKey)
}
privateKey := parsedKey.(ed25519.PrivateKey)
privateKey, ok := parsedKey.(ed25519.PrivateKey)
if ok {
ke.Key = privateKey
publicKey := privateKey.Public().(ed25519.PublicKey)
ke.FormattedPublicKey = base64.StdEncoding.EncodeToString(publicKey)
} else {
publicKey := parsedKey.(ed25519.PublicKey)
ke.FormattedPublicKey = base64.StdEncoding.EncodeToString(publicKey)
}
case *rsa.PrivateKey, *rsa.PublicKey:
if PolKeyType != KeyType_RSA2048 && PolKeyType != KeyType_RSA3072 && PolKeyType != KeyType_RSA4096 {
return fmt.Errorf("invalid key type: expected %s, got %T", PolKeyType, parsedKey)

View File

@ -109,7 +109,17 @@ $ curl \
This endpoint imports existing key material into a new transit-managed encryption key.
To import key material into an existing key, see the `import_version/` endpoint.
// TODO: Has to be updated.
This supports one of two forms:
1. Private/Symmetric Key import, requiring the `ciphertext`, `hash_function`
parameters be set (and automatically deriving the public key), or
2. Public Key-only import, restricting the operations that can be done with
this key, and requiring only the `public_key` parameter.
The remaining parameters (including `name`, `type`, `allow_rotation`,
`derived`, `context`, `exportable`, `allow_plaintext_backup`, and
`auto_rotate_period`) remain the same across both versions of this call.
| Method | Path |
| :----- | :--------------------------- |
@ -153,8 +163,10 @@ the hash function defaults to SHA256.
- `rsa-3072` - RSA with bit size of 3072 (asymmetric)
- `rsa-4096` - RSA with bit size of 4096 (asymmetric)
- `public_key` `(string: "", optional)` - A plaintext PEM public key to be imported.
If `ciphertext` is set, this field is ignored.
- `public_key` `(string: "", optional)` - A plaintext PEM public key to be
imported. This limits the operations available under this key to verification
and encryption, depending on the key type and algorithm, as no private key
is available.
- `allow_rotation` `(bool: false)` - If set, the imported key can be rotated
within Vault by using the `rotate` endpoint.
@ -203,7 +215,12 @@ $ curl \
## Import Key Version
This endpoint imports new key material into an existing imported key.
// TODO: Has to be updated.
See description and note in [Import Key](#import-key) above about importing
public and private keys.
Notably, using this method, a private key matching a public key can be
imported at a later date.
| Method | Path |
| :----- | :----------------------------------- |
@ -225,15 +242,16 @@ provided AES key. The wrapped AES key should be the first 512 bytes of the
ciphertext, and the encrypted key material should be the remaining bytes.
See the BYOK section of the [Transit secrets engine documentation](/vault/docs/secrets/transit#bring-your-own-key-byok)
for more information on constructing the ciphertext.
// TODO: Update text
- `hash_function` `(string: "SHA256")` - The hash function used for the
RSA-OAEP step of creating the ciphertext. Supported hash functions are:
`SHA1`, `SHA224`, `SHA256`, `SHA384`, and `SHA512`. If not specified,
the hash function defaults to SHA256.
- `public_key` `(string: "", optional)` - A plaintext PEM public key to be imported.
If `ciphertext` is set, this field is ignored.
- `public_key` `(string: "", optional)` - A plaintext PEM public key to be
imported. This limits the operations available under this key to verification
and encryption, depending on the key type and algorithm, as no private key
is available.
- `bump_version` - By default, each operator will create a new key version.
If set to "false", will try to update the latest version of the key,
@ -513,6 +531,8 @@ be valid.
- `encryption-key`
- `signing-key`
- `hmac-key`
- `public-key`, to return the corresponding public keys of private key
asymmetric keys (EC with NIST P-curves or Ed25519 and RSA).
- `name` `(string: <required>)` Specifies the name of the key to read
information about. This is specified as part of the URL.