mirror of
https://github.com/luxfi/dkg.git
synced 2026-10-30 16:58:45 +00:00
PROOFS (DESIGN.md hard invariants):
- No-reconstruct STRUCTURAL: go/ast scan of every non-test vss source
asserts no reconstruction primitive (reconstruct/lagrange/interpolate/
modinverse/keyfromseed/combineshares) is defined or called in the
output path. Comments excluded, so prose never trips it.
- No-reconstruct RUNTIME: no single share equals s1; t-1 shares cannot
reconstruct (threshold privacy). Reconstruction is test-only scaffolding.
- Both-ring parameterization: full dealerless DKG over Ringtail AND
ML-DSA produces a valid group key with T=A·s1+B·u.
- FIPS-204 ring fidelity: generic lattice ring negacyclic multiply ==
independent schoolbook convolution over q=8380417 (and the Ringtail q).
- KeyFinalize<->FIPS identity: ML-DSA t1 satisfies T=t1·2^13+t0, 10-bit width.
- Stock circl: real mldsa65 keygen->sign->verify round-trip + param align.
FIX (correctness-critical): ConvertVecFromNTT dropped the spurious IMForm.
The commits are plain-NTT products N(A·c) (MulCoeffsMontgomery already
cancels R); INTT alone recovers the TRUE A·c. The prior IMForm scaled the
group key by R^{-1} mod q — self-consistent under a same-scaled verifier
(so the lattice-vs-lattice round-trip test missed it) but WRONG vs ground
truth, and fatal to FIPS-204 t1. Caught by the schoolbook fidelity proof.
SCOPE (precise obstruction, documented not faked): a circl-byte-identical
pk for a specific rho and a stock-verifiable threshold SIGNATURE require
the pulsar ExpandA(rho) binding (Phase 3) + the TALUS signer (Phase 2);
the no-reconstruct T=A·s1+B·u has a large s2=B·u, so stock FIPS-204 signing
does not apply unchanged. The foundation proves ring + key-map fidelity.
128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package vss
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"crypto/rand"
|
|
|
|
"github.com/luxfi/dkg/ring"
|
|
)
|
|
|
|
// noreconstruct_test.go — the headline structural + runtime proof that the vss
|
|
// output path NEVER reconstructs the master secret. Mirrors the pulsar CSCP
|
|
// leak-free structural tests: it parses the package's own non-test source with
|
|
// go/ast (comments excluded) and fails if any function defined or called in the
|
|
// output path implements secret reconstruction.
|
|
|
|
// forbiddenIdentifiers are reconstruction primitives that must NOT appear as a
|
|
// function name or call target anywhere in the vss output path. The DKG derives
|
|
// the group key from PUBLIC commits and keeps only per-party shares; forming
|
|
// s1/u/seed is exactly what these names denote, and exactly what is forbidden.
|
|
var forbiddenIdentifiers = []string{
|
|
"reconstruct",
|
|
"lagrange",
|
|
"interpolate",
|
|
"keyfromseed",
|
|
"combineshares",
|
|
"recoversecret",
|
|
"modinverse", // Lagrange-at-0 needs a field inversion; its absence is a tell
|
|
}
|
|
|
|
// TestNoReconstruct_SourceStructural parses every non-test .go file in the vss
|
|
// package and asserts no forbidden reconstruction identifier is defined or
|
|
// called. go/ast skips comments, so prose like "never reconstructing" does not
|
|
// trip the scan — only real code does.
|
|
func TestNoReconstruct_SourceStructural(t *testing.T) {
|
|
entries, err := os.ReadDir(".")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fset := token.NewFileSet()
|
|
scanned := 0
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
|
continue
|
|
}
|
|
path := filepath.Join(".", name)
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file, err := parser.ParseFile(fset, path, src, 0) // 0 = no comments retained
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v", name, err)
|
|
}
|
|
scanned++
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
switch node := n.(type) {
|
|
case *ast.FuncDecl:
|
|
checkForbidden(t, name, node.Name.Name)
|
|
case *ast.Ident:
|
|
checkForbidden(t, name, node.Name)
|
|
case *ast.SelectorExpr:
|
|
checkForbidden(t, name, node.Sel.Name)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
if scanned == 0 {
|
|
t.Fatal("no source files scanned — test is vacuous")
|
|
}
|
|
t.Logf("scanned %d non-test source files; no reconstruction primitive present", scanned)
|
|
}
|
|
|
|
func checkForbidden(t *testing.T, file, ident string) {
|
|
t.Helper()
|
|
low := strings.ToLower(ident)
|
|
for _, bad := range forbiddenIdentifiers {
|
|
if strings.Contains(low, bad) {
|
|
t.Errorf("%s: forbidden reconstruction identifier %q (matches %q) in the vss output path", file, ident, bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNoReconstruct_Runtime confirms at runtime that a party retains only a
|
|
// SHARE, not the secret: no individual share equals the reconstructed s1, and
|
|
// any t-1 shares Lagrange-combine to the WRONG value (reconstruction is
|
|
// impossible below threshold). The reconstruction here is TEST-ONLY scaffolding
|
|
// to demonstrate the property; the library performs none of it.
|
|
func TestNoReconstruct_Runtime(t *testing.T) {
|
|
p, _ := ring.Ringtail()
|
|
n, tt := 5, 3
|
|
ids, nodes := committee(t, n)
|
|
res, err := RunDKG(p, n, tt, ids, nodes, [32]byte{}, rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// True secret from t shares.
|
|
full := []int{0, 1, 2}
|
|
s1 := reconstruct(p.Ring, res.Shares, full, p.L)
|
|
|
|
// No single party share equals s1 (sharing is non-trivial: s1 is masked by
|
|
// the higher-degree coefficients).
|
|
for idx, share := range res.Shares {
|
|
if ring.ConstantTimeVecEqual(share, s1) == 1 {
|
|
t.Fatalf("party %d's share equals the master secret — sharing is trivial/broken", idx)
|
|
}
|
|
}
|
|
|
|
// t-1 shares cannot reconstruct: Lagrange over {0,1} (degree-1 fit through 2
|
|
// points of a degree-2 polynomial) yields a DIFFERENT value than the true
|
|
// s1. This is the threshold privacy property: < t shares reveal nothing.
|
|
below := reconstruct(p.Ring, res.Shares, []int{0, 1}, p.L)
|
|
if ring.ConstantTimeVecEqual(below, s1) == 1 {
|
|
t.Fatal("t-1 shares reconstructed the secret — threshold broken")
|
|
}
|
|
}
|