Merge branch 'research/magnetar-threshold-slhdsa'

This commit is contained in:
zeekay
2026-06-30 14:01:57 -07:00
4 changed files with 1324 additions and 0 deletions
+530
View File
@@ -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
}
@@ -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)
}
}
@@ -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 }
@@ -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)
}
}