Files
magnetar/ref/go/pkg/thbs/thbs.go
T
Hanzo AI 3951c5a46c v0.4.2: TRUE threshold HBS (pkg/thbs) + public-BFT-safe aggregate-sigs
Introduce TWO new signing modes plus a clarifying rename of the legacy
v0.1 path.

1. pkg/thbs/ — TRUE threshold hash-based signatures in the McGrew et al.
   sense (IACR ePrint 2019/793 / IRTF draft-mcgrew-hash-sigs line). For
   HBS schemes the signature reveals SELECTED secret elements (WOTS+
   chain heads selected by the message-digest base-w digits + FORS
   secret leaves selected by the FORS index digest). Threshold signing
   here Shamir-shares each secret element across the committee; for
   each message parties release shares ONLY for the SELECTED elements;
   the combiner Lagrange-reconstructs just those elements; the
   verifier sees an ordinary HBS-style signature.

   Subpackage layout:
   - thbs.go   — types per the requested API shape (DKGConfig,
                 PublicKey, PrivateShare, PartialSignature,
                 FinalSignature, Evidence, EquivocationError).
   - dealer.go — v1 dealer-backed DKG. The dealer Shamir-shares each
                 secret element across the committee via per-byte
                 GF(257); the dealer seed is zeroised before return.
                 v2 will replace with public DKG.
   - wots.go   — WOTS+ (Winternitz w=16, FIPS 205-style base-w digit
                 + checksum decomposition; cSHAKE-256 hash chains).
   - fors.go   — FORS (k subtrees, height a, per-leaf binary Merkle).
   - tree.go   — public Merkle tree over WOTS+ leaf-roots.
   - slot.go   — anti-equivocation slot guard. Same-slot-different-
                 digest emits Evidence{party, slot, digest_a/b,
                 share_a/b} for the slashing layer.
   - sign.go   — SignShare + Aggregate + Verify.
   - shamir.go — byte-wise Shamir over GF(257); elements are shared
                 directly, not seeds.
   - hash.go   — cSHAKE-256 with the "Magnetar-THBS" function-name
                 and per-tag domain separation.

   24 unit tests pin every invariant: no-seed-exposure,
   selected-elements-only for both WOTS+ and FORS, t-of-n threshold,
   anti-equivocation, cross-slot/cross-message rejection, tamper
   detection.

   Honest v1 scope (documented in THBS-SPEC.md):
   - Setup is DEALER-BACKED. v2 replaces with public DKG.
   - Helper data shipped alongside the public key (McGrew et al.
     permit this).
   - Verifier is a CUSTOM HBS verifier; v3 will produce FIPS 205-byte-
     identical output.

   Hard invariant enforced by the package shape:
     OK:        reconstructElement(slot, elementID, shares)
     Forbidden: ReconstructSeed, ReconstructPrivateKey,
                ExpandPrivateKey, DeriveAllFutureElements
   The only Reconstruct symbol in thbs/*.go is the unexported
   reconstructElement in shamir.go.

2. pkg/magnetar/aggregate.go — public-BFT-safe N-of-N collected
   signatures. Each validator holds its OWN SLH-DSA keypair (no DKG,
   no shared seed). Primitives: GenerateValidatorKey, SignBundle,
   VerifyBundle, AggregateSignatures, VerifyAggregated. 10 tests.

3. pkg/magnetar/combine.go — Combine renamed to
   CombineWithSeedReconstruction throughout the package + callers
   (e2e_test, threshold_test, n1_byte_equality_test, fuzz_test,
   genkat, ct/dudect bridge) to make the TEE-only trust caveat
   explicit at the API surface. KAT byte-equality preserved: the
   function body is unchanged.

Refs: McGrew, Fluhrer, Gazdag, Kampanakis, Morton, Westerbaan,
"Coalition and Threshold Hash-Based Signatures" (IACR ePrint 2019/793);
Bonte, Smart, Tan, "Threshold SPHINCS+", PKC 2024 (the negative result
informing our v1-ships-a-custom-HBS-verifier scope choice).
2026-05-21 17:34:41 -07:00

361 lines
13 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package thbs implements Magnetar-THBS v1: true threshold hash-based
// signatures in the sense of McGrew, Fluhrer, Gazdag, Kampanakis, Morton,
// Westerbaan, "Coalition and Threshold Hash-Based Signatures"
// (IACR ePrint 2019/793 and the IRTF draft-mcgrew-hash-sigs line of
// work).
//
// CRITICAL DISTINCTION (this is what makes the package "true" threshold):
//
// - For HBS schemes a signature reveals SELECTED secret elements
// (WOTS+ chain values selected by message-digest chunks, and FORS
// leaves selected by the FORS index digest).
// - Threshold signing in this package shares the SECRET ELEMENTS
// (the WOTS+ leaf chain heads and the FORS secret leaves) across
// parties via byte-wise Shamir over GF(257).
// - For each message, parties release shares ONLY for the elements
// SELECTED by the message digest. Combiner Lagrange-reconstructs
// just those elements; the verifier sees an ordinary HBS-style
// signature.
//
// This is NOT "collect N independent SLH-DSA signatures and average
// them"; that's the wrong thing. This is the right thing.
//
// V1 SCOPE — honest:
//
// - Setup is DEALER-BACKED. A trusted dealer generates the shared
// WOTS+/FORS material and distributes shares. (Public DKG = v2.)
// - Helper data (the Merkle authentication paths and the public-key
// chain endpoints) is shipped alongside the public key. McGrew et
// al. allow this.
// - The verifier is a CUSTOM HBS verifier defined in this package,
// NOT FIPS 205 SLH-DSA. v3 will produce FIPS 205-byte-identical
// output; that is significantly more involved and is out of scope
// for v1.
// - Anti-equivocation: each party stores slot_id -> message_digest
// and refuses (with evidence) any subsequent sign attempt against
// the same slot under a different digest.
//
// HARD INVARIANT (cf. THBS-SPEC.md):
//
// OK: ReconstructElement(slot, elementID, shares)
// Forbidden: ReconstructSeed, ReconstructPrivateKey,
// ExpandPrivateKey, DeriveAllFutureElements
//
// The only Reconstruct symbol in this package is the file-scope
// unexported reconstructElement in sign.go. No exported API returns
// future-signing material.
//
// Reference parameter set (one-slot WOTS+ + FORS Merkle, n=24,
// w=16, FORS k=14 a=12, tree height H=10):
//
// WOTS+: 51 chain values per leaf; 24-byte chain elements
// FORS: 14 of 4096 leaves per signature
// Tree: height 10 (1024 slots)
//
// These parameters are not FIPS 205 wire-compatible; that's v3.
package thbs
import "errors"
// HBSParams captures the concrete shape of the threshold HBS instance.
//
// All sizes are in bytes unless otherwise noted.
type HBSParams struct {
// N is the hash output size in bytes (e.g. 24 for SHAKE-192-like).
N int
// Winternitz parameter; w in {4, 16, 256}. Number of chain values per
// WOTS+ leaf is ceil(8n / log2 w) + checksum chains. v1 fixes w=16
// (LogW=4), giving 51 chains per leaf for n=24.
W int
LogW int
// WOTSChains is the total number of WOTS+ chains per signature.
// = (8n / LogW) + checksum chains.
WOTSChains int
// FORSK is the number of FORS Merkle trees per signature (k).
// Each tree has 2^FORSA leaves; the signature reveals 1 leaf and the
// auth path per tree.
FORSK int
// FORSA is the height of each FORS tree (a). 2^a leaves per tree.
FORSA int
// Height is the height of the top Merkle tree over WOTS+ leaves.
// 2^Height slots in this v1 instance.
Height int
}
// DefaultParams returns the v1 reference parameter set. Not FIPS 205
// wire-compatible; see THBS-SPEC.md for the v3 roadmap.
func DefaultParams() HBSParams {
// n=24, w=16 -> WOTS+ chains = 8*24/4 + checksum
// checksum chains for w=16, n=24: len2 = floor(log2(48*15)/4) + 1 = 3
// (48 message chains * (w-1) = 720; log2(720) ≈ 9.49; /4 -> 2; +1 -> 3)
return HBSParams{
N: 24,
W: 16,
LogW: 4,
WOTSChains: 51, // 48 message + 3 checksum
FORSK: 14,
FORSA: 12,
Height: 10,
}
}
// Participant identifies one party in a dealer-backed THBS committee.
type Participant struct {
ID PartyID
EvalPoint uint16 // Shamir x-coordinate in [1, 257); MUST be distinct per party
}
// PartyID is the canonical party identifier. 32 bytes wide to host any
// upstream identity (Lux validator ID, Hanzo IAM hash, etc.).
type PartyID [32]byte
// DKGConfig parametrises a dealer-backed v1 DKG ceremony.
type DKGConfig struct {
ChainID []byte // domain separation tag (e.g. Lux chain ID)
Epoch uint64 // monotonic epoch counter for key versioning
Threshold uint16 // t in t-of-n; >= 1 and <= len(Participants)
Participants []Participant // committee
Params HBSParams
}
// PublicKey is the threshold-HBS public key.
//
// Root is the Merkle root over the WOTS+ public-key leaves for every slot
// in [0, 2^Height). DKGTranscript is the transcript hash binding this
// keygen ceremony (committee, params, epoch, chain ID). QualifiedSet
// is a bitmap marking which dealer-broadcast envelopes the committee
// agreed on (v1: all parties; v2 may carry survivors of complaint
// rounds).
type PublicKey struct {
Params HBSParams
Root []byte
DKGTranscript []byte
QualifiedSet Bitmap
// HelperData carries the dealer-precomputed authentication paths
// AND the public WOTS+ chain endpoints (the "compressed" leaf
// values W_i = H_w-1(x_i) per chain). McGrew et al. allow this:
// the chain endpoints are NOT secret (they're already part of the
// signature when used) and shipping them up-front lets the
// combiner construct the final signature without contacting the
// dealer again.
HelperData *HelperData
}
// HelperData is the public auxiliary material the dealer emits alongside
// the threshold key. It is the "we precomputed the public tree for you"
// payload that McGrew et al. call out as a v1 simplification.
type HelperData struct {
// LeafRoots[slot] is the Merkle leaf at slot index `slot`, i.e.
// H(W_{slot, 0} || W_{slot, 1} || ... || W_{slot, WOTSChains-1}).
// Length = 2^Height; each entry = N bytes.
LeafRoots [][]byte
// AuthPaths[slot] is the authentication path from leaf `slot` to the
// root. Length = Height nodes, each N bytes. Public information.
AuthPaths [][][]byte
// ChainEndpoints[slot] are the public WOTS+ chain endpoints
// W_{slot, j} = H^{w-1}(x_{slot, j}) for j in [0, WOTSChains).
// Each entry = N bytes. The combiner uses these to validate that
// reconstructed chain heads x_{slot, j} chain to the right
// endpoint.
ChainEndpoints [][][]byte
// FORSPubRoots[slot][tree] is the FORS subtree root for slot's
// tree `tree`. FORSK trees per slot. Each entry = N bytes. The
// combiner uses these (with the per-leaf auth paths in the FORS
// section) to validate the FORS portion of the signature.
FORSPubRoots [][][]byte
// FORSAuthPaths[slot][tree][leafIdx] is the FORS authentication
// path inside tree `tree` at slot `slot` for leaf index `leafIdx`.
// We materialise ALL FORSA-step paths so the combiner does not
// have to reconstruct the tree. Each path is FORSA entries of N
// bytes.
FORSAuthPaths [][][][]byte
}
// Bitmap is a compact dense bitmap over committee positions. Bit i
// indexes the i-th participant in the DKGConfig.Participants list.
type Bitmap []byte
// Set marks bit i.
func (b Bitmap) Set(i int) {
b[i>>3] |= 1 << uint(i&7)
}
// Has reports whether bit i is set.
func (b Bitmap) Has(i int) bool {
if i>>3 >= len(b) {
return false
}
return b[i>>3]&(1<<uint(i&7)) != 0
}
// NewBitmap allocates a Bitmap large enough to address `bits` positions,
// with every bit cleared.
func NewBitmap(bits int) Bitmap {
return make(Bitmap, (bits+7)/8)
}
// PrivateShare is one party's local secret material. It is NEVER a
// reconstructable seed; it is a collection of per-element Shamir shares
// keyed by (slot, elementID).
type PrivateShare struct {
PartyID PartyID
Epoch uint64
// ElementShares is the per-element share store. Keyed by
// (slot, elementType, elementIdx). Values are GF(257) per-byte
// shares of length N.
ElementShares ShareStore
// AntiEquivState records per-slot the digest the party has
// committed to. Reused-slot detection lives here.
AntiEquivState StateStore
}
// ElementType distinguishes the two threshold-shared element families:
// WOTS+ chain heads and FORS secret leaves.
type ElementType uint8
const (
// ElementWOTS is a WOTS+ chain head x_{slot, j} prior to any hash
// chain steps. Reconstructed value is the secret leaf the
// combiner then chains forward to the public endpoint.
ElementWOTS ElementType = 1
// ElementFORS is a FORS secret leaf sk_{slot, tree, leaf}. The
// combiner reveals these directly.
ElementFORS ElementType = 2
)
// ElementID identifies one threshold-shared element.
//
// For ElementWOTS: (Slot, ChainIdx) where ChainIdx in [0, WOTSChains).
// For ElementFORS: (Slot, TreeIdx, LeafIdx) where TreeIdx in [0, FORSK)
// and LeafIdx in [0, 2^FORSA).
type ElementID struct {
Type ElementType
Slot uint32
ChainIdx uint16
TreeIdx uint16
LeafIdx uint32
}
// ShareStore is a map-of-shares keyed by element identity. Concrete
// implementation in dealer.go.
type ShareStore map[ElementID][]uint16
// StateStore is a slot -> message-digest map. Concrete implementation
// is in slot.go (defensive copy via slotStateImpl).
type StateStore map[Slot][32]byte
// Slot identifies a one-shot WOTS+ leaf slot (the equivalent of an
// XMSS leaf in HBS). Same slot used twice with distinct messages =
// identifiable abort.
type Slot uint32
// PartialSignature is one party's contribution to a threshold signature.
type PartialSignature struct {
PartyID PartyID
SlotID [32]byte
MessageDigest [32]byte
// Shares are the GF(257) per-byte share values for the SELECTED
// elements only. Other element shares MUST NOT appear here; the
// invariant test TestTHBS_WOTS_ReconstructsOnlySelectedChainValues
// asserts this.
Shares []ElementShare
// Proofs are per-share consistency proofs (in v1: a cSHAKE256 MAC
// binding the share to the slot, message digest, and party ID).
// The combiner verifies each before reconstructing.
Proofs []ShareProof
}
// ElementShare is one party's share for one element.
type ElementShare struct {
ID ElementID
X uint16 // Shamir evaluation point (matches Participant.EvalPoint)
Y []uint16
Steps uint8 // For ElementWOTS: the number of hash chain steps to apply
// to the reconstructed value. = b_j (the j-th base-w
// digit of the message digest). For ElementFORS: ignored.
}
// ShareProof binds an ElementShare to (PartyID, SlotID, MessageDigest)
// so the combiner can reject malformed contributions.
type ShareProof struct {
Tag [32]byte
}
// FinalSignature is the assembled threshold-HBS signature.
//
// Wire layout (v1): SlotID || ForsSig || WotsSig0 || ... || WotsSigK ||
// AuthPath. ForsSig carries the FORS leaves + per-leaf auth paths;
// WotsSig is the WOTS+ chain values for this slot; AuthPath is the
// top-tree Merkle path from leaf SlotID to Root.
//
// V1 verifier (Verify in sign.go) recomputes Root from this signature
// and rejects if it does not match PublicKey.Root.
type FinalSignature struct {
Params HBSParams
SlotID [32]byte
ForsSig []byte
WotsSigs [][]byte // WotsSigs[j] = chain value at chain j post-Steps[j] hash steps
AuthPaths [][]byte // Top-tree auth path (Height entries of N bytes)
}
// Errors returned by the thbs package.
var (
ErrInvalidThreshold = errors.New("thbs: invalid threshold (n < t or t < 1)")
ErrInvalidParams = errors.New("thbs: invalid HBS params")
ErrDuplicateParty = errors.New("thbs: duplicate party in committee")
ErrZeroEvalPoint = errors.New("thbs: evaluation point x=0 is reserved")
ErrTooManyParties = errors.New("thbs: committee too large for GF(257)")
ErrSlotOutOfRange = errors.New("thbs: slot out of range")
ErrUnknownParty = errors.New("thbs: party not in committee")
ErrInsufficient = errors.New("thbs: insufficient partial signatures")
ErrInconsistent = errors.New("thbs: partial signatures disagree on slot/message")
ErrShareCount = errors.New("thbs: partial signature has wrong number of shares")
ErrShareProof = errors.New("thbs: share consistency proof failed")
ErrShareSelection = errors.New("thbs: partial signature includes unselected element shares")
ErrChainEndpoint = errors.New("thbs: reconstructed WOTS+ chain endpoint mismatch")
ErrFORSPub = errors.New("thbs: reconstructed FORS root mismatch")
ErrVerifyRoot = errors.New("thbs: signature does not chain to public-key root")
ErrEquivocation = errors.New("thbs: party signed two distinct messages under same slot")
)
// Evidence carries proof of identifiable double-signing. The
// consensus layer slashes the offending party using this payload.
type Evidence struct {
PartyID PartyID
SlotID [32]byte
DigestA [32]byte
ShareA PartialSignature // the first PartialSignature emitted at SlotID
DigestB [32]byte
ShareB PartialSignature // the conflicting PartialSignature
}
// EquivocationError wraps ErrEquivocation with the evidence payload so
// callers can recover it via errors.As.
type EquivocationError struct {
Err error
Evidence Evidence
}
// Error returns the underlying error message.
func (e *EquivocationError) Error() string { return e.Err.Error() }
// Unwrap allows errors.Is to match ErrEquivocation.
func (e *EquivocationError) Unwrap() error { return e.Err }