mirror of
https://github.com/luxfi/pulsar.git
synced 2026-07-27 06:55:04 +00:00
pulsar: GATE-2 hardened to call-graph reachability (GATE C) + CI legacy-tag invariant (items 3-4)
RED GATE-2 bypass: the structural gate greps only distributed_bcc.go for
KeyFromSeed(/func LargeCombine, so a reconstruct-then-sign wired via
deriveKeyMaterial+bccSign elsewhere PASSES.
GATE C (gate2_reachability_test.go): stdlib go/ast name call-graph over the
default-build files. Bans {KeyFromSeed, deriveKeyMaterial, bccSign,
shamirReconstruct*, LargeCombine, DealAlgShares} from being REACHABLE from the
committee sign entrypoints (AggregateBCC[WithBlame], NewDistributedBCCSigner,
DistributedBCCSigner.{Round1,Round2,Finalize,FinalizeWithBlame,SetNonceShare}).
Callee side over-approximated by name -> sound bias (over-report, never miss).
Two tests: (1) the REAL package is clean (8 entrypoints, none reach a banned
primitive); (2) the checker CATCHES RED's deriveKeyMaterial+bccSign bypass, both
as a synthetic graph AND via the real parse->graph pipeline on rogue source;
plus a clean-path negative control (no false positive).
Item 4 (ci_build_invariant_test.go): the production (default-tag) build never
carries legacy_trusted_dealer -- asserts the tag gates real reconstruct files,
they are absent from the default build, LargeCombine is quarantined, no
default file requires the tag, and DealAlgShares (trusted-dealer keygen) is
defined ONLY in _test.go.
Full suite green.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pulsar
|
||||
|
||||
// ci_build_invariant_test.go — item 4: the PRODUCTION build must never ship the
|
||||
// reconstruct-at-sign quarantine. RED LOW: the structural GATE-2 inspects
|
||||
// build.Default (no tags), so CI must ALSO assert the production binary is never
|
||||
// compiled with `-tags legacy_trusted_dealer` (which would re-link LargeCombine,
|
||||
// the H-1 reconstruct footgun) and that the trusted-dealer keygen is test-only.
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const legacyTrustedDealerTag = "legacy_trusted_dealer"
|
||||
|
||||
func defaultBuildGoFiles(t *testing.T) (dir string, files map[string]bool) {
|
||||
t.Helper()
|
||||
pkg, err := build.Default.ImportDir(".", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("enumerate default-build package: %v", err)
|
||||
}
|
||||
files = make(map[string]bool, len(pkg.GoFiles))
|
||||
for _, f := range pkg.GoFiles {
|
||||
files[f] = true
|
||||
}
|
||||
return pkg.Dir, files
|
||||
}
|
||||
|
||||
func taggedBuildGoFiles(t *testing.T, tag string) map[string]bool {
|
||||
t.Helper()
|
||||
ctx := build.Default
|
||||
ctx.BuildTags = append(append([]string{}, ctx.BuildTags...), tag)
|
||||
pkg, err := ctx.ImportDir(".", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("enumerate %s-tagged package: %v", tag, err)
|
||||
}
|
||||
out := make(map[string]bool, len(pkg.GoFiles))
|
||||
for _, f := range pkg.GoFiles {
|
||||
out[f] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// goBuildLine returns the `//go:build ...` constraint line of a file (or "").
|
||||
func goBuildLine(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
for _, line := range strings.Split(string(src), "\n") {
|
||||
l := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(l, "//go:build ") {
|
||||
return l
|
||||
}
|
||||
// build constraints must precede the package clause.
|
||||
if strings.HasPrefix(l, "package ") {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TestCI_ProductionBuildExcludesLegacyTrustedDealer asserts the default build
|
||||
// excludes every legacy_trusted_dealer file, that the tag actually gates real
|
||||
// reconstruct files, and that the reconstruct-at-sign combiner is never in prod.
|
||||
func TestCI_ProductionBuildExcludesLegacyTrustedDealer(t *testing.T) {
|
||||
dir, defaultFiles := defaultBuildGoFiles(t)
|
||||
taggedFiles := taggedBuildGoFiles(t, legacyTrustedDealerTag)
|
||||
|
||||
// Files the tag ADDS (present under -tags legacy_trusted_dealer, absent by
|
||||
// default): these are the quarantined reconstruct files.
|
||||
var legacyOnly []string
|
||||
for f := range taggedFiles {
|
||||
if !defaultFiles[f] {
|
||||
legacyOnly = append(legacyOnly, f)
|
||||
}
|
||||
}
|
||||
if len(legacyOnly) == 0 {
|
||||
t.Fatalf("CI invariant MISCONFIGURED: the %q tag gates NO files — quarantine mechanism is inert", legacyTrustedDealerTag)
|
||||
}
|
||||
|
||||
// Every tag-gated file must (a) be absent from the default/production build
|
||||
// and (b) carry the legacy_trusted_dealer constraint (intentional gating).
|
||||
for _, f := range legacyOnly {
|
||||
if defaultFiles[f] {
|
||||
t.Fatalf("CI FAILED: %s is in BOTH the default and tagged build", f)
|
||||
}
|
||||
line := goBuildLine(t, filepath.Join(dir, f))
|
||||
if !strings.Contains(line, legacyTrustedDealerTag) {
|
||||
t.Fatalf("CI FAILED: %s is tag-gated but its build line %q does not mention %q", f, line, legacyTrustedDealerTag)
|
||||
}
|
||||
}
|
||||
|
||||
// The reconstruct-at-sign combiner MUST be quarantined, never in prod.
|
||||
if defaultFiles["large_threshold.go"] {
|
||||
t.Fatalf("CI FAILED: large_threshold.go (LargeCombine, reconstruct-at-sign) is in the PRODUCTION build")
|
||||
}
|
||||
if !taggedFiles["large_threshold.go"] {
|
||||
t.Fatalf("CI MISCONFIGURED: large_threshold.go not found even under the legacy tag")
|
||||
}
|
||||
|
||||
// No default-build file may itself require the legacy tag (belt-and-suspenders
|
||||
// against an accidental positive constraint that only builds under the tag).
|
||||
for f := range defaultFiles {
|
||||
if line := goBuildLine(t, filepath.Join(dir, f)); strings.Contains(line, legacyTrustedDealerTag) {
|
||||
t.Fatalf("CI FAILED: default-build file %s carries a %q constraint", f, legacyTrustedDealerTag)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("CI PASS: production build excludes %d legacy_trusted_dealer file(s) %v; LargeCombine quarantined", len(legacyOnly), legacyOnly)
|
||||
}
|
||||
|
||||
// TestCI_TrustedDealerKeygenIsTestOnly asserts the trusted-dealer s1-share
|
||||
// keygen (DealAlgShares) is defined ONLY in _test.go (uncompilable into any
|
||||
// production binary) — the quarantine the no-reconstruct claim rests on.
|
||||
func TestCI_TrustedDealerKeygenIsTestOnly(t *testing.T) {
|
||||
dir, defaultFiles := defaultBuildGoFiles(t)
|
||||
for f := range defaultFiles {
|
||||
src, err := os.ReadFile(filepath.Join(dir, f))
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if strings.Contains(string(src), "func DealAlgShares(") {
|
||||
t.Fatalf("CI FAILED: DealAlgShares (trusted-dealer keygen) defined in PRODUCTION file %s — must be _test.go only", f)
|
||||
}
|
||||
}
|
||||
// Sanity: it IS defined somewhere in the test build (the bootstrap file).
|
||||
if _, err := os.Stat(filepath.Join(dir, "bootstrap_dealer_test.go")); err != nil {
|
||||
t.Fatalf("bootstrap_dealer_test.go missing — trusted-dealer keygen quarantine file gone: %v", err)
|
||||
}
|
||||
t.Logf("CI PASS: DealAlgShares is test-only (bootstrap_dealer_test.go); never in the production build")
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pulsar
|
||||
|
||||
// gate2_reachability_test.go — GATE 2 HARDENED to call-graph REACHABILITY,
|
||||
// closing RED's GATE-2 bypass (the old gate greps only distributed_bcc.go for
|
||||
// `KeyFromSeed(` / `func LargeCombine`, so a reconstruct-then-sign wired via
|
||||
// deriveKeyMaterial+bccSign elsewhere PASSES the scan).
|
||||
//
|
||||
// The hardened gate proves the no-reconstruct invariant where it matters: NO
|
||||
// reconstruct / seed-expand / single-key-sign primitive is REACHABLE in the
|
||||
// default-build call graph from ANY committee sign entrypoint. It is a stdlib
|
||||
// (go/ast) name call-graph — no new dependency. The callee side is
|
||||
// OVER-APPROXIMATED by name (a CallExpr to name X follows every definition
|
||||
// named X), which is the SOUND bias for a security gate: it can over-report a
|
||||
// reach, never miss one.
|
||||
//
|
||||
// GATE C is two tests: (1) the REAL package is clean (the invariant holds);
|
||||
// (2) the checker actually CATCHES a deriveKeyMaterial+bccSign bypass wired into
|
||||
// a sign entrypoint (a synthetic graph modeling RED's PoC).
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// bannedReconstructPrimitives are the seed-expand / reconstruct / single-key
|
||||
// reconstruct-then-sign primitives that MUST be unreachable from the committee
|
||||
// sign path. All are free functions (defKey == name).
|
||||
var bannedReconstructPrimitives = map[string]bool{
|
||||
"KeyFromSeed": true, // seed -> sk expansion
|
||||
"deriveKeyMaterial": true, // seed -> full FIPS key material
|
||||
"bccSign": true, // single-key reconstruct-then-sign reference
|
||||
"LargeCombine": true, // reconstruct-at-sign combiner
|
||||
"DealAlgShares": true, // trusted-dealer s1-share keygen
|
||||
"shamirReconstruct": true,
|
||||
"shamirReconstructGF": true,
|
||||
"shamirReconstructGFQ": true,
|
||||
"shamirReconstructQ": true,
|
||||
}
|
||||
|
||||
// committeeSignEntrypoints are the production committee sign-combine roots that
|
||||
// MUST stay reconstruct-free. Method keys are receiver-qualified.
|
||||
var committeeSignEntrypoints = []string{
|
||||
"AggregateBCC",
|
||||
"AggregateBCCWithBlame",
|
||||
"NewDistributedBCCSigner",
|
||||
"DistributedBCCSigner.Round1",
|
||||
"DistributedBCCSigner.Round2",
|
||||
"DistributedBCCSigner.Finalize",
|
||||
"DistributedBCCSigner.FinalizeWithBlame",
|
||||
"DistributedBCCSigner.SetNonceShare",
|
||||
}
|
||||
|
||||
// callGraph: defKey -> set of callee (unqualified) names, plus an index from
|
||||
// unqualified name -> all defKeys with that name.
|
||||
type callGraph struct {
|
||||
calls map[string]map[string]bool
|
||||
nameIndex map[string][]string
|
||||
}
|
||||
|
||||
func recvTypeName(fn *ast.FuncDecl) string {
|
||||
if fn.Recv == nil || len(fn.Recv.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
switch t := fn.Recv.List[0].Type.(type) {
|
||||
case *ast.StarExpr:
|
||||
if id, ok := t.X.(*ast.Ident); ok {
|
||||
return id.Name
|
||||
}
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func defKeyFor(fn *ast.FuncDecl) (key, unqualified string) {
|
||||
if rt := recvTypeName(fn); rt != "" {
|
||||
return rt + "." + fn.Name.Name, fn.Name.Name
|
||||
}
|
||||
return fn.Name.Name, fn.Name.Name
|
||||
}
|
||||
|
||||
// calleeName extracts the called function/method name from a CallExpr.
|
||||
func calleeName(call *ast.CallExpr) string {
|
||||
switch fn := call.Fun.(type) {
|
||||
case *ast.Ident:
|
||||
return fn.Name
|
||||
case *ast.SelectorExpr:
|
||||
return fn.Sel.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildCallGraph(files []*ast.File) *callGraph {
|
||||
g := &callGraph{calls: map[string]map[string]bool{}, nameIndex: map[string][]string{}}
|
||||
for _, f := range files {
|
||||
for _, decl := range f.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || fn.Body == nil {
|
||||
continue
|
||||
}
|
||||
key, unqual := defKeyFor(fn)
|
||||
if g.calls[key] == nil {
|
||||
g.calls[key] = map[string]bool{}
|
||||
}
|
||||
g.nameIndex[unqual] = appendUnique(g.nameIndex[unqual], key)
|
||||
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
||||
if call, ok := n.(*ast.CallExpr); ok {
|
||||
if name := calleeName(call); name != "" {
|
||||
g.calls[key][name] = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func appendUnique(s []string, v string) []string {
|
||||
for _, x := range s {
|
||||
if x == v {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return append(s, v)
|
||||
}
|
||||
|
||||
// reachBanned does BFS from the entrypoint defKeys over callee names and reports
|
||||
// the first banned primitive reached, with the path that reaches it.
|
||||
func (g *callGraph) reachBanned(entrypoints []string, banned map[string]bool) (hit string, path []string, found bool) {
|
||||
type item struct {
|
||||
key string
|
||||
path []string
|
||||
}
|
||||
visited := map[string]bool{}
|
||||
var queue []item
|
||||
for _, e := range entrypoints {
|
||||
queue = append(queue, item{key: e, path: []string{e}})
|
||||
visited[e] = true
|
||||
}
|
||||
for len(queue) > 0 {
|
||||
cur := queue[0]
|
||||
queue = queue[1:]
|
||||
callees := make([]string, 0, len(g.calls[cur.key]))
|
||||
for c := range g.calls[cur.key] {
|
||||
callees = append(callees, c)
|
||||
}
|
||||
sort.Strings(callees) // deterministic path reporting
|
||||
for _, callee := range callees {
|
||||
if banned[callee] {
|
||||
return callee, append(append([]string{}, cur.path...), callee), true
|
||||
}
|
||||
for _, nextKey := range g.nameIndex[callee] {
|
||||
if !visited[nextKey] {
|
||||
visited[nextKey] = true
|
||||
queue = append(queue, item{key: nextKey, path: append(append([]string{}, cur.path...), nextKey)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// defaultBuildFuncFiles parses the package's DEFAULT-build (non-test,
|
||||
// tag-honouring) Go files into ASTs — the same file set a production binary
|
||||
// links (legacy_trusted_dealer files excluded).
|
||||
func defaultBuildFuncFiles(t *testing.T) []*ast.File {
|
||||
t.Helper()
|
||||
pkg, err := build.Default.ImportDir(".", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("enumerate default-build package: %v", err)
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
files := make([]*ast.File, 0, len(pkg.GoFiles))
|
||||
for _, name := range pkg.GoFiles {
|
||||
f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, name), nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", name, err)
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// GATE C (the invariant) — no reconstruct primitive is reachable from the
|
||||
// committee sign path in the real default-build call graph.
|
||||
func TestGATE_C_NoReconstructReachableFromSignPath(t *testing.T) {
|
||||
files := defaultBuildFuncFiles(t)
|
||||
g := buildCallGraph(files)
|
||||
|
||||
// Every entrypoint must actually exist (guard against a renamed entrypoint
|
||||
// silently emptying the gate).
|
||||
for _, e := range committeeSignEntrypoints {
|
||||
if _, ok := g.calls[e]; !ok {
|
||||
t.Fatalf("GATE C MISCONFIGURED: sign entrypoint %q not found in the default build", e)
|
||||
}
|
||||
}
|
||||
|
||||
if hit, path, found := g.reachBanned(committeeSignEntrypoints, bannedReconstructPrimitives); found {
|
||||
t.Fatalf("GATE C FAILED: reconstruct primitive %q is REACHABLE from the committee sign path:\n %v", hit, path)
|
||||
}
|
||||
|
||||
// Also assert the banned primitives are actually DEFINED in the build (so we
|
||||
// are testing reachability of real functions, not dead names).
|
||||
defined := 0
|
||||
for name := range bannedReconstructPrimitives {
|
||||
if _, ok := g.nameIndex[name]; ok {
|
||||
defined++
|
||||
}
|
||||
}
|
||||
t.Logf("GATE C PASS: %d sign entrypoints; %d/%d banned reconstruct primitives present in the build, NONE reachable from the sign path",
|
||||
len(committeeSignEntrypoints), defined, len(bannedReconstructPrimitives))
|
||||
}
|
||||
|
||||
// GATE C (the catch) — the checker actually DETECTS RED's bypass: a synthetic
|
||||
// graph in which the committee aggregator reaches deriveKeyMaterial+bccSign
|
||||
// (reconstruct-then-sign) must be flagged. This proves the gate would FAIL if
|
||||
// the bypass were wired in, rather than silently passing like the old grep.
|
||||
func TestGATE_C_CatchesReconstructBypass(t *testing.T) {
|
||||
// Model: AggregateBCC -> rogueReconstructSign -> {deriveKeyMaterial, bccSign}
|
||||
g := &callGraph{
|
||||
calls: map[string]map[string]bool{
|
||||
"AggregateBCC": {"rogueReconstructSign": true},
|
||||
"rogueReconstructSign": {"deriveKeyMaterial": true, "bccSign": true},
|
||||
"deriveKeyMaterial": {},
|
||||
"bccSign": {},
|
||||
},
|
||||
nameIndex: map[string][]string{
|
||||
"AggregateBCC": {"AggregateBCC"},
|
||||
"rogueReconstructSign": {"rogueReconstructSign"},
|
||||
"deriveKeyMaterial": {"deriveKeyMaterial"},
|
||||
"bccSign": {"bccSign"},
|
||||
},
|
||||
}
|
||||
hit, path, found := g.reachBanned([]string{"AggregateBCC"}, bannedReconstructPrimitives)
|
||||
if !found {
|
||||
t.Fatalf("GATE C BROKEN: the reachability checker did NOT catch the deriveKeyMaterial+bccSign bypass")
|
||||
}
|
||||
t.Logf("GATE C catch PASS: bypass detected — banned %q reachable via %v", hit, path)
|
||||
|
||||
// Negative control: a clean sign path that never reaches a banned primitive
|
||||
// must NOT be flagged (no false positive).
|
||||
clean := &callGraph{
|
||||
calls: map[string]map[string]bool{
|
||||
"AggregateBCC": {"VerifyPartialProof": true, "FlatAggregateZ": true},
|
||||
"VerifyPartialProof": {"partialFSChallenges": true},
|
||||
"partialFSChallenges": {},
|
||||
"FlatAggregateZ": {},
|
||||
},
|
||||
nameIndex: map[string][]string{
|
||||
"AggregateBCC": {"AggregateBCC"},
|
||||
"VerifyPartialProof": {"VerifyPartialProof"},
|
||||
"partialFSChallenges": {"partialFSChallenges"},
|
||||
"FlatAggregateZ": {"FlatAggregateZ"},
|
||||
},
|
||||
}
|
||||
if _, _, found := clean.reachBanned([]string{"AggregateBCC"}, bannedReconstructPrimitives); found {
|
||||
t.Fatalf("GATE C false positive: a clean sign path was flagged")
|
||||
}
|
||||
|
||||
// Full-pipeline catch: parse ROGUE source (RED's exact bypass — a sign
|
||||
// entrypoint that reconstructs via deriveKeyMaterial then bccSign) through
|
||||
// the REAL buildCallGraph and assert reachBanned flags it. This exercises
|
||||
// the actual AST extraction the gate runs on production files, not just a
|
||||
// hand-built graph.
|
||||
rogueSrc := `package pulsar
|
||||
func AggregateBCC() { rogueReconstructSign() }
|
||||
func rogueReconstructSign() {
|
||||
km, _ := deriveKeyMaterial(ModeP65, nil)
|
||||
_, _, _ = bccSign(km, ModeP65, nil, nil, nil, 1)
|
||||
}
|
||||
`
|
||||
fset := token.NewFileSet()
|
||||
rf, err := parser.ParseFile(fset, "rogue.go", rogueSrc, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse rogue source: %v", err)
|
||||
}
|
||||
rg := buildCallGraph([]*ast.File{rf})
|
||||
hit2, path2, found2 := rg.reachBanned([]string{"AggregateBCC"}, bannedReconstructPrimitives)
|
||||
if !found2 {
|
||||
t.Fatalf("GATE C BROKEN: real parse→graph pipeline missed the deriveKeyMaterial+bccSign bypass")
|
||||
}
|
||||
t.Logf("GATE C full-pipeline catch PASS: parsed rogue source flagged banned %q via %v", hit2, path2)
|
||||
}
|
||||
Reference in New Issue
Block a user