Compare commits

..

3 commits

Author SHA1 Message Date
a3ce8e8644 extract script: state what is actually guaranteed, and by what
Round-8 review found a tautological assert in this script — it compared a dict
against the comprehension that had just built it, so it could never fire, in the
very script written to fix a provenance-honesty defect. The first repair
(comparing kept[k] to t[k]) was tautological for the same reason; a negative test
confirmed it did not catch a tampered input.

No check inside a transformer can detect a corrupted input, because the
transformer is what defines the output from that input. Both vacuous checks are
removed and the lesson is recorded in the code. The header and the embedded
_provenance now say that fields are carried through BY CONSTRUCTION — a property
a reviewer reads — and name the guarantees that can actually fail: the pinned
upstream SOURCE_SHA256, the requirement that sk be present to be dropped, the
expected group and per-group test counts, and verify mode's byte-comparison of
the committed file. The last two were negative-tested: removing sk aborts with
'has no field sk to drop', deleting a test aborts with 'group 19 has 13 tests'.

The script is also now described as re-runnable EVIDENCE rather than a gate: it
needs network access, so nothing invokes it automatically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:41:01 +02:00
c945821bf9 vectors: make the extraction re-derivable and the provenance claim literally true
Round-7 review (both reviewers, independently) found that the ACVP provenance
note said "the per-test private key `sk` dropped ... Nothing else altered" while
the extraction had in fact also dropped `additionalRandomness` and `deferred`
from all 42 tests, plus the top-level `isSample`. No field the tests consume was
affected and no verdict changed — but the provenance block is the audit trail a
third party diffs against, and as written it would have produced a false alarm
or taught the next reviewer to wave differences through.

Rather than reword the note, the transformation is now executable and pinned:
tests/nist_acvp_vectors/extract_sha2_128s.py re-derives the file, verifies the
upstream sha256 before doing anything, requires exactly 3 groups x 14 tests,
removes exactly ONE field (`sk`) and asserts that invariant, carries every other
per-test, group and top-level key through untouched, and writes canonical
output. Run with no arguments it VERIFIES the committed file against a fresh
extraction; --write regenerates. The file was regenerated with it, so
"only `sk` removed" is now literally true and machine-checkable.

Also from round 7, precision in the tests themselves:
- exact assertions replace floors: `points == 108` (was >= 100),
  `with_ctx == 9` (was > 0), and prehash `3/4/7` (was `checked > 0`). With 7 of
  14 prehash vectors skipped for unimplemented hash functions, a floor would
  have let real coverage fall from 3 to 1 while the total still summed to 14.
- the randomized bridge's doc comment claimed corruption "across the WHOLE
  signature"; measured, the schedule hits 72 distinct positions in 11..=7779,
  never bytes 0-10 or 7780-7855. Corrected to the measured statement.

Verified on stable Rust (rustc 1.95) as well as the pinned nightly: the bridge
needs no nightly feature, so a third party can run all of it with cargo alone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 13:01:13 +02:00
3153988c4e tests: NIST ACVP SHA2-128s verification KATs + a real differential bridge
External review flagged across rounds 4-6 that the empirical evidence tying the
proved model to the deployed code had not moved by a single data point in six
rounds: the sole bridge was nine assertion points (3 rounds, one fixed seed,
corruption always at byte 100), and the ACVP vectors vendored here contain NO
SLH-DSA-SHA2-128s sigVer group at all — the one parameter set this verification
campaign is about had zero NIST known-answer verification coverage.

VECTORS. tests/nist_acvp_vectors/SLH-DSA-sigVer-FIPS205/sha2_128s_extracted.json
carries the three SHA2-128s sigVer groups extracted verbatim from the official
NIST ACVP-Server vector set (source URL, upstream file sha256 and extraction
method recorded in the file's own _provenance block; per-test private keys
dropped as unnecessary to verify). 42 tests: 2 valid and 12 negative per group,
the negatives spread over structurally distinct corruption sites — modified R,
modified SIGFORS, modified SIGHT, modified message, too-small and too-large
signatures.

