mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
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.
123 lines
3.5 KiB
Go
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)"
|
|
}
|