diff --git a/builtin/logical/transit/path_export.go b/builtin/logical/transit/path_export.go index 17ebe796a5..c36cac8042 100644 --- a/builtin/logical/transit/path_export.go +++ b/builtin/logical/transit/path_export.go @@ -7,6 +7,7 @@ import ( "context" "crypto" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/x509" "encoding/base64" @@ -28,6 +29,13 @@ const ( exportTypePublicKey = "public-key" ) +const ( + formatTypeDefault = "" + formatTypeRaw = "raw" + formatTypeDer = "der" + formatTypePem = "pem" +) + func (b *backend) pathExportKeys() *framework.Path { return &framework.Path{ Pattern: "export/" + framework.GenericNameRegex("type") + "/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("version"), @@ -51,6 +59,10 @@ func (b *backend) pathExportKeys() *framework.Path { Type: framework.TypeString, Description: "Version of the key", }, + "format": { + Type: framework.TypeString, + Description: "Format to export the key in: `` for the default format dependent on the key type; `raw` for the raw key value in base64 (applicable to symmetric keys and ed25519); `der` for a base64 encoded PKIX (SubjectPublicKeyInfo or PKCS8/PrivateKeyInfo) format (applicable to asymmetric keys); or `pem` for a PEM-encoded PKIX format (applicable to asymmetric keys).", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -66,6 +78,7 @@ func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request exportType := d.Get("type").(string) name := d.Get("name").(string) version := d.Get("version").(string) + format := d.Get("format").(string) switch exportType { case exportTypeEncryptionKey: @@ -76,6 +89,15 @@ func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request return logical.ErrorResponse(fmt.Sprintf("invalid export type: %s", exportType)), logical.ErrInvalidRequest } + switch format { + case formatTypeDefault: + case formatTypeRaw: + case formatTypeDer: + case formatTypePem: + default: + return logical.ErrorResponse(fmt.Sprintf("invalid format: %s", format)), logical.ErrInvalidRequest + } + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, @@ -114,7 +136,7 @@ func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request switch version { case "": for k, v := range p.Keys { - exportKey, err := getExportKey(p, &v, exportType) + exportKey, err := getExportKey(p, &v, exportType, format) if err != nil { return nil, err } @@ -141,7 +163,7 @@ func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request return logical.ErrorResponse("version does not exist or cannot be found"), logical.ErrInvalidRequest } - exportKey, err := getExportKey(p, &key, exportType) + exportKey, err := getExportKey(p, &key, exportType, format) if err != nil { return nil, err } @@ -160,7 +182,7 @@ func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request return resp, nil } -func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType string) (string, error) { +func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType string, format string) (string, error) { if policy == nil { return "", errors.New("nil policy provided") } @@ -171,21 +193,26 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st if policy.Type == keysutil.KeyType_HMAC { src = key.Key } - return strings.TrimSpace(base64.StdEncoding.EncodeToString(src)), nil + if format == "der" || format == "pem" { + return "", fmt.Errorf("unknown format for HMAC key; supported values are `` or `raw`") + } + return strings.TrimSpace(base64.StdEncoding.EncodeToString(src)), nil case exportTypeEncryptionKey: switch policy.Type { case keysutil.KeyType_AES128_GCM96, keysutil.KeyType_AES256_GCM96, keysutil.KeyType_ChaCha20_Poly1305, keysutil.KeyType_XChaCha20_Poly1305: - return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil + if format == "der" || format == "pem" { + return "", fmt.Errorf("unknown format for HMAC key; supported values are `` or `raw`") + } + return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096: - rsaKey, err := encodeRSAPrivateKey(key) + rsaKey, err := encodeRSAPrivateKey(key, format) if err != nil { return "", err } return rsaKey, nil } - case exportTypeSigningKey: switch policy.Type { case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521: @@ -198,21 +225,20 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st default: curve = elliptic.P256() } - ecKey, err := keyEntryToECPrivateKey(key, curve) + + ecKey, err := keyEntryToECPrivateKey(key, curve, format) if err != nil { return "", err } return ecKey, nil - case keysutil.KeyType_ED25519: if len(key.Key) == 0 { return "", nil } - return strings.TrimSpace(base64.StdEncoding.EncodeToString(key.Key)), nil - + return encodeED25519PrivateKey(key, format) case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096: - rsaKey, err := encodeRSAPrivateKey(key) + rsaKey, err := encodeRSAPrivateKey(key, format) if err != nil { return "", err } @@ -230,17 +256,16 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st default: curve = elliptic.P256() } - ecKey, err := keyEntryToECPublicKey(key, curve) + + ecKey, err := keyEntryToECPublicKey(key, curve, format) if err != nil { return "", err } return ecKey, nil - case keysutil.KeyType_ED25519: - return strings.TrimSpace(key.FormattedPublicKey), nil - + return encodeED25519PublicKey(key, format) case keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096: - rsaKey, err := encodeRSAPublicKey(key) + rsaKey, err := encodeRSAPublicKey(key, format) if err != nil { return "", err } @@ -251,7 +276,11 @@ func getExportKey(policy *keysutil.Policy, key *keysutil.KeyEntry, exportType st return "", fmt.Errorf("unknown key type %v for export type %v", policy.Type, exportType) } -func encodeRSAPrivateKey(key *keysutil.KeyEntry) (string, error) { +func encodeRSAPrivateKey(key *keysutil.KeyEntry, format string) (string, error) { + if format == "raw" { + return "", fmt.Errorf("unknown key format for rsa key; supported values are ``, `der`, or `pem`") + } + if key == nil { return "", errors.New("nil KeyEntry provided") } @@ -260,10 +289,24 @@ func encodeRSAPrivateKey(key *keysutil.KeyEntry) (string, error) { return "", nil } - // When encoding PKCS1, the PEM header should be `RSA PRIVATE KEY`. When Go - // has PKCS8 encoding support, we may want to change this. - blockType := "RSA PRIVATE KEY" - derBytes := x509.MarshalPKCS1PrivateKey(key.RSAKey) + var derBytes []byte + var blockType string + var err error + if format == "" { + derBytes = x509.MarshalPKCS1PrivateKey(key.RSAKey) + blockType = "RSA PRIVATE KEY" + } else if format == "der" || format == "pem" { + derBytes, err = x509.MarshalPKCS8PrivateKey(key.RSAKey) + blockType = "PRIVATE KEY" + } + if err != nil { + return "", err + } + + if format == "der" { + return base64.StdEncoding.EncodeToString(derBytes), nil + } + pemBlock := pem.Block{ Type: blockType, Bytes: derBytes, @@ -273,7 +316,11 @@ func encodeRSAPrivateKey(key *keysutil.KeyEntry) (string, error) { return string(pemBytes), nil } -func encodeRSAPublicKey(key *keysutil.KeyEntry) (string, error) { +func encodeRSAPublicKey(key *keysutil.KeyEntry, format string) (string, error) { + if format == "raw" { + return "", fmt.Errorf("unknown key format for rsa key; supported values are ``, `der`, or `pem`") + } + if key == nil { return "", errors.New("nil KeyEntry provided") } @@ -294,6 +341,11 @@ func encodeRSAPublicKey(key *keysutil.KeyEntry) (string, error) { if err != nil { return "", fmt.Errorf("error marshaling RSA public key: %w", err) } + + if format == "der" { + return base64.StdEncoding.EncodeToString(derBytes), nil + } + pemBlock := &pem.Block{ Type: "PUBLIC KEY", Bytes: derBytes, @@ -306,7 +358,11 @@ func encodeRSAPublicKey(key *keysutil.KeyEntry) (string, error) { return string(pemBytes), nil } -func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) { +func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve, format string) (string, error) { + if format == "raw" { + return "", fmt.Errorf("unknown key format for ec key; supported values are ``, `der`, or `pem`") + } + if k == nil { return "", errors.New("nil KeyEntry provided") } @@ -321,16 +377,29 @@ func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, Y: k.EC_Y, } - blockType := "EC PRIVATE KEY" privKey := &ecdsa.PrivateKey{ PublicKey: pubKey, D: k.EC_D, } - derBytes, err := x509.MarshalECPrivateKey(privKey) + + var blockType string + var derBytes []byte + var err error + if format == "" { + derBytes, err = x509.MarshalECPrivateKey(privKey) + blockType = "EC PRIVATE KEY" + } else if format == "der" || format == "pem" { + derBytes, err = x509.MarshalPKCS8PrivateKey(privKey) + blockType = "PRIVATE KEY" + } if err != nil { return "", err } + if format == "der" { + return base64.StdEncoding.EncodeToString(derBytes), nil + } + pemBlock := pem.Block{ Type: blockType, Bytes: derBytes, @@ -339,7 +408,11 @@ func keyEntryToECPrivateKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, return strings.TrimSpace(string(pem.EncodeToMemory(&pemBlock))), nil } -func keyEntryToECPublicKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, error) { +func keyEntryToECPublicKey(k *keysutil.KeyEntry, curve elliptic.Curve, format string) (string, error) { + if format == "raw" { + return "", fmt.Errorf("unknown key format for ec key; supported values are ``, `der`, or `pem`") + } + if k == nil { return "", errors.New("nil KeyEntry provided") } @@ -356,6 +429,66 @@ func keyEntryToECPublicKey(k *keysutil.KeyEntry, curve elliptic.Curve) (string, return "", err } + if format == "der" { + return base64.StdEncoding.EncodeToString(derBytes), nil + } + + pemBlock := pem.Block{ + Type: blockType, + Bytes: derBytes, + } + + return strings.TrimSpace(string(pem.EncodeToMemory(&pemBlock))), nil +} + +func encodeED25519PrivateKey(k *keysutil.KeyEntry, format string) (string, error) { + if format == "" || format == "raw" { + return base64.StdEncoding.EncodeToString(k.Key), nil + } + + privKey := ed25519.PrivateKey(k.Key) + + blockType := "PRIVATE KEY" + derBytes, err := x509.MarshalPKCS8PrivateKey(privKey) + if err != nil { + return "", err + } + + if format == "der" { + return base64.StdEncoding.EncodeToString(derBytes), nil + } + + pemBlock := pem.Block{ + Type: blockType, + Bytes: derBytes, + } + + return strings.TrimSpace(string(pem.EncodeToMemory(&pemBlock))), nil +} + +func encodeED25519PublicKey(k *keysutil.KeyEntry, format string) (string, error) { + pubStr := strings.TrimSpace(k.FormattedPublicKey) + if format == "" || format == "raw" { + return pubStr, nil + } + + pubRaw, err := base64.StdEncoding.DecodeString(pubStr) + if err != nil { + return "", err + } + + pubKey := ed25519.PublicKey(pubRaw) + + blockType := "PUBLIC KEY" + derBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return "", err + } + + if format == "der" { + return base64.StdEncoding.EncodeToString(derBytes), nil + } + pemBlock := pem.Block{ Type: blockType, Bytes: derBytes, diff --git a/builtin/logical/transit/path_export_test.go b/builtin/logical/transit/path_export_test.go index d6b26fc918..fd485e64fe 100644 --- a/builtin/logical/transit/path_export_test.go +++ b/builtin/logical/transit/path_export_test.go @@ -5,9 +5,13 @@ package transit import ( "context" + "crypto/x509" + "encoding/base64" + "encoding/pem" "fmt" "reflect" "strconv" + "strings" "testing" "github.com/openbao/openbao/sdk/logical" @@ -386,3 +390,168 @@ func TestTransit_Export_EncryptionKey_DoesNotExportHMACKey(t *testing.T) { t.Fatal("Encryption key data matched hmac key data") } } + +func TestTransit_Export_CorrectFormat(t *testing.T) { + verifyExportsCorrectFormat(t, "encryption-key", "aes128-gcm96") + verifyExportsCorrectFormat(t, "encryption-key", "aes256-gcm96") + verifyExportsCorrectFormat(t, "encryption-key", "chacha20-poly1305") + verifyExportsCorrectFormat(t, "encryption-key", "xchacha20-poly1305") + verifyExportsCorrectFormat(t, "encryption-key", "rsa-2048") + verifyExportsCorrectFormat(t, "encryption-key", "rsa-3072") + verifyExportsCorrectFormat(t, "encryption-key", "rsa-4096") + verifyExportsCorrectFormat(t, "signing-key", "ecdsa-p256") + verifyExportsCorrectFormat(t, "signing-key", "ecdsa-p384") + verifyExportsCorrectFormat(t, "signing-key", "ecdsa-p521") + verifyExportsCorrectFormat(t, "signing-key", "ed25519") + verifyExportsCorrectFormat(t, "signing-key", "rsa-2048") + verifyExportsCorrectFormat(t, "signing-key", "rsa-3072") + verifyExportsCorrectFormat(t, "signing-key", "rsa-4096") + verifyExportsCorrectFormat(t, "public-key", "ecdsa-p256") + verifyExportsCorrectFormat(t, "public-key", "ecdsa-p384") + verifyExportsCorrectFormat(t, "public-key", "ecdsa-p521") + verifyExportsCorrectFormat(t, "public-key", "ed25519") + verifyExportsCorrectFormat(t, "public-key", "rsa-2048") + verifyExportsCorrectFormat(t, "public-key", "rsa-3072") + verifyExportsCorrectFormat(t, "public-key", "rsa-4096") + verifyExportsCorrectFormat(t, "hmac-key", "aes128-gcm96") + verifyExportsCorrectFormat(t, "hmac-key", "aes256-gcm96") + verifyExportsCorrectFormat(t, "hmac-key", "chacha20-poly1305") + verifyExportsCorrectFormat(t, "hmac-key", "xchacha20-poly1305") + verifyExportsCorrectFormat(t, "hmac-key", "ecdsa-p256") + verifyExportsCorrectFormat(t, "hmac-key", "ecdsa-p384") + verifyExportsCorrectFormat(t, "hmac-key", "ecdsa-p521") + verifyExportsCorrectFormat(t, "hmac-key", "rsa-2048") + verifyExportsCorrectFormat(t, "hmac-key", "rsa-3072") + verifyExportsCorrectFormat(t, "hmac-key", "rsa-4096") + verifyExportsCorrectFormat(t, "hmac-key", "ed25519") + verifyExportsCorrectFormat(t, "hmac-key", "hmac") +} + +func verifyExportsCorrectFormat(t *testing.T, exportType, keyType string) { + b, storage := createBackendWithSysView(t) + + // First create a key + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "keys/foo", + } + req.Data = map[string]interface{}{ + "exportable": true, + "type": keyType, + } + if keyType == "hmac" { + req.Data["key_size"] = 32 + } + _, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + verifyFormat := func(formatRequest string) { + t.Logf("handling key: %v / %v / %v", exportType, keyType, formatRequest) + + req := &logical.Request{ + Storage: storage, + Operation: logical.ReadOperation, + Path: fmt.Sprintf("export/%s/foo", exportType), + Data: map[string]interface{}{ + "format": formatRequest, + }, + } + + rsp, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("on req to %v: %v", req.Path, err) + } + + keysRaw, ok := rsp.Data["keys"] + if !ok { + t.Fatal("could not find keys value") + } + keys, ok := keysRaw.(map[string]string) + if !ok { + t.Fatal("could not cast to keys map") + } + if len(keys) != 1 { + t.Fatal("unexpected number of keys found") + } + + for _, k := range keys { + if exportType != "hmac-key" && formatRequest == "" && (strings.HasPrefix(keyType, "rsa") || strings.HasPrefix(keyType, "ecdsa")) { + block, rest := pem.Decode([]byte(k)) + if len(strings.TrimSpace(string(rest))) > 0 { + t.Fatalf("remainder when decoding raw %v key (%v): block=%v rest=%v", keyType, k, block, rest) + } + + if block == nil { + t.Fatalf("no pem block when decoding raw %v key (%v): block=%v rest=%v", keyType, k, block, rest) + } + + if exportType == "public-key" { + if _, err := x509.ParsePKIXPublicKey(block.Bytes); err != nil { + t.Fatalf("failed to parse raw rsa key (%v): %v", k, err) + } + } else if strings.HasPrefix(keyType, "rsa") { + if _, err := x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + t.Fatalf("failed to parse raw rsa key (%v): %v", k, err) + } + } else { + if _, err := x509.ParseECPrivateKey(block.Bytes); err != nil { + t.Fatalf("failed to parse raw ec key (%v): %v", k, err) + } + } + } else if formatRequest == "" && strings.HasPrefix(keyType, "ec") { + } else if formatRequest == "der" || formatRequest == "pem" { + var keyData []byte + var err error + + if formatRequest == "der" { + keyData, err = base64.StdEncoding.DecodeString(k) + if err != nil { + t.Fatalf("error decoding der key (%v): %v", k, err) + } + } else { + block, rest := pem.Decode([]byte(k)) + if len(strings.TrimSpace(string(rest))) > 0 { + t.Fatalf("remainder when decoding pem key (%v): block=%v rest=%v", k, block, rest) + } + + if block == nil { + t.Fatalf("no pem block when decoding pem key (%v): block=%v rest=%v", k, block, rest) + } + + keyData = block.Bytes + } + + if exportType == "public-key" { + _, err := x509.ParsePKIXPublicKey(keyData) + if err != nil { + t.Fatalf("error decoding `%v` key (%v): %v", formatRequest, k, err) + } + } else { + _, err := x509.ParsePKCS8PrivateKey(keyData) + if err != nil { + t.Fatalf("error decoding `%v` key (%v): %v", formatRequest, k, err) + } + } + } else { + if _, err := base64.StdEncoding.DecodeString(k); err != nil { + t.Fatalf("error decoding raw key (%v / %v): %v", formatRequest, keyType, k) + } + } + } + } + + verifyFormat("") + if exportType == "hmac-key" || strings.Contains(keyType, "aes") || strings.Contains(keyType, "chacha20") || keyType == "hmac" { + verifyFormat("raw") + } else if keyType == "ed25519" { + verifyFormat("raw") + verifyFormat("der") + verifyFormat("pem") + } else { + verifyFormat("der") + verifyFormat("pem") + } +} diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index 4c4b79a7f6..ab98efda7d 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -455,7 +455,7 @@ func (b *backend) formatKeyPolicy(p *keysutil.Policy, context []byte) (*logical. key.Name = "rsa-4096" } - pubKey, err := encodeRSAPublicKey(&v) + pubKey, err := encodeRSAPublicKey(&v, "") if err != nil { return nil, err } diff --git a/changelog/212.txt b/changelog/212.txt new file mode 100644 index 0000000000..d399ab4315 --- /dev/null +++ b/changelog/212.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secret/transit: Allow choosing export key format, specifying format=der or format=pem for consistent PKIX encoded public keys. +``` diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index e56782e2a2..b2750c6997 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -659,6 +659,15 @@ be valid. all versions of the key will be returned. This is specified as part of the URL. If the version is set to `latest`, the current key will be returned. +- `format` `(string: "")` - Specifies the format of the exported key. The + empty string preserves existing behavior, with format unique to each key + type (`base64` encoded raw keys for symmetric keys or Ed25519 keys, PKCS#1 + for RSA private keys, SEC 1 for EC private keys, and PKIX format for + RSA or EC public keys). The `raw` format always returns a base64 encoded + raw key (only applicable to symmetric keys and Ed25519 keys). The `der` and + `pem` formats always returns a PKIX (SubjectPublicKeyInfo) or PrivateKeyInfo + encoded object for asymmetric keys. + ### Sample request ```shell-session