mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
Lands the Lux FHE-KAT corpus generators consumed by the C++ production
runtime at github.com/luxfi/luxcpp/crypto/fhe (LP-167). Mirrors the
pulsar/lens/warp regen script shape; emits a sha256 manifest and
supports --verify mode for determinism diffing.
Delivered:
cmd/kat_oracle/main.go KAT oracle. --emit --out <dir>
writes one JSON entry per
(param_set, seed) tuple. Each
entry pins parameter-set ID,
seed, operation tag, and a
CRC32 fingerprint of the SHA-256
digest of the canonical SecretKey
MarshalBinary output.
scripts/regen-kats.sh Driver: runs kat_oracle, runs the
in-tree determinism tests
(TestNewKeyGeneratorFromSeed_Deterministic
+ TestNewKeyGeneratorFromSeed_DifferentSeeds),
emits manifest. --verify mode
diffs against existing manifest;
fails on mismatch.
scripts/regen-kats.manifest.sha256 Initial 4-entry manifest:
PN10QP27 / PN11QP54 × seed-zero /
seed-lp167-stable.
Verification:
$ LUXCPP_DIR=$HOME/work/luxcpp scripts/regen-kats.sh --verify
OK: Lux FHE KAT regeneration is byte-equal across runs (4 files)
$ cd \$HOME/work/luxcpp/crypto/fhe
$ ctest --test-dir build -R fhe_kat_replay
OK lux-fhe-cpp KAT replay (4 entries)
LP-167 §"Cross-runtime KAT contract" requires both directions of
byte-equality. The Go→C++ direction is exercised by this commit and
the C++ replay test. The reverse direction (C++ generates, Go
verifies) is queued behind v0.1.0-rc2-fhe-gpu in lockstep with the
production CUDA NTT + key-switch + bootstrap kernels.
go test -count=1 -timeout=300s ./... -> 3/3 ok
ok github.com/luxfi/fhe 48.345s
ok github.com/luxfi/fhe/pkg/encrypted 98.833s
ok github.com/luxfi/fhe/pkg/threshold 0.778s
186 lines
5.8 KiB
Go
186 lines
5.8 KiB
Go
// Copyright (c) 2026, Lux Industries Inc
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
//
|
|
// kat_oracle emits the canonical Lux FHE-KAT corpus consumed by the
|
|
// C++ production runtime (luxcpp/crypto/fhe/test/cpp/fhe_kat_replay_test).
|
|
//
|
|
// Each invocation writes one JSON record per (param_set, seed) tuple to
|
|
// stdout. The regen script (lux/fhe/scripts/regen-kats.sh) drives a
|
|
// fixed parameter-set / seed matrix into a deterministic corpus
|
|
// directory and writes a SHA-256 manifest to confirm byte-equality
|
|
// across runs.
|
|
//
|
|
// LP-167 §"Cross-runtime KAT contract" specifies the JSON shape:
|
|
//
|
|
// {
|
|
// "id": "<test-name>",
|
|
// "param_set": "PN10QP27" | "PN11QP54" | "PN9QP28_STD128",
|
|
// "seed": "<32 hex bytes>",
|
|
// "operation": "secret_key" | "encrypt_decrypt",
|
|
// "inputs": [...plaintext bits...],
|
|
// "expected_ct": "<base64 ciphertext>",
|
|
// "expected_decode": [...plaintext bits...]
|
|
// }
|
|
//
|
|
// The scaffold milestone (v0.1.0-rc1-fhe-scaffold) emits the
|
|
// "secret_key" entries — they pin the (param_set, seed) → SecretKey
|
|
// bytes mapping. The "encrypt_decrypt" entries land at v0.1.0-rc2-fhe-gpu
|
|
// in lockstep with the C++ programmable-bootstrap path.
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/luxfi/fhe"
|
|
)
|
|
|
|
type katEntry struct {
|
|
ID string `json:"id"`
|
|
ParamSet string `json:"param_set"`
|
|
Seed string `json:"seed"` // 32 hex bytes
|
|
Operation string `json:"operation"`
|
|
Inputs []int `json:"inputs,omitempty"`
|
|
ExpectedCT string `json:"expected_ct,omitempty"` // base64
|
|
ExpectedDecode []int `json:"expected_decode,omitempty"`
|
|
// Manifest fingerprint (CRC32 of canonical encoding) — independent
|
|
// proof of byte-stability across runs without requiring a full
|
|
// C++-side parse.
|
|
Fingerprint string `json:"fingerprint"`
|
|
}
|
|
|
|
// paramSetByName resolves a Lux FHE parameter-set ID string to the
|
|
// canonical lattice Parameters. The names match exactly across runtimes
|
|
// per LP-167 §"Parameter sets".
|
|
func paramSetByName(name string) (fhe.Parameters, error) {
|
|
switch name {
|
|
case "PN10QP27":
|
|
return fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
case "PN11QP54":
|
|
return fhe.NewParametersFromLiteral(fhe.PN11QP54)
|
|
default:
|
|
return fhe.Parameters{}, fmt.Errorf("kat_oracle: unsupported param_set %q (allowed: PN10QP27, PN11QP54)", name)
|
|
}
|
|
}
|
|
|
|
// seedFromHex parses a 64-char hex string into 32 bytes. Required so
|
|
// the C++ side can reuse the same byte sequence verbatim.
|
|
func seedFromHex(seedHex string) ([]byte, error) {
|
|
if len(seedHex) != 64 {
|
|
return nil, fmt.Errorf("kat_oracle: seed must be 32 bytes hex (64 chars), got %d", len(seedHex))
|
|
}
|
|
return hex.DecodeString(seedHex)
|
|
}
|
|
|
|
// emitSecretKeyEntry generates a SecretKey from (param_set, seed) and
|
|
// returns the canonical KAT JSON entry. The Fingerprint is the CRC32 of
|
|
// the marshaled SKBR + SKLWE bytes — a small, fixed-size byte-stability
|
|
// witness suitable for cross-runtime equality assertion.
|
|
func emitSecretKeyEntry(id, paramSetName, seedHex string) (katEntry, error) {
|
|
params, err := paramSetByName(paramSetName)
|
|
if err != nil {
|
|
return katEntry{}, err
|
|
}
|
|
seed, err := seedFromHex(seedHex)
|
|
if err != nil {
|
|
return katEntry{}, err
|
|
}
|
|
kg, err := fhe.NewKeyGeneratorFromSeed(params, seed)
|
|
if err != nil {
|
|
return katEntry{}, fmt.Errorf("kat_oracle: keygen: %w", err)
|
|
}
|
|
sk := kg.GenSecretKey()
|
|
|
|
skBytes, err := sk.MarshalBinary()
|
|
if err != nil {
|
|
return katEntry{}, fmt.Errorf("kat_oracle: marshal sk: %w", err)
|
|
}
|
|
|
|
digest := sha256.Sum256(skBytes)
|
|
fingerprint := fmt.Sprintf("%08x", crc32.ChecksumIEEE(digest[:]))
|
|
|
|
return katEntry{
|
|
ID: id,
|
|
ParamSet: paramSetName,
|
|
Seed: seedHex,
|
|
Operation: "secret_key",
|
|
Fingerprint: fingerprint,
|
|
}, nil
|
|
}
|
|
|
|
// emitMatrix writes the full default matrix to outDir, one JSON entry per
|
|
// (param_set, seed) tuple. Returns the slice of relative paths emitted
|
|
// in sorted order so the manifest builder can hash them deterministically.
|
|
func emitMatrix(outDir string) ([]string, error) {
|
|
matrix := []struct {
|
|
ID, ParamSet, Seed string
|
|
}{
|
|
{"sk_pn10qp27_seed_zero", "PN10QP27",
|
|
"0000000000000000000000000000000000000000000000000000000000000000"},
|
|
{"sk_pn10qp27_seed_lp167", "PN10QP27",
|
|
"4c503136372d6b61742d76312d7365656420666f72204c75782046484520202020"[:64]},
|
|
{"sk_pn11qp54_seed_zero", "PN11QP54",
|
|
"0000000000000000000000000000000000000000000000000000000000000000"},
|
|
{"sk_pn11qp54_seed_lp167", "PN11QP54",
|
|
"4c503136372d6b61742d76312d7365656420666f72204c75782046484520202020"[:64]},
|
|
}
|
|
|
|
if err := os.MkdirAll(outDir, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
emitted := make([]string, 0, len(matrix))
|
|
for _, e := range matrix {
|
|
entry, err := emitSecretKeyEntry(e.ID, e.ParamSet, e.Seed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := json.MarshalIndent(entry, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fname := e.ID + ".json"
|
|
path := outDir + "/" + fname
|
|
if err := os.WriteFile(path, append(body, '\n'), 0o644); err != nil {
|
|
return nil, err
|
|
}
|
|
emitted = append(emitted, fname)
|
|
}
|
|
|
|
sort.Strings(emitted)
|
|
return emitted, nil
|
|
}
|
|
|
|
func main() {
|
|
out := flag.String("out", "", "output directory for KAT entries")
|
|
emit := flag.Bool("emit", false, "emit the canonical KAT matrix to --out")
|
|
flag.Parse()
|
|
|
|
if *emit {
|
|
if *out == "" {
|
|
fmt.Fprintln(os.Stderr, "kat_oracle: --emit requires --out")
|
|
os.Exit(2)
|
|
}
|
|
entries, err := emitMatrix(*out)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "kat_oracle: emit: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("emitted %d KAT entries to %s\n", len(entries), *out)
|
|
for _, e := range entries {
|
|
fmt.Println(" " + e)
|
|
}
|
|
return
|
|
}
|
|
|
|
fmt.Fprintln(os.Stderr, "usage: kat_oracle --emit --out <dir>")
|
|
os.Exit(2)
|
|
}
|