mirror of
https://github.com/luxfi/kms.git
synced 2026-10-30 16:23:36 +00:00
* 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]>
109 lines
3.9 KiB
Go
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)
|