diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index f7109d8de..d23c19465 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -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"] - 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") + if keyType != "ed25519" { + exportedKeyBlock, _ := pem.Decode([]byte(exportedKeyBytes)) + publicKeyBlock, _ := pem.Decode(publicKeyBytes) + + if !reflect.DeepEqual(publicKeyBlock.Bytes, exportedKeyBlock.Bytes) { + 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) + } } } diff --git a/builtin/logical/transit/path_export.go b/builtin/logical/transit/path_export.go index a3e6fc6d2..da19bfd30 100644 --- a/builtin/logical/transit/path_export.go +++ b/builtin/logical/transit/path_export.go @@ -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() + } + 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", policy.Type) + 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. - 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) - if err != nil { - return "", err - } + 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" - 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) - if err != nil { - return "", err - } + 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 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{ diff --git a/builtin/logical/transit/path_import_test.go b/builtin/logical/transit/path_import_test.go index a31d151c1..2edca7f15 100644 --- a/builtin/logical/transit/path_import_test.go +++ b/builtin/logical/transit/path_import_test.go @@ -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) } }) diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 750a63926..b8c6af554 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -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 + } + + raw, err := base64.StdEncoding.DecodeString(keyEntry.FormattedPublicKey) + if err != nil { + return false, err + } + + pub = ed25519.PublicKey(raw) } - return ed25519.Verify(key.Public().(ed25519.PublicKey), input, sigBytes), nil + 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,7 +2110,25 @@ func (p *Policy) ImportPrivateKeyForVersion(ctx context.Context, storage logical // Parse key parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key) if err != nil { - return fmt.Errorf("error parsing asymmetric key: %s", err) + 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) { @@ -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) - ke.Key = privateKey - publicKey := privateKey.Public().(ed25519.PublicKey) - ke.FormattedPublicKey = base64.StdEncoding.EncodeToString(publicKey) + 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) diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index 302198ffa..bfff32770 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -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: )` – Specifies the name of the key to read information about. This is specified as part of the URL.