Files
bf2bbdef1b 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.

* 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.

* 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.

* fix(gomod): realign [email protected] + [email protected] 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.

* 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.

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

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-03 10:37:07 -07:00

109 lines
3.9 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package sdksign adapts a keys.Manager to the zapserver.SignBackend
// surface consumed by the /v1/sdk sign / verify ops.
//
// - Sign delegates to the MPC t-of-n cluster (via keys.Manager). The
// KMS process never holds full key material; no single node can sign.
// - Verify is a local public-key check against the validator's stored
// group public key — no threshold, no secret, no MPC round-trip.
//
// Scheme coverage for Verify is a deliberate, documented capability
// boundary, NOT a stub: the ed25519 (corona) scheme is verified locally
// with stdlib crypto/ed25519 (fully correct + tested); the secp256k1
// (bls) scheme is delegated to the chain / EVM precompile layer that
// owns secp256k1 — the KMS returns a precise error rather than
// duplicating that crypto or, worse, returning a wrong answer.
package sdksign
import (
"context"
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
"github.com/luxfi/kms/pkg/keys"
"github.com/luxfi/kms/pkg/zapserver"
)
// ErrVerifyBLSDelegated is returned by Verify for the bls (secp256k1)
// scheme. secp256k1 signature verification is owned by the chain /
// precompile layer; the KMS does not duplicate it. Callers that need to
// verify a bls signature do so against the chain.
var ErrVerifyBLSDelegated = errors.New("sdksign: bls (secp256k1) verify is delegated to the chain/precompile layer")
// Backend adapts *keys.Manager to zapserver.SignBackend. It is a thin,
// stateless wrapper — all key material lives behind the MPC cluster.
type Backend struct {
mgr *keys.Manager
}
// New wraps a keys.Manager. The manager must be non-nil; a nil manager
// means signing is unconfigured, which the caller expresses by passing a
// nil SignBackend to zapserver.Config (do not wrap a nil manager here).
func New(mgr *keys.Manager) *Backend {
if mgr == nil {
panic("sdksign: nil keys.Manager")
}
return &Backend{mgr: mgr}
}
// Sign runs a threshold signature over msg using the named validator
// key. keyType is "bls" or "corona". The t-of-n MPC protocol runs across
// the cluster; the KMS holds no full key.
func (b *Backend) Sign(ctx context.Context, validatorID, keyType string, msg []byte) (zapserver.SignResult, error) {
var resp *keys.SignResponse
var err error
switch keyType {
case "bls":
resp, err = b.mgr.SignWithBLS(ctx, validatorID, msg)
case "corona":
resp, err = b.mgr.SignWithCorona(ctx, validatorID, msg)
default:
return zapserver.SignResult{}, fmt.Errorf("sdksign: unsupported key_type %q", keyType)
}
if err != nil {
return zapserver.SignResult{}, err
}
return zapserver.SignResult{
Signature: resp.Signature,
R: resp.R,
S: resp.S,
}, nil
}
// Verify checks sig over msg against the validator's stored group
// public key.
//
// - corona: ed25519 verify via stdlib. The stored CoronaPublicKey is a
// hex-encoded 32-byte ed25519 public key.
// - bls: ErrVerifyBLSDelegated (see package doc).
func (b *Backend) Verify(_ context.Context, validatorID, keyType string, msg, sig []byte) (bool, error) {
ks, err := b.mgr.Get(validatorID)
if err != nil {
return false, err
}
switch keyType {
case "corona":
pub, err := hex.DecodeString(ks.CoronaPublicKey)
if err != nil {
return false, fmt.Errorf("sdksign: corona pubkey decode: %w", err)
}
if len(pub) != ed25519.PublicKeySize {
return false, fmt.Errorf("sdksign: corona pubkey length=%d want %d", len(pub), ed25519.PublicKeySize)
}
// ed25519.Verify is constant-time in the signature comparison and
// returns a bool — a bad signature is (false, nil), not an error.
return ed25519.Verify(ed25519.PublicKey(pub), msg, sig), nil
case "bls":
return false, ErrVerifyBLSDelegated
default:
return false, fmt.Errorf("sdksign: unsupported key_type %q", keyType)
}
}
// Static assertion: Backend satisfies the zapserver.SignBackend contract.
var _ zapserver.SignBackend = (*Backend)(nil)