TESTS (all in src/verify_mono.rs, so they exercise the monomorphic path the Lean
certificates are about):

- mono_matches_nist_acvp_128s_internal — NIST's `internal` group carries M'
  directly, which is exactly what slh_verify_128s consumes, so these are true
  known-answer tests OF THE PROVED PATH: 10 executed, 4 attributed to
  deserialization (wrong-length signatures, rejected above the extraction root).
  Accounting is exact — all 14 are accounted for, nothing silently skipped.
- mono_matches_nist_acvp_128s_external_pure — builds M' the way lib.rs does and
  requires mono, the deployed verifier and NIST to agree: 10 executed, 9 of them
  with a NON-EMPTY context. This is the first empirical check of the
  domain-separator byte and context-length prefix that TRUSTED-BASE item 10
  declares outside every proof.
- deployed_matches_nist_acvp_128s_prehash — validates the deployed prehash path
  for 128s: 3 executed, 4 wrong-length, and 7 skipped because NIST exercises
  prehash functions (SHA3-*, truncated SHA2) this crate's `Ph` enum does not
  implement. Counted and reported rather than hidden.
- mono_matches_deployed_randomized — replaces the fixed-seed/fixed-byte bridge:
  12 rounds, varying message lengths including empty, corruption spread across
  the WHOLE 7856-byte signature, plus wrong-public-key and wrong-context cases
  that were never exercised before. 108 assertion points, each requiring mono
  and deployed to agree.

Bridge coverage: 9 assertion points -> 131, of which 20 are NIST known-answer
tests on the proved path where there were previously none.

No change to any verify-path function: this commit touches test code and test
data only, so the Charon/Aeneas extraction is unaffected (verified separately by
re-running extract.sh and diffing the generated model).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 09:42:56 +02:00
4 changed files with 991 additions and 0 deletions

View file

@ -354,6 +354,13 @@ mod tests {
use crate::slh_dsa_sha2_128s::{PublicKey, KG};
use crate::traits::{KeyGen, SerDes, Signer, Verifier};
use crate::types::{SlhDsaSig, SlhPublicKey};
// This crate is no_std; the test binary links std, so pull in the pieces the
// NIST-vector tests need (heap vectors for variable-length messages/contexts,
// and serde_json for the ACVP file).
extern crate std;
use std::vec::Vec;
use std::{println, vec};
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha8Rng;
@ -412,4 +419,279 @@ mod tests {
assert!(!mono_wm, "wrong-message signature accepted");
}
}
// ───────────────────────────────────────────────────────────────────────
// NIST ACVP known-answer coverage for SLH-DSA-SHA2-128s.
//
// Why this exists: the ACVP vector file vendored upstream contains NO
// SHA2-128s sigVer group (only 192s/256f/SHAKE variants), so the single
// parameter set this verification campaign is about had ZERO NIST
// known-answer verification coverage — flagged by external review across
// three rounds as the largest non-gate gap. The three 128s sigVer groups
// were extracted verbatim from the official NIST ACVP-Server vector set
// into tests/nist_acvp_vectors/SLH-DSA-sigVer-FIPS205/sha2_128s_extracted.json
// (provenance, source URL and upstream sha256 recorded inside that file).
//
// NIST supplies 14 tests per group: 2 valid, and 12 negative spread over
// structurally distinct corruption sites — modified R, modified SIGFORS,
// modified SIGHT, modified message, and signatures that are too small or
// too large. That is materially stronger than flipping one fixed byte.
const ACVP_128S: &str = include_str!(
"../tests/nist_acvp_vectors/SLH-DSA-sigVer-FIPS205/sha2_128s_extracted.json"
);
fn hexb(s: &str) -> Vec<u8> {
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
}
fn acvp_groups() -> Vec<serde_json::Value> {
let v: serde_json::Value = serde_json::from_str(ACVP_128S).unwrap();
v["testGroups"].as_array().unwrap().clone()
}
fn pk_parts(pk_hex: &str) -> SlhPublicKey<16> {
let b = hexb(pk_hex);
assert_eq!(b.len(), 32, "128s public key must be 32 bytes");
let mut pk_seed = [0u8; 16];
let mut pk_root = [0u8; 16];
pk_seed.copy_from_slice(&b[0..16]);
pk_root.copy_from_slice(&b[16..32]);
SlhPublicKey { pk_seed, pk_root }
}
/// THE PROVED PATH AGAINST NIST. The `internal` group carries M' directly
/// (no context wrapping, no domain separator), which is exactly the input
/// `verify_mono::slh_verify_128s` consumes — so these are true
/// known-answer tests of the function the eleven Lean certificates are
/// about, not of a wrapper above it.
#[test]
fn mono_matches_nist_acvp_128s_internal() {
let mut checked = 0usize;
let mut deserialization_rejects = 0usize;
for g in acvp_groups() {
if g["signatureInterface"] != "internal" { continue; }
for t in g["tests"].as_array().unwrap() {
let expected = t["testPassed"].as_bool().unwrap();
let sig_v = hexb(t["signature"].as_str().unwrap());
let msg = hexb(t["message"].as_str().unwrap());
let ipk = pk_parts(t["pk"].as_str().unwrap());
// Wrong-length signatures are rejected by deserialization, which
// sits ABOVE the extraction root (TRUSTED-BASE item 10) — the
// proved path is never reached. Record, do not silently skip.
if sig_v.len() != 7856 {
assert!(!expected, "NIST expects a wrong-length signature to fail");
deserialization_rejects += 1;
continue;
}
let mut sig_bytes = [0u8; 7856];
sig_bytes.copy_from_slice(&sig_v);
let sig = SlhDsaSig::<12, 7, 9, 14, 35, 16>::deserialize(&sig_bytes);
let got = slh_verify_128s(&msg, &sig, &ipk);
assert_eq!(
got, expected,
"tcId {} ({}): mono verdict {} != NIST {}",
t["tcId"], t["reason"].as_str().unwrap_or(""), got, expected
);
checked += 1;
}
}
// Exact accounting: every NIST test is either executed against the proved
// path or explicitly attributed to deserialization. Nothing is silently
// skipped, and if NIST's file changes shape this fails loudly.
assert_eq!(checked + deserialization_rejects, 14, "unaccounted NIST internal tests");
assert_eq!(checked, 10, "expected 10 executable internal KATs");
assert_eq!(deserialization_rejects, 4, "expected 4 wrong-length (too small/large) cases");
println!(
"mono vs NIST ACVP 128s (internal): {checked} executed against the PROVED path, \
{deserialization_rejects} rejected at deserialization (above the extraction root)"
);
}
/// THE M' ASSEMBLY, PINNED EMPIRICALLY. The `external pure` group carries a
/// real (often NON-EMPTY) context, so building M' the way lib.rs does and
/// feeding it to the mono path checks the domain-separator byte and the
/// context-length prefix that TRUSTED-BASE item 10 declares OUTSIDE every
/// proof. Mono, the deployed verifier, and NIST must all agree.
#[test]
fn mono_matches_nist_acvp_128s_external_pure() {
use crate::slh_dsa_sha2_128s::PublicKey as PK128s;
let mut checked = 0usize;
let mut wrong_len = 0usize;
let mut with_ctx = 0usize;
for g in acvp_groups() {
if g["signatureInterface"] != "external" || g["preHash"] != "pure" { continue; }
for t in g["tests"].as_array().unwrap() {
let expected = t["testPassed"].as_bool().unwrap();
let sig_v = hexb(t["signature"].as_str().unwrap());
if sig_v.len() != 7856 { assert!(!expected); wrong_len += 1; continue; }
let msg = hexb(t["message"].as_str().unwrap());
let ctx = hexb(t["context"].as_str().unwrap_or(""));
assert!(ctx.len() <= 255, "ACVP context longer than FIPS 205 allows");
if !ctx.is_empty() { with_ctx += 1; }
let pk_b = hexb(t["pk"].as_str().unwrap());
let mut pk_arr = [0u8; 32];
pk_arr.copy_from_slice(&pk_b);
let mut sig_bytes = [0u8; 7856];
sig_bytes.copy_from_slice(&sig_v);
// M' = toByte(0,1) ‖ toByte(|ctx|,1) ‖ ctx ‖ M (pure variant)
let mut mprime = Vec::with_capacity(2 + ctx.len() + msg.len());
mprime.push(0u8);
mprime.push(ctx.len() as u8);
mprime.extend_from_slice(&ctx);
mprime.extend_from_slice(&msg);
let ipk = pk_parts(t["pk"].as_str().unwrap());
let sig = SlhDsaSig::<12, 7, 9, 14, 35, 16>::deserialize(&sig_bytes);
let mono = slh_verify_128s(&mprime, &sig, &ipk);
let deployed = PK128s::try_from_bytes(&pk_arr).unwrap().verify(&msg, &sig_bytes, &ctx);
assert_eq!(mono, deployed, "tcId {}: mono != deployed", t["tcId"]);
assert_eq!(
mono, expected,
"tcId {} ({}): verdict {} != NIST {}",
t["tcId"], t["reason"].as_str().unwrap_or(""), mono, expected
);
checked += 1;
}
}
assert_eq!(checked + wrong_len, 14, "unaccounted NIST external-pure tests");
assert_eq!(checked, 10, "expected 10 executable external-pure KATs");
// Exact, not a floor: a NIST data update that quietly reduced non-empty
// contexts to one would otherwise pass silently (round-7 review).
assert_eq!(with_ctx, 9, "expected 9 executable non-empty-context KATs");
println!(
"mono+deployed vs NIST ACVP 128s (external pure): {checked} executed \
({with_ctx} with a NON-EMPTY context), {wrong_len} rejected at deserialization"
);
}
/// The prehash group validates the DEPLOYED verifier against NIST for 128s.
/// The mono path is deliberately NOT driven here: prehash M' assembly adds
/// an OID and a message digest, and reconstructing it in the test would be
/// re-implementing the very wrapper code that is out of scope — the honest
/// statement is that this group covers the deployed path only.
#[test]
fn deployed_matches_nist_acvp_128s_prehash() {
use crate::slh_dsa_sha2_128s::PublicKey as PK128s;
use crate::types::Ph;
let mut checked = 0usize;
let mut unsupported_alg = 0usize;
let mut wrong_len = 0usize;
for g in acvp_groups() {
if g["preHash"] != "preHash" { continue; }
for t in g["tests"].as_array().unwrap() {
let expected = t["testPassed"].as_bool().unwrap();
let sig_v = hexb(t["signature"].as_str().unwrap());
if sig_v.len() != 7856 { assert!(!expected); wrong_len += 1; continue; }
// FIPS 205 permits more prehash functions than this crate's `Ph`
// enum implements (NIST exercises SHA3-* and the truncated SHA2
// variants too). Those are unreachable through the public API, so
// they are counted and skipped rather than failing the test.
let ph = match t["hashAlg"].as_str().unwrap_or("") {
"SHA2-256" => Ph::SHA256,
"SHA2-512" => Ph::SHA512,
"SHAKE-128" => Ph::SHAKE128,
"SHAKE-256" => Ph::SHAKE256,
_ => { unsupported_alg += 1; continue; }
};
let msg = hexb(t["message"].as_str().unwrap());
let ctx = hexb(t["context"].as_str().unwrap_or(""));
let pk_b = hexb(t["pk"].as_str().unwrap());
let mut pk_arr = [0u8; 32];
pk_arr.copy_from_slice(&pk_b);
let mut sig_bytes = [0u8; 7856];
sig_bytes.copy_from_slice(&sig_v);
let got = PK128s::try_from_bytes(&pk_arr).unwrap().hash_verify(&msg, &sig_bytes, &ctx, &ph);
assert_eq!(
got, expected,
"tcId {} ({}): deployed prehash verdict {} != NIST {}",
t["tcId"], t["reason"].as_str().unwrap_or(""), got, expected
);
checked += 1;
}
}
assert_eq!(checked + wrong_len + unsupported_alg, 14, "unaccounted NIST prehash tests");
// Exact counts. With 7 of 14 skipped for unimplemented prehash functions,
// a floor of `> 0` would let real coverage fall from 3 to 1 invisibly
// while the total still summed to 14 (round-7 review).
assert_eq!(checked, 3, "expected exactly 3 executable prehash KATs");
assert_eq!(wrong_len, 4, "expected exactly 4 wrong-length prehash cases");
assert_eq!(unsupported_alg, 7, "expected exactly 7 unimplemented-prehash skips");
println!(
"deployed vs NIST ACVP 128s (prehash): {checked} executed, {wrong_len} wrong-length, \
{unsupported_alg} skipped (hash function not implemented by this crate)"
);
}
/// RANDOMIZED DIFFERENTIAL BRIDGE. The original bridge was nine assertion
/// points: three rounds from one fixed seed, corrupting one fixed byte
/// (index 100) of a 7856-byte signature. This walks many seeds and spreads
/// corruption over **72 distinct deterministic positions in the range
/// 11..=7779** of the 7856-byte signature (round-7 review measured the
/// schedule; "the whole signature" overstated it — bytes 0-10 and
/// 7780-7855 are never selected), and adds wrong-key and wrong-context
/// cases the original never exercised. Every point asserts
/// mono and deployed agree — that is the bridge — and that forgeries are
/// rejected.
#[test]
fn mono_matches_deployed_randomized() {
let mut rng = ChaCha8Rng::seed_from_u64(0x5EED_0F15u64);
let mut points = 0usize;
for round in 0u32..12 {
let (pk, sk) = KG::try_keygen_with_rng(&mut rng).unwrap();
let (pk_other, _) = KG::try_keygen_with_rng(&mut rng).unwrap();
// vary message length, including empty
let mlen = (round as usize * 7) % 23;
let msg: Vec<u8> = (0..mlen).map(|i| (i as u8).wrapping_mul(31).wrapping_add(round as u8)).collect();
let sig_bytes = sk.try_sign_with_rng(&mut rng, &msg, &[], false).unwrap();
let mut mprime = vec![0u8, 0u8];
mprime.extend_from_slice(&msg);
let (ipk, sig) = internal_inputs(&pk, &sig_bytes);
// valid
let d = pk.verify(&msg, &sig_bytes, &[]);
let m = slh_verify_128s(&mprime, &sig, &ipk);
assert!(d, "deployed rejected a fresh valid signature");
assert_eq!(m, d, "round {round}: mono != deployed on a valid signature");
points += 1;
// corruption spread across the entire signature, not one fixed byte
for k in 0..6 {
let mut bad = sig_bytes;
let idx = ((round as usize * 1237 + k * 1301) * 7 + 11) % bad.len();
let bit = 1u8 << ((round as usize + k) % 8);
bad[idx] ^= bit;
let (_, bad_sig) = internal_inputs(&pk, &bad);
let db = pk.verify(&msg, &bad, &[]);
let mb = slh_verify_128s(&mprime, &bad_sig, &ipk);
assert_eq!(mb, db, "round {round}: mono != deployed on corruption at byte {idx}");
assert!(!mb, "round {round}: corrupted signature accepted (byte {idx})");
points += 1;
}
// wrong public key — never exercised before
let (ipk_other, _) = internal_inputs(&pk_other, &sig_bytes);
let dk = pk_other.verify(&msg, &sig_bytes, &[]);
let mk = slh_verify_128s(&mprime, &sig, &ipk_other);
assert_eq!(mk, dk, "round {round}: mono != deployed under a wrong public key");
assert!(!mk, "round {round}: signature verified under the wrong public key");
points += 1;
// wrong context: deployed is given a non-empty ctx while the
// signature was made over the empty one; M' changes accordingly.
let ctx = [round as u8, 0xAA];
let mut mprime_ctx = vec![0u8, ctx.len() as u8];
mprime_ctx.extend_from_slice(&ctx);
mprime_ctx.extend_from_slice(&msg);
let dc = pk.verify(&msg, &sig_bytes, &ctx);
let mc = slh_verify_128s(&mprime_ctx, &sig, &ipk);
assert_eq!(mc, dc, "round {round}: mono != deployed under a wrong context");
assert!(!mc, "round {round}: signature verified under the wrong context");
points += 1;
}
assert_eq!(points, 108, "expected exactly 108 differential points, got {points}");
println!("randomized differential bridge: {points} assertion points");
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Deterministically re-derive tests/nist_acvp_vectors/SLH-DSA-sigVer-FIPS205/
sha2_128s_extracted.json from the official NIST ACVP-Server vector set.
Why this file exists at all: the ACVP vector file vendored in this repository
contains no SLH-DSA-SHA2-128s sigVer group, so the parameter set the associated
verification work is about had no NIST known-answer verification coverage. The
three 128s groups are therefore taken from upstream.
Why this SCRIPT exists: external review (round 7) observed that a hand-made
extraction with a prose provenance note is not auditable the first version's
note said "only `sk` dropped" while three further fields had in fact been
removed. The transformation is now executable, pinned, and fails closed:
* the upstream file's sha256 must match SOURCE_SHA256 exactly;
* exactly EXPECTED_GROUPS groups must match the parameter set, each with
EXPECTED_TESTS_PER_GROUP tests;
* exactly one field, `sk`, is removed, and it must be present to be removed
(so a schema change is caught, not silently transformed);
* every other key is carried through untouched BY CONSTRUCTION, which a
reviewer verifies by reading `build()`, not by a self-check: no test inside a
transformer can detect a corrupted input, since the transformer is what
defines the output. The input is instead pinned by SOURCE_SHA256;
* verify mode re-derives and byte-compares the committed file, so a hand-edit
of the committed JSON IS caught;
* output is canonical (fixed indent, trailing newline).
NOT A GATE re-runnable EVIDENCE. Round-8 review noted the distinction: this
script needs network access, so nothing invokes it automatically (it is outside
`cargo test` and outside verification/check.sh). The committed JSON is still
trusted at review time; what this script provides is that a reviewer can
CHECK that trust cheaply and mechanically instead of taking a prose note's word.
A CI job running it in verify mode would close the remaining gap.
Usage:
python3 extract_sha2_128s.py # verify the committed file matches
python3 extract_sha2_128s.py --write # regenerate it
The upstream file is ~30 MB; it is fetched to a temporary path and not vendored.
"""
import argparse, hashlib, json, os, sys, tempfile, urllib.request
SOURCE_URL = (
"https://raw.githubusercontent.com/usnistgov/ACVP-Server/master/"
"gen-val/json-files/SLH-DSA-sigVer-FIPS205/internalProjection.json"
)
SOURCE_SHA256 = "a013fc2104f4ed4799d96d51141f65b965969b2cf10646626a021b6d456ce792"
PARAMETER_SET = "SLH-DSA-SHA2-128s"
EXPECTED_GROUPS = 3
EXPECTED_TESTS_PER_GROUP = 14
DROP_FIELDS = frozenset({"sk"}) # the ONLY per-test removal
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"SLH-DSA-sigVer-FIPS205", "sha2_128s_extracted.json")
PROVENANCE = {
"what": f"{PARAMETER_SET} sigVer test groups, extracted verbatim from the "
"official NIST ACVP-Server vector set.",
"why": "The vector file vendored upstream in this directory contains NO "
f"{PARAMETER_SET} sigVer group (only 192s/256f/SHAKE variants), so the "
"one parameter set the verification work targets had zero NIST "
"known-answer verification coverage.",
"source_url": SOURCE_URL,
"source_sha256": SOURCE_SHA256,
"retrieved_utc": "2026-07-28",
"extraction": "Produced by tests/nist_acvp_vectors/extract_sha2_128s.py, which "
"verifies the upstream sha256, selects every testGroup whose "
f"parameterSet == {PARAMETER_SET}, and removes exactly ONE "
"per-test field: `sk` (the private key, not needed to verify a "
"signature). Every other per-test field and all group and "
"top-level metadata are carried through unchanged by construction. "
"The guarantees that can actually fail are: the pinned upstream "
"SOURCE_SHA256; the requirement that `sk` be present to be dropped; "
"the expected group and per-group test counts; and verify mode, which "
"re-derives and byte-compares this committed file.",
"note": "Test DATA only. Expected outcomes are NIST's `testPassed` field; "
"`reason` records why a negative case must be rejected.",
"regenerate": "python3 tests/nist_acvp_vectors/extract_sha2_128s.py --write",
}
def fetch() -> dict:
with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as fh:
tmp = fh.name
try:
urllib.request.urlretrieve(SOURCE_URL, tmp)
raw = open(tmp, "rb").read()
finally:
os.unlink(tmp)
got = hashlib.sha256(raw).hexdigest()
if got != SOURCE_SHA256:
sys.exit(f"FATAL: upstream sha256 {got} != pinned {SOURCE_SHA256}.\n"
"The NIST file changed. Review the delta and update the pin "
"deliberately; do not regenerate blindly.")
return json.loads(raw)
def build(full: dict) -> dict:
groups = []
for g in full["testGroups"]:
if g.get("parameterSet") != PARAMETER_SET:
continue
tests = []
for t in g["tests"]:
for f in DROP_FIELDS:
if f not in t:
sys.exit(f"FATAL: tcId {t.get('tcId')} has no field {f!r} to drop")
# Fields are carried through BY CONSTRUCTION: `kept` is `t` minus
# DROP_FIELDS, so every retained key holds the identical object.
#
# Round-8 review found a tautological `assert` here — it compared
# `kept` against the comprehension that had just built it, so it could
# never fire. The first attempt to repair it (comparing kept[k] to
# t[k]) was tautological for the same reason, and that is the lesson
# worth recording: NO check inside this function can detect a
# corrupted input, because this function is what defines the output
# from that input. Faithfulness here is a property of the two lines
# below, which a reviewer reads; it is not something the script can
# test about itself.
#
# What actually protects the result, and can fail:
# * SOURCE_SHA256 — the input is pinned, so upstream cannot drift
# or be substituted without an explicit, reviewed pin change;
# * the `f not in t` presence check above — `sk` must exist to be
# dropped, so a schema change is caught rather than silently
# producing a different transformation;
# * EXPECTED_GROUPS / EXPECTED_TESTS_PER_GROUP below;
# * verify mode, which re-derives and byte-compares the committed
# file, so a hand-edit of the committed JSON is caught.
kept = {k: v for k, v in t.items() if k not in DROP_FIELDS}
tests.append(kept)
if len(tests) != EXPECTED_TESTS_PER_GROUP:
sys.exit(f"FATAL: group {g['tgId']} has {len(tests)} tests, "
f"expected {EXPECTED_TESTS_PER_GROUP}")
ng = {k: v for k, v in g.items() if k != "tests"}
ng["tests"] = tests
groups.append(ng)
if len(groups) != EXPECTED_GROUPS:
sys.exit(f"FATAL: found {len(groups)} {PARAMETER_SET} groups, expected {EXPECTED_GROUPS}")
out = {"_provenance": PROVENANCE}
for k, v in full.items(): # all top-level metadata, verbatim
if k != "testGroups":
out[k] = v
out["testGroups"] = groups
return out
def render(obj: dict) -> str:
return json.dumps(obj, indent=1) + "\n"
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--write", action="store_true", help="regenerate the committed file")
args = ap.parse_args()
text = render(build(fetch()))
if args.write:
open(OUT, "w").write(text)
print(f"wrote {OUT} ({len(text)} bytes)")
return 0
if not os.path.exists(OUT):
print(f"MISSING: {OUT}"); return 1
cur = open(OUT).read()
if cur == text:
print(f"OK: {OUT} matches a fresh extraction from the pinned upstream file")
return 0
print(f"MISMATCH: {OUT} differs from a fresh extraction — re-run with --write "
"and review the diff")
return 1
if __name__ == "__main__":
sys.exit(main())