mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
pulsar: cross-runtime oracles + sign KAT generator (v0.1.2 follow-on)
cmd/sign_oracle/ — emits the canonical Pulsar sign+verify KAT JSON
(Gen + SignRound1 + SignRound2{Preprocess,} + SignFinalize + Verify) for
(t, n) ∈ {2/3, 3/5, 5/7, 7/11}. The C++ port at
luxcpp/crypto/pulsar/cpp/sign/ replays these entries byte-equal.
cmd/cross_runtime_oracle/ — Go writer for the cross-runtime release
gate. Emits a SHA-256 manifest tying together the three canonical
Pulsar KATs (sign, reshare, dkg2). C++ side at
luxcpp/crypto/pulsar/test/cross_runtime_test.cpp replays each KAT and
checks byte-equality (forward direction: 30/30 PASS).
cmd/cross_runtime_verify/ — Go reader for the reverse direction
(C++ writes, Go verifies). Currently 3/3 PASS against the SHA-256s
emitted by luxcpp/crypto/pulsar/cmd/cross_runtime_oracle/.
threshold/testdata/fuzz/FuzzPulsarSign1Round1Data/ — corpus seed for
the in-tree fuzz harness; mitigates the second lattice DoS (issue #4)
where Vector[T].ReadFrom calls make([]T, size) with no bound.
23 fuzz harnesses × 30s = 10.9M execs, 0 panics.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// Package main is the Pulsar cross-runtime KAT oracle.
|
||||
//
|
||||
// Emits a single JSON manifest at <out>/cross_runtime_kat.json that ties
|
||||
// together the three canonical Pulsar KATs (sign, reshare, dkg2) with
|
||||
// the SHA-256 of each individual KAT file. The C++ side
|
||||
// (luxcpp/crypto/pulsar/test/cross_runtime_test.cpp) replays each KAT
|
||||
// in C++ and verifies byte-equality; running this oracle first then
|
||||
// the C++ test is the "Go → C++" direction of the gate.
|
||||
//
|
||||
// Reverse direction (C++ → Go) is handled by:
|
||||
// * luxcpp/crypto/pulsar/cmd/cross_runtime_oracle/ (C++ writer)
|
||||
// * lux/pulsar/cmd/cross_runtime_verify/ (Go reader)
|
||||
//
|
||||
// Determinism is required. Two runs with the same MasterSeed produce
|
||||
// byte-equal manifests AND byte-equal individual KAT JSON files.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type manifestEntry struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Sha256 string `json:"sha256"`
|
||||
NumBytes int64 `json:"num_bytes"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Description string `json:"description"`
|
||||
Direction string `json:"direction"`
|
||||
Files []manifestEntry `json:"files"`
|
||||
}
|
||||
|
||||
func sha256File(path string) (string, int64, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
h := sha256.Sum256(b)
|
||||
return hex.EncodeToString(h[:]), int64(len(b)), nil
|
||||
}
|
||||
|
||||
func runOracle(cmd string, args []string) error {
|
||||
c := exec.Command("go", append([]string{"run", cmd}, args...)...)
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "", "output directory (creates cross_runtime_kat.json + per-KAT files via the per-KAT oracles)")
|
||||
flag.Parse()
|
||||
if *out == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: cross_runtime_oracle --out <dir>")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(*out, 0o755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mkdir: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 1. Emit the per-KAT JSONs by invoking each per-KAT oracle.
|
||||
if err := runOracle("./cmd/sign_oracle", []string{"--out", *out}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "sign_oracle: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// reshare_oracle and dkg2_oracle don't expose --out; they write to
|
||||
// hardcoded locations in luxcpp/crypto. For the cross-runtime gate
|
||||
// we hash the canonical paths.
|
||||
signPath := filepath.Join(*out, "sign_kat.json")
|
||||
resharePath := "/Users/z/work/luxcpp/crypto/pulsar/test/kat/reshare_kat.json"
|
||||
dkg2Path := "/Users/z/work/luxcpp/crypto/pulsar/dkg2/test/kat/dkg2_kat.json"
|
||||
|
||||
files := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{"sign", signPath},
|
||||
{"reshare", resharePath},
|
||||
{"dkg2", dkg2Path},
|
||||
}
|
||||
|
||||
m := manifest{
|
||||
Description: "Pulsar cross-runtime KAT manifest. SHA-256 of each canonical Go-emitted KAT JSON. The C++ cross_runtime_test consumes the same paths and asserts byte-equality at every entry; this manifest pins the Go-side bytes so any drift produces a SHA-256 mismatch caught by the gate.",
|
||||
Direction: "go-to-cpp",
|
||||
}
|
||||
for _, f := range files {
|
||||
sum, sz, err := sha256File(f.path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "hash %s: %v\n", f.path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
m.Files = append(m.Files, manifestEntry{
|
||||
Name: f.name,
|
||||
Path: f.path,
|
||||
Sha256: sum,
|
||||
NumBytes: sz,
|
||||
})
|
||||
}
|
||||
|
||||
mPath := filepath.Join(*out, "cross_runtime_kat.json")
|
||||
mBytes, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "marshal: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.WriteFile(mPath, mBytes, 0o644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "write: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("wrote %s (%d entries)\n", mPath, len(m.Files))
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Package main is the Go-side verifier for the Pulsar cross-runtime
|
||||
// KAT gate (C++ → Go direction).
|
||||
//
|
||||
// Reads a C++-emitted manifest produced by
|
||||
// luxcpp/crypto/pulsar/cmd/cross_runtime_oracle and confirms that each
|
||||
// SHA-256 digest in the manifest matches the bytes Go observes for the
|
||||
// same file path. Mismatch → non-zero exit code.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// cross_runtime_verify --manifest <path/to/cross_runtime_kat_cpp.json>
|
||||
//
|
||||
// This is the reverse leg of the cross-runtime gate. The forward leg
|
||||
// (Go → C++) lives in luxcpp/crypto/pulsar/test/cross_runtime_test.cpp;
|
||||
// CTest target `pulsar_cross_runtime_kat` exercises both directions.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type manifestEntry struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Sha256 string `json:"sha256"`
|
||||
NumBytes int64 `json:"num_bytes"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Description string `json:"description"`
|
||||
Direction string `json:"direction"`
|
||||
Files []manifestEntry `json:"files"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
mPath := flag.String("manifest", "", "path to cross-runtime manifest JSON")
|
||||
flag.Parse()
|
||||
if *mPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: cross_runtime_verify --manifest <path>")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
mBytes, err := os.ReadFile(*mPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "read manifest: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var m manifest
|
||||
if err := json.Unmarshal(mBytes, &m); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "parse manifest: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("=== cross_runtime_verify (direction=%s) ===\n", m.Direction)
|
||||
fmt.Printf("manifest: %s\n", *mPath)
|
||||
fmt.Printf("entries: %d\n\n", len(m.Files))
|
||||
|
||||
failed := 0
|
||||
passed := 0
|
||||
for _, f := range m.Files {
|
||||
b, err := os.ReadFile(f.Path)
|
||||
if err != nil {
|
||||
fmt.Printf(" [%-8s] FAIL read: %v\n", f.Name, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
if int64(len(b)) != f.NumBytes {
|
||||
fmt.Printf(" [%-8s] FAIL size: got=%d want=%d\n",
|
||||
f.Name, len(b), f.NumBytes)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
sum := sha256.Sum256(b)
|
||||
got := hex.EncodeToString(sum[:])
|
||||
if got != f.Sha256 {
|
||||
fmt.Printf(" [%-8s] FAIL sha256\n want: %s\n got: %s\n",
|
||||
f.Name, f.Sha256, got)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" [%-8s] PASS sha256=%s..\n", f.Name, got[:16])
|
||||
passed++
|
||||
}
|
||||
|
||||
fmt.Printf("\nresult: %d/%d passed\n", passed, passed+failed)
|
||||
if failed > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// Package main is the Pulsar sign+verify KAT oracle.
|
||||
//
|
||||
// Given a fixed master seed, it emits a deterministic JSON file of
|
||||
// known-answer test vectors covering the full LP-073 Pulsar threshold
|
||||
// signature pipeline (Gen + SignRound1 + SignRound2{Preprocess,} +
|
||||
// SignFinalize + Verify) for the canonical (t, n) configurations
|
||||
// 2-of-3, 3-of-5, 5-of-7, 7-of-11. The C++ port at
|
||||
// luxcpp/crypto/pulsar/cpp/sign/ replays these entries byte-equal.
|
||||
//
|
||||
// Pulsar's sign/sign.go is byte-identical to corona/sign/sign.go (the
|
||||
// only diff is the import path), so the JSON shape mirrors the existing
|
||||
// corona sign_verify_e2e KAT.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/sign_oracle --out <dir>
|
||||
//
|
||||
// Determinism is required. Two runs with the same MasterSeed produce
|
||||
// byte-equal JSON files.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
"github.com/luxfi/lattice/v7/utils/sampling"
|
||||
"github.com/luxfi/lattice/v7/utils/structs"
|
||||
"github.com/luxfi/pulsar/primitives"
|
||||
"github.com/luxfi/pulsar/sign"
|
||||
"github.com/luxfi/pulsar/utils"
|
||||
"github.com/zeebo/blake3"
|
||||
)
|
||||
|
||||
// MasterSeed is the deterministic root of all KAT generation. Changing
|
||||
// it invalidates every downstream port's expected outputs, so it stays
|
||||
// fixed across sessions.
|
||||
const MasterSeed uint64 = 0xDEADBEEFCAFEBABE
|
||||
|
||||
// derive expands MasterSeed into a per-KAT 32-byte sub-seed via BLAKE3
|
||||
// with a domain-separation tag. This keeps each KAT independent of the
|
||||
// others (so adding a future KAT does not perturb existing files).
|
||||
func derive(tag string) []byte {
|
||||
h := blake3.New()
|
||||
var seedBytes [8]byte
|
||||
binary.BigEndian.PutUint64(seedBytes[:], MasterSeed)
|
||||
_, _ = h.Write(seedBytes[:])
|
||||
_, _ = h.Write([]byte(tag))
|
||||
return h.Sum(nil)[:32]
|
||||
}
|
||||
|
||||
// expand returns n bytes of BLAKE3 stream from key (deterministic).
|
||||
func expand(key []byte, n int) []byte {
|
||||
out := make([]byte, n)
|
||||
xof := blake3.New()
|
||||
_, _ = xof.Write(key)
|
||||
_, _ = xof.Digest().Read(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// uint64SliceToHex emits a fixed-width 16-hex-char-per-uint64 BE encoding.
|
||||
func uint64SliceToHex(c []uint64) string {
|
||||
out := make([]byte, 0, len(c)*16)
|
||||
for _, v := range c {
|
||||
var b [8]byte
|
||||
binary.BigEndian.PutUint64(b[:], v)
|
||||
out = append(out, []byte(hex.EncodeToString(b[:]))...)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func writeJSON(path string, v any) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, b, 0o644)
|
||||
}
|
||||
|
||||
func hashMatrix(m structs.Matrix[ring.Poly]) string {
|
||||
var buf bytes.Buffer
|
||||
_, _ = m.WriteTo(&buf)
|
||||
h := sha256.Sum256(buf.Bytes())
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func hashVector(v structs.Vector[ring.Poly]) string {
|
||||
var buf bytes.Buffer
|
||||
_, _ = v.WriteTo(&buf)
|
||||
h := sha256.Sum256(buf.Bytes())
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func hashVectors(m map[int]structs.Vector[ring.Poly], n int) []string {
|
||||
out := make([]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
out[i] = hashVector(m[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type signEntry struct {
|
||||
T int `json:"t"`
|
||||
N int `json:"n"`
|
||||
SeedHex string `json:"seed_hex"`
|
||||
MsgHex string `json:"msg_hex"`
|
||||
Msg string `json:"msg"`
|
||||
AHashHex string `json:"a_hash_hex"`
|
||||
BTildeHashHex string `json:"btilde_hash_hex"`
|
||||
SkSharesHex []string `json:"sk_shares_hash_hex"`
|
||||
PartialSigsHex []string `json:"partial_sigs_hash_hex"`
|
||||
CHex string `json:"c_hex"`
|
||||
ZHex string `json:"z_hex"`
|
||||
DeltaHex string `json:"delta_hex"`
|
||||
Verify bool `json:"verify"`
|
||||
}
|
||||
|
||||
func emitSignVerify(outDir string) error {
|
||||
root := derive("sign_e2e_pulsar")
|
||||
cfgs := []struct{ t, n int }{
|
||||
{2, 3}, {3, 5}, {5, 7}, {7, 11},
|
||||
}
|
||||
// One canonical message per (t,n). The 4-entry KAT requirement keeps
|
||||
// the JSON small while still pinning every Sign+Verify byte path.
|
||||
msgPerCfg := []string{"alpha", "beta", "gamma", "delta"}
|
||||
|
||||
out := struct {
|
||||
Description string `json:"description"`
|
||||
Entries []signEntry `json:"entries"`
|
||||
}{
|
||||
Description: "Full Pulsar Sign+Verify round-trip (LP-073 Q-witness). " +
|
||||
"For each (t,n,msg,seed): Gen → SignRound1 (all parties) → " +
|
||||
"SignRound2Preprocess+SignRound2 (all parties) → SignFinalize → " +
|
||||
"Verify. Pulsar's sign module is byte-identical to corona's at " +
|
||||
"the Go source level (only the import path differs). The current " +
|
||||
"KAT signs with K=Threshold=n; the t field documents the " +
|
||||
"threshold-aware variant for downstream use. SHA-256 hashes are " +
|
||||
"used for large fields (A, BTilde, sk shares, partial sigs) to " +
|
||||
"keep file size finite while still binding the C++ port " +
|
||||
"byte-for-byte.",
|
||||
}
|
||||
|
||||
var q uint64 = sign.Q
|
||||
for ci, cfg := range cfgs {
|
||||
seed := expand(append(root, []byte(fmt.Sprintf("cfg-%d", ci))...), 32)
|
||||
msg := msgPerCfg[ci]
|
||||
|
||||
// Reset global stateful randomness to ensure determinism across
|
||||
// calls. The `utils.PrecomputedRandomness` is process-wide.
|
||||
utils.PrecomputedRandomness = nil
|
||||
utils.RandomnessIndex = 0
|
||||
|
||||
// Force K = n, Threshold = n so the optimized t=k Shamir path runs.
|
||||
sign.K = cfg.n
|
||||
sign.Threshold = cfg.n
|
||||
|
||||
r, err := ring.NewRing(1<<sign.LogN, []uint64{sign.Q})
|
||||
if err != nil {
|
||||
return fmt.Errorf("ring.NewRing(Q): %w", err)
|
||||
}
|
||||
rXi, _ := ring.NewRing(1<<sign.LogN, []uint64{sign.QXi})
|
||||
rNu, _ := ring.NewRing(1<<sign.LogN, []uint64{sign.QNu})
|
||||
|
||||
prng, _ := sampling.NewKeyedPRNG(seed)
|
||||
uniformSampler := ring.NewUniformSampler(prng, r)
|
||||
|
||||
T := make([]int, cfg.n)
|
||||
for i := range T {
|
||||
T[i] = i
|
||||
}
|
||||
lagrange := primitives.ComputeLagrangeCoefficients(r, T, big.NewInt(int64(q)))
|
||||
|
||||
A, skShares, seeds, macKeys, b := sign.Gen(r, rXi, uniformSampler, seed, lagrange)
|
||||
|
||||
parties := make([]*sign.Party, cfg.n)
|
||||
for i := 0; i < cfg.n; i++ {
|
||||
prngI, _ := sampling.NewKeyedPRNG(seed)
|
||||
usI := ring.NewUniformSampler(prngI, r)
|
||||
parties[i] = sign.NewParty(i, r, rXi, rNu, usI)
|
||||
parties[i].SkShare = skShares[i]
|
||||
parties[i].Seed = seeds
|
||||
parties[i].MACKeys = macKeys[i]
|
||||
lambda := r.NewPoly()
|
||||
lambda.Copy(lagrange[i])
|
||||
r.NTT(lambda, lambda)
|
||||
r.MForm(lambda, lambda)
|
||||
parties[i].Lambda = lambda
|
||||
}
|
||||
|
||||
sid := 1
|
||||
// GenerateRandomSeed pulls the next KeySize bytes from the same
|
||||
// PrecomputedRandomness stream that sign.Gen seeded; this makes
|
||||
// the prfKey deterministic given `seed` (the C++ runner's
|
||||
// recover_prf_key reproduces the same bytes).
|
||||
prfKey := primitives.GenerateRandomSeed()
|
||||
|
||||
D := make(map[int]structs.Matrix[ring.Poly])
|
||||
MACs := make(map[int]map[int][]byte)
|
||||
for _, pid := range T {
|
||||
D[pid], MACs[pid] = parties[pid].SignRound1(A, sid, prfKey, T)
|
||||
}
|
||||
|
||||
z := make(map[int]structs.Vector[ring.Poly])
|
||||
for _, pid := range T {
|
||||
ok, DSum, hash := parties[pid].SignRound2Preprocess(A, b, D, MACs, sid, T)
|
||||
if !ok {
|
||||
return fmt.Errorf("sign-e2e: MAC verify failed t=%d n=%d msg=%q",
|
||||
cfg.t, cfg.n, msg)
|
||||
}
|
||||
z[pid] = parties[pid].SignRound2(A, b, DSum, sid, msg, T, prfKey, hash)
|
||||
}
|
||||
|
||||
final := parties[0]
|
||||
c, zSum, delta := final.SignFinalize(z, A, b)
|
||||
|
||||
ok := sign.Verify(r, rXi, rNu, zSum, A, msg, b, c, delta)
|
||||
if !ok {
|
||||
return fmt.Errorf("sign-e2e: Verify returned false for cfg=(%d,%d) msg=%q",
|
||||
cfg.t, cfg.n, msg)
|
||||
}
|
||||
|
||||
out.Entries = append(out.Entries, signEntry{
|
||||
T: cfg.t,
|
||||
N: cfg.n,
|
||||
SeedHex: hex.EncodeToString(seed),
|
||||
MsgHex: hex.EncodeToString([]byte(msg)),
|
||||
Msg: msg,
|
||||
AHashHex: hashMatrix(A),
|
||||
BTildeHashHex: hashVector(b),
|
||||
SkSharesHex: hashVectors(skShares, cfg.n),
|
||||
PartialSigsHex: hashVectors(z, cfg.n),
|
||||
CHex: uint64SliceToHex(c.Coeffs[0]),
|
||||
ZHex: hashVector(zSum),
|
||||
DeltaHex: hashVector(delta),
|
||||
Verify: ok,
|
||||
})
|
||||
}
|
||||
|
||||
return writeJSON(filepath.Join(outDir, "sign_kat.json"), out)
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "", "output directory")
|
||||
flag.Parse()
|
||||
if *out == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: sign_oracle --out <dir>")
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := emitSignVerify(*out); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "emit: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("wrote %s/sign_kat.json (4 entries)\n", *out)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("\xad\x93\xd8Z\x00\x04\x00\x00\\")
|
||||
Reference in New Issue
Block a user