Files
crypto/pq/mldsa/kats/regen_test.go
T
Hanzo AI 1f07474b54 crypto/pq/mldsa: KAT vectors + expanded-key form + ethdilithium-compat
Adds NIST CAVS-style KAT regression for ML-DSA-44/65/87 and an
expanded-public-key form per EIP-8051 (~22,080-byte precomputed
A matrix for ML-DSA-65) for verifier-throughput experiments.

ethdilithium_compat subpackage re-implements the Keccak-substituted
verifier described in ZKNoxHQ/ETHDILITHIUM. NOT FIPS 204; for benchmark
and Ethereum-fallback only. Re-implemented from spec - no LGPL code
vendored.

The strict-PQ path remains crypto/pq/mldsa/mldsa{44,65,87}.{Sign,Verify}.

Patch-bump.
2026-05-10 21:38:38 -07:00

123 lines
3.5 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package kats
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
// TestRegen_Deterministic invokes the same generator the Makefile
// target uses (`go run ./pq/mldsa/kats/internal/gen`) against a
// temporary output directory and asserts the resulting files are
// byte-identical to the checked-in vectors_mldsaXX.go files.
//
// This is the "two-pass discipline" guard. A drift here means
// either (a) someone hand-edited the generated vectors, or (b) the
// generator changed but `make gen_kats` was not re-run before
// commit. Either is a defect.
//
// The test is skipped when the test binary cannot find a `go`
// executable on PATH (e.g. in a stripped CI image); the regression
// risk in that case is bounded — the gen_kats Makefile target is
// expected to be re-run separately.
func TestRegen_Deterministic(t *testing.T) {
goBin, err := exec.LookPath("go")
if err != nil {
t.Skipf("go not on PATH: %v", err)
}
if os.Getenv("LUX_SKIP_REGEN") == "1" {
// Escape hatch for environments where invoking `go run`
// inside a test is undesirable (sandboxed runners that
// disallow nested compilation).
t.Skip("LUX_SKIP_REGEN=1")
}
tmp := t.TempDir()
// Resolve repo root from this source file's location: kats lives
// at <repo>/pq/mldsa/kats; go up three to reach <repo>.
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller(0) failed; cannot locate source file")
}
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", ".."))
cmd := exec.Command(goBin, "run",
"github.com/luxfi/crypto/pq/mldsa/kats/internal/gen",
"-out", tmp,
)
cmd.Dir = repoRoot
// Inherit Go env including GOPROXY; explicitly disable workspace
// so the generator builds against this module's own go.mod
// (matches CI behavior).
cmd.Env = append(os.Environ(), "GOWORK=off")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("go run gen: %v\n%s", err, out)
}
for _, name := range []string{
"vectors_mldsa44.go",
"vectors_mldsa65.go",
"vectors_mldsa87.go",
} {
freshPath := filepath.Join(tmp, name)
pinnedPath := filepath.Join(filepath.Dir(thisFile), name)
fresh, err := os.ReadFile(freshPath)
if err != nil {
t.Fatalf("read fresh %s: %v", name, err)
}
pinned, err := os.ReadFile(pinnedPath)
if err != nil {
t.Fatalf("read pinned %s: %v", name, err)
}
if !bytes.Equal(fresh, pinned) {
// Surface the first differing line so the diagnosis is
// short. Big hex literals make a full diff useless.
line, freshLine, pinnedLine := firstDiffLine(fresh, pinned)
t.Fatalf("%s: regenerated bytes differ from pinned source\n"+
"first diff at line %d:\n fresh: %q\n pinned: %q\n"+
"hint: run `make gen_kats` and commit the result",
name, line,
truncate(freshLine, 120), truncate(pinnedLine, 120))
}
}
}
func firstDiffLine(a, b []byte) (line int, aLine, bLine string) {
la := strings.Split(string(a), "\n")
lb := strings.Split(string(b), "\n")
n := len(la)
if len(lb) < n {
n = len(lb)
}
for i := 0; i < n; i++ {
if la[i] != lb[i] {
return i + 1, la[i], lb[i]
}
}
if len(la) != len(lb) {
// One file has trailing lines.
if len(la) > n {
return n + 1, la[n], ""
}
return n + 1, "", lb[n]
}
return 0, "", ""
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max] + "...(truncated)"
}