Files
kms/pkg/zapserver/consensus_e2e_test.go
T
343872f5da feat: native /v1/sdk enveloped secrets + threshold-sign plane (red-approved) (#9)
* test(zapserver): fix e2e seed/assert mismatch (secret-value vs sk_live_real)

TestConsensusE2E_ValidatorReadsSecret seeded "secret-value" but asserted
"sk_live_real" — the E2E read path was correct, the assertion constant
disagreed with the seed. Align the seed to the asserted live-secret value
so the green test actually guards the consensus read path.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* feat(zapserver): expose enveloped secret+sign plane over HTTP /v1/sdk

Decomplect the verify->authorize->dispatch core so the ZAP wire and a new
HTTP /v1/sdk transport share ONE implementation (Server.dispatch). Add:

- POST /v1/sdk/secrets: single RPC endpoint. The op is read from the
  SIGNED env.Op field, never the URL, so no framing can escalate a read
  identity into a write. Maps status byte -> HTTP code; replay masked as
  generic 403 'forbidden' (nonce ledger unprobeable); 4 MiB body cap.
- OpSign (0x0050, write/operator) + OpVerify (0x0051, read/validator):
  deliberate, documented widening of the authorizer. Dispatch to a narrow
  SignBackend that delegates to luxfi/mpc t-of-n; KMS holds no key
  material. nil backend -> clear 'signing not configured'.

18 new httptest end-to-end tests: validator read / operator write splits,
replay/stale/tamper/oversize/malformed/unknown-op rejection, and sign
delegation gated BEFORE the backend (forbidden sign never reaches MPC).
All prior authz tests stay green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* feat(sdksign): keys.Manager -> zapserver.SignBackend adapter

Sign delegates to the MPC t-of-n cluster (KMS holds no full key). Verify
is a local public-key check: ed25519 (corona) via stdlib crypto/ed25519
(real, tested with a live keypair); secp256k1 (bls) returns a precise
ErrVerifyBLSDelegated — a documented capability boundary (secp256k1
verification is owned by the chain/precompile layer), not a fake.

Tests (CGO=0, no MPC daemon needed): sign routes to the correct wallet
per scheme, result propagates; ed25519 valid verifies + tampered
sig/message do NOT; bls verify hits the delegated boundary; unknown
validator errors.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix(gomod): realign age@v1.5.0 + keys@v1.1.0 go.sum to re-tagged hashes

Both tags were force-moved (vendor/docs sync lineage, per LLM.md); go.sum
held the pre-re-tag zip hashes, so any GOWORK=off / clean-cache build
(CI) failed 'checksum mismatch' before compiling. Updated the two h1
lines to the authoritative direct-fetch hashes; the GOWORK=off build
re-verifies the downloaded bits against these, which is the proof they
are correct. Never bypassed the check.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* feat(kms): mount /v1/sdk enveloped secrets+sign surface in kmsd

Construct the zapserver.Server once from the loaded REK and back BOTH
transports with ONE consensus authorizer + ONE nonce ledger: the HTTP
/v1/sdk surface (SDK-facing) and the in-cluster ZAP wire. The authorizer
and ledger are built whenever the REK is present (fail-closed: refuse to
boot without them), independent of ZAP_PORT — so /v1/sdk is available
even when the ZAP wire is disabled. Wire the MPC-backed sign/verify via
sdksign.New(mgr) when a vault is configured; nil otherwise.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* docs(llm): document /v1/sdk enveloped secrets+sign surface

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 10:37:07 -07:00

265 lines
7.7 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// consensus_e2e_test.go — end-to-end test of the consensus-native auth
// flow over real ZAP transport.
//
// What this proves:
//
// 1. A client built with a mnemonic-derived ServiceIdentity (luxfi/keys)
// signs every envelope with ML-DSA-65.
// 2. The kmsd verifies the envelope, asks the ConsensusAuthorizer (an
// in-process Polaris-cert-attested validator set), and dispatches
// to the SecretStore on Allow.
// 3. Identities outside the validator set are rejected at the wire
// with statusForbid.
// 4. The full chain — handshake → envelope sign → consensus check →
// secret release — works under a real luxfi/zap Node.
//
// This is the "spin up a luxd, mock-or-real Polaris cert authorization,
// kmsd with consensus pointer, kmsclient with mnemonic" gate from the
// LP roadmap. The Polaris cert is mocked in-process via the consensus
// authority providers; the wire path is real.
package zapserver
import (
"context"
"crypto/rand"
"net"
"strconv"
"testing"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/keys"
"github.com/luxfi/kms/pkg/envelope"
"github.com/luxfi/kms/pkg/store"
"github.com/luxfi/kms/pkg/zapclient"
"github.com/luxfi/log"
"github.com/luxfi/zap"
badger "github.com/luxfi/zapdb"
)
// bootConsensusNativeServer spins up a real luxfi/zap.Node backed by
// an InProcessAuthorizer whose Validator + Operator authorities are
// caller-supplied. Same shape as the boot path on prod (kmsd) — only
// the authority data is in-memory instead of luxd-RPC sourced.
func bootConsensusNativeServer(t *testing.T, validators, operators []ids.NodeID) (addr string) {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
port := l.Addr().(*net.TCPAddr).Port
_ = l.Close()
n := zap.NewNode(zap.NodeConfig{
NodeID: "kms-e2e-" + strconv.Itoa(port),
ServiceType: "_kms._tcp",
Port: port,
NoDiscovery: true,
})
if err := n.Start(); err != nil {
t.Fatalf("zap.Node start: %v", err)
}
t.Cleanup(func() { n.Stop() })
opts := badger.DefaultOptions("").WithInMemory(true)
db, err := badger.Open(opts)
if err != nil {
t.Fatalf("zapdb open: %v", err)
}
t.Cleanup(func() { db.Close() })
mk := make([]byte, 32)
if _, err := rand.Read(mk); err != nil {
t.Fatalf("rand: %v", err)
}
secStore := store.NewSecretStore(db)
// Pre-populate so an authorized Get returns OK rather than NotFound.
sec, err := store.Seal(mk, "hanzo/commerce", "api-key", "prod", []byte("sk_live_real"))
if err != nil {
t.Fatalf("store.Seal: %v", err)
}
if err := secStore.Put(sec); err != nil {
t.Fatalf("store.Put: %v", err)
}
authz, err := NewInProcessAuthorizer(InProcessAuthorizerConfig{
Validators: NewStaticAuthorityProvider(validators),
Operator: NewStaticAuthorityProvider(operators),
})
if err != nil {
t.Fatalf("authorizer: %v", err)
}
srv := New(Config{
Store: secStore,
MasterKey: mk,
Authorizer: authz,
Logger: log.NewNoOpLogger(),
})
srv.Register(n)
return "127.0.0.1:" + strconv.Itoa(port)
}
// TestConsensusE2E_ValidatorReadsSecret — the canonical happy path:
// service identity is in the validator authority, signs envelopes
// over ZAP, server verifies + authorizes + releases.
func TestConsensusE2E_ValidatorReadsSecret(t *testing.T) {
// Service mnemonic is provisioned out-of-band by the kms-operator
// (k8s Secret). Same mnemonic + path on every pod replica → same
// NodeID byte-for-byte.
const serviceMnemonic = "abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about"
const servicePath = "hanzo/commerce"
ident, err := keys.NewServiceIdentity(serviceMnemonic, servicePath)
if err != nil {
t.Fatalf("identity: %v", err)
}
defer ident.Wipe()
// Consensus authority: the operator's snapshot says this NodeID is
// in the validator set (read-authorised).
addr := bootConsensusNativeServer(t,
[]ids.NodeID{ident.NodeID},
nil,
)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
hdr := envelope.IdentityHeader{
NodeID: ident.NodeID,
FullDigest: ident.FullDigest,
ServicePath: ident.ServicePath,
PublicKey: ident.PublicKey,
}
c, err := zapclient.DialWithConfig(ctx, zapclient.Config{
NodeID: "test-client",
PeerAddr: addr,
DefaultPath: "hanzo/commerce",
IdentityHeader: hdr,
Signer: ident,
})
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer c.Close()
value, err := c.Get(ctx, "api-key", "prod")
if err != nil {
t.Fatalf("Get: %v", err)
}
if value != "sk_live_real" {
t.Fatalf("Get value mismatch: got %q want %q", value, "sk_live_real")
}
}
// TestConsensusE2E_NonValidatorDenied — an identity NOT in the
// validator authority is rejected at the wire. statusForbid surfaces
// as zapclient.ErrForbidden.
func TestConsensusE2E_NonValidatorDenied(t *testing.T) {
const serviceMnemonic = "abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about"
knownIdent, err := keys.NewServiceIdentity(serviceMnemonic, "hanzo/known")
if err != nil {
t.Fatal(err)
}
defer knownIdent.Wipe()
strangerIdent, err := keys.NewServiceIdentity(serviceMnemonic, "stranger/service")
if err != nil {
t.Fatal(err)
}
defer strangerIdent.Wipe()
// Authority lists only the known identity. The stranger is
// outside the validator set.
addr := bootConsensusNativeServer(t,
[]ids.NodeID{knownIdent.NodeID},
nil,
)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
hdr := envelope.IdentityHeader{
NodeID: strangerIdent.NodeID,
FullDigest: strangerIdent.FullDigest,
ServicePath: strangerIdent.ServicePath,
PublicKey: strangerIdent.PublicKey,
}
c, err := zapclient.DialWithConfig(ctx, zapclient.Config{
NodeID: "test-client-stranger",
PeerAddr: addr,
DefaultPath: "hanzo/commerce",
IdentityHeader: hdr,
Signer: strangerIdent,
})
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer c.Close()
if _, err := c.Get(ctx, "api-key", "prod"); err == nil {
t.Fatalf("stranger Get must be rejected (got nil error)")
}
}
// TestConsensusE2E_OperatorWritesSecret — a NodeID in BOTH validator
// and operator authorities may Put. Writes are the kms-operator's
// privilege; read-only validators never touch the store.
func TestConsensusE2E_OperatorWritesSecret(t *testing.T) {
const serviceMnemonic = "abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about"
opIdent, err := keys.NewServiceIdentity(serviceMnemonic, "hanzo/kms-operator")
if err != nil {
t.Fatal(err)
}
defer opIdent.Wipe()
addr := bootConsensusNativeServer(t,
[]ids.NodeID{opIdent.NodeID},
[]ids.NodeID{opIdent.NodeID},
)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
hdr := envelope.IdentityHeader{
NodeID: opIdent.NodeID,
FullDigest: opIdent.FullDigest,
ServicePath: opIdent.ServicePath,
PublicKey: opIdent.PublicKey,
}
c, err := zapclient.DialWithConfig(ctx, zapclient.Config{
NodeID: "test-client-operator",
PeerAddr: addr,
DefaultPath: "hanzo/commerce",
IdentityHeader: hdr,
Signer: opIdent,
})
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer c.Close()
if err := c.PutAt(ctx, "hanzo/commerce", "rotation-key", "prod", "newSecret"); err != nil {
t.Fatalf("operator Put: %v", err)
}
got, err := c.GetAt(ctx, "hanzo/commerce", "rotation-key", "prod")
if err != nil {
t.Fatalf("operator Get-after-Put: %v", err)
}
if got != "newSecret" {
t.Fatalf("round-trip mismatch: got %q want newSecret", got)
}
}