Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions cmd/secrets/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@ func (h *Handler) optionalCapRegVaultPublicKeyHex(ctx context.Context) (key stri
}

// vaultMasterPublicKeyHex loads the vault master public key from the gateway and, when CapabilitiesRegistry
// RPC is configured, verifies it matches the on-chain commitment before encryption.
// RPC is configured, verifies it matches the on-chain commitment before encryption. When validation is
// enabled, TOFU pinning detects gateway key changes that are not reflected on-chain.
func (h *Handler) vaultMasterPublicKeyHex(ctx context.Context) (string, error) {
gatewayKey, err := h.fetchVaultMasterPublicKeyHex()
if err != nil {
Expand All @@ -363,12 +364,31 @@ func (h *Handler) vaultMasterPublicKeyHex(ctx context.Context) (string, error) {
if err != nil {
return "", err
}
if !compare {
if !compare || h.SkipVaultValidation() {
return gatewayKey, nil
}

gatewayFP, err := tenantctx.VaultPublicKeyFingerprint(gatewayKey)
if err != nil {
return "", err
}
onChainFP, err := tenantctx.VaultPublicKeyFingerprint(onChainKey)
if err != nil {
return "", err
}

if err := h.verifyVaultKeyTOFU(gatewayFP, onChainFP); err != nil {
return "", err
}

if !strings.EqualFold(gatewayKey, onChainKey) {
return "", fmt.Errorf("vault public key from gateway does not match CapabilitiesRegistry")
}

if err := h.persistVaultKeyPin(gatewayFP); err != nil {
return "", err
}

return gatewayKey, nil
}

Expand Down
6 changes: 6 additions & 0 deletions cmd/secrets/common/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ func TestEncryptSecrets(t *testing.T) {
})

t.Run("success - gateway key matches CapabilitiesRegistry when RPC is configured", func(t *testing.T) {
pinTestHome(t)
h, _, _ := newMockHandler(t)
h.TenantContext = tofuTestTenantContext()
h.EnvironmentSet.EnvName = "staging"
h.OwnerAddress = "0xabc"
attachGatewayPublicKeyMock(t, h, vaultPublicKeyHex)
attachMockVaultDONResolver(t, h, vaultPublicKeyHex)
Expand All @@ -153,7 +156,10 @@ func TestEncryptSecrets(t *testing.T) {
})

t.Run("failure - gateway key does not match CapabilitiesRegistry", func(t *testing.T) {
pinTestHome(t)
h, _, _ := newMockHandler(t)
h.TenantContext = tofuTestTenantContext()
h.EnvironmentSet.EnvName = "staging"
h.OwnerAddress = "0xabc"
attachGatewayPublicKeyMock(t, h, vaultPublicKeyHex)
attachMockVaultDONResolver(t, h, "deadbeef")
Expand Down
68 changes: 68 additions & 0 deletions cmd/secrets/common/vault_tofu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package common

import (
"fmt"
"strings"

"github.com/smartcontractkit/cre-cli/cmd/secrets/common/gateway"
"github.com/smartcontractkit/cre-cli/internal/creconfig"
"github.com/smartcontractkit/cre-cli/internal/tenantctx"
)

func (h *Handler) vaultKeyPinScope() (tenantctx.VaultKeyPinScope, error) {
if h.TenantContext == nil {
return tenantctx.VaultKeyPinScope{}, fmt.Errorf("tenant context is not available; run `cre login` to refresh")
}
if h.TenantContext.CapabilitiesRegistry == nil {
return tenantctx.VaultKeyPinScope{}, fmt.Errorf("capabilities registry is not configured in your user context; run `cre login` to refresh")
}
if h.EnvironmentSet == nil {
return tenantctx.VaultKeyPinScope{}, fmt.Errorf("environment is not configured")
}

envName := strings.TrimSpace(h.EnvironmentSet.EnvName)
if envName == "" {
envName = "production"
}

return tenantctx.VaultKeyPinScope{
EnvName: envName,
TenantID: h.TenantContext.TenantID,
CapRegChainSelector: h.TenantContext.CapabilitiesRegistry.ChainSelector,
CapRegAddress: h.TenantContext.CapabilitiesRegistry.Address,
VaultGatewayURL: gateway.ResolveVaultGatewayURL(h.TenantContext, h.EnvironmentSet),
}, nil
}

func (h *Handler) verifyVaultKeyTOFU(gatewayFP, onChainFP string) error {
scope, err := h.vaultKeyPinScope()
if err != nil {
return err
}

pinnedFP, ok, err := tenantctx.LoadVaultKeyPin(scope)
if err != nil {
return fmt.Errorf("load vault public key pin: %w", err)
}
if !ok || tenantctx.FingerprintsMatch(pinnedFP, gatewayFP) {
return nil
}
if tenantctx.FingerprintsMatch(pinnedFP, onChainFP) {
return fmt.Errorf(
"vault public key from gateway changed without a matching on-chain update; remove %s to re-trust after verifying the gateway",
creconfig.FilePathHint(tenantctx.VaultKeyPinsFile),
)
}
return nil
}

func (h *Handler) persistVaultKeyPin(gatewayFP string) error {
scope, err := h.vaultKeyPinScope()
if err != nil {
return err
}
if err := tenantctx.SaveVaultKeyPin(scope, gatewayFP); err != nil {
return fmt.Errorf("persist vault public key pin: %w", err)
}
return nil
}
121 changes: 121 additions & 0 deletions cmd/secrets/common/vault_tofu_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package common

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cre-cli/internal/tenantctx"
)

func pinTestHome(t *testing.T) {
t.Helper()
t.Setenv("HOME", t.TempDir())
}

func tofuTestTenantContext() *tenantctx.EnvironmentContext {
return &tenantctx.EnvironmentContext{
TenantID: "tenant-1",
VaultGatewayURL: "https://gateway.example.com/",
CapabilitiesRegistry: &tenantctx.OnChainContract{
ChainSelector: 16015286601757825753,
Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f",
},
}
}

func TestVaultMasterPublicKeyHex_TOFUFirstPin(t *testing.T) {
pinTestHome(t)

h, _, _ := newMockHandler(t)
h.TenantContext = tofuTestTenantContext()
h.EnvironmentSet.EnvName = "staging"
attachGatewayPublicKeyMock(t, h, vaultPublicKeyHex)
attachMockVaultDONResolver(t, h, vaultPublicKeyHex)

key, err := h.vaultMasterPublicKeyHex(h.execCtx)
require.NoError(t, err)
require.Equal(t, vaultPublicKeyHex, key)

fp, ok, err := tenantctx.LoadVaultKeyPin(tenantctx.VaultKeyPinScope{
EnvName: "staging",
TenantID: "tenant-1",
CapRegChainSelector: h.TenantContext.CapabilitiesRegistry.ChainSelector,
CapRegAddress: h.TenantContext.CapabilitiesRegistry.Address,
VaultGatewayURL: h.TenantContext.VaultGatewayURL,
})
require.NoError(t, err)
require.True(t, ok)
require.NotEmpty(t, fp)
}

func TestVaultMasterPublicKeyHex_TOFUAbortsGatewayChangeWithoutOnChainUpdate(t *testing.T) {
pinTestHome(t)

h, _, _ := newMockHandler(t)
h.TenantContext = tofuTestTenantContext()
h.EnvironmentSet.EnvName = "staging"
attachGatewayPublicKeyMock(t, h, vaultPublicKeyHex)
attachMockVaultDONResolver(t, h, vaultPublicKeyHex)

_, err := h.vaultMasterPublicKeyHex(h.execCtx)
require.NoError(t, err)

attachGatewayPublicKeyMock(t, h, "deadbeef")
_, err = h.vaultMasterPublicKeyHex(h.execCtx)
require.ErrorContains(t, err, "changed without a matching on-chain update")
}

func TestVaultMasterPublicKeyHex_TOFUAllowsOnChainRotation(t *testing.T) {
pinTestHome(t)

h, _, _ := newMockHandler(t)
h.TenantContext = tofuTestTenantContext()
h.EnvironmentSet.EnvName = "staging"
attachGatewayPublicKeyMock(t, h, vaultPublicKeyHex)
attachMockVaultDONResolver(t, h, vaultPublicKeyHex)

_, err := h.vaultMasterPublicKeyHex(h.execCtx)
require.NoError(t, err)

rotatedKey := "cafebabe"
attachGatewayPublicKeyMock(t, h, rotatedKey)
attachMockVaultDONResolver(t, h, rotatedKey)

key, err := h.vaultMasterPublicKeyHex(h.execCtx)
require.NoError(t, err)
require.Equal(t, rotatedKey, key)

fp, ok, err := tenantctx.LoadVaultKeyPin(tenantctx.VaultKeyPinScope{
EnvName: "staging",
TenantID: "tenant-1",
CapRegChainSelector: h.TenantContext.CapabilitiesRegistry.ChainSelector,
CapRegAddress: h.TenantContext.CapabilitiesRegistry.Address,
VaultGatewayURL: h.TenantContext.VaultGatewayURL,
})
require.NoError(t, err)
require.True(t, ok)

rotatedFP, err := tenantctx.VaultPublicKeyFingerprint(rotatedKey)
require.NoError(t, err)
require.True(t, tenantctx.FingerprintsMatch(fp, rotatedFP))
}

func TestVaultMasterPublicKeyHex_SkipsTOFUWhenValidationOptedOut(t *testing.T) {
pinTestHome(t)

h, _, _ := newMockHandler(t)
h.TenantContext = tofuTestTenantContext()
h.EnvironmentSet.EnvName = "staging"
attachGatewayPublicKeyMock(t, h, vaultPublicKeyHex)
attachMockVaultDONResolver(t, h, "deadbeef")
h.skipVaultValidation = true

key, err := h.vaultMasterPublicKeyHex(h.execCtx)
require.NoError(t, err)
require.Equal(t, vaultPublicKeyHex, key)

_, ok, err := tenantctx.LoadVaultKeyPin(tenantctx.VaultKeyPinScope{EnvName: "staging"})
require.NoError(t, err)
require.False(t, ok)
}
Loading
Loading