mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 18:48:08 +00:00 
			
		
		
		
	Provide public key encryption via transit engine (#17934)
* import rsa and ecdsa public keys * allow import_version to update public keys - wip * allow import_version to update public keys * move check key fields into func * put private/public keys in same switch cases * fix method in UpdateKeyVersion * move asymmetrics keys switch to its own method - WIP * test import public and update it with private counterpart * test import public keys * use public_key to encrypt if RSAKey is not present and failed to decrypt if key version does not have a private key * move key to KeyEntry parsing from Policy to KeyEntry method * move extracting of key from input fields into helper function * change back policy Import signature to keep backwards compatibility and add new method to import private or public keys * test import with imported public rsa and ecdsa keys * descriptions and error messages * error messages, remove comments and unused code * changelog * documentation - wip * suggested changes - error messages/typos and unwrap public key passed * fix unwrap key error * fail if both key fields have been set * fix in extractKeyFromFields, passing a PolicyRequest wouldn't not work * checks for read, sign and verify endpoints so they don't return errors when a private key was not imported and tests * handle panic on "export key" endpoint if imported key is public * fmt * remove 'isPrivateKey' argument from 'UpdateKeyVersion' and 'parseFromKey' methods also: rename 'UpdateKeyVersion' method to 'ImportPrivateKeyForVersion' and 'IsPublicKeyImported' to 'IsPrivateKeyMissing' * delete 'RSAPublicKey' when private key is imported * path_export: return public_key for ecdsa and rsa when there's no private key imported * allow signed data validation with pss algorithm * remove NOTE comment * fix typo in EC public key export where empty derBytes was being used * export rsa public key in pkcs8 format instead of pkcs1 and improve test * change logic on how check for is private key missing is calculated --------- Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
		| @@ -2022,3 +2022,257 @@ func TestTransitPKICSR(t *testing.T) { | |||||||
| 	t.Logf("root: %v", rootCertPEM) | 	t.Logf("root: %v", rootCertPEM) | ||||||
| 	t.Logf("leaf: %v", leafCertPEM) | 	t.Logf("leaf: %v", leafCertPEM) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestTransit_ReadPublicKeyImported(t *testing.T) { | ||||||
|  | 	testTransit_ReadPublicKeyImported(t, "rsa-2048") | ||||||
|  | 	testTransit_ReadPublicKeyImported(t, "ecdsa-p256") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testTransit_ReadPublicKeyImported(t *testing.T, keyType string) { | ||||||
|  | 	generateKeys(t) | ||||||
|  | 	b, s := createBackendWithStorage(t) | ||||||
|  | 	keyID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get key | ||||||
|  | 	privateKey := getKey(t, keyType) | ||||||
|  | 	publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to extract the public key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Import key | ||||||
|  | 	importReq := &logical.Request{ | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"public_key": publicKeyBytes, | ||||||
|  | 			"type":       keyType, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	importResp, err := b.HandleRequest(context.Background(), importReq) | ||||||
|  | 	if err != nil || (importResp != nil && importResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importResp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Read key | ||||||
|  | 	readReq := &logical.Request{ | ||||||
|  | 		Operation: logical.ReadOperation, | ||||||
|  | 		Path:      "keys/" + keyID, | ||||||
|  | 		Storage:   s, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	readResp, err := b.HandleRequest(context.Background(), readReq) | ||||||
|  | 	if err != nil || (readResp != nil && readResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to read key. err: %s\nresp: %#v", err, readResp) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestTransit_SignWithImportedPublicKey(t *testing.T) { | ||||||
|  | 	testTransit_SignWithImportedPublicKey(t, "rsa-2048") | ||||||
|  | 	testTransit_SignWithImportedPublicKey(t, "ecdsa-p256") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testTransit_SignWithImportedPublicKey(t *testing.T, keyType string) { | ||||||
|  | 	generateKeys(t) | ||||||
|  | 	b, s := createBackendWithStorage(t) | ||||||
|  | 	keyID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get key | ||||||
|  | 	privateKey := getKey(t, keyType) | ||||||
|  | 	publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to extract the public key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Import key | ||||||
|  | 	importReq := &logical.Request{ | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"public_key": publicKeyBytes, | ||||||
|  | 			"type":       keyType, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	importResp, err := b.HandleRequest(context.Background(), importReq) | ||||||
|  | 	if err != nil || (importResp != nil && importResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importResp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Sign text | ||||||
|  | 	signReq := &logical.Request{ | ||||||
|  | 		Path:      "sign/" + keyID, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"plaintext": base64.StdEncoding.EncodeToString([]byte(testPlaintext)), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = b.HandleRequest(context.Background(), signReq) | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("expected error, should have failed to sign input") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestTransit_VerifyWithImportedPublicKey(t *testing.T) { | ||||||
|  | 	generateKeys(t) | ||||||
|  | 	keyType := "rsa-2048" | ||||||
|  | 	b, s := createBackendWithStorage(t) | ||||||
|  | 	keyID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get key | ||||||
|  | 	privateKey := getKey(t, keyType) | ||||||
|  | 	publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Retrieve public wrapping key | ||||||
|  | 	wrappingKey, err := b.getWrappingKey(context.Background(), s) | ||||||
|  | 	if err != nil || wrappingKey == nil { | ||||||
|  | 		t.Fatalf("failed to retrieve public wrapping key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	privWrappingKey := wrappingKey.Keys[strconv.Itoa(wrappingKey.LatestVersion)].RSAKey | ||||||
|  | 	pubWrappingKey := &privWrappingKey.PublicKey | ||||||
|  |  | ||||||
|  | 	// generate ciphertext | ||||||
|  | 	importBlob := wrapTargetKeyForImport(t, pubWrappingKey, privateKey, keyType, "SHA256") | ||||||
|  |  | ||||||
|  | 	// Import private key | ||||||
|  | 	importReq := &logical.Request{ | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"ciphertext": importBlob, | ||||||
|  | 			"type":       keyType, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	importResp, err := b.HandleRequest(context.Background(), importReq) | ||||||
|  | 	if err != nil || (importResp != nil && importResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to import key. err: %s\nresp: %#v", err, importResp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Sign text | ||||||
|  | 	signReq := &logical.Request{ | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Path:      "sign/" + keyID, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"plaintext": base64.StdEncoding.EncodeToString([]byte(testPlaintext)), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	signResp, err := b.HandleRequest(context.Background(), signReq) | ||||||
|  | 	if err != nil || (signResp != nil && signResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to sign plaintext. err: %s\nresp: %#v", err, signResp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get signature | ||||||
|  | 	signature := signResp.Data["signature"].(string) | ||||||
|  |  | ||||||
|  | 	// Import new key as public key | ||||||
|  | 	importPubReq := &logical.Request{ | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Path:      fmt.Sprintf("keys/%s/import", "public-key-rsa"), | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"public_key": publicKeyBytes, | ||||||
|  | 			"type":       keyType, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	importPubResp, err := b.HandleRequest(context.Background(), importPubReq) | ||||||
|  | 	if err != nil || (importPubResp != nil && importPubResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importPubResp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Verify signed text | ||||||
|  | 	verifyReq := &logical.Request{ | ||||||
|  | 		Path:      "verify/public-key-rsa", | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"input":     base64.StdEncoding.EncodeToString([]byte(testPlaintext)), | ||||||
|  | 			"signature": signature, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	verifyResp, err := b.HandleRequest(context.Background(), verifyReq) | ||||||
|  | 	if err != nil || (importResp != nil && verifyResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to verify signed data. err: %s\nresp: %#v", err, importResp) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestTransit_ExportPublicKeyImported(t *testing.T) { | ||||||
|  | 	testTransit_ExportPublicKeyImported(t, "rsa-2048") | ||||||
|  | 	testTransit_ExportPublicKeyImported(t, "ecdsa-p256") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testTransit_ExportPublicKeyImported(t *testing.T, keyType string) { | ||||||
|  | 	generateKeys(t) | ||||||
|  | 	b, s := createBackendWithStorage(t) | ||||||
|  | 	keyID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get key | ||||||
|  | 	privateKey := getKey(t, keyType) | ||||||
|  | 	publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to extract the public key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Import key | ||||||
|  | 	importReq := &logical.Request{ | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"public_key": publicKeyBytes, | ||||||
|  | 			"type":       keyType, | ||||||
|  | 			"exportable": true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	importResp, err := b.HandleRequest(context.Background(), importReq) | ||||||
|  | 	if err != nil || (importResp != nil && importResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to import public key. err: %s\nresp: %#v", err, importResp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Export key | ||||||
|  | 	exportReq := &logical.Request{ | ||||||
|  | 		Operation: logical.ReadOperation, | ||||||
|  | 		Path:      fmt.Sprintf("export/signing-key/%s/latest", keyID), | ||||||
|  | 		Storage:   s, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	exportResp, err := b.HandleRequest(context.Background(), exportReq) | ||||||
|  | 	if err != nil || (exportResp != nil && exportResp.IsError()) { | ||||||
|  | 		t.Fatalf("failed to export key. err: %v\nresp: %#v", err, 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") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -6,12 +6,14 @@ package transit | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/keysutil" | 	"github.com/hashicorp/vault/sdk/helper/keysutil" | ||||||
|  |  | ||||||
|  | 	uuid "github.com/hashicorp/go-uuid" | ||||||
| 	"github.com/hashicorp/vault/sdk/logical" | 	"github.com/hashicorp/vault/sdk/logical" | ||||||
| 	"github.com/mitchellh/mapstructure" | 	"github.com/mitchellh/mapstructure" | ||||||
| ) | ) | ||||||
| @@ -944,3 +946,48 @@ func TestShouldWarnAboutNonceUsage(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestTransit_EncryptWithRSAPublicKey(t *testing.T) { | ||||||
|  | 	generateKeys(t) | ||||||
|  | 	b, s := createBackendWithStorage(t) | ||||||
|  | 	keyType := "rsa-2048" | ||||||
|  | 	keyID, err := uuid.GenerateUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get key | ||||||
|  | 	privateKey := getKey(t, keyType) | ||||||
|  | 	publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Import key | ||||||
|  | 	req := &logical.Request{ | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Operation: logical.UpdateOperation, | ||||||
|  | 		Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"public_key": publicKeyBytes, | ||||||
|  | 			"type":       keyType, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	_, err = b.HandleRequest(context.Background(), req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to import public key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	req = &logical.Request{ | ||||||
|  | 		Operation: logical.CreateOperation, | ||||||
|  | 		Path:      fmt.Sprintf("encrypt/%s", keyID), | ||||||
|  | 		Storage:   s, | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"plaintext": "bXkgc2VjcmV0IGRhdGE=", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	_, err = b.HandleRequest(context.Background(), req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"crypto/ecdsa" | 	"crypto/ecdsa" | ||||||
| 	"crypto/elliptic" | 	"crypto/elliptic" | ||||||
| 	"crypto/rsa" |  | ||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/pem" | 	"encoding/pem" | ||||||
| @@ -169,7 +168,11 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st | |||||||
| 			return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil | 			return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil | ||||||
|  |  | ||||||
| 		case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096: | 		case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096: | ||||||
| 			return encodeRSAPrivateKey(key.RSAKey), nil | 			rsaKey, err := encodeRSAPrivateKey(key) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return "", err | ||||||
|  | 			} | ||||||
|  | 			return rsaKey, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	case exportTypeSigningKey: | 	case exportTypeSigningKey: | ||||||
| @@ -194,23 +197,41 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st | |||||||
| 			return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil | 			return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil | ||||||
|  |  | ||||||
| 		case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096: | 		case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096: | ||||||
| 			return encodeRSAPrivateKey(key.RSAKey), nil | 			rsaKey, err := encodeRSAPrivateKey(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", policy.Type) | ||||||
| } | } | ||||||
|  |  | ||||||
| func encodeRSAPrivateKey(key *rsa.PrivateKey) string { | func encodeRSAPrivateKey(key *keysutil.KeyEntry) (string, error) { | ||||||
| 	// When encoding PKCS1, the PEM header should be `RSA PRIVATE KEY`. When Go | 	// When encoding PKCS1, the PEM header should be `RSA PRIVATE KEY`. When Go | ||||||
| 	// has PKCS8 encoding support, we may want to change this. | 	// has PKCS8 encoding support, we may want to change this. | ||||||
| 	derBytes := x509.MarshalPKCS1PrivateKey(key) | 	var blockType string | ||||||
| 	pemBlock := &pem.Block{ | 	var derBytes []byte | ||||||
| 		Type:  "RSA PRIVATE KEY", | 	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 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pemBlock := pem.Block{ | ||||||
|  | 		Type:  blockType, | ||||||
| 		Bytes: derBytes, | 		Bytes: derBytes, | ||||||
| 	} | 	} | ||||||
| 	pemBytes := pem.EncodeToMemory(pemBlock) |  | ||||||
| 	return string(pemBytes) | 	pemBytes := pem.EncodeToMemory(&pemBlock) | ||||||
|  | 	return string(pemBytes), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) { | func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) { | ||||||
| @@ -218,27 +239,46 @@ func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, | |||||||
| 		return "", errors.New("nil KeyEntry provided") | 		return "", errors.New("nil KeyEntry provided") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	privKey := &ecdsa.PrivateKey{ | 	pubKey := ecdsa.PublicKey{ | ||||||
| 		PublicKey: ecdsa.PublicKey{ |  | ||||||
| 		Curve: curve, | 		Curve: curve, | ||||||
| 		X:     k.EC_X, | 		X:     k.EC_X, | ||||||
| 		Y:     k.EC_Y, | 		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, | 			D:         k.EC_D, | ||||||
| 		} | 		} | ||||||
| 	ecder, err := x509.MarshalECPrivateKey(privKey) | 		derBytes, err = x509.MarshalECPrivateKey(privKey) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return "", err | 			return "", err | ||||||
| 		} | 		} | ||||||
| 	if ecder == nil { | 		if derBytes == nil { | ||||||
| 			return "", errors.New("no data returned when marshalling to private key") | 			return "", errors.New("no data returned when marshalling to private key") | ||||||
| 		} | 		} | ||||||
|  | 	} else { | ||||||
| 	block := pem.Block{ | 		blockType = "PUBLIC KEY" | ||||||
| 		Type:  "EC PRIVATE KEY", | 		derBytes, err = x509.MarshalPKIXPublicKey(&pubKey) | ||||||
| 		Bytes: ecder, | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
| 		} | 		} | ||||||
| 	return strings.TrimSpace(string(pem.EncodeToMemory(&block))), nil |  | ||||||
|  | 		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 | ||||||
| } | } | ||||||
|  |  | ||||||
| const pathExportHelpSyn = `Export named encryption or signing key` | const pathExportHelpSyn = `Export named encryption or signing key` | ||||||
|   | |||||||
| @@ -59,6 +59,10 @@ ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", | |||||||
| 				Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP  | 				Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP  | ||||||
| with the wrapping key and then concatenated with the import key, wrapped by the AES key.`, | with the wrapping key and then concatenated with the import key, wrapped by the AES key.`, | ||||||
| 			}, | 			}, | ||||||
|  | 			"public_key": { | ||||||
|  | 				Type:        framework.TypeString, | ||||||
|  | 				Description: `The plaintext PEM public key to be imported. If "ciphertext" is set, this field is ignored.`, | ||||||
|  | 			}, | ||||||
| 			"allow_rotation": { | 			"allow_rotation": { | ||||||
| 				Type:        framework.TypeBool, | 				Type:        framework.TypeBool, | ||||||
| 				Description: "True if the imported key may be rotated within Vault; false otherwise.", | 				Description: "True if the imported key may be rotated within Vault; false otherwise.", | ||||||
| @@ -128,12 +132,27 @@ func (b *backend) pathImportVersion() *framework.Path { | |||||||
| 				Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP  | 				Description: `The base64-encoded ciphertext of the keys. The AES key should be encrypted using OAEP  | ||||||
| with the wrapping key and then concatenated with the import key, wrapped by the AES key.`, | with the wrapping key and then concatenated with the import key, wrapped by the AES key.`, | ||||||
| 			}, | 			}, | ||||||
|  | 			"public_key": { | ||||||
|  | 				Type:        framework.TypeString, | ||||||
|  | 				Description: `The plaintext public key to be imported. If "ciphertext" is set, this field is ignored.`, | ||||||
|  | 			}, | ||||||
| 			"hash_function": { | 			"hash_function": { | ||||||
| 				Type:    framework.TypeString, | 				Type:    framework.TypeString, | ||||||
| 				Default: "SHA256", | 				Default: "SHA256", | ||||||
| 				Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated, | 				Description: `The hash function used as a random oracle in the OAEP wrapping of the user-generated, | ||||||
| ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`, | ephemeral AES key. Can be one of "SHA1", "SHA224", "SHA256" (default), "SHA384", or "SHA512"`, | ||||||
| 			}, | 			}, | ||||||
|  | 			"bump_version": { | ||||||
|  | 				Type:    framework.TypeBool, | ||||||
|  | 				Default: true, | ||||||
|  | 				Description: `By default, each operation will create a new key version. | ||||||
|  | If set to 'false', will try to update the 'Latest' version of the key, unless changed in the "version" field.`, | ||||||
|  | 			}, | ||||||
|  | 			"version": { | ||||||
|  | 				Type: framework.TypeInt, | ||||||
|  | 				Description: `Key version to be updated, if left empty 'Latest' version will be updated. | ||||||
|  | If "bump_version" is set to 'true', this field is ignored.`, | ||||||
|  | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||||
| 			logical.UpdateOperation: b.pathImportVersionWrite, | 			logical.UpdateOperation: b.pathImportVersionWrite, | ||||||
| @@ -147,11 +166,9 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * | |||||||
| 	name := d.Get("name").(string) | 	name := d.Get("name").(string) | ||||||
| 	derived := d.Get("derived").(bool) | 	derived := d.Get("derived").(bool) | ||||||
| 	keyType := d.Get("type").(string) | 	keyType := d.Get("type").(string) | ||||||
| 	hashFnStr := d.Get("hash_function").(string) |  | ||||||
| 	exportable := d.Get("exportable").(bool) | 	exportable := d.Get("exportable").(bool) | ||||||
| 	allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool) | 	allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool) | ||||||
| 	autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int)) | 	autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int)) | ||||||
| 	ciphertextString := d.Get("ciphertext").(string) |  | ||||||
| 	allowRotation := d.Get("allow_rotation").(bool) | 	allowRotation := d.Get("allow_rotation").(bool) | ||||||
|  |  | ||||||
| 	// Ensure the caller didn't supply "convergent_encryption" as a field, since it's not supported on import. | 	// Ensure the caller didn't supply "convergent_encryption" as a field, since it's not supported on import. | ||||||
| @@ -163,6 +180,12 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * | |||||||
| 		return nil, errors.New("allow_rotation must be set to true if auto-rotation is enabled") | 		return nil, errors.New("allow_rotation must be set to true if auto-rotation is enabled") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Ensure that at least on `key` field has been set | ||||||
|  | 	isCiphertextSet, err := checkKeyFieldsSet(d) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	polReq := keysutil.PolicyRequest{ | 	polReq := keysutil.PolicyRequest{ | ||||||
| 		Storage:                  req.Storage, | 		Storage:                  req.Storage, | ||||||
| 		Name:                     name, | 		Name:                     name, | ||||||
| @@ -171,6 +194,7 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * | |||||||
| 		AllowPlaintextBackup:     allowPlaintextBackup, | 		AllowPlaintextBackup:     allowPlaintextBackup, | ||||||
| 		AutoRotatePeriod:         autoRotatePeriod, | 		AutoRotatePeriod:         autoRotatePeriod, | ||||||
| 		AllowImportedKeyRotation: allowRotation, | 		AllowImportedKeyRotation: allowRotation, | ||||||
|  | 		IsPrivateKey:             isCiphertextSet, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	switch strings.ToLower(keyType) { | 	switch strings.ToLower(keyType) { | ||||||
| @@ -200,11 +224,6 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * | |||||||
| 		return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest | 		return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	hashFn, err := parseHashFn(hashFnStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) | 	p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -217,14 +236,9 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * | |||||||
| 		return nil, errors.New("the import path cannot be used with an existing key; use import-version to rotate an existing imported key") | 		return nil, errors.New("the import path cannot be used with an existing key; use import-version to rotate an existing imported key") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ciphertext, err := base64.StdEncoding.DecodeString(ciphertextString) | 	key, resp, err := b.extractKeyFromFields(ctx, req, d, polReq.KeyType, isCiphertextSet) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return resp, err | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	key, err := b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = b.lm.ImportPolicy(ctx, polReq, key, b.GetRandomReader()) | 	err = b.lm.ImportPolicy(ctx, polReq, key, b.GetRandomReader()) | ||||||
| @@ -237,20 +251,19 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * | |||||||
|  |  | ||||||
| func (b *backend) pathImportVersionWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { | func (b *backend) pathImportVersionWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { | ||||||
| 	name := d.Get("name").(string) | 	name := d.Get("name").(string) | ||||||
| 	hashFnStr := d.Get("hash_function").(string) | 	bumpVersion := d.Get("bump_version").(bool) | ||||||
| 	ciphertextString := d.Get("ciphertext").(string) |  | ||||||
|  | 	isCiphertextSet, err := checkKeyFieldsSet(d) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	polReq := keysutil.PolicyRequest{ | 	polReq := keysutil.PolicyRequest{ | ||||||
| 		Storage:      req.Storage, | 		Storage:      req.Storage, | ||||||
| 		Name:         name, | 		Name:         name, | ||||||
| 		Upsert:       false, | 		Upsert:       false, | ||||||
|  | 		IsPrivateKey: isCiphertextSet, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	hashFn, err := parseHashFn(hashFnStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) | 	p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -270,15 +283,26 @@ func (b *backend) pathImportVersionWrite(ctx context.Context, req *logical.Reque | |||||||
| 	} | 	} | ||||||
| 	defer p.Unlock() | 	defer p.Unlock() | ||||||
|  |  | ||||||
| 	ciphertext, err := base64.StdEncoding.DecodeString(ciphertextString) | 	// Get param version if set else LatestVersion | ||||||
| 	if err != nil { | 	versionToUpdate := p.LatestVersion | ||||||
| 		return nil, err | 	if version, ok := d.Raw["version"]; ok { | ||||||
|  | 		versionToUpdate = version.(int) | ||||||
| 	} | 	} | ||||||
| 	importKey, err := b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn) |  | ||||||
|  | 	key, resp, err := b.extractKeyFromFields(ctx, req, d, p.Type, isCiphertextSet) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return resp, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if bumpVersion { | ||||||
|  | 		err = p.ImportPublicOrPrivate(ctx, req.Storage, key, isCiphertextSet, b.GetRandomReader()) | ||||||
|  | 	} else { | ||||||
|  | 		// Check if given version can be updated given input | ||||||
|  | 		err := p.KeyVersionCanBeUpdated(versionToUpdate, isCiphertextSet) | ||||||
|  | 		if err == nil { | ||||||
|  | 			err = p.ImportPrivateKeyForVersion(ctx, req.Storage, versionToUpdate, key) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	err = p.Import(ctx, req.Storage, importKey, b.GetRandomReader()) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| @@ -336,6 +360,36 @@ func (b *backend) decryptImportedKey(ctx context.Context, storage logical.Storag | |||||||
| 	return importKey, nil | 	return importKey, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (b *backend) extractKeyFromFields(ctx context.Context, req *logical.Request, d *framework.FieldData, keyType keysutil.KeyType, isPrivateKey bool) ([]byte, *logical.Response, error) { | ||||||
|  | 	var key []byte | ||||||
|  | 	if isPrivateKey { | ||||||
|  | 		hashFnStr := d.Get("hash_function").(string) | ||||||
|  | 		hashFn, err := parseHashFn(hashFnStr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return key, logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		ciphertextString := d.Get("ciphertext").(string) | ||||||
|  | 		ciphertext, err := base64.StdEncoding.DecodeString(ciphertextString) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return key, nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		key, err = b.decryptImportedKey(ctx, req.Storage, ciphertext, hashFn) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return key, nil, err | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		publicKeyString := d.Get("public_key").(string) | ||||||
|  | 		if !keyType.ImportPublicKeySupported() { | ||||||
|  | 			return key, nil, errors.New("provided type does not support public_key import") | ||||||
|  | 		} | ||||||
|  | 		key = []byte(publicKeyString) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return key, nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func parseHashFn(hashFn string) (hash.Hash, error) { | func parseHashFn(hashFn string) (hash.Hash, error) { | ||||||
| 	switch strings.ToUpper(hashFn) { | 	switch strings.ToUpper(hashFn) { | ||||||
| 	case "SHA1": | 	case "SHA1": | ||||||
| @@ -353,6 +407,29 @@ func parseHashFn(hashFn string) (hash.Hash, error) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // checkKeyFieldsSet: Checks which key fields are set. If both are set, an error is returned | ||||||
|  | func checkKeyFieldsSet(d *framework.FieldData) (bool, error) { | ||||||
|  | 	ciphertextSet := isFieldSet("ciphertext", d) | ||||||
|  | 	publicKeySet := isFieldSet("publicKey", d) | ||||||
|  |  | ||||||
|  | 	if ciphertextSet && publicKeySet { | ||||||
|  | 		return false, errors.New("only one of the following fields, ciphertext and public_key, can be set") | ||||||
|  | 	} else if ciphertextSet { | ||||||
|  | 		return true, nil | ||||||
|  | 	} else { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isFieldSet(fieldName string, d *framework.FieldData) bool { | ||||||
|  | 	_, fieldSet := d.Raw[fieldName] | ||||||
|  | 	if !fieldSet { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	pathImportWriteSyn  = "Imports an externally-generated key into a new transit key" | 	pathImportWriteSyn  = "Imports an externally-generated key into a new transit key" | ||||||
| 	pathImportWriteDesc = "This path is used to import an externally-generated " + | 	pathImportWriteDesc = "This path is used to import an externally-generated " + | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ package transit | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"crypto" | ||||||
| 	"crypto/ecdsa" | 	"crypto/ecdsa" | ||||||
| 	"crypto/ed25519" | 	"crypto/ed25519" | ||||||
| 	"crypto/elliptic" | 	"crypto/elliptic" | ||||||
| @@ -12,6 +13,7 @@ import ( | |||||||
| 	"crypto/rsa" | 	"crypto/rsa" | ||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"encoding/pem" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"sync" | 	"sync" | ||||||
| @@ -427,6 +429,70 @@ func TestTransit_Import(t *testing.T) { | |||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
|  | 	t.Run( | ||||||
|  | 		"import public key ed25519", | ||||||
|  | 		func(t *testing.T) { | ||||||
|  | 			keyType := "ed25519" | ||||||
|  | 			keyID, err := uuid.GenerateUUID() | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get keys | ||||||
|  | 			privateKey := getKey(t, keyType) | ||||||
|  | 			publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Import key | ||||||
|  | 			req := &logical.Request{ | ||||||
|  | 				Storage:   s, | ||||||
|  | 				Operation: logical.UpdateOperation, | ||||||
|  | 				Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 				Data: map[string]interface{}{ | ||||||
|  | 					"public_key": publicKeyBytes, | ||||||
|  | 					"type":       keyType, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 			_, err = b.HandleRequest(context.Background(), req) | ||||||
|  | 			if err == nil { | ||||||
|  | 				t.Fatalf("invalid public_key import incorrectly succeeeded") | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 	t.Run( | ||||||
|  | 		"import public key ecdsa", | ||||||
|  | 		func(t *testing.T) { | ||||||
|  | 			keyType := "ecdsa-p256" | ||||||
|  | 			keyID, err := uuid.GenerateUUID() | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get keys | ||||||
|  | 			privateKey := getKey(t, keyType) | ||||||
|  | 			publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Import key | ||||||
|  | 			req := &logical.Request{ | ||||||
|  | 				Storage:   s, | ||||||
|  | 				Operation: logical.UpdateOperation, | ||||||
|  | 				Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 				Data: map[string]interface{}{ | ||||||
|  | 					"public_key": publicKeyBytes, | ||||||
|  | 					"type":       keyType, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 			_, err = b.HandleRequest(context.Background(), req) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("failed to import public key: %s", err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestTransit_ImportVersion(t *testing.T) { | func TestTransit_ImportVersion(t *testing.T) { | ||||||
| @@ -573,6 +639,53 @@ func TestTransit_ImportVersion(t *testing.T) { | |||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
|  | 	t.Run( | ||||||
|  | 		"import rsa public key and update version with private counterpart", | ||||||
|  | 		func(t *testing.T) { | ||||||
|  | 			keyType := "rsa-2048" | ||||||
|  | 			keyID, err := uuid.GenerateUUID() | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("failed to generate key ID: %s", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get keys | ||||||
|  | 			privateKey := getKey(t, keyType) | ||||||
|  | 			importBlob := wrapTargetKeyForImport(t, pubWrappingKey, privateKey, keyType, "SHA256") | ||||||
|  | 			publicKeyBytes, err := getPublicKey(privateKey, keyType) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Import RSA public key | ||||||
|  | 			req := &logical.Request{ | ||||||
|  | 				Storage:   s, | ||||||
|  | 				Operation: logical.UpdateOperation, | ||||||
|  | 				Path:      fmt.Sprintf("keys/%s/import", keyID), | ||||||
|  | 				Data: map[string]interface{}{ | ||||||
|  | 					"public_key": publicKeyBytes, | ||||||
|  | 					"type":       keyType, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 			_, err = b.HandleRequest(context.Background(), req) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("failed to import public key: %s", err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Update version - import RSA private key | ||||||
|  | 			req = &logical.Request{ | ||||||
|  | 				Storage:   s, | ||||||
|  | 				Operation: logical.UpdateOperation, | ||||||
|  | 				Path:      fmt.Sprintf("keys/%s/import_version", keyID), | ||||||
|  | 				Data: map[string]interface{}{ | ||||||
|  | 					"ciphertext": importBlob, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 			_, err = b.HandleRequest(context.Background(), req) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("failed to update key: %s", err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey interface{}, targetKeyType string, hashFnName string) string { | func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey interface{}, targetKeyType string, hashFnName string) string { | ||||||
| @@ -663,3 +776,40 @@ func generateKey(keyType string) (interface{}, error) { | |||||||
| 		return nil, fmt.Errorf("failed to generate unsupported key type: %s", keyType) | 		return nil, fmt.Errorf("failed to generate unsupported key type: %s", keyType) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func getPublicKey(privateKey crypto.PrivateKey, keyType string) ([]byte, error) { | ||||||
|  | 	var publicKey crypto.PublicKey | ||||||
|  | 	var publicKeyBytes []byte | ||||||
|  | 	switch keyType { | ||||||
|  | 	case "rsa-2048", "rsa-3072", "rsa-4096": | ||||||
|  | 		publicKey = privateKey.(*rsa.PrivateKey).Public() | ||||||
|  | 	case "ecdsa-p256", "ecdsa-p384", "ecdsa-p521": | ||||||
|  | 		publicKey = privateKey.(*ecdsa.PrivateKey).Public() | ||||||
|  | 	case "ed25519": | ||||||
|  | 		publicKey = privateKey.(ed25519.PrivateKey).Public() | ||||||
|  | 	default: | ||||||
|  | 		return publicKeyBytes, fmt.Errorf("failed to get public key from %s key", keyType) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	publicKeyBytes, err := publicKeyToBytes(publicKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return publicKeyBytes, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return publicKeyBytes, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func publicKeyToBytes(publicKey crypto.PublicKey) ([]byte, error) { | ||||||
|  | 	var publicKeyBytesPem []byte | ||||||
|  | 	publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return publicKeyBytesPem, fmt.Errorf("failed to marshal public key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pemBlock := &pem.Block{ | ||||||
|  | 		Type:  "PUBLIC KEY", | ||||||
|  | 		Bytes: publicKeyBytes, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return pem.EncodeToMemory(pemBlock), nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ package transit | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"crypto" | ||||||
| 	"crypto/elliptic" | 	"crypto/elliptic" | ||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| @@ -408,9 +409,15 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f | |||||||
| 					key.Name = "rsa-4096" | 					key.Name = "rsa-4096" | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | 				var publicKey crypto.PublicKey | ||||||
|  | 				publicKey = v.RSAPublicKey | ||||||
|  | 				if !v.IsPrivateKeyMissing() { | ||||||
|  | 					publicKey = v.RSAKey.Public() | ||||||
|  | 				} | ||||||
|  |  | ||||||
| 				// Encode the RSA public key in PEM format to return over the | 				// Encode the RSA public key in PEM format to return over the | ||||||
| 				// API | 				// API | ||||||
| 				derBytes, err := x509.MarshalPKIXPublicKey(v.RSAKey.Public()) | 				derBytes, err := x509.MarshalPKIXPublicKey(publicKey) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					return nil, fmt.Errorf("error marshaling RSA public key: %w", err) | 					return nil, fmt.Errorf("error marshaling RSA public key: %w", err) | ||||||
| 				} | 				} | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								changelog/17934.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/17934.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:improvement | ||||||
|  | secrets/transit: Add support to import public keys in transit engine and allow encryption and verification of signed data | ||||||
|  | ``` | ||||||
| @@ -63,6 +63,9 @@ type PolicyRequest struct { | |||||||
| 	// AllowImportedKeyRotation indicates whether an imported key may be rotated by Vault | 	// AllowImportedKeyRotation indicates whether an imported key may be rotated by Vault | ||||||
| 	AllowImportedKeyRotation bool | 	AllowImportedKeyRotation bool | ||||||
|  |  | ||||||
|  | 	// Indicates whether a private or public key is imported/upserted | ||||||
|  | 	IsPrivateKey bool | ||||||
|  |  | ||||||
| 	// The UUID of the managed key, if using one | 	// The UUID of the managed key, if using one | ||||||
| 	ManagedKeyUUID string | 	ManagedKeyUUID string | ||||||
| } | } | ||||||
| @@ -511,7 +514,7 @@ func (lm *LockManager) ImportPolicy(ctx context.Context, req PolicyRequest, key | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = p.Import(ctx, req.Storage, key, rand) | 	err = p.ImportPublicOrPrivate(ctx, req.Storage, key, req.IsPrivateKey, rand) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("error importing key: %s", err) | 		return fmt.Errorf("error importing key: %s", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -167,6 +167,14 @@ func (kt KeyType) AssociatedDataSupported() bool { | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (kt KeyType) ImportPublicKeySupported() bool { | ||||||
|  | 	switch kt { | ||||||
|  | 	case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096, KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521: | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
| func (kt KeyType) String() string { | func (kt KeyType) String() string { | ||||||
| 	switch kt { | 	switch kt { | ||||||
| 	case KeyType_AES128_GCM96: | 	case KeyType_AES128_GCM96: | ||||||
| @@ -219,6 +227,7 @@ type KeyEntry struct { | |||||||
| 	EC_D *big.Int `json:"ec_d"` | 	EC_D *big.Int `json:"ec_d"` | ||||||
|  |  | ||||||
| 	RSAKey       *rsa.PrivateKey `json:"rsa_key"` | 	RSAKey       *rsa.PrivateKey `json:"rsa_key"` | ||||||
|  | 	RSAPublicKey *rsa.PublicKey  `json:"rsa_public_key"` | ||||||
|  |  | ||||||
| 	// The public key in an appropriate format for the type of key | 	// The public key in an appropriate format for the type of key | ||||||
| 	FormattedPublicKey string `json:"public_key"` | 	FormattedPublicKey string `json:"public_key"` | ||||||
| @@ -234,6 +243,14 @@ type KeyEntry struct { | |||||||
| 	ManagedKeyUUID string `json:"managed_key_id,omitempty"` | 	ManagedKeyUUID string `json:"managed_key_id,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (ke *KeyEntry) IsPrivateKeyMissing() bool { | ||||||
|  | 	if ke.RSAKey != nil || ke.EC_D != nil || len(ke.Key) != 0 { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
| // deprecatedKeyEntryMap is used to allow JSON marshal/unmarshal | // deprecatedKeyEntryMap is used to allow JSON marshal/unmarshal | ||||||
| type deprecatedKeyEntryMap map[int]KeyEntry | type deprecatedKeyEntryMap map[int]KeyEntry | ||||||
|  |  | ||||||
| @@ -969,6 +986,9 @@ func (p *Policy) DecryptWithFactory(context, nonce []byte, value string, factori | |||||||
| 			return "", err | 			return "", err | ||||||
| 		} | 		} | ||||||
| 		key := keyEntry.RSAKey | 		key := keyEntry.RSAKey | ||||||
|  | 		if key == nil { | ||||||
|  | 			return "", errutil.InternalError{Err: fmt.Sprintf("cannot decrypt ciphertext, key version does not have a private counterpart")} | ||||||
|  | 		} | ||||||
| 		plain, err = rsa.DecryptOAEP(sha256.New(), rand.Reader, key, decoded, nil) | 		plain, err = rsa.DecryptOAEP(sha256.New(), rand.Reader, key, decoded, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return "", errutil.InternalError{Err: fmt.Sprintf("failed to RSA decrypt the ciphertext: %v", err)} | 			return "", errutil.InternalError{Err: fmt.Sprintf("failed to RSA decrypt the ciphertext: %v", err)} | ||||||
| @@ -1043,13 +1063,13 @@ func (p *Policy) minRSAPSSSaltLength() int { | |||||||
| 	return rsa.PSSSaltLengthEqualsHash | 	return rsa.PSSSaltLengthEqualsHash | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *Policy) maxRSAPSSSaltLength(priv *rsa.PrivateKey, hash crypto.Hash) int { | func (p *Policy) maxRSAPSSSaltLength(keyBitLen int, hash crypto.Hash) int { | ||||||
| 	// https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/crypto/rsa/pss.go;l=288 | 	// https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/crypto/rsa/pss.go;l=288 | ||||||
| 	return (priv.N.BitLen()-1+7)/8 - 2 - hash.Size() | 	return (keyBitLen-1+7)/8 - 2 - hash.Size() | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *Policy) validRSAPSSSaltLength(priv *rsa.PrivateKey, hash crypto.Hash, saltLength int) bool { | func (p *Policy) validRSAPSSSaltLength(keyBitLen int, hash crypto.Hash, saltLength int) bool { | ||||||
| 	return p.minRSAPSSSaltLength() <= saltLength && saltLength <= p.maxRSAPSSSaltLength(priv, hash) | 	return p.minRSAPSSSaltLength() <= saltLength && saltLength <= p.maxRSAPSSSaltLength(keyBitLen, hash) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *Policy) SignWithOptions(ver int, context, input []byte, options *SigningOptions) (*SigningResult, error) { | func (p *Policy) SignWithOptions(ver int, context, input []byte, options *SigningOptions) (*SigningResult, error) { | ||||||
| @@ -1076,6 +1096,11 @@ func (p *Policy) SignWithOptions(ver int, context, input []byte, options *Signin | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Before signing, check if key has its private part, if not return error | ||||||
|  | 	if keyParams.IsPrivateKeyMissing() { | ||||||
|  | 		return nil, errutil.UserError{Err: "requested version for signing does not contain a private part"} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	hashAlgorithm := options.HashAlgorithm | 	hashAlgorithm := options.HashAlgorithm | ||||||
| 	marshaling := options.Marshaling | 	marshaling := options.Marshaling | ||||||
| 	saltLength := options.SaltLength | 	saltLength := options.SaltLength | ||||||
| @@ -1182,7 +1207,7 @@ func (p *Policy) SignWithOptions(ver int, context, input []byte, options *Signin | |||||||
|  |  | ||||||
| 		switch sigAlgorithm { | 		switch sigAlgorithm { | ||||||
| 		case "pss": | 		case "pss": | ||||||
| 			if !p.validRSAPSSSaltLength(key, algo, saltLength) { | 			if !p.validRSAPSSSaltLength(key.N.BitLen(), algo, saltLength) { | ||||||
| 				return nil, errutil.UserError{Err: fmt.Sprintf("requested salt length %d is invalid", saltLength)} | 				return nil, errutil.UserError{Err: fmt.Sprintf("requested salt length %d is invalid", saltLength)} | ||||||
| 			} | 			} | ||||||
| 			sig, err = rsa.SignPSS(rand.Reader, key, algo, input, &rsa.PSSOptions{SaltLength: saltLength}) | 			sig, err = rsa.SignPSS(rand.Reader, key, algo, input, &rsa.PSSOptions{SaltLength: saltLength}) | ||||||
| @@ -1357,8 +1382,6 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o | |||||||
| 			return false, err | 			return false, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		key := keyEntry.RSAKey |  | ||||||
|  |  | ||||||
| 		algo, ok := CryptoHashMap[hashAlgorithm] | 		algo, ok := CryptoHashMap[hashAlgorithm] | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			return false, errutil.InternalError{Err: "unsupported hash algorithm"} | 			return false, errutil.InternalError{Err: "unsupported hash algorithm"} | ||||||
| @@ -1370,12 +1393,20 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o | |||||||
|  |  | ||||||
| 		switch sigAlgorithm { | 		switch sigAlgorithm { | ||||||
| 		case "pss": | 		case "pss": | ||||||
| 			if !p.validRSAPSSSaltLength(key, algo, saltLength) { | 			publicKey := keyEntry.RSAPublicKey | ||||||
|  | 			if !keyEntry.IsPrivateKeyMissing() { | ||||||
|  | 				publicKey = &keyEntry.RSAKey.PublicKey | ||||||
|  | 			} | ||||||
|  | 			if !p.validRSAPSSSaltLength(publicKey.N.BitLen(), algo, saltLength) { | ||||||
| 				return false, errutil.UserError{Err: fmt.Sprintf("requested salt length %d is invalid", saltLength)} | 				return false, errutil.UserError{Err: fmt.Sprintf("requested salt length %d is invalid", saltLength)} | ||||||
| 			} | 			} | ||||||
| 			err = rsa.VerifyPSS(&key.PublicKey, algo, input, sigBytes, &rsa.PSSOptions{SaltLength: saltLength}) | 			err = rsa.VerifyPSS(publicKey, algo, input, sigBytes, &rsa.PSSOptions{SaltLength: saltLength}) | ||||||
| 		case "pkcs1v15": | 		case "pkcs1v15": | ||||||
| 			err = rsa.VerifyPKCS1v15(&key.PublicKey, algo, input, sigBytes) | 			publicKey := keyEntry.RSAPublicKey | ||||||
|  | 			if !keyEntry.IsPrivateKeyMissing() { | ||||||
|  | 				publicKey = &keyEntry.RSAKey.PublicKey | ||||||
|  | 			} | ||||||
|  | 			err = rsa.VerifyPKCS1v15(publicKey, algo, input, sigBytes) | ||||||
| 		default: | 		default: | ||||||
| 			return false, errutil.InternalError{Err: fmt.Sprintf("unsupported rsa signature algorithm %s", sigAlgorithm)} | 			return false, errutil.InternalError{Err: fmt.Sprintf("unsupported rsa signature algorithm %s", sigAlgorithm)} | ||||||
| 		} | 		} | ||||||
| @@ -1396,6 +1427,10 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o | |||||||
| } | } | ||||||
|  |  | ||||||
| func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte, randReader io.Reader) error { | func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte, randReader io.Reader) error { | ||||||
|  | 	return p.ImportPublicOrPrivate(ctx, storage, key, true, randReader) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Policy) ImportPublicOrPrivate(ctx context.Context, storage logical.Storage, key []byte, isPrivateKey bool, randReader io.Reader) error { | ||||||
| 	now := time.Now() | 	now := time.Now() | ||||||
| 	entry := KeyEntry{ | 	entry := KeyEntry{ | ||||||
| 		CreationTime:           now, | 		CreationTime:           now, | ||||||
| @@ -1422,19 +1457,22 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte | |||||||
| 			p.KeySize = len(key) | 			p.KeySize = len(key) | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key) | 		var parsedKey any | ||||||
|  | 		var err error | ||||||
|  | 		if isPrivateKey { | ||||||
|  | 			parsedKey, err = x509.ParsePKCS8PrivateKey(key) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if strings.Contains(err.Error(), "unknown elliptic curve") { | 				if strings.Contains(err.Error(), "unknown elliptic curve") { | ||||||
| 					var edErr error | 					var edErr error | ||||||
| 				parsedPrivateKey, edErr = ParsePKCS8Ed25519PrivateKey(key) | 					parsedKey, edErr = ParsePKCS8Ed25519PrivateKey(key) | ||||||
| 					if edErr != nil { | 					if edErr != nil { | ||||||
| 					return fmt.Errorf("error parsing asymmetric key:\n - assuming contents are an ed25519 private key: %v\n - original error: %w", edErr, err) | 						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! | 					// Parsing as Ed25519-in-PKCS8-ECPrivateKey succeeded! | ||||||
| 				} else if strings.Contains(err.Error(), oidSignatureRSAPSS.String()) { | 				} else if strings.Contains(err.Error(), oidSignatureRSAPSS.String()) { | ||||||
| 					var rsaErr error | 					var rsaErr error | ||||||
| 				parsedPrivateKey, rsaErr = ParsePKCS8RSAPSSPrivateKey(key) | 					parsedKey, rsaErr = ParsePKCS8RSAPSSPrivateKey(key) | ||||||
| 					if rsaErr != nil { | 					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) | 						return fmt.Errorf("error parsing asymmetric key:\n - assuming contents are an RSA/PSS private key: %v\n - original error: %w", rsaErr, err) | ||||||
| 					} | 					} | ||||||
| @@ -1444,69 +1482,17 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte | |||||||
| 					return fmt.Errorf("error parsing asymmetric key: %s", err) | 					return fmt.Errorf("error parsing asymmetric key: %s", err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 		} else { | ||||||
| 		switch parsedPrivateKey.(type) { | 			pemBlock, _ := pem.Decode(key) | ||||||
| 		case *ecdsa.PrivateKey: | 			parsedKey, err = x509.ParsePKIXPublicKey(pemBlock.Bytes) | ||||||
| 			if p.Type != KeyType_ECDSA_P256 && p.Type != KeyType_ECDSA_P384 && p.Type != KeyType_ECDSA_P521 { |  | ||||||
| 				return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			ecdsaKey := parsedPrivateKey.(*ecdsa.PrivateKey) |  | ||||||
| 			curve := elliptic.P256() |  | ||||||
| 			if p.Type == KeyType_ECDSA_P384 { |  | ||||||
| 				curve = elliptic.P384() |  | ||||||
| 			} else if p.Type == KeyType_ECDSA_P521 { |  | ||||||
| 				curve = elliptic.P521() |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if ecdsaKey.Curve != curve { |  | ||||||
| 				return fmt.Errorf("invalid curve: expected %s, got %s", curve.Params().Name, ecdsaKey.Curve.Params().Name) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			entry.EC_D = ecdsaKey.D |  | ||||||
| 			entry.EC_X = ecdsaKey.X |  | ||||||
| 			entry.EC_Y = ecdsaKey.Y |  | ||||||
| 			derBytes, err := x509.MarshalPKIXPublicKey(ecdsaKey.Public()) |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return errwrap.Wrapf("error marshaling public key: {{err}}", err) | 				return fmt.Errorf("error parsing public key: %s", err) | ||||||
| 			} | 			} | ||||||
| 			pemBlock := &pem.Block{ |  | ||||||
| 				Type:  "PUBLIC KEY", |  | ||||||
| 				Bytes: derBytes, |  | ||||||
| 			} |  | ||||||
| 			pemBytes := pem.EncodeToMemory(pemBlock) |  | ||||||
| 			if pemBytes == nil || len(pemBytes) == 0 { |  | ||||||
| 				return fmt.Errorf("error PEM-encoding public key") |  | ||||||
| 			} |  | ||||||
| 			entry.FormattedPublicKey = string(pemBytes) |  | ||||||
| 		case ed25519.PrivateKey: |  | ||||||
| 			if p.Type != KeyType_ED25519 { |  | ||||||
| 				return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) |  | ||||||
| 			} |  | ||||||
| 			privateKey := parsedPrivateKey.(ed25519.PrivateKey) |  | ||||||
|  |  | ||||||
| 			entry.Key = privateKey |  | ||||||
| 			publicKey := privateKey.Public().(ed25519.PublicKey) |  | ||||||
| 			entry.FormattedPublicKey = base64.StdEncoding.EncodeToString(publicKey) |  | ||||||
| 		case *rsa.PrivateKey: |  | ||||||
| 			if p.Type != KeyType_RSA2048 && p.Type != KeyType_RSA3072 && p.Type != KeyType_RSA4096 { |  | ||||||
| 				return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 			keyBytes := 256 | 		err = entry.parseFromKey(p.Type, parsedKey) | ||||||
| 			if p.Type == KeyType_RSA3072 { | 		if err != nil { | ||||||
| 				keyBytes = 384 | 			return err | ||||||
| 			} else if p.Type == KeyType_RSA4096 { |  | ||||||
| 				keyBytes = 512 |  | ||||||
| 			} |  | ||||||
| 			rsaKey := parsedPrivateKey.(*rsa.PrivateKey) |  | ||||||
| 			if rsaKey.Size() != keyBytes { |  | ||||||
| 				return fmt.Errorf("invalid key size: expected %d bytes, got %d bytes", keyBytes, rsaKey.Size()) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			entry.RSAKey = rsaKey |  | ||||||
| 		default: |  | ||||||
| 			return fmt.Errorf("invalid key type: expected %s, got %T", p.Type, parsedPrivateKey) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -2021,8 +2007,13 @@ func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return "", err | 			return "", err | ||||||
| 		} | 		} | ||||||
| 		key := keyEntry.RSAKey | 		var publicKey *rsa.PublicKey | ||||||
| 		ciphertext, err = rsa.EncryptOAEP(sha256.New(), rand.Reader, &key.PublicKey, plaintext, nil) | 		if keyEntry.RSAKey != nil { | ||||||
|  | 			publicKey = &keyEntry.RSAKey.PublicKey | ||||||
|  | 		} else { | ||||||
|  | 			publicKey = keyEntry.RSAPublicKey | ||||||
|  | 		} | ||||||
|  | 		ciphertext, err = rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, plaintext, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return "", errutil.InternalError{Err: fmt.Sprintf("failed to RSA encrypt the plaintext: %v", err)} | 			return "", errutil.InternalError{Err: fmt.Sprintf("failed to RSA encrypt the plaintext: %v", err)} | ||||||
| 		} | 		} | ||||||
| @@ -2067,3 +2058,163 @@ func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value | |||||||
|  |  | ||||||
| 	return encoded, nil | 	return encoded, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (p *Policy) KeyVersionCanBeUpdated(keyVersion int, isPrivateKey bool) error { | ||||||
|  | 	keyEntry, err := p.safeGetKeyEntry(keyVersion) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !p.Type.ImportPublicKeySupported() { | ||||||
|  | 		return errors.New("provided type does not support importing key versions") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	isPrivateKeyMissing := keyEntry.IsPrivateKeyMissing() | ||||||
|  | 	if isPrivateKeyMissing && !isPrivateKey { | ||||||
|  | 		return errors.New("cannot add a public key to a key version that already has a public key set") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !isPrivateKeyMissing { | ||||||
|  | 		return errors.New("private key imported, key version cannot be updated") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Policy) ImportPrivateKeyForVersion(ctx context.Context, storage logical.Storage, keyVersion int, key []byte) error { | ||||||
|  | 	keyEntry, err := p.safeGetKeyEntry(keyVersion) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Parse key | ||||||
|  | 	parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error parsing asymmetric key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch parsedPrivateKey.(type) { | ||||||
|  | 	case *ecdsa.PrivateKey: | ||||||
|  | 		ecdsaKey := parsedPrivateKey.(*ecdsa.PrivateKey) | ||||||
|  | 		pemBlock, _ := pem.Decode([]byte(keyEntry.FormattedPublicKey)) | ||||||
|  | 		publicKey, err := x509.ParsePKIXPublicKey(pemBlock.Bytes) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("failed to parse key entry public key: %v", err) | ||||||
|  | 		} | ||||||
|  | 		if !publicKey.(*ecdsa.PublicKey).Equal(ecdsaKey.PublicKey) { | ||||||
|  | 			return fmt.Errorf("cannot import key, key pair does not match") | ||||||
|  | 		} | ||||||
|  | 	case *rsa.PrivateKey: | ||||||
|  | 		rsaKey := parsedPrivateKey.(*rsa.PrivateKey) | ||||||
|  | 		if !rsaKey.PublicKey.Equal(keyEntry.RSAPublicKey) { | ||||||
|  | 			return fmt.Errorf("cannot import key, key pair does not match") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = keyEntry.parseFromKey(p.Type, parsedPrivateKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	p.Keys[strconv.Itoa(keyVersion)] = keyEntry | ||||||
|  |  | ||||||
|  | 	return p.Persist(ctx, storage) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ke *KeyEntry) parseFromKey(PolKeyType KeyType, parsedKey any) error { | ||||||
|  | 	switch parsedKey.(type) { | ||||||
|  | 	case *ecdsa.PrivateKey, *ecdsa.PublicKey: | ||||||
|  | 		if PolKeyType != KeyType_ECDSA_P256 && PolKeyType != KeyType_ECDSA_P384 && PolKeyType != KeyType_ECDSA_P521 { | ||||||
|  | 			return fmt.Errorf("invalid key type: expected %s, got %T", PolKeyType, parsedKey) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		curve := elliptic.P256() | ||||||
|  | 		if PolKeyType == KeyType_ECDSA_P384 { | ||||||
|  | 			curve = elliptic.P384() | ||||||
|  | 		} else if PolKeyType == KeyType_ECDSA_P521 { | ||||||
|  | 			curve = elliptic.P521() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var derBytes []byte | ||||||
|  | 		var err error | ||||||
|  | 		ecdsaKey, ok := parsedKey.(*ecdsa.PrivateKey) | ||||||
|  | 		if ok { | ||||||
|  |  | ||||||
|  | 			if ecdsaKey.Curve != curve { | ||||||
|  | 				return fmt.Errorf("invalid curve: expected %s, got %s", curve.Params().Name, ecdsaKey.Curve.Params().Name) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			ke.EC_D = ecdsaKey.D | ||||||
|  | 			ke.EC_X = ecdsaKey.X | ||||||
|  | 			ke.EC_Y = ecdsaKey.Y | ||||||
|  |  | ||||||
|  | 			derBytes, err = x509.MarshalPKIXPublicKey(ecdsaKey.Public()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return errwrap.Wrapf("error marshaling public key: {{err}}", err) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			ecdsaKey := parsedKey.(*ecdsa.PublicKey) | ||||||
|  |  | ||||||
|  | 			if ecdsaKey.Curve != curve { | ||||||
|  | 				return fmt.Errorf("invalid curve: expected %s, got %s", curve.Params().Name, ecdsaKey.Curve.Params().Name) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			ke.EC_X = ecdsaKey.X | ||||||
|  | 			ke.EC_Y = ecdsaKey.Y | ||||||
|  |  | ||||||
|  | 			derBytes, err = x509.MarshalPKIXPublicKey(ecdsaKey) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return errwrap.Wrapf("error marshaling public key: {{err}}", err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		pemBlock := &pem.Block{ | ||||||
|  | 			Type:  "PUBLIC KEY", | ||||||
|  | 			Bytes: derBytes, | ||||||
|  | 		} | ||||||
|  | 		pemBytes := pem.EncodeToMemory(pemBlock) | ||||||
|  | 		if pemBytes == nil || len(pemBytes) == 0 { | ||||||
|  | 			return fmt.Errorf("error PEM-encoding public key") | ||||||
|  | 		} | ||||||
|  | 		ke.FormattedPublicKey = string(pemBytes) | ||||||
|  | 	case ed25519.PrivateKey: | ||||||
|  | 		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) | ||||||
|  | 	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) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		keyBytes := 256 | ||||||
|  | 		if PolKeyType == KeyType_RSA3072 { | ||||||
|  | 			keyBytes = 384 | ||||||
|  | 		} else if PolKeyType == KeyType_RSA4096 { | ||||||
|  | 			keyBytes = 512 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		rsaKey, ok := parsedKey.(*rsa.PrivateKey) | ||||||
|  | 		if ok { | ||||||
|  | 			if rsaKey.Size() != keyBytes { | ||||||
|  | 				return fmt.Errorf("invalid key size: expected %d bytes, got %d bytes", keyBytes, rsaKey.Size()) | ||||||
|  | 			} | ||||||
|  | 			ke.RSAKey = rsaKey | ||||||
|  | 			ke.RSAPublicKey = nil | ||||||
|  | 		} else { | ||||||
|  | 			rsaKey := parsedKey.(*rsa.PublicKey) | ||||||
|  | 			if rsaKey.Size() != keyBytes { | ||||||
|  | 				return fmt.Errorf("invalid key size: expected %d bytes, got %d bytes", keyBytes, rsaKey.Size()) | ||||||
|  | 			} | ||||||
|  | 			ke.RSAPublicKey = rsaKey | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("invalid key type: expected %s, got %T", PolKeyType, parsedKey) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -846,7 +846,7 @@ func Test_RSA_PSS(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 		cryptoHash := CryptoHashMap[hashType] | 		cryptoHash := CryptoHashMap[hashType] | ||||||
| 		minSaltLength := p.minRSAPSSSaltLength() | 		minSaltLength := p.minRSAPSSSaltLength() | ||||||
| 		maxSaltLength := p.maxRSAPSSSaltLength(rsaKey, cryptoHash) | 		maxSaltLength := p.maxRSAPSSSaltLength(rsaKey.N.BitLen(), cryptoHash) | ||||||
| 		hash := cryptoHash.New() | 		hash := cryptoHash.New() | ||||||
| 		hash.Write(input) | 		hash.Write(input) | ||||||
| 		input = hash.Sum(nil) | 		input = hash.Sum(nil) | ||||||
|   | |||||||
| @@ -109,6 +109,7 @@ $ curl \ | |||||||
|  |  | ||||||
| This endpoint imports existing key material into a new transit-managed encryption key. | 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. | To import key material into an existing key, see the `import_version/` endpoint. | ||||||
|  | // TODO: Has to be updated. | ||||||
|  |  | ||||||
| | Method | Path                         | | | Method | Path                         | | ||||||
| | :----- | :--------------------------- | | | :----- | :--------------------------- | | ||||||
| @@ -125,7 +126,8 @@ returned by Vault and the encryption of the import key material under the | |||||||
| provided AES key. The wrapped AES key should be the first 512 bytes of the | 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. | 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) | 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. | for more information on constructing the ciphertext. If `public_key` is set, | ||||||
|  | this field is not required. | ||||||
|  |  | ||||||
| - `hash_function` `(string: "SHA256")` - The hash function used for the | - `hash_function` `(string: "SHA256")` - The hash function used for the | ||||||
| RSA-OAEP step of creating the ciphertext. Supported hash functions are: | RSA-OAEP step of creating the ciphertext. Supported hash functions are: | ||||||
| @@ -151,6 +153,9 @@ the hash function defaults to SHA256. | |||||||
|   - `rsa-3072` - RSA with bit size of 3072 (asymmetric) |   - `rsa-3072` - RSA with bit size of 3072 (asymmetric) | ||||||
|   - `rsa-4096` - RSA with bit size of 4096 (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. | ||||||
|  |  | ||||||
| - `allow_rotation` `(bool: false)` - If set, the imported key can be rotated | - `allow_rotation` `(bool: false)` - If set, the imported key can be rotated | ||||||
| within Vault by using the `rotate` endpoint. | within Vault by using the `rotate` endpoint. | ||||||
|  |  | ||||||
| @@ -198,6 +203,7 @@ $ curl \ | |||||||
| ## Import Key Version | ## Import Key Version | ||||||
|  |  | ||||||
| This endpoint imports new key material into an existing imported key. | This endpoint imports new key material into an existing imported key. | ||||||
|  | // TODO: Has to be updated. | ||||||
|  |  | ||||||
| | Method | Path                                 | | | Method | Path                                 | | ||||||
| | :----- | :----------------------------------- | | | :----- | :----------------------------------- | | ||||||
| @@ -219,12 +225,23 @@ 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. | 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) | 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. | for more information on constructing the ciphertext. | ||||||
|  | // TODO: Update text | ||||||
|  |  | ||||||
| - `hash_function` `(string: "SHA256")` - The hash function used for the | - `hash_function` `(string: "SHA256")` - The hash function used for the | ||||||
| RSA-OAEP step of creating the ciphertext. Supported hash functions are: | RSA-OAEP step of creating the ciphertext. Supported hash functions are: | ||||||
| `SHA1`, `SHA224`, `SHA256`, `SHA384`, and `SHA512`. If not specified, | `SHA1`, `SHA224`, `SHA256`, `SHA384`, and `SHA512`. If not specified, | ||||||
| the hash function defaults to SHA256. | 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. | ||||||
|  |  | ||||||
|  | - `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, | ||||||
|  | unless changed in parameter `version`. | ||||||
|  |  | ||||||
|  | - `version` - Key version to be updated, if left empty "Latest" version will be updated. | ||||||
|  | If `bump_version` is set to "true", this field is ignored. | ||||||
|  |  | ||||||
| ### Sample Payload | ### Sample Payload | ||||||
|  |  | ||||||
| ```json | ```json | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Gabriel Santos
					Gabriel Santos