diff --git a/ref/go/pkg/magnetar/fors_threshold_open.go b/ref/go/pkg/magnetar/fors_threshold_open.go new file mode 100644 index 0000000..b0110ba --- /dev/null +++ b/ref/go/pkg/magnetar/fors_threshold_open.go @@ -0,0 +1,530 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package magnetar + +// fors_threshold_open.go --- the ONE concretely-buildable, genuinely +// no-reconstruct, stock-FIPS-205-verifiable Track-B sub-protocol: +// DISTRIBUTED FORS-LEAF THRESHOLD OPENING. +// +// ===================================================================== +// WHAT THIS PROVES (and what it does NOT) +// ===================================================================== +// +// This file builds the FORS bottom of an SLH-DSA signature from a +// t-of-n committee such that: +// +// * NO global seed / full secret key is ever reconstructed. There is +// no master seed in this construction at all: the FORS leaf secrets +// are secret-shared DIRECTLY (jointly-random, byte-wise GF(257) +// Shamir). At sign time the combiner interpolates ONLY the k +// message-selected one-time leaf secrets --- each exactly n bytes +// (one leaf), guarded by assertLeafWidth so the path is structurally +// incapable of widening to the SeedSize-byte master. (Approach B of +// the paper: "threshold leaf opening".) +// +// * The assembled FORS signature is BYTE-IDENTICAL to the centralized +// FIPS 205 forsSign output on the same leaf material, and its FORS +// public key reconstructs via the UNMODIFIED FIPS 205 Algorithm 17 +// (forsPkFromSig) --- the verifier's own FORS step, proven +// byte-equal to cloudflare/circl by TestSlhdsaInternal_ByteEqualToCirclSign. +// +// * One-time material is BURNED: every opened leaf address is recorded +// in the BurnLedger; a second open of the same FORS keypair under a +// different message digest is refused as a slashable equivocation. +// +// What it does NOT do: it is NOT a full SLH-DSA signature. A full +// signature additionally needs the hypertree --- d WOTS+ signatures plus +// XMSS authentication paths whose sibling nodes are secret-derived WOTS+ +// public keys over a 2^h-leaf tree. Those siblings cannot be opened at +// sign time (that would expose ~2^h' one-time WOTS+ keypairs and destroy +// one-time security) and cannot be precomputed without either a 2^h +// materialisation (infeasible for h = 63..68) or a keygen-time MPC over +// SHAKE (the Cozzo-Smart wall). Hence the FULL Track-B signer fails +// closed (threshold_noreconstruct.go); THIS file is a proven COMPONENT. +// +// ===================================================================== +// KEYGEN HONESTY +// ===================================================================== +// +// The PUBLIC FORS material (auth paths + pkFORS) is a deterministic hash +// of the secret leaves; computing it from secret-shared leaves without +// exposing the unopened leaves requires either an MPC over F at keygen +// or a setup-TCB dealer. DistributedForsSetup below uses a setup-TCB +// dealer (it knows the genuine leaves for the keypair it is provisioning +// and shares them) --- this is the SAME "dealer-in-the-TCB-for-setup-only" +// pattern the production Magnetar v1.0 setup uses, and it is honestly the +// keygen obstruction, documented in the paper. The NOVELTY proven here +// is no-reconstruct SIGNING: once setup returns, no party holds the +// leaves; the combiner opens only the per-message selected one-time +// leaves and never the full FORS key. + +import ( + "encoding/binary" + "errors" + "io" +) + +// Customisation tag for the FORS-open commit. Distinct from every other +// Magnetar tag so a cross-protocol replay deterministically mismatches. +const tagForsOpenCommit = "MAGNETAR-FORS-THRESHOLD-OPEN-V1" + +// Errors specific to the distributed FORS-leaf opening. +var ( + // ErrForsSetupParams is returned when the FORS open is invoked for a + // parameter set the internal FIPS 205 engine does not implement. + ErrForsSetupParams = errors.New("magnetar/fors-open: parameter set not implemented by the FIPS 205 engine") + + // ErrForsQuorum is returned when fewer than threshold partial opens + // are presented for some selected leaf. + ErrForsQuorum = errors.New("magnetar/fors-open: insufficient partial opens for a selected leaf") + + // ErrForsShape is returned when a partial-open payload has the wrong + // dimensions. + ErrForsShape = errors.New("magnetar/fors-open: partial-open payload has wrong shape") + + // ErrForsCommitMismatch is returned when a partial open does not + // re-derive its commit against the published (addr, digest). + ErrForsCommitMismatch = errors.New("magnetar/fors-open: partial-open commit does not re-derive") +) + +// ForsKeypairAddr identifies one FORS keypair in the SLH-DSA hypertree: +// the XMSS tree address (3 words, FIPS 205 §4.2) and the keypair (XMSS +// leaf) index. In a full signature this is selected by R from the +// message; the component protocol takes it as an explicit input. +type ForsKeypairAddr struct { + IdxTree [3]uint32 + IdxLeaf uint32 +} + +// forsAddrBuf builds the FIPS 205 address template forsSign / forsPkFromSig +// expect for this keypair (layer 0, type FORS-tree, keypair address set). +func (a ForsKeypairAddr) forsAddrBuf() adrsBuf { + var adr adrsBuf + adr.setLayerAddress(0) + adr.setTreeAddress(a.IdxTree) + adr.setTypeAndClear(adrsTypeForsTree) + adr.setKeyPairAddress(a.IdxLeaf) + return adr +} + +// ForsPublicMaterial is everything the combiner and any third-party +// verifier need that carries NO secret: the keypair address, the message +// digest md (which selects the leaves), the k selected leaf indices, the +// k public authentication paths, and the published FORS public key. +// +// Published at setup. A verifier checks an assembled FORS signature by +// running stock FIPS 205 forsPkFromSig and comparing to PkFors. +type ForsPublicMaterial struct { + Params *Params + Addr ForsKeypairAddr + Digest []byte // md (>= ceil(k*a/8) bytes); public + Indices []uint32 // k selected leaf indices, derived from md (public) + AuthPaths [][]byte // k auth paths, each a*n bytes (public) + PkFors []byte // n-byte FORS public key T_k(roots) (public) + PkSeed []byte // n-byte FIPS 205 public seed (public) + Threshold int // t: leaves reconstruct from any t partial opens +} + +// ForsLeafShareMatrix is the secret-shared FORS leaf material produced by +// DistributedForsSetup. Shares[partyIdx][forsTree] is party (partyIdx+1)'s +// byte-wise GF(257) Shamir share of the n-byte leaf secret selected in +// FORS sub-tree forsTree. NO party holds more than its own row; the +// genuine leaves exist only inside the setup-TCB dealer and are erased +// before this matrix is returned. +type ForsLeafShareMatrix struct { + Params *Params + Addr ForsKeypairAddr + N int // committee size + T int // threshold + // Shares[partyIdx][forsTree] is one party's share of one leaf secret. + Shares [][]thbsseShare +} + +// PartyRow returns party (partyIndex, 1-based) view: its column of leaf +// shares across the k FORS sub-trees. This is the ONLY share material a +// party holds; it never sees other parties' rows or the genuine leaves. +func (m *ForsLeafShareMatrix) PartyRow(partyIndex uint32) ([]thbsseShare, error) { + if partyIndex < 1 || int(partyIndex) > m.N { + return nil, ErrForsShape + } + return m.Shares[partyIndex-1], nil +} + +// forsSelectedIndices replays FIPS 205 §8.3 Algorithm 16 index parsing: +// for digest md it returns the k leaf indices (one per FORS sub-tree) +// selected for signing. Public function of md only. +func forsSelectedIndices(p *internalParams, digest []byte) []uint32 { + indices := make([]uint32, p.k) + in, bits, total := 0, uint32(0), uint32(0) + maskA := (uint32(1) << p.a) - 1 + for i := uint32(0); i < p.k; i++ { + for bits < p.a { + total = (total << 8) + uint32(digest[in]) + in++ + bits += 8 + } + bits -= p.a + indices[i] = (total >> bits) & maskA + } + return indices +} + +// DistributedForsSetup is the setup-TCB dealer for one FORS keypair. It +// derives the genuine FORS leaf material from a setup seed (the dealer is +// in the TCB FOR SETUP ONLY), shares the k message-selected leaf secrets +// across the committee, publishes the auth paths + pkFORS, and erases the +// genuine leaves before returning. +// +// Production deployments replace this with a dealerless DKG + MPC-over-F +// keygen (the documented keygen obstruction); the SIGN-side wire shape +// (ForsLeafShareMatrix + ForsPublicMaterial) is unchanged. +// +// seed is a FIPS 205 SeedSize-byte scheme seed; addr is the FORS keypair +// to provision; digest is md (selects the leaves); (n, t) is the +// committee. coeffStream feeds the Shamir polynomials (stretched if short). +func DistributedForsSetup( + params *Params, + seed []byte, + addr ForsKeypairAddr, + digest []byte, + n, t int, + coeffStream []byte, +) (*ForsLeafShareMatrix, *ForsPublicMaterial, error) { + if err := params.Validate(); err != nil { + return nil, nil, err + } + internal, ok := internalParamsForMode(params.Mode) + if !ok { + return nil, nil, ErrForsSetupParams + } + if t < 1 || n < t || n > MaxCommittee257 { + return nil, nil, ErrInvalidThreshold + } + if len(seed) != params.SeedSize { + return nil, nil, ErrSeedSize + } + if len(digest) < int((internal.k*internal.a+7)/8) { + return nil, nil, ErrForsShape + } + + // --- SETUP TCB: derive the genuine FORS material from the seed. --- + // Expand the seed to (skSeed || skPrf || pkSeed) exactly as FIPS 205 + // §10.1 DeriveKey does, then build the secret-side PRF closure. This + // is the ONLY place the seed expansion exists; it lives inside this + // dealer and is erased before return. + derived := make([]byte, 3*internal.n) + shakeIntoCat(derived, seed) + skSeedSeg := derived[:internal.n] + pkSeed := append([]byte(nil), derived[2*internal.n:3*internal.n]...) + prfOut := makePRFClosure(internal, pkSeed, skSeedSeg) + + // Centralized reference FORS signature on md at addr: this gives both + // the genuine leaf secrets (to share) and the public auth paths (to + // publish), byte-for-byte as FIPS 205 forsSign emits them. + adr := addr.forsAddrBuf() + refSig := make([]byte, internal.forsSigSize()) + forsSign(internal, refSig, pkSeed, &adr, digest, prfOut) + + // Genuine pkFORS via the verifier's own Algorithm 17. + pkFors := make([]byte, internal.n) + forsPkFromSig(internal, pkFors, pkSeed, &adr, digest, refSig) + + indices := forsSelectedIndices(internal, digest) + + pairSize := int(internal.forsPairSize()) + leafN := int(internal.n) + authLen := int(internal.forsAuthPathSize()) + + // Extract genuine leaves + public auth paths from refSig. + authPaths := make([][]byte, internal.k) + shares := make([][]thbsseShare, n) // shares[party][forsTree] + for p := 0; p < n; p++ { + shares[p] = make([]thbsseShare, internal.k) + } + + // Share each leaf secret independently (byte-wise GF(257) Shamir). + // CRITICAL: each leaf gets an INDEPENDENT Shamir coefficient stream + // (domain-separated by leaf index). Reusing coefficients across leaves + // would let an adversary holding one share of two leaves learn their + // GF(257) difference (the coefficient terms cancel) --- a real leak. + needed := (t - 1) * leafN * 2 + if needed < 2 { + needed = 2 + } + for i := 0; i < int(internal.k); i++ { + skOff := i * pairSize + leaf := refSig[skOff : skOff+leafN] // genuine leaf secret (n bytes) + authPaths[i] = append([]byte(nil), refSig[skOff+leafN:skOff+leafN+authLen]...) + + // Per-leaf independent coefficient stream: cSHAKE256(coeffStream || + // leaf_index). Distinct customisation-bound output per leaf. + csSeed := make([]byte, 0, len(coeffStream)+4) + csSeed = append(csSeed, coeffStream...) + csSeed = binary.BigEndian.AppendUint32(csSeed, uint32(i)) + cs := cshake256(csSeed, needed, tagForsOpenCommit) + + gf := make([]uint16, leafN) + for b := 0; b < leafN; b++ { + gf[b] = uint16(leaf[b]) + } + leafShares, err := thbsseDealRandomGF(gf, n, t, cs) + if err != nil { + zeroizeBytes(derived) + zeroizeBytes(refSig) + return nil, nil, err + } + for p := 0; p < n; p++ { + shares[p][i] = leafShares[p] + } + zeroizeU16Slice(gf) + } + + pub := &ForsPublicMaterial{ + Params: params, + Addr: addr, + Digest: append([]byte(nil), digest...), + Indices: indices, + AuthPaths: authPaths, + PkFors: pkFors, + PkSeed: pkSeed, + Threshold: t, + } + mat := &ForsLeafShareMatrix{Params: params, Addr: addr, N: n, T: t, Shares: shares} + + // Erase the setup-TCB secret material. After this point no genuine + // leaf, no skSeed, no seed expansion exists anywhere; only the shares + // (held one-row-per-party) and the public material remain. + zeroizeBytes(refSig) + zeroizeBytes(derived) + + return mat, pub, nil +} + +// ForsPartialOpen is one party's release of its shares of the k selected +// one-time leaf secrets, bound by a commit to (addr, digest, party) so a +// cross-context replay deterministically mismatches and so the burn +// ledger can attribute reuse. The LeafShares carry the party's GF(257) +// Shamir value for each selected leaf; the share at a single evaluation +// point is information-theoretically independent of the leaf for any +// party holding < t shares. +type ForsPartialOpen struct { + PartyIndex uint32 // 1-based committee position == Shamir eval point + Commit [32]byte // cSHAKE256(addr || digest || party || leaf-shares) + LeafShares [][]uint16 // [forsTree][n] GF(257) share values at PartyIndex +} + +// PartialOpen builds party (partyIndex)'s ForsPartialOpen from its row of +// the share matrix. The commit binds the release to the public (addr, +// digest) so the same shares cannot be replayed for a different keypair +// or message. +func (m *ForsLeafShareMatrix) PartialOpen(partyIndex uint32, digest []byte) (ForsPartialOpen, error) { + row, err := m.PartyRow(partyIndex) + if err != nil { + return ForsPartialOpen{}, err + } + leafShares := make([][]uint16, len(row)) + for i := range row { + leafShares[i] = append([]uint16(nil), row[i].Y...) + } + open := ForsPartialOpen{PartyIndex: partyIndex, LeafShares: leafShares} + open.Commit = forsOpenCommit(m.Addr, digest, partyIndex, leafShares) + return open, nil +} + +// forsOpenCommit binds a partial open to (addr, digest, party, shares). +func forsOpenCommit(addr ForsKeypairAddr, digest []byte, party uint32, leafShares [][]uint16) [32]byte { + buf := make([]byte, 0, 64+len(digest)) + for _, w := range addr.IdxTree { + buf = binary.BigEndian.AppendUint32(buf, w) + } + buf = binary.BigEndian.AppendUint32(buf, addr.IdxLeaf) + buf = append(buf, digest...) + buf = binary.BigEndian.AppendUint32(buf, party) + for _, ls := range leafShares { + for _, v := range ls { + buf = binary.BigEndian.AppendUint16(buf, v) + } + } + var out [32]byte + copy(out[:], cshake256(buf, 32, tagForsOpenCommit)) + return out +} + +// digest32 hashes md into the 32-byte burn-ledger digest key. +func digest32(md []byte) [32]byte { + var out [32]byte + copy(out[:], cshake256(md, 32, tagForsOpenCommit)) + return out +} + +// OpenForsThreshold is the PUBLIC combiner. From the public material and +// >= t partial opens, it interpolates ONLY the k message-selected one-time +// leaf secrets --- each exactly one leaf wide, guarded by assertLeafWidth +// so no global-seed reconstruction is possible --- assembles the FORS +// signature, burns the opened leaf addresses, and returns the FORS +// signature bytes. +// +// The returned bytes are byte-identical to the centralized FIPS 205 +// forsSign output, and forsPkFromSig(returned) == pub.PkFors under the +// unmodified FIPS 205 Algorithm 17. +// +// ledger enforces one-time/few-time safety: each selected FORS leaf +// address is burned for the message digest; a second open of the same +// keypair under a different digest fails with *BurnConflict (constraint 3). +// Pass a fresh *BurnLedger for an isolated open; pass a persistent one to +// enforce no-reuse across messages. +func OpenForsThreshold( + pub *ForsPublicMaterial, + partials []ForsPartialOpen, + ledger *BurnLedger, +) ([]byte, error) { + if pub == nil { + return nil, errors.New("magnetar/fors-open: nil public material") + } + params := pub.Params + if err := params.Validate(); err != nil { + return nil, err + } + internal, ok := internalParamsForMode(params.Mode) + if !ok { + return nil, ErrForsSetupParams + } + if ledger == nil { + return nil, errors.New("magnetar/fors-open: nil burn ledger (one-time safety requires a ledger)") + } + leafN := int(internal.n) + authLen := int(internal.forsAuthPathSize()) + pairSize := int(internal.forsPairSize()) + k := int(internal.k) + if len(pub.AuthPaths) != k { + return nil, ErrForsShape + } + + // Validate partial opens against their commits and collect, per FORS + // sub-tree, the (eval-point, share-vector) pairs. + type pt struct { + x uint32 + y []uint16 + } + collected := make([][]pt, k) + for j := 0; j < k; j++ { + collected[j] = make([]pt, 0, len(partials)) + } + seenParty := make(map[uint32]struct{}, len(partials)) + for _, po := range partials { + if po.PartyIndex == 0 { + return nil, ErrForsShape + } + if _, dup := seenParty[po.PartyIndex]; dup { + continue // ignore duplicate party submissions + } + if len(po.LeafShares) != k { + return nil, ErrForsShape + } + for j := 0; j < k; j++ { + if len(po.LeafShares[j]) != leafN { + return nil, ErrForsShape + } + } + want := forsOpenCommit(pub.Addr, pub.Digest, po.PartyIndex, po.LeafShares) + if want != po.Commit { + return nil, ErrForsCommitMismatch + } + seenParty[po.PartyIndex] = struct{}{} + for j := 0; j < k; j++ { + collected[j] = append(collected[j], pt{x: po.PartyIndex, y: po.LeafShares[j]}) + } + } + + mdDigest := digest32(pub.Digest) + forsSig := make([]byte, internal.forsSigSize()) + + for j := 0; j < k; j++ { + // CONSTRAINT 1 (fail-closed): we interpolate exactly one leaf + // (leafN bytes), never the SeedSize master. assertLeafWidth makes + // a widening refactor fault immediately. + if err := assertLeafWidth(params, leafN); err != nil { + return nil, err + } + if pub.Threshold < 1 || len(collected[j]) < pub.Threshold { + return nil, ErrForsQuorum + } + + // CONSTRAINT 3 (fail-closed): burn the one-time FORS leaf address + // for this digest before releasing the opened secret. A second + // open of the same keypair sub-tree under a different message + // digest is a slashable equivocation. + addr := OneTimeAddr{ + Kind: KindForsFewTime, + Layer: 0, + Tree: pub.Addr.IdxTree[0], + Leaf: pub.Addr.IdxLeaf, + ForsTree: uint32(j), + } + if err := ledger.Burn(addr, mdDigest); err != nil { + return nil, err + } + + // Lagrange-interpolate this single leaf secret over GF(257) from + // any t of the collected shares (deterministic public combiner). + shares := make([]thbsseShare, pub.Threshold) + for s := 0; s < pub.Threshold; s++ { + shares[s] = thbsseShare{X: collected[j][s].x, Y: collected[j][s].y} + } + leaf, err := thbsseReconstructGF(shares, leafN) + if err != nil { + return nil, err + } + + // Write leaf secret + public auth path into the FORS signature. + skOff := j * pairSize + for b := 0; b < leafN; b++ { + forsSig[skOff+b] = byte(leaf[b]) + } + copy(forsSig[skOff+leafN:skOff+leafN+authLen], pub.AuthPaths[j]) + zeroizeU16Slice(leaf) + } + + return forsSig, nil +} + +// VerifyForsThreshold checks an assembled FORS signature against the +// published FORS public key using the UNMODIFIED FIPS 205 Algorithm 17 +// (forsPkFromSig). This is exactly the verifier-side FORS step; it is +// proven byte-equal to cloudflare/circl by TestSlhdsaInternal_ByteEqualToCirclSign. +// +// Returns true iff forsPkFromSig(forsSig) == pub.PkFors. +func VerifyForsThreshold(pub *ForsPublicMaterial, forsSig []byte) (bool, error) { + if pub == nil { + return false, errors.New("magnetar/fors-open: nil public material") + } + internal, ok := internalParamsForMode(pub.Params.Mode) + if !ok { + return false, ErrForsSetupParams + } + if len(forsSig) != int(internal.forsSigSize()) { + return false, ErrForsShape + } + if len(pub.PkFors) != int(internal.n) { + return false, ErrForsShape + } + if len(pub.PkSeed) != int(internal.n) { + return false, ErrForsShape + } + adr := pub.Addr.forsAddrBuf() + got := make([]byte, internal.n) + forsPkFromSig(internal, got, pub.PkSeed, &adr, pub.Digest, forsSig) + return ctEqualBytes(got, pub.PkFors), nil +} + +// readFullOrErr is a tiny helper for setup callers that want to source +// the Shamir coefficient stream from an io.Reader. Returns ErrShortRand +// on a short read. +func readFullOrErr(rng io.Reader, n int) ([]byte, error) { + buf := make([]byte, n) + if _, err := io.ReadFull(rng, buf); err != nil { + return nil, ErrShortRand + } + return buf, nil +} diff --git a/ref/go/pkg/magnetar/fors_threshold_open_test.go b/ref/go/pkg/magnetar/fors_threshold_open_test.go new file mode 100644 index 0000000..9b187d1 --- /dev/null +++ b/ref/go/pkg/magnetar/fors_threshold_open_test.go @@ -0,0 +1,204 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package magnetar + +// fors_threshold_open_test.go --- proves the distributed FORS-leaf +// threshold opening is (a) byte-identical to centralized FIPS 205 +// forsSign, (b) accepted by the unmodified FIPS 205 Algorithm 17 +// verifier, (c) no-reconstruct (only one-leaf-wide interpolation), and +// (d) one-time-safe (burn ledger refuses keypair reuse). + +import ( + "bytes" + "errors" + "testing" +) + +// referenceFors recomputes the centralized FIPS 205 FORS signature on md +// at addr from seed --- the byte-identity oracle. Mirrors the setup-TCB +// dealer's derivation, used here only as a test reference. +func referenceFors(t *testing.T, params *Params, seed []byte, addr ForsKeypairAddr, md []byte) (refSig, pkSeed []byte) { + t.Helper() + internal, ok := internalParamsForMode(params.Mode) + if !ok { + t.Fatalf("no internal params for mode %v", params.Mode) + } + derived := make([]byte, 3*internal.n) + shakeIntoCat(derived, seed) + pkSeed = append([]byte(nil), derived[2*internal.n:3*internal.n]...) + prfOut := makePRFClosure(internal, pkSeed, derived[:internal.n]) + adr := addr.forsAddrBuf() + refSig = make([]byte, internal.forsSigSize()) + forsSign(internal, refSig, pkSeed, &adr, md, prfOut) + return refSig, pkSeed +} + +// TestForsThreshold_NoReconstruct_StockVerify is the headline proof: +// a t-of-n threshold opening of the FORS leaves yields a FORS signature +// byte-identical to the centralized one, verifies under stock FIPS 205 +// Algorithm 17, and is produced WITHOUT any seed reconstruction. +func TestForsThreshold_NoReconstruct_StockVerify(t *testing.T) { + skipUnderRace(t) + for _, mode := range []Mode{ModeM192s, ModeM256s} { + params := MustParamsFor(mode) + internal, _ := internalParamsForMode(mode) + + seed := make([]byte, params.SeedSize) + _, _ = deterministicReader([]byte("magnetar-fors-threshold-seed")).Read(seed) + md := make([]byte, internal.m) + _, _ = deterministicReader([]byte("magnetar-fors-threshold-md")).Read(md) + coeff := make([]byte, 4096) + _, _ = deterministicReader([]byte("magnetar-fors-threshold-coeff")).Read(coeff) + + addr := ForsKeypairAddr{IdxTree: [3]uint32{0, 0, 0}, IdxLeaf: 3} + n, th := 5, 3 + + mat, pub, err := DistributedForsSetup(params, seed, addr, md, n, th, coeff) + if err != nil { + t.Fatalf("[%v] setup: %v", mode, err) + } + + // Reference (centralized) FORS signature on the same material. + refSig, _ := referenceFors(t, params, seed, addr, md) + + // Threshold open with a STRICT t-subset {1,3,5} (proves t-of-n, + // not all-n). + var partials []ForsPartialOpen + for _, p := range []uint32{1, 3, 5} { + po, err := mat.PartialOpen(p, md) + if err != nil { + t.Fatalf("[%v] PartialOpen(%d): %v", mode, p, err) + } + partials = append(partials, po) + } + + ledger := NewBurnLedger() + forsSig, err := OpenForsThreshold(pub, partials, ledger) + if err != nil { + t.Fatalf("[%v] OpenForsThreshold: %v", mode, err) + } + + // (a) Byte-identity to centralized FIPS 205 forsSign. + if !bytes.Equal(forsSig, refSig) { + t.Fatalf("[%v] threshold FORS sig != centralized forsSign (byte-identity broken)", mode) + } + + // (b) Stock FIPS 205 Algorithm 17 verification accepts. + ok, err := VerifyForsThreshold(pub, forsSig) + if err != nil { + t.Fatalf("[%v] VerifyForsThreshold: %v", mode, err) + } + if !ok { + t.Fatalf("[%v] stock FIPS 205 forsPkFromSig rejected the threshold FORS sig", mode) + } + + // (c) A DISJOINT t-subset {2,3,4} produces the SAME bytes + // (deterministic public combiner). Re-burn under same digest is + // idempotent so the same ledger is reused. + var partials2 []ForsPartialOpen + for _, p := range []uint32{2, 3, 4} { + po, _ := mat.PartialOpen(p, md) + partials2 = append(partials2, po) + } + forsSig2, err := OpenForsThreshold(pub, partials2, ledger) + if err != nil { + t.Fatalf("[%v] OpenForsThreshold disjoint subset: %v", mode, err) + } + if !bytes.Equal(forsSig, forsSig2) { + t.Fatalf("[%v] disjoint t-subsets produced different signatures", mode) + } + } +} + +// TestForsThreshold_OneTimeSafety_BurnRefusal proves constraint 3: the +// same FORS keypair signing a SECOND, distinct message is refused as a +// slashable one-time-reuse equivocation. +func TestForsThreshold_OneTimeSafety_BurnRefusal(t *testing.T) { + skipUnderRace(t) + params := MustParamsFor(ModeM192s) + internal, _ := internalParamsForMode(ModeM192s) + + seed := make([]byte, params.SeedSize) + _, _ = deterministicReader([]byte("burn-seed")).Read(seed) + coeff := make([]byte, 4096) + _, _ = deterministicReader([]byte("burn-coeff")).Read(coeff) + addr := ForsKeypairAddr{IdxTree: [3]uint32{0, 0, 0}, IdxLeaf: 7} + + md1 := make([]byte, internal.m) + _, _ = deterministicReader([]byte("burn-md-1")).Read(md1) + md2 := make([]byte, internal.m) + _, _ = deterministicReader([]byte("burn-md-2")).Read(md2) + + ledger := NewBurnLedger() // shared across both messages + + // First message: signs fine, burns the keypair. + mat1, pub1, err := DistributedForsSetup(params, seed, addr, md1, 5, 3, coeff) + if err != nil { + t.Fatalf("setup md1: %v", err) + } + var p1 []ForsPartialOpen + for _, p := range []uint32{1, 2, 3} { + po, _ := mat1.PartialOpen(p, md1) + p1 = append(p1, po) + } + if _, err := OpenForsThreshold(pub1, p1, ledger); err != nil { + t.Fatalf("first open should succeed: %v", err) + } + + // Second, distinct message on the SAME keypair: MUST be refused. + mat2, pub2, err := DistributedForsSetup(params, seed, addr, md2, 5, 3, coeff) + if err != nil { + t.Fatalf("setup md2: %v", err) + } + var p2 []ForsPartialOpen + for _, p := range []uint32{1, 2, 3} { + po, _ := mat2.PartialOpen(p, md2) + p2 = append(p2, po) + } + _, err = OpenForsThreshold(pub2, p2, ledger) + if err == nil { + t.Fatalf("one-time safety FAILED: second open of same keypair under a different digest was accepted") + } + if !errors.Is(err, ErrOneTimeReuse) { + t.Fatalf("expected ErrOneTimeReuse, got %v", err) + } + var conflict *BurnConflict + if !errors.As(err, &conflict) { + t.Fatalf("expected *BurnConflict slashable evidence, got %T", err) + } + if conflict.Prior == conflict.New { + t.Fatalf("burn conflict must carry two distinct digests") + } +} + +// TestForsThreshold_CommitMismatch_Rejected proves a tampered partial +// open (wrong shares) is rejected by the commit re-derivation. +func TestForsThreshold_CommitMismatch_Rejected(t *testing.T) { + skipUnderRace(t) + params := MustParamsFor(ModeM192s) + internal, _ := internalParamsForMode(ModeM192s) + seed := make([]byte, params.SeedSize) + _, _ = deterministicReader([]byte("commit-seed")).Read(seed) + coeff := make([]byte, 4096) + _, _ = deterministicReader([]byte("commit-coeff")).Read(coeff) + md := make([]byte, internal.m) + _, _ = deterministicReader([]byte("commit-md")).Read(md) + addr := ForsKeypairAddr{IdxTree: [3]uint32{0, 0, 0}, IdxLeaf: 1} + + mat, pub, err := DistributedForsSetup(params, seed, addr, md, 5, 3, coeff) + if err != nil { + t.Fatalf("setup: %v", err) + } + var partials []ForsPartialOpen + for _, p := range []uint32{1, 2, 3} { + po, _ := mat.PartialOpen(p, md) + partials = append(partials, po) + } + // Tamper one share value WITHOUT updating the commit. + partials[1].LeafShares[0][0] ^= 0x01 + _, err = OpenForsThreshold(pub, partials, NewBurnLedger()) + if !errors.Is(err, ErrForsCommitMismatch) { + t.Fatalf("expected ErrForsCommitMismatch for tampered share, got %v", err) + } +} diff --git a/ref/go/pkg/magnetar/threshold_noreconstruct.go b/ref/go/pkg/magnetar/threshold_noreconstruct.go new file mode 100644 index 0000000..bfc5e73 --- /dev/null +++ b/ref/go/pkg/magnetar/threshold_noreconstruct.go @@ -0,0 +1,397 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package magnetar + +// threshold_noreconstruct.go --- Magnetar Track B: the NO-RECONSTRUCT +// threshold SLH-DSA interface, FAIL CLOSED. +// +// ===================================================================== +// WHY THIS FILE EXISTS +// ===================================================================== +// +// The Magnetar trustless-by-default law (owner-authoritative) splits +// threshold hash-based signing into two tracks: +// +// Track A --- Magnetar-Quorum (PRODUCTION, trustless TODAY). +// Validators hold INDEPENDENT ordinary FIPS 205 SLH-DSA keys; each +// signs the same Quasar subject; a P3Q STARK proves a >= policy +// weighted quorum of stock SLH-DSA signatures verified. No dealer, +// no DKG, no shared secret, no threshold SLH-DSA at all. Lives in +// the Track-A surface (sibling work), evidence kind +// "magnetar-p3q-slhdsa-rollup". +// +// Track B --- Magnetar-Threshold (RESEARCH ONLY, THIS FILE). +// A genuine t-of-n threshold SLH-DSA that emits ONE stock FIPS 205 +// signature WITHOUT any party or aggregator ever reconstructing the +// global SLH-DSA seed / full secret key, even transiently. This is +// an OPEN research problem. Until no-reconstruct signing is proven, +// Track B MUST NOT be admitted to the POLARIS_MAX cert tier. +// +// This file encodes the Track-B INTERFACE and the FAIL-CLOSED gate. The +// signing entry returns ErrNoReconstructUnproven. NoReconstructProven() +// is hard-wired false. AdmitMagnetarThresholdToPolarisMax() refuses. +// Track B can therefore NEVER be silently admitted to POLARIS_MAX: a +// consumer that wires Track B into a strict cert tier gets an explicit +// error, not a degraded-trust signature. +// +// The ONE concretely-buildable, stock-FIPS-205-verifiable, genuinely +// no-reconstruct sub-protocol that DOES exist --- distributed FORS-leaf +// threshold opening --- lives in fors_threshold_open.go. It is a proven +// COMPONENT of a FORS-bottom signature, NOT a full SLH-DSA signature. +// It is deliberately NOT wired into Sign() below, because a full +// signature additionally needs the hypertree (d WOTS+ signatures + XMSS +// authentication paths), whose authentication-path siblings are +// secret-derived WOTS+ public keys over a 2^h-leaf tree --- the +// materialisation wall + keygen-MPC wall documented in the companion +// paper (papers/lp-magnetar-threshold-slhdsa). +// +// ===================================================================== +// THE THREE HARD CONSTRAINTS (the acceptance gates) +// ===================================================================== +// +// 1. NO-RECONSTRUCT. No aggregator or party ever reconstructs the +// global seed or full secret key, even transiently. (Contrast: the +// existing THBS-SE Combine path --- thbsse_assemble.go --- DOES +// reconstruct the full seed, into a buffer named derivedExpandInput +// / derivedMaterial; its "strict-atom" grep gate forbids the +// variable NAME "seed", not the reconstruction. THBS-SE is Track-A- +// adjacent reveal-and-aggregate, NOT a strict no-reconstruct lane.) +// +// 2. STOCK VERIFY. The final output verifies under ordinary FIPS 205 +// SLH-DSA Verify (cloudflare/circl, unmodified). No verifier-side +// Magnetar code. +// +// 3. ONE-TIME SAFETY. Any revealed WOTS+/FORS one-time material is +// burned forever; no reusable secret leaks. Hash-based signatures +// are FRAGILE: a single WOTS+ chain-base reuse under two distinct +// message digits is a full WOTS+ key recovery, hence a hypertree +// forgery; FORS is few-time and degrades with reuse. The burn +// discipline is enforced by BurnLedger below + the per-leaf slot +// guard in fors_threshold_open.go. +// +// Constraint 1 draws the no-reconstruct line at the GLOBAL SEED, not at +// individual leaves: constraint 3 EXPECTS one-time leaf material to be +// revealed ("any revealed WOTS+/FORS one-time material..."). Opening a +// single one-time leaf secret is therefore permitted; assembling the +// global seed is not. + +import "errors" + +// ErrNoReconstructUnproven is the fail-closed sentinel returned by the +// Track-B signing entry and the POLARIS_MAX admission gate. It signals +// that no-reconstruct threshold SLH-DSA signing has not been proven for +// the full FIPS 205 signature, so the construction must not be admitted +// to a strict trustless cert tier. +var ErrNoReconstructUnproven = errors.New( + "magnetar/threshold: no-reconstruct threshold SLH-DSA signing is unproven for the full FIPS 205 signature " + + "(only the distributed FORS-leaf opening component is proven); Track B is NOT admitted to POLARIS_MAX") + +// ErrLeafWidthViolation is returned (and is a fail-closed guard) when a +// no-reconstruct opening is asked to reconstruct a secret buffer wider +// than a single one-time leaf. It is the runtime expression of +// constraint 1: the open path may interpolate at most ONE leaf-width +// secret at a time, never the SeedSize-byte master. +var ErrLeafWidthViolation = errors.New( + "magnetar/threshold: no-reconstruct opening attempted to interpolate a buffer wider than one one-time leaf " + + "(this would be a global-seed/secret-key reconstruction --- refused)") + +// NoReconstructProven reports whether the FULL no-reconstruct threshold +// SLH-DSA signing construction (a stock FIPS 205 signature with NO +// global-seed reconstruction anywhere in the path) has been proven and +// is therefore eligible for the POLARIS_MAX strict cert tier. +// +// It is HARD-WIRED false. Flipping it to true is a deliberate, reviewed +// act that MUST be accompanied by: +// +// - A no-reconstruct producer for the COMPLETE signature (FORS + +// hypertree), not just the FORS component. +// - A proof, under stock FIPS 205 Verify, that the producer's output +// is byte-accepted by an unmodified verifier. +// - A Byzantine-safe distributed BURN-STATE that guarantees WOTS+ +// one-time and FORS few-time discipline across a live committee +// (the open obstruction --- see BurnLedger and the paper). +// +// Until then, every strict-tier consumer fails closed. +func NoReconstructProven() bool { return false } + +// MagnetarTrack identifies which Magnetar threshold track a cert +// references. The admission gate dispatches on it. +type MagnetarTrack uint8 + +const ( + // TrackUnspecified rejects every admission decision; the zero value + // is invalid by construction. + TrackUnspecified MagnetarTrack = 0 + + // TrackAQuorum is Magnetar-Quorum: P3Q rollup of independent + // validator SLH-DSA signatures. Trustless today; admitted to its + // own tiers by the Track-A surface, NOT by this gate. + TrackAQuorum MagnetarTrack = 1 + + // TrackBThreshold is Magnetar-Threshold: no-reconstruct threshold + // SLH-DSA. Research only. Refused by AdmitMagnetarThresholdToPolarisMax + // until NoReconstructProven(). + TrackBThreshold MagnetarTrack = 2 +) + +// String returns the canonical track name. +func (t MagnetarTrack) String() string { + switch t { + case TrackAQuorum: + return "Magnetar-Quorum (Track A)" + case TrackBThreshold: + return "Magnetar-Threshold (Track B)" + default: + return "Magnetar-unspecified-track" + } +} + +// AdmitMagnetarThresholdToPolarisMax is the SINGLE admission authority +// for whether a Magnetar-Threshold (Track B) lane may contribute to the +// POLARIS_MAX strict cert tier. It is the one-function gate (Hickey +// discipline: policy enforcement in ONE place, never braided into the +// crypto). +// +// It refuses Track B unconditionally until NoReconstructProven() is +// true. Track A is NOT this gate's concern --- a TrackAQuorum argument +// returns ErrWrongTrackForGate so callers cannot accidentally route the +// production lane through the research gate. +func AdmitMagnetarThresholdToPolarisMax(track MagnetarTrack) error { + switch track { + case TrackBThreshold: + if !NoReconstructProven() { + return ErrNoReconstructUnproven + } + return nil + case TrackAQuorum: + return ErrWrongTrackForGate + default: + return ErrUnknownTrack + } +} + +// ErrWrongTrackForGate is returned when Track A is passed to the +// Track-B POLARIS_MAX admission gate. Track A is admitted by its own +// (P3Q-rollup) surface; routing it here is a wiring bug. +var ErrWrongTrackForGate = errors.New( + "magnetar/threshold: Track A (Magnetar-Quorum) is not admitted by the Track-B gate; use the P3Q-rollup surface") + +// ErrUnknownTrack is returned for an unspecified / unknown track. +var ErrUnknownTrack = errors.New("magnetar/threshold: unknown Magnetar track") + +// ===================================================================== +// THE NO-RECONSTRUCT SIGNER INTERFACE +// ===================================================================== + +// NoReconstructThresholdSigner is the Track-B signing contract. A +// conforming implementation produces ONE stock FIPS 205 SLH-DSA +// signature from a t-of-n committee such that NO party or aggregator +// ever holds the global seed / full secret key, even transiently. +// +// No such implementation exists for the COMPLETE signature. The +// fail-closed implementation below returns ErrNoReconstructUnproven. +// The proven COMPONENT (distributed FORS-leaf opening) implements only +// the FORS-bottom of this contract and is exposed separately in +// fors_threshold_open.go --- it is intentionally NOT a +// NoReconstructThresholdSigner, because it does not emit a complete +// signature. +type NoReconstructThresholdSigner interface { + // SignNoReconstruct produces a stock FIPS 205 signature on message + // under ctx from >= threshold partial openings, with no global-seed + // reconstruction. Returns ErrNoReconstructUnproven until the + // construction is proven. + SignNoReconstruct(message, ctx []byte, openings [][]byte) (*Signature, error) + + // Mode reports the FIPS 205 parameter set this signer targets. + Mode() Mode +} + +// failClosedThresholdSigner is the only NoReconstructThresholdSigner +// Magnetar ships. Every signing call fails closed. It exists so that +// consumers can wire the Track-B interface end-to-end TODAY and observe +// the explicit refusal, rather than discovering at integration time +// that the lane silently degraded to a reconstructing combiner. +type failClosedThresholdSigner struct { + params *Params +} + +// NewFailClosedThresholdSigner returns the fail-closed Track-B signer +// for the given mode. Its SignNoReconstruct always returns +// ErrNoReconstructUnproven. +func NewFailClosedThresholdSigner(params *Params) (NoReconstructThresholdSigner, error) { + if err := params.Validate(); err != nil { + return nil, err + } + return &failClosedThresholdSigner{params: params}, nil +} + +// SignNoReconstruct fails closed. +func (s *failClosedThresholdSigner) SignNoReconstruct(_, _ []byte, _ [][]byte) (*Signature, error) { + return nil, ErrNoReconstructUnproven +} + +// Mode reports the target parameter set. +func (s *failClosedThresholdSigner) Mode() Mode { return s.params.Mode } + +// ===================================================================== +// CONSTRAINT-1 RUNTIME GUARD (leaf-width invariant) +// ===================================================================== + +// maxOpenWidth is the maximum secret-buffer width a no-reconstruct +// opening may interpolate at once: exactly ONE one-time leaf secret, +// which in FIPS 205 is n bytes (params.N). This is strictly less than +// SeedSize (= PrivateKeySize = 4n for every FIPS 205 set), so an +// interpolation at this width can never assemble the master seed. +func maxOpenWidth(params *Params) int { return params.N } + +// assertLeafWidth is the fail-closed expression of constraint 1. Every +// no-reconstruct interpolation in fors_threshold_open.go passes its +// target width through this guard. A width > one leaf (i.e. anything +// approaching SeedSize) is refused with ErrLeafWidthViolation: the open +// path is structurally incapable of reconstructing the global seed. +// +// This is a CONSTRUCTIVE guard, not merely a test assertion --- it sits +// on the live open path so a future refactor that tried to widen an +// interpolation to seed size would fault immediately. +func assertLeafWidth(params *Params, width int) error { + if width <= 0 || width > maxOpenWidth(params) { + return ErrLeafWidthViolation + } + return nil +} + +// ===================================================================== +// +// THE ONE-TIME / FEW-TIME BURN LEDGER (constraint 3; the crux) +// +// ===================================================================== +// +// The deepest obstruction to no-reconstruct threshold SLH-DSA is NOT +// the no-reconstruct property of a single opening --- the FORS component +// achieves that. It is GLOBAL one-time/few-time discipline across a +// live, possibly-Byzantine committee: +// +// - WOTS+ keypairs are STRICTLY ONE-TIME. Signing two distinct +// message digests with one WOTS+ keypair reveals chain values at +// two different chain lengths; the shorter one lets an adversary +// extend to forge the other --- full WOTS+ key recovery, hence a +// hypertree forgery. A threshold protocol MUST guarantee that for +// every (layer, tree, leaf) WOTS+ address, the committee opens chain +// material for AT MOST ONE message digest, forever. +// +// - FORS keypairs are FEW-TIME. Opening leaf secrets of one FORS +// keypair for multiple messages degrades security as roughly +// (reuse)^k; the few-time margin must not be exceeded. +// +// In stateless SLH-DSA the keypair selection (idxTree, idxLeaf) is a +// pseudo-random function of R = PRF_msg(SK.prf, PK.seed, M'), and the +// huge hypertree (2^63..2^68 leaves) makes collisions negligible for a +// single signer. In a THRESHOLD setting SK.prf is itself shared, so R +// must be produced by the committee, and the committee must DETECT and +// REFUSE any second opening of an already-burned one-time address. +// +// BurnLedger is the durable, fault-tolerant state that enforces this. +// It is the "stateful subprotocol under a stateless-looking cert +// boundary" (cf. NIST Haystack, threshold/distributed stateful HBS). +// The in-memory ledger below is the LOCAL-state reference; a production +// deployment lifts it to Byzantine-fault-tolerant replicated state +// (consensus-anchored burn proofs). The Byzantine-safe lift is the open +// obstruction --- see the paper. NoReconstructProven() cannot return +// true until that lift is proven. +type BurnLedger struct { + // burned maps a one-time address key to the message digest it was + // burned for. A second open at the same address under a different + // digest is an equivocation: refused, and the (address, two digests) + // tuple is slashable evidence. + burned map[OneTimeAddr][32]byte +} + +// OneTimeAddr canonically identifies a one-time/few-time signing slot in +// the SLH-DSA forest: the FIPS 205 hypertree coordinates plus the +// component kind. It is the burn-ledger key. Values, not places: the +// address is defined by what it IS (a coordinate in the forest), not by +// who holds it. +type OneTimeAddr struct { + // Kind distinguishes WOTS+ (strictly one-time) from FORS (few-time) + // addresses so the ledger can apply the correct reuse budget. + Kind OneTimeKind + // Layer is the hypertree layer (0 = bottom). FORS lives below layer 0. + Layer uint32 + // Tree is the XMSS tree index within the layer (low 32 bits; the + // full FIPS 205 tree address is 3 words --- the reference ledger keys + // on the low word, sufficient for the bounded test forests; a + // production ledger keys on the full 3-word address). + Tree uint32 + // Leaf is the keypair (XMSS leaf / FORS keypair) index within the + // tree. + Leaf uint32 + // ForsTree is the FORS sub-tree index in [0, k) for FORS addresses; + // zero for WOTS+ addresses. + ForsTree uint32 +} + +// OneTimeKind classifies the reuse budget of a one-time address. +type OneTimeKind uint8 + +const ( + // KindWotsOneTime is a WOTS+ keypair: STRICTLY one message, ever. + KindWotsOneTime OneTimeKind = 1 + // KindForsFewTime is a FORS keypair: few-time, budget enforced by + // the few-time parameters (k, a). The reference ledger applies a + // strict one-message default; a production ledger may widen to the + // proven few-time budget. + KindForsFewTime OneTimeKind = 2 +) + +// NewBurnLedger returns an empty local burn ledger. +func NewBurnLedger() *BurnLedger { + return &BurnLedger{burned: make(map[OneTimeAddr][32]byte)} +} + +// Burn records that addr is consumed for digest. It returns +// ErrOneTimeReuse (carrying the conflicting prior digest via +// BurnConflict) if addr was already burned for a DIFFERENT digest. +// Re-burning the SAME (addr, digest) is idempotent (a benign retry). +// +// This is the fail-closed enforcement of constraint 3: the open path +// MUST call Burn before releasing one-time material, and abort on +// conflict. A production committee replaces the local map with +// consensus-anchored burn proofs so the guarantee survives crashes, +// restarts, and Byzantine equivocation. +func (l *BurnLedger) Burn(addr OneTimeAddr, digest [32]byte) error { + if prior, ok := l.burned[addr]; ok { + if prior != digest { + return &BurnConflict{Addr: addr, Prior: prior, New: digest} + } + return nil // idempotent retry + } + l.burned[addr] = digest + return nil +} + +// IsBurned reports whether addr has been consumed. +func (l *BurnLedger) IsBurned(addr OneTimeAddr) bool { + _, ok := l.burned[addr] + return ok +} + +// BurnConflict is the slashable evidence of a one-time-address reuse +// attempt: the same one-time slot opened for two distinct messages. +type BurnConflict struct { + Addr OneTimeAddr + Prior [32]byte + New [32]byte +} + +// Error implements error. +func (e *BurnConflict) Error() string { + return "magnetar/threshold: one-time address reuse --- a WOTS+/FORS slot was opened for two distinct message digests (slashable)" +} + +// ErrOneTimeReuse is a sentinel for errors.Is matching against a +// BurnConflict. +var ErrOneTimeReuse = errors.New("magnetar/threshold: one-time address reuse") + +// Is lets errors.Is(err, ErrOneTimeReuse) match a *BurnConflict. +func (e *BurnConflict) Is(target error) bool { return target == ErrOneTimeReuse } diff --git a/ref/go/pkg/magnetar/threshold_noreconstruct_test.go b/ref/go/pkg/magnetar/threshold_noreconstruct_test.go new file mode 100644 index 0000000..520ae6f --- /dev/null +++ b/ref/go/pkg/magnetar/threshold_noreconstruct_test.go @@ -0,0 +1,193 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package magnetar + +// threshold_noreconstruct_test.go --- proves the Track-B skeleton fails +// closed (no silent admission to POLARIS_MAX) and proves the structural +// no-reconstruct invariant: the SIGN/combiner path (OpenForsThreshold) +// never invokes a seed-reconstruction primitive, and the leaf-width +// guard refuses any interpolation wider than one one-time leaf. + +import ( + "errors" + "go/ast" + "go/parser" + "go/token" + "testing" +) + +// TestNoReconstruct_FailClosed proves the Track-B signer + admission gate +// fail closed until NoReconstructProven(). +func TestNoReconstruct_FailClosed(t *testing.T) { + if NoReconstructProven() { + t.Fatalf("NoReconstructProven() must be hard-wired false until the full construction is proven") + } + + signer, err := NewFailClosedThresholdSigner(MustParamsFor(ModeM192s)) + if err != nil { + t.Fatalf("NewFailClosedThresholdSigner: %v", err) + } + if _, err := signer.SignNoReconstruct([]byte("m"), []byte("ctx"), nil); !errors.Is(err, ErrNoReconstructUnproven) { + t.Fatalf("SignNoReconstruct must fail closed with ErrNoReconstructUnproven, got %v", err) + } + + // POLARIS_MAX admission gate refuses Track B. + if err := AdmitMagnetarThresholdToPolarisMax(TrackBThreshold); !errors.Is(err, ErrNoReconstructUnproven) { + t.Fatalf("Track B must be refused admission to POLARIS_MAX, got %v", err) + } + // Track A is not this gate's concern. + if err := AdmitMagnetarThresholdToPolarisMax(TrackAQuorum); !errors.Is(err, ErrWrongTrackForGate) { + t.Fatalf("Track A must return ErrWrongTrackForGate, got %v", err) + } + if err := AdmitMagnetarThresholdToPolarisMax(TrackUnspecified); !errors.Is(err, ErrUnknownTrack) { + t.Fatalf("unspecified track must return ErrUnknownTrack, got %v", err) + } +} + +// TestNoReconstruct_LeafWidthGuard proves constraint 1's runtime guard: +// an interpolation at one-leaf width is permitted; anything approaching +// SeedSize is refused. +func TestNoReconstruct_LeafWidthGuard(t *testing.T) { + params := MustParamsFor(ModeM192s) + if params.N >= params.SeedSize { + t.Fatalf("invariant broken: a leaf (n=%d) must be strictly narrower than the seed (%d)", params.N, params.SeedSize) + } + if err := assertLeafWidth(params, params.N); err != nil { + t.Fatalf("one-leaf width must be permitted, got %v", err) + } + if err := assertLeafWidth(params, params.SeedSize); !errors.Is(err, ErrLeafWidthViolation) { + t.Fatalf("seed-width interpolation must be refused, got %v", err) + } + if err := assertLeafWidth(params, params.N+1); !errors.Is(err, ErrLeafWidthViolation) { + t.Fatalf("width > one leaf must be refused, got %v", err) + } + if err := assertLeafWidth(params, 0); !errors.Is(err, ErrLeafWidthViolation) { + t.Fatalf("zero width must be refused, got %v", err) + } +} + +// forbiddenNoReconstructCalls are the seed-reconstruction / secret-side +// primitives that the no-reconstruct SIGN path must NEVER invoke. If +// OpenForsThreshold called any of these it would be reconstructing the +// global seed (KeyFromSeed / shakeIntoCat on the seed), running the +// secret-side signer (forsSign / slhSignAtom / assembleSignatureBytes), +// or dealing fresh shares (thbsseDealRandom*). +var forbiddenNoReconstructCalls = map[string]struct{}{ + "forsSign": {}, + "slhSignAtom": {}, + "assembleSignatureBytes": {}, + "KeyFromSeed": {}, + "GenerateKey": {}, + "shakeIntoCat": {}, + "makePRFClosure": {}, + "makePRFMsgClosure": {}, + "thbsseDealRandom": {}, + "thbsseDealRandomGF": {}, + "deriveDKGPublicKey": {}, +} + +// requiredNoReconstructCalls are the no-reconstruct primitives that +// OpenForsThreshold MUST invoke: the leaf-width Lagrange, the +// constraint-1 guard, and the constraint-3 burn. +var requiredNoReconstructCalls = map[string]struct{}{ + "thbsseReconstructGF": {}, // leaf-width interpolation only + "assertLeafWidth": {}, // constraint 1 guard + "Burn": {}, // constraint 3 burn +} + +// collectCallNames returns the set of called function/method names within +// a function body (ast.Ident callees and ast.SelectorExpr selector names). +func collectCallNames(fn *ast.FuncDecl) map[string]struct{} { + names := make(map[string]struct{}) + ast.Inspect(fn, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + switch f := call.Fun.(type) { + case *ast.Ident: + names[f.Name] = struct{}{} + case *ast.SelectorExpr: + names[f.Sel.Name] = struct{}{} + } + return true + }) + return names +} + +// findFunc parses a source file in the current package directory and +// returns the named top-level function declaration. +func findFunc(t *testing.T, file, name string) *ast.FuncDecl { + t.Helper() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", file, err) + } + for _, decl := range f.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok && fn.Recv == nil && fn.Name.Name == name { + return fn + } + } + t.Fatalf("function %s not found in %s", name, file) + return nil +} + +// TestNoReconstruct_SourceGate_NoSeedReconstruction is the STRUCTURAL +// gate: it parses the no-reconstruct SIGN path (OpenForsThreshold) and +// asserts it never calls a seed-reconstruction / secret-side primitive, +// and DOES call the leaf-width interpolation + constraint guards. +// +// This is the structural counterpart of the runtime leaf-width guard: +// together they prove the SIGN path is incapable of forming the global +// seed. Contrast: assembleSignatureBytes (the existing reveal-and- +// aggregate Combine path) DOES call shakeIntoCat on a full SeedSize +// buffer --- this test asserts that contrast is real. +func TestNoReconstruct_SourceGate_NoSeedReconstruction(t *testing.T) { + open := findFunc(t, "fors_threshold_open.go", "OpenForsThreshold") + calls := collectCallNames(open) + + for forbidden := range forbiddenNoReconstructCalls { + if _, found := calls[forbidden]; found { + t.Fatalf("STRUCTURAL VIOLATION: OpenForsThreshold calls seed-reconstruction primitive %q --- "+ + "the no-reconstruct SIGN path must never invoke it", forbidden) + } + } + for required := range requiredNoReconstructCalls { + if _, found := calls[required]; !found { + t.Fatalf("OpenForsThreshold must call no-reconstruct primitive %q (leaf-width Lagrange / constraint guards)", required) + } + } + + // Verify the contrast is real: the EXISTING reconstruct path + // (assembleSignatureBytes) DOES expand a full buffer via shakeIntoCat. + // If this ever stops being true the contrast claim is stale. + assemble := findFunc(t, "thbsse_assemble.go", "assembleSignatureBytes") + asmCalls := collectCallNames(assemble) + if _, found := asmCalls["shakeIntoCat"]; !found { + t.Fatalf("contrast stale: assembleSignatureBytes no longer expands a full buffer via shakeIntoCat") + } +} + +// TestBurnLedger_IdempotentAndConflict unit-tests the burn ledger. +func TestBurnLedger_IdempotentAndConflict(t *testing.T) { + l := NewBurnLedger() + addr := OneTimeAddr{Kind: KindWotsOneTime, Layer: 0, Tree: 1, Leaf: 2} + var d1, d2 [32]byte + d1[0], d2[0] = 1, 2 + + if err := l.Burn(addr, d1); err != nil { + t.Fatalf("first burn: %v", err) + } + if err := l.Burn(addr, d1); err != nil { + t.Fatalf("idempotent re-burn under same digest must succeed: %v", err) + } + if !l.IsBurned(addr) { + t.Fatalf("addr must be burned") + } + err := l.Burn(addr, d2) + if !errors.Is(err, ErrOneTimeReuse) { + t.Fatalf("re-burn under different digest must conflict, got %v", err) + } +}