mirror of
https://github.com/hanzoai/kms-operator.git
synced 2026-10-30 18:32:57 +00:00
Closes the operator-side gap from the consensus-native-identity
agent. Adds `internal/bootstrap/` controller-runtime Runnable that:
- generates and persists the operator's `kms-operator-mnemonic`
Secret on first boot (no plaintext outside Secret);
- derives the operator NodeID via a byte-for-byte port of
`luxfi/keys.NewServiceIdentity` (pinned by determinism tests
against v1.0.10);
- ensures per-service mnemonic Secrets exist for every KMSSecret
CR (generate-on-miss, idempotent);
- pulls the live primary-network validator set from luxd
`platform.getCurrentValidators` on `KMS_CONSENSUS_TTL` (default
30s);
- composes the `kms-consensus-authority` Secret kmsd reads via
`KMS_CONSENSUS_FILE` and updates only when the canonical bytes
differ;
- fails closed: luxd unreachable leaves the prior Secret intact.
CRD adds two backward-compatible fields on `KMSSecret` /
`KMSPushSecret`: `spec.servicePath` and `spec.mnemonicSecretRef`.
Defaults derive both from the CR name so existing manifests are
untouched.
Mnemonic discipline: BIP-39 phrase NEVER hits CR status, operator
logs, or any non-Secret object. Only `{source: secret|env|generate,
nodeID: NodeID-...}` surfaces.
Tests (8/8 pass): determinism vs canonical NodeID vectors, bootstrap
end-to-end with sigs.k8s.io fake client, service-mnemonic discovery,
luxd-unreachable fail-closed, EnsureServiceIdentity idempotency.
Local reimplementation of the keys.ServiceIdentity derivation avoids
a transitive `luxfi/crypto/bls` → `supranational/blst` cgo dep whose
vendor layout is incompatible with `go mod vendor`. Determinism is
contractually enforced; see `service_identity_test.go`.
117 lines
4.2 KiB
Go
117 lines
4.2 KiB
Go
// Copyright (C) 2026, Hanzo Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// service_identity_test.go — byte-for-byte determinism contract with
|
|
// luxfi/keys.NewServiceIdentity.
|
|
//
|
|
// The test vectors below were captured from the canonical impl at
|
|
// ~/work/lux/keys/service_identity.go (commit 5c8083a, v1.0.10) using
|
|
// the BIP-39 reference mnemonic "abandon abandon abandon abandon abandon
|
|
// abandon abandon abandon abandon abandon abandon about".
|
|
//
|
|
// If this test fails, the kms-operator and the kmsd will derive
|
|
// different NodeIDs from the same mnemonic, which silently revokes
|
|
// every service's KMS access at deploy time. Treat any breakage here
|
|
// as an emergency: regenerate the vectors from upstream, audit the
|
|
// derivation diff (typically a constant renamed in luxfi/keys), and
|
|
// only then bump the vectors here.
|
|
|
|
package bootstrap
|
|
|
|
import "testing"
|
|
|
|
const canonicalReferenceMnemonic = "abandon abandon abandon abandon abandon abandon " +
|
|
"abandon abandon abandon abandon abandon about"
|
|
|
|
// canonicalVectors pins the NodeID string each (mnemonic, path) tuple
|
|
// MUST derive to. Captured via:
|
|
//
|
|
// cd ~/work/lux/keys && go run /tmp/derive_canonical.go
|
|
func canonicalVectors() map[string]string {
|
|
return map[string]string{
|
|
"hanzo/kms-operator": "NodeID-6MHbbHVyQULbQiY45LKq4FSe4eDAJXYFt",
|
|
"hanzo/commerce": "NodeID-JKgePFRhXAu5jcBuUpWZboahUqdgMD35V",
|
|
"hanzo/paas": "NodeID-FwEcuXgS5fS1VTdSGaAmE9i8kM7UUL7oT",
|
|
"lux/kms-operator": "NodeID-6HnEhTtDAah1D8Vx7yF5wobCRVEg7junj",
|
|
"hanzo/hanzo-base": "NodeID-GpEAPpXCkDkJTuZSrTyVm9mjcRV9JAgcx",
|
|
}
|
|
}
|
|
|
|
// TestDeriveNodeIDString_MatchesCanonical pins each canonical vector.
|
|
// Failure means the local derivation has diverged from luxfi/keys and
|
|
// would silently break consensus-native auth.
|
|
func TestDeriveNodeIDString_MatchesCanonical(t *testing.T) {
|
|
for path, want := range canonicalVectors() {
|
|
got, err := deriveNodeIDString(canonicalReferenceMnemonic, path)
|
|
if err != nil {
|
|
t.Errorf("derive(%q): %v", path, err)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("path %q: got %q, want %q (canonical drift)", path, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDeriveNodeIDString_DistinctPaths sanity-checks that different
|
|
// paths produce different NodeIDs. A collision here would mean the
|
|
// derivation collapsed two services into one identity.
|
|
func TestDeriveNodeIDString_DistinctPaths(t *testing.T) {
|
|
seen := make(map[string]string)
|
|
for _, path := range []string{
|
|
"hanzo/kms-operator", "hanzo/commerce", "hanzo/paas",
|
|
"lux/kms-operator", "hanzo/hanzo-base", "zoo/registry",
|
|
} {
|
|
id, err := deriveNodeIDString(canonicalReferenceMnemonic, path)
|
|
if err != nil {
|
|
t.Fatalf("%q: %v", path, err)
|
|
}
|
|
if prev, dup := seen[id]; dup {
|
|
t.Errorf("NodeID collision: %q and %q both → %s", prev, path, id)
|
|
}
|
|
seen[id] = path
|
|
}
|
|
}
|
|
|
|
// TestDeriveNodeIDString_PathTrimming verifies that surrounding slashes
|
|
// and whitespace do not change the derivation, matching luxfi/keys'
|
|
// trimServicePath semantics.
|
|
func TestDeriveNodeIDString_PathTrimming(t *testing.T) {
|
|
base, err := deriveNodeIDString(canonicalReferenceMnemonic, "hanzo/kms-operator")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, dirty := range []string{
|
|
"/hanzo/kms-operator/",
|
|
" hanzo/kms-operator ",
|
|
"hanzo/kms-operator\n",
|
|
"/hanzo/kms-operator",
|
|
} {
|
|
got, err := deriveNodeIDString(canonicalReferenceMnemonic, dirty)
|
|
if err != nil {
|
|
t.Errorf("derive(%q): %v", dirty, err)
|
|
continue
|
|
}
|
|
if got != base {
|
|
t.Errorf("path trimming diverged: %q → %s, base → %s", dirty, got, base)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDeriveNodeIDString_BadInput surfaces malformed mnemonic + empty
|
|
// path as errors rather than silent collapse.
|
|
func TestDeriveNodeIDString_BadInput(t *testing.T) {
|
|
if _, err := deriveNodeIDString("", "hanzo/foo"); err == nil {
|
|
t.Error("empty mnemonic should error")
|
|
}
|
|
if _, err := deriveNodeIDString(canonicalReferenceMnemonic, ""); err == nil {
|
|
t.Error("empty path should error")
|
|
}
|
|
if _, err := deriveNodeIDString(canonicalReferenceMnemonic, " "); err == nil {
|
|
t.Error("whitespace-only path should error")
|
|
}
|
|
if _, err := deriveNodeIDString("not a valid bip39 phrase", "hanzo/foo"); err == nil {
|
|
t.Error("invalid mnemonic should error")
|
|
}
|
|
}
|