Add Transit BYOK wrapping key endpoint (#15271)
* add wrapping key endpoint * change how wrapping key is stored * move wrapping key func to backend * refactor wrapping key generation * Initial unit tests for Transit wrapping key endpoint * Wire up wrapping key unit tests to actual implementation. * Clean up Transit BYOK wrapping key tests and imports. * Fix Transit wrapping key endpoint formatting. * Update transit wrapping key to use lock manager for safe concurrent use. * Rename some Transit wrapping key variables. Ensure the Transit wrapping key is correctly typed and formatted in a unit test. * Fix spacing issue in Transit wrapping key endpoint help string. Co-authored-by: rculpepper <rculpepper@hashicorp.com>
This commit is contained in:
parent
9d608a2e38
commit
5aabe4a5f8
|
@ -46,6 +46,7 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error)
|
||||||
b.pathConfig(),
|
b.pathConfig(),
|
||||||
b.pathRotate(),
|
b.pathRotate(),
|
||||||
b.pathRewrap(),
|
b.pathRewrap(),
|
||||||
|
b.pathWrappingKey(),
|
||||||
b.pathKeys(),
|
b.pathKeys(),
|
||||||
b.pathListKeys(),
|
b.pathListKeys(),
|
||||||
b.pathExportKeys(),
|
b.pathExportKeys(),
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package transit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/framework"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/keysutil"
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
)
|
||||||
|
|
||||||
|
const WrappingKeyName = "wrapping-key"
|
||||||
|
|
||||||
|
func (b *backend) pathWrappingKey() *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "wrapping_key",
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ReadOperation: b.pathWrappingKeyRead,
|
||||||
|
},
|
||||||
|
HelpSynopsis: pathWrappingKeyHelpSyn,
|
||||||
|
HelpDescription: pathWrappingKeyHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathWrappingKeyRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||||
|
polReq := keysutil.PolicyRequest{
|
||||||
|
Upsert: true,
|
||||||
|
Storage: req.Storage,
|
||||||
|
Name: fmt.Sprintf("import/%s", WrappingKeyName),
|
||||||
|
KeyType: keysutil.KeyType_RSA4096,
|
||||||
|
Derived: false,
|
||||||
|
Convergent: false,
|
||||||
|
Exportable: false,
|
||||||
|
AllowPlaintextBackup: false,
|
||||||
|
AutoRotatePeriod: 0,
|
||||||
|
}
|
||||||
|
p, _, err := b.GetPolicy(ctx, polReq, b.GetRandomReader())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if p == nil {
|
||||||
|
return nil, fmt.Errorf("error retrieving wrapping key: returned policy was nil")
|
||||||
|
}
|
||||||
|
if b.System().CachingDisabled() {
|
||||||
|
p.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappingKey := p.Keys[strconv.Itoa(p.LatestVersion)]
|
||||||
|
|
||||||
|
derBytes, err := x509.MarshalPKIXPublicKey(wrappingKey.RSAKey.Public())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling RSA public key: %w", err)
|
||||||
|
}
|
||||||
|
pemBlock := &pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: derBytes,
|
||||||
|
}
|
||||||
|
pemBytes := pem.EncodeToMemory(pemBlock)
|
||||||
|
if pemBytes == nil || len(pemBytes) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to PEM-encode RSA public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyString := string(pemBytes)
|
||||||
|
|
||||||
|
resp := &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"public_key": publicKeyString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathWrappingKeyHelpSyn = "Returns the public key to use for wrapping imported keys"
|
||||||
|
pathWrappingKeyHelpDesc = "This path is used to retrieve the RSA-4096 wrapping key " +
|
||||||
|
"for wrapping keys that are being imported into transit."
|
||||||
|
)
|
|
@ -0,0 +1,72 @@
|
||||||
|
package transit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
storagePath = "policy/import/" + WrappingKeyName
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransit_WrappingKey(t *testing.T) {
|
||||||
|
// Set up shared backend for subtests
|
||||||
|
b, s := createBackendWithStorage(t)
|
||||||
|
|
||||||
|
// Ensure the key does not exist before requesting it.
|
||||||
|
keyEntry, err := s.Get(context.Background(), storagePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error retrieving wrapping key from storage: %s", err)
|
||||||
|
}
|
||||||
|
if keyEntry != nil {
|
||||||
|
t.Fatal("wrapping key unexpectedly exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the key pair by requesting the public key.
|
||||||
|
req := &logical.Request{
|
||||||
|
Storage: s,
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "wrapping_key",
|
||||||
|
}
|
||||||
|
resp, err := b.HandleRequest(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected request error: %s", err)
|
||||||
|
}
|
||||||
|
if resp == nil || resp.Data == nil || resp.Data["public_key"] == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
pubKeyPEM := resp.Data["public_key"]
|
||||||
|
|
||||||
|
// Ensure the returned key is a 4096-bit RSA key.
|
||||||
|
pubKeyBlock, _ := pem.Decode([]byte(pubKeyPEM.(string)))
|
||||||
|
rawPubKey, err := x509.ParsePKIXPublicKey(pubKeyBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse public wrapping key: %s", err)
|
||||||
|
}
|
||||||
|
wrappingKey, ok := rawPubKey.(*rsa.PublicKey)
|
||||||
|
if !ok || wrappingKey.Size() != 512 {
|
||||||
|
t.Fatal("public wrapping key is not a 4096-bit RSA key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request the wrapping key again to ensure it isn't regenerated.
|
||||||
|
req = &logical.Request{
|
||||||
|
Storage: s,
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "wrapping_key",
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected request error: %s", err)
|
||||||
|
}
|
||||||
|
if resp == nil || resp.Data == nil || resp.Data["public_key"] == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
if resp.Data["public_key"] != pubKeyPEM {
|
||||||
|
t.Fatal("wrapping key public component changed between requests")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue