mirror of
https://github.com/luxfi/keys.git
synced 2026-07-26 23:58:11 +00:00
keys: rip *ServiceIdentity from LoadMnemonic API
Fetching a secret should not know about who is asking — that's a transport-layer concern. Trust at the network boundary (NetworkPolicy + ZAP wire), not in the application envelope. API change (breaking): LoadMnemonic(ctx, addr, env, path) string, error LoadMnemonicFromKMS(ctx, addr, env, path) string, error dialKMS(ctx, addr) (MnemonicReader, error) ServiceIdentity type is retained for callers that need signing elsewhere; it's just no longer threaded through the mnemonic-fetch operation. Companion rip: KMS_ZAP_AUTH_ENABLED, ZAP_AUTH_REQUIRED, KMS_IAM_URL, KMS_IAM_CLIENT_ID, KMS_IAM_CLIENT_SECRET, KMS_ZAP_AUDIENCE — see luxfi/kms + luxfi/mpc follow-ups.
This commit is contained in:
@@ -33,7 +33,6 @@ import (
|
||||
"strings"
|
||||
|
||||
bip39 "github.com/luxfi/go-bip39"
|
||||
"github.com/luxfi/kms/pkg/envelope"
|
||||
"github.com/luxfi/kms/pkg/zapclient"
|
||||
)
|
||||
|
||||
@@ -48,22 +47,11 @@ type MnemonicReader interface {
|
||||
// dialKMS is the production seam. Tests override to inject a fake
|
||||
// without touching the network.
|
||||
//
|
||||
// identity is REQUIRED for the production path (the KMS server's
|
||||
// consensus-auth gate rejects envelopes without a signed identity).
|
||||
// Passing nil dials anonymously — accepted only by legacy KMS peers
|
||||
// that have explicitly disabled the auth gate (dev / loopback only).
|
||||
var dialKMS = func(ctx context.Context, addr string, identity *ServiceIdentity) (MnemonicReader, error) {
|
||||
cfg := zapclient.Config{PeerAddr: addr}
|
||||
if identity != nil {
|
||||
cfg.IdentityHeader = envelope.IdentityHeader{
|
||||
NodeID: identity.NodeID,
|
||||
FullDigest: identity.FullDigest,
|
||||
ServicePath: identity.ServicePath,
|
||||
PublicKey: identity.PublicKey,
|
||||
}
|
||||
cfg.Signer = identity
|
||||
}
|
||||
c, err := zapclient.DialWithConfig(ctx, cfg)
|
||||
// Trust is enforced at the network boundary (NetworkPolicy + ZAP
|
||||
// wire), not in the application-layer envelope. The mnemonic-fetch
|
||||
// operation does not know about who is asking; it just asks.
|
||||
var dialKMS = func(ctx context.Context, addr string) (MnemonicReader, error) {
|
||||
c, err := zapclient.DialWithConfig(ctx, zapclient.Config{PeerAddr: addr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,38 +62,28 @@ var dialKMS = func(ctx context.Context, addr string, identity *ServiceIdentity)
|
||||
// MNEMONIC env wins when set; otherwise dials KMS over native ZAP at
|
||||
// `addr` and reads the secret at `path` under `env`.
|
||||
//
|
||||
// ctx cancellable context
|
||||
// addr KMS host:port (e.g. "kms.internal.svc:9999")
|
||||
// env KMS env scope ("mainnet" | "testnet" | "devnet")
|
||||
// path KMS secret path (e.g. "/mnemonic" or "/foo/master")
|
||||
// identity ServiceIdentity to sign the secret-opcode envelope. May
|
||||
// be nil for legacy peers and the env-win short-circuit
|
||||
// (the KMS dial is never reached); required for production
|
||||
// peers whose consensus-auth gate is engaged.
|
||||
// ctx cancellable context
|
||||
// addr KMS host:port (e.g. "kms.internal.svc:9999")
|
||||
// env KMS env scope ("mainnet" | "testnet" | "devnet")
|
||||
// path KMS secret path (e.g. "/mnemonic" or "/foo/master")
|
||||
//
|
||||
// Returns the validated BIP-39 phrase. Caller is responsible for
|
||||
// keeping it on the goroutine stack and not logging / persisting it.
|
||||
func LoadMnemonic(ctx context.Context, addr, env, path string, identity *ServiceIdentity) (string, error) {
|
||||
func LoadMnemonic(ctx context.Context, addr, env, path string) (string, error) {
|
||||
if e := strings.TrimSpace(os.Getenv("MNEMONIC")); e != "" {
|
||||
if !bip39.IsMnemonicValid(e) {
|
||||
return "", errors.New("MNEMONIC env is not a valid BIP-39 phrase")
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
return LoadMnemonicFromKMS(ctx, addr, env, path, identity)
|
||||
return LoadMnemonicFromKMS(ctx, addr, env, path)
|
||||
}
|
||||
|
||||
// LoadMnemonicFromKMS is the production-only path (no MNEMONIC env
|
||||
// short-circuit). Useful when a caller wants to force the KMS read
|
||||
// regardless of ambient env — e.g., a regression test pinning the
|
||||
// production code path.
|
||||
//
|
||||
// identity is the *ServiceIdentity threaded into the ZAP dial: its
|
||||
// IdentityHeader becomes envelope.Identity and the same identity
|
||||
// signs every secret-opcode envelope. Pass nil only when dialling a
|
||||
// legacy KMS peer that has not enabled the consensus-auth gate
|
||||
// (dev / loopback fakes — see zap_test.go's withDial seam).
|
||||
func LoadMnemonicFromKMS(ctx context.Context, addr, env, path string, identity *ServiceIdentity) (string, error) {
|
||||
func LoadMnemonicFromKMS(ctx context.Context, addr, env, path string) (string, error) {
|
||||
if addr == "" {
|
||||
return "", errors.New("keys.LoadMnemonicFromKMS: KMS addr is required")
|
||||
}
|
||||
@@ -116,7 +94,7 @@ func LoadMnemonicFromKMS(ctx context.Context, addr, env, path string, identity *
|
||||
return "", errors.New("keys.LoadMnemonicFromKMS: KMS path is required")
|
||||
}
|
||||
|
||||
c, err := dialKMS(ctx, addr, identity)
|
||||
c, err := dialKMS(ctx, addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dial KMS %s: %w", addr, err)
|
||||
}
|
||||
|
||||
+14
-42
@@ -31,20 +31,16 @@ func (f *fakeKMS) GetAt(_ context.Context, path, name, env string) (string, erro
|
||||
}
|
||||
func (f *fakeKMS) Close() { f.closed = true }
|
||||
|
||||
// gotIdent captures the identity threaded into the dial seam so the
|
||||
// per-call-site identity-wiring test can assert it reached zapclient.
|
||||
type gotIdent struct {
|
||||
called bool
|
||||
identity *ServiceIdentity
|
||||
}
|
||||
// dialState captures dial-seam invocations so tests can assert the
|
||||
// dial path was (or wasn't) reached.
|
||||
type dialState struct{ called bool }
|
||||
|
||||
func withDial(t *testing.T, f *fakeKMS, err error) *gotIdent {
|
||||
func withDial(t *testing.T, f *fakeKMS, err error) *dialState {
|
||||
t.Helper()
|
||||
prev := dialKMS
|
||||
g := &gotIdent{}
|
||||
dialKMS = func(_ context.Context, _ string, id *ServiceIdentity) (MnemonicReader, error) {
|
||||
g := &dialState{}
|
||||
dialKMS = func(_ context.Context, _ string) (MnemonicReader, error) {
|
||||
g.called = true
|
||||
g.identity = id
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,7 +55,7 @@ func TestLoadMnemonic_EnvWins(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", validBIP39+"\n")
|
||||
f := &fakeKMS{value: "ignored"}
|
||||
g := withDial(t, f, nil)
|
||||
got, err := LoadMnemonic(context.Background(), "addr", "main", "/mnemonic", nil)
|
||||
got, err := LoadMnemonic(context.Background(), "addr", "main", "/mnemonic")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMnemonic: %v", err)
|
||||
}
|
||||
@@ -74,7 +70,7 @@ func TestLoadMnemonic_EnvWins(t *testing.T) {
|
||||
// Invalid MNEMONIC env fails fast — does NOT silently fall through to KMS.
|
||||
func TestLoadMnemonic_EnvInvalid(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", "not a bip39 phrase")
|
||||
_, err := LoadMnemonic(context.Background(), "addr", "main", "/mnemonic", nil)
|
||||
_, err := LoadMnemonic(context.Background(), "addr", "main", "/mnemonic")
|
||||
if err == nil || !strings.Contains(err.Error(), "MNEMONIC env") {
|
||||
t.Fatalf("expected MNEMONIC-env error, got %v", err)
|
||||
}
|
||||
@@ -85,7 +81,7 @@ func TestLoadMnemonic_FallsThroughToKMS(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", "")
|
||||
f := &fakeKMS{value: validBIP39}
|
||||
withDial(t, f, nil)
|
||||
got, err := LoadMnemonic(context.Background(), "addr", "main", "/mnemonic", nil)
|
||||
got, err := LoadMnemonic(context.Background(), "addr", "main", "/mnemonic")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMnemonic: %v", err)
|
||||
}
|
||||
@@ -110,7 +106,7 @@ func TestLoadMnemonicFromKMS_RequiredArgs(t *testing.T) {
|
||||
{"a", "main", "", "path is required"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), c.addr, c.env, c.path, nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), c.addr, c.env, c.path)
|
||||
if err == nil || !strings.Contains(err.Error(), c.wantMsg) {
|
||||
t.Errorf("addr=%q env=%q path=%q → %v, want %q",
|
||||
c.addr, c.env, c.path, err, c.wantMsg)
|
||||
@@ -121,7 +117,7 @@ func TestLoadMnemonicFromKMS_RequiredArgs(t *testing.T) {
|
||||
func TestLoadMnemonicFromKMS_InvalidValue(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", "")
|
||||
withDial(t, &fakeKMS{value: "not a bip39"}, nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m", nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m")
|
||||
if err == nil || !strings.Contains(err.Error(), "not a valid BIP-39") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
@@ -130,7 +126,7 @@ func TestLoadMnemonicFromKMS_InvalidValue(t *testing.T) {
|
||||
func TestLoadMnemonicFromKMS_EmptyValue(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", "")
|
||||
withDial(t, &fakeKMS{value: ""}, nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m", nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m")
|
||||
if err == nil || !strings.Contains(err.Error(), "is empty") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
@@ -139,7 +135,7 @@ func TestLoadMnemonicFromKMS_EmptyValue(t *testing.T) {
|
||||
func TestLoadMnemonicFromKMS_DialError(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", "")
|
||||
withDial(t, nil, errors.New("dial timeout"))
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m", nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m")
|
||||
if err == nil || !strings.Contains(err.Error(), "dial timeout") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
@@ -149,7 +145,7 @@ func TestLoadMnemonicFromKMS_GetError(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", "")
|
||||
f := &fakeKMS{getErr: errors.New("kms 403")}
|
||||
withDial(t, f, nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m", nil)
|
||||
_, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m")
|
||||
if err == nil || !strings.Contains(err.Error(), "kms 403") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
@@ -158,30 +154,6 @@ func TestLoadMnemonicFromKMS_GetError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadMnemonicFromKMS_ThreadsIdentity asserts the *ServiceIdentity
|
||||
// argument reaches the dial seam unchanged. Without this guarantee
|
||||
// the production dial would silently drop identity and the KMS
|
||||
// server's consensus-auth gate would refuse the envelope at runtime.
|
||||
func TestLoadMnemonicFromKMS_ThreadsIdentity(t *testing.T) {
|
||||
t.Setenv("MNEMONIC", "")
|
||||
id, err := NewServiceIdentity(validBIP39, "luxd/staking-bootstrap")
|
||||
if err != nil {
|
||||
t.Fatalf("NewServiceIdentity: %v", err)
|
||||
}
|
||||
defer id.Wipe()
|
||||
f := &fakeKMS{value: validBIP39}
|
||||
g := withDial(t, f, nil)
|
||||
if _, err := LoadMnemonicFromKMS(context.Background(), "a", "main", "/m", id); err != nil {
|
||||
t.Fatalf("LoadMnemonicFromKMS: %v", err)
|
||||
}
|
||||
if !g.called {
|
||||
t.Fatal("dial seam not called")
|
||||
}
|
||||
if g.identity != id {
|
||||
t.Fatalf("identity not threaded: got %p want %p", g.identity, id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSecretPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, dir, name string
|
||||
|
||||
Reference in New Issue
Block a user