-
-
Notifications
You must be signed in to change notification settings - Fork 1
vector_audit
How leviathan-crypto's test vectors are sourced, classified, and independently verified. The verification chain mixes pinned external authorities, an immutability-checking shasum gate in CI, and an independent Rust verifier that re-derives every self-generated wire format from primitives that share zero code with leviathan-crypto.
Test vectors fall into four tiers based on their authority and how their correctness can be checked. Verification strategy varies by tier.
Tier 1: External authority. Vectors come from NIST CAVP, NIST ACVP, RFC test appendices, or NESSIE. The vector files in test/vectors/ are byte-for-byte copies of the upstream files, with provenance recorded below. These vectors define correctness for their primitives, so re-deriving the expected outputs in a parallel implementation does not establish a new fact about the primitive itself; the alternative implementation is tested against the same vectors. The verifier still re-derives several Tier 1 primitives (AES across modes, POLYVAL, ML-KEM, ML-DSA) against RustCrypto as a transcription audit, catching copy-paste errors when ACVP / CAVP / RFC records were ported into the repo. The first-line audit discipline remains provenance: the upstream URL is recorded, the file checksum is pinned in test/vectors/SHA256SUMS, and any change requires a fresh download from the authoritative source.
Tier 2: Self-generated over standard primitives. Vectors encode wire formats designed by leviathan-crypto, but the underlying primitives are well-defined and have multiple independent implementations. The seal and sealstream KAT vectors live here. Re-deriving them in a different language with a different crypto stack is meaningful evidence the wire format claim holds. This is the target of the Rust verifier.
Tier 3: Self-generated over custom primitives. The construction is unique to leviathan-crypto and has no external reference implementation. The SPQR ratchet KATs and Fortuna PRNG KATs live here. The audit discipline is internal consistency: round-trip tests, never-reuse-nonce invariants, forward-secrecy property tests. Cross-language verification has no external authority to verify against.
Tier 4: Hybrid. Vectors that wrap a Tier 1 primitive in a Tier 2 construction. ML-KEM-suite seal blobs are an example. The KEM ciphertext piece is Tier 1 (NIST ACVP defines correctness). The seal-format wrapper is Tier 2 (we designed it). The verifier independently covers the ML-KEM primitive against ACVP and covers symmetric Tier 2 wrappers, but it does not yet exercise KEM-wrapped seal blobs end to end. The two pieces are verified separately; their composition is not yet a single verifier target.
The Rust verifier at scripts/verify-vectors/ re-derives every byte of every Tier 2 vector from primitives that share zero code with leviathan-crypto's WASM implementation. Specifically:
XChaCha20 v3 seal and sealstream. Verified against:
- HKDF-SHA-256 from RustCrypto's
hkdf+sha2crates. - HChaCha20 hand-rolled from RFC 8439 §2.3 in pure Rust with no external dependency.
- ChaCha20-Poly1305 from RustCrypto's
chacha20poly1305crate.
The verifier independently computes the 32-byte key commitment from HKDF bytes 32..64 and asserts it matches the pinned preamble, then encrypts each chunk with the derived subkey and compares the wire bytes. Multi-chunk path verifies per-chunk counter increment, TAG_DATA versus TAG_FINAL flag handling, and framed-mode u32be length prefixes.
Serpent v3 seal and sealstream. Verified against:
- HKDF-SHA-256 from RustCrypto's
hkdf+sha2crates (96-byte output). - HMAC-SHA-256 from RustCrypto's
hmaccrate, used both for per-chunk IV derivation and for chunk authentication. - Serpent block cipher from RustCrypto's
serpent = "0.6"crate, separately confirmed byte-correct against NESSIE Set 1 vector#0 across all three key sizes (128/192/256). - CBC chaining and PKCS#7 padding hand-rolled from spec.
Both leviathan-crypto v3 and RustCrypto's serpent crate use NIST natural byte order at their public APIs. The verifier feeds keys, IVs, and plaintext blocks through unchanged; no byte-reversal dance applies at the block-cipher boundary. v2 used the AES-submission floppy byte order at its public API and required a reversal at this boundary; v3 removes that asymmetry.
AES-GCM-SIV v3 seal and sealstream. Verified against:
- HKDF-SHA-256 from RustCrypto's
hkdf+sha2crates. - AES-256-GCM-SIV from RustCrypto's
aes-gcm-sivcrate, already pinned for the Tier 1 RFC 8452 target and reused here as the per-chunk AEAD for the Tier 2 seal wrapper.
The structural shape mirrors XChaCha20 v3 byte for byte: same HKDF info-binding pattern with the 20-byte header concatenated to the info string, same 52-byte preamble (20-byte header plus 32-byte commitment), same 12-byte counter-nonce per chunk, same framed-mode u32be length prefix on the wire. The only difference is the AEAD primitive and the absence of an HChaCha20-equivalent subkey step; AES-GCM-SIV consumes the 32-byte HKDF output directly. The verifier independently recomputes the commitment from HKDF bytes 32..64, asserts byte-equality with the pinned preamble, encrypts each chunk with the derived key and counter nonce, and compares wire bytes for both unframed and framed shapes.
AES symmetric primitives. Verified against:
- AES block cipher (FIPS 197) from RustCrypto's
aescrate, exercising all three key sizes (128, 192, 256) against the FIPS 197 known-answer vectors. - AES-CBC (NIST SP 800-38A) from RustCrypto's
cbccrate. - AES-CTR (NIST SP 800-38A) from RustCrypto's
ctrcrate. - AES-GCM (NIST SP 800-38D + January 2004 submission CAVP
.rspvectors) from RustCrypto'saes-gcmcrate. - AES-GCM-SIV (RFC 8452) from RustCrypto's
aes-gcm-sivcrate.
Each AES target reads its respective KAT file and asserts byte-for-byte agreement with RustCrypto's output for every record. The role here is transcription audit: catching any error introduced when the original NIST or RFC records were ported into the repo's .ts vector format.
POLYVAL primitive. Verified against RustCrypto's polyval crate, which implements the universal hash directly per RFC 8452 §3. POLYVAL stands as its own target separate from AES-GCM-SIV because the test corpus includes the §7 / Appendix A KATs that exercise POLYVAL's reflected-GHASH structure independent of the AEAD wrapper.
ML-KEM primitive (FIPS 203). Verified against RustCrypto's ml-kem crate. The verifier reads the NIST ACVP keyGen and encap+decap records (mlkem_keygen.ts, mlkem_encapdecap.ts) and reproduces every ACVP-published expected output:
- §6.1 KeyGen_internal:
KeyGen::from_seed(d ‖ z)returns dk; the matching ek encoding compares topkand the dk encoding tosk. - §6.2 Encaps_internal:
EncapsulationKey::encapsulate_deterministic(m)reproduces the ACVP(c, k)pair given the published 32-byte message m. - §6.3 Decaps_internal:
Decapsulate::decapsulate_slice(c)reproduces the expected k. For modified-ciphertext records, the FO transform's implicit-rejection branch returns a pseudorandom secret matching the published k. - §7.2 / §7.3:
EncapsulationKey::newand the deprecatedfrom_expandednatively perform the encap-key and decap-key validity round-trip checks, withErrreturned on failure.
ML-DSA primitive (FIPS 204). Verified against RustCrypto's ml-dsa crate (rc.9). The verifier reads the NIST ACVP keyGen, sigGen, and sigVer records (mldsa_keygen.ts, mldsa_siggen.ts, mldsa_sigver.ts, ACVP vsId=42) and reproduces:
- KeyGen:
KeyGen::from_seed(ξ)returns aSigningKey<P>; the pk and expanded sk encodings (FIPS 204 Algorithms 22 + 24) compare to ACVPpkandsk. - SigGen: the verifier rebuilds M' per (signatureInterface, preHash, externalMu) per FIPS 204 §6.2 / §5.4 and calls
sign_internal(&[M'], &rnd)(orsign_mu_*for externalMu). Deterministic mode passes the all-zero 32-byte vector as rnd; hedged signing checks against ACVP's published per-record rnd. - SigVer:
VerifyingKey::decode(pk_bytes)plusSignature::decode(...), thenverify_internal(&M', &sig)(orverify_mu(mu, &sig)). The boolean result compares to ACVPtestPassed. The pinned rc.9 sits on the patched side of GHSA-5x2r-hc65-25f9 (sigVer previously accepted hint vectors with non-strictly-increasing indices), so hint-malleability rejection records validate cleanly.
A fourth ML-DSA vector file, mldsa_siggen_kats.ts, ships ACVP §6.1.2 Tables 1 and 2 (Sign_internal rejection-path KATs plus high-rejection-count KATs, 27 records total). It is sourced verbatim from the spec's asciidoc at usnistgov/ACVP/src/ml-dsa/sections/04-testtypes.adoc (revision f66d187, Nov 19 2025), pinned in SHA256SUMS, and exercised by test/unit/mldsa/mldsa_siggen_kats.test.ts via hash-comparison rather than Rust-verifier re-derivation. The spec stores SHA2-256(pk‖sk) and SHA2-256(σ) rather than the full bytes for compactness; the unit test reconstructs both halves locally and compares hashes. The Rust verifier does not exercise this file (the spec is itself the external authority; no second-source re-derivation is available).
Ed25519 and X25519 (RFC 8032 / RFC 7748). Verified against:
- ed25519-dalek 2.2.0 (dalek-cryptography organisation).
- x25519-dalek 2.0.1 (dalek-cryptography organisation).
- curve25519-dalek 4.1.3 (dalek-cryptography organisation), pinned
explicitly so the curve arithmetic crate is part of the audit
surface rather than left to whatever happens to satisfy the open
^4bound at build time.
dalek-cryptography is the first verifier lineage outside the RustCrypto
organisation and outside tiny-keccak. The choice is driven by what
exists, not preference: ed25519-dalek and x25519-dalek are the de
facto reference Rust implementations of their respective primitives
and the only widely-audited independent stacks for Curve25519
arithmetic. RustCrypto's ed25519 crate is a trait-only crate (it
defines Signer<Signature> and the encoded-signature shape) and does
not ship its own EdDSA implementation; RustCrypto has no first-party
X25519 crate at all. Selecting dalek keeps the verifier on a
maintained, independently-developed stack while preserving the "no
shared source with leviathan-crypto's WASM" property that the other
oracles rely on.
The Ed25519 verifier reads four files: ed25519.ts (RFC 8032 §7 KATs,
4 pure + 1 prehash, transcribed by hand from the RFC text and run
first as the gate), ed25519_keygen.ts, ed25519_siggen.ts, and
ed25519_sigver.ts (ACVP EDDSA-1.0 records filtered to the ed25519
curve only; ed448 is out of scope for v3). Per-record dispatch:
- keyGen:
SigningKey::from_bytes(&seed)and compare.verifying_key().to_bytes()to ACVPq. - sigGen pure:
SigningKey::sign(&message)and compare to ACVPsignature. The ed25519 sigGen corpus has context length 0 in every preHash=false record, so the verifier routes them through the baresignpath and never touches the (dalek-2.x-unavailable) Ed25519ctx signing API. - sigGen prehash: build a
sha2::Sha512digest pre-updated with the message and callSigningKey::sign_prehashed(prehashed, Some(&context)). Context may be empty. - sigVer:
VerifyingKey::verify_strict(pure) /VerifyingKey::verify_prehashed_strict(prehash). The boolean result is compared to ACVPtestPassed.
Strict-verification posture. The verifier uses the _strict
variants exclusively, matching RFC 8032 §5.1.7 cofactored
verification, FIPS 186-5 §7.6.4, and ACVP testPassed semantics.
_strict rejects mixed-order public keys, small-order public keys,
and non-canonical scalars S; the non-strict verify would diverge
from testPassed on records that exercise those edge cases. The
RFC 8032 §7 gate corpus runs first; if it fails the ACVP corpus is
skipped (a transcription or oracle problem would otherwise look like
"signing works for some records but not others", which is a confusing
failure mode).
The X25519 verifier reads x25519.ts (RFC 7748 §5 iterated KATs at
iter=1 and iter=1000, plus the §6.1 Diffie-Hellman exchange between
Alice and Bob; iter=1000000 is omitted from the corpus, the runtime
is too long for a CI-fast verifier and the iter=1000 case already
catches the same correctness bugs). Per-record dispatch:
- Exchange: both directions of the DH must yield the same shared
secret and must equal the RFC value;
StaticSecret::from(alice_sk).diffie_hellman(&PublicKey::from(bob_pk))and the symmetric Bob-from-Alice path are computed, both compared toshared, and the round-tripStaticSecret::from(alice_sk) → PublicKeyis checked againstalicePk(and similarly for Bob) so the clamp + scalar-mult path is exercised, not just the final shared-secret bytes. - Iterated: implement the RFC 7748 §5.2 loop in Rust over
x25519_dalek::x25519(scalar, u)(the standalone primitive, not the Diffie-Hellman wrapper).
X25519 all-zero shared secret. x25519-dalek does NOT reject the all-zero shared secret at the function-call level; the spec's §6 small-order rejection is the consumer's responsibility, and the RFC explicitly allows that check to be performed by the application above the primitive. The verifier's job here is byte agreement on the raw scalar-mult output. Rejection-of-degenerate-public-keys is exercised separately at the TypeScript layer, where the leviathan-crypto wrapper checks for the all-zero shared secret and throws.
Provenance for the five new files is recorded inline:
- RFC 8032:
https://www.rfc-editor.org/rfc/rfc8032.txt(consumed byed25519.ts; 4 §7.1 records + 1 §7.3 record, hand-transcribed). - RFC 7748:
https://www.rfc-editor.org/rfc/rfc7748.txt(consumed byx25519.ts; §6.1 exchange + §5 iter=1 + §5 iter=1000, hand-transcribed). - ACVP EDDSA-KeyGen-1.0:
https://github.com/usnistgov/ACVP-Server/tree/15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0/gen-val/json-files/EDDSA-KeyGen-1.0(consumed byed25519_keygen.ts; 3 ed25519 AFT records; ed448 records filtered out at transcription time). - ACVP EDDSA-SigGen-1.0:
https://github.com/usnistgov/ACVP-Server/tree/15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0/gen-val/json-files/EDDSA-SigGen-1.0(consumed byed25519_siggen.ts; 84 ed25519 records across 4 groups: AFT pure 10, AFT prehash 10, BFT pure 32, BFT prehash 32). - ACVP EDDSA-SigVer-1.0:
https://github.com/usnistgov/ACVP-Server/tree/15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0/gen-val/json-files/EDDSA-SigVer-1.0(consumed byed25519_sigver.ts; 10 ed25519 records, mixed pass/fail pertestPassed, 5 AFT pure + 5 AFT prehash).
The ACVP-Server commit hash 15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0
is the same snapshot already pinned for the SLH-DSA corpus
(slhdsa_keygen.ts, slhdsa_siggen.ts, slhdsa_sigver.ts). The
internalProjection.json files at this commit declare
algorithm=EDDSA mode=keyGen|sigGen|sigVer revision=1.0. FIPS 186-5
codifies EdDSA at the standards level but the ACVP-Server checkout
on disk does not currently expose an EDDSA-...-FIPS186-5 directory;
the 1.0 corpus is the on-disk authority. Provenance follows the
on-disk state.
ECDSA-P256 (FIPS 186-5 §6.4 + RFC 6979). The ECDSA-P256 corpus is a Tier 1 combination: NIST ACVP, C2SP Wycheproof, and RFC 6979 §A.2.5 are all external authority. The verifier's role is a transcription audit identical to the ed25519 framing.
The ECDSA-P256 verifier reads five files: ecdsa_p256.ts (RFC 6979
§A.2.5 deterministic-K gate, 2 records hand-transcribed from the RFC
text and run first as the gate), ecdsa_p256_keygen.ts,
ecdsa_p256_siggen.ts, ecdsa_p256_sigver.ts (ACVP
ECDSA-FIPS186-5 records filtered to the P-256 curve and SHA-256 hash;
other curves and other hashes are out of scope), and
ecdsa_p256_wycheproof.ts (the strict-gate + malleability corpus
that NIST ACVP does not exercise). Per-record dispatch:
- keyGen: rederive
q = d*GviaProjectivePoint::generator() * d_scalarand compare the uncompressed SEC1 encoding's(qx, qy)to ACVP. The twosecretGenerationModepaths (FIPS 186-5 §A.2.1 'extra bits' and §A.2.2 'testing candidates') produce the same q given the same d; the mode discriminator is surfaced in the audit log but does not branch. - sigGen: ACVP supplies an explicit per-record
k, so the verifier drivesecdsa::hazmat::sign_prehashed::<NistP256>(d, k, sha256(m))and compares(r, s)byte-for-byte. The ACVP corpus pinned here is componentTest=false for every record; if upstream ever adds componentTest=true P-256 SHA-256 records, the hedged path feedsrndas theadargument tosign_prehashed_rfc6979. - sigVer: build a
VerifyingKeyfrom(qx, qy)viafrom_sec1_bytes, build aSignaturefrom(r, s)viafrom_slice, callverify_prehash(sha256(m), sig), compare the boolean to ACVPtestPassed. - Wycheproof: same shape as sigVer. Result discriminator:
'valid'→ verifier MUST return true;'invalid'→ verifier MUST return false;'acceptable'→ either outcome counts as ok (the p1363 file pinned here contains only'valid'and'invalid', but the parser surface mirrors the upstream schema).
Verification posture. The p256 crate sets NORMALIZE_S = false
for NistP256, so verify_prehash does NOT enforce low-S. This
matches FIPS 186-5 §6.4.4 verbatim (no low-S restriction).
leviathan-crypto's WASM verifier WILL enforce low-S and reject
non-canonical encodings; the strict-vs-non-strict divergence is
exercised by the Wycheproof corpus from the leviathan-crypto side,
not from this oracle. The Rust oracle's job is to reproduce the
published result byte-for-byte for the non-strict semantics, so
its bool matches Wycheproof's recorded discriminator. The RFC 6979
§A.2.5 gate corpus runs first; if it fails, the ACVP and Wycheproof
corpora are skipped (a transcription or oracle problem would
otherwise look like "signing works for some records but not others",
the same confusing failure mode the Ed25519 dispatcher guards
against).
RFC 6979 deterministic-K gate. ACVP supplies an explicit per-record
k, so it cannot exercise RFC 6979's k-from-(d, H(m)) derivation; only
this corpus does. The verifier runs three paths against every gate
record: (a)
ecdsa::hazmat::sign_prehashed_rfc6979::<NistP256, sha2::Sha256>(d, z, &[]) deterministic, (b)
ecdsa::hazmat::sign_prehashed::<NistP256>(d, k, z) with the
RFC-supplied k, (c) verify_prehash(z, recorded_sig) against the
§A.2.5 fixed public key (Ux, Uy). All three must agree with the
recorded (r, s); the dual check catches the case where the explicit-k
arithmetic is right but the deterministic K derivation is wrong, and
vice versa.
Provenance for the five new files is recorded inline:
- RFC 6979 §A.2.5:
https://www.rfc-editor.org/rfc/rfc6979(consumed byecdsa_p256.ts; SHA-256 records over messages "sample" and "test", 2 records hand-transcribed). - ACVP ECDSA-KeyGen-FIPS186-5:
https://github.com/usnistgov/ACVP-Server/tree/15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0/gen-val/json-files/ECDSA-KeyGen-FIPS186-5(consumed byecdsa_p256_keygen.ts; 6 P-256 records across 2 groups, 3 'testing candidates' + 3 'extra bits'; other curves filtered out at transcription time). - ACVP ECDSA-SigGen-FIPS186-5:
https://github.com/usnistgov/ACVP-Server/tree/15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0/gen-val/json-files/ECDSA-SigGen-FIPS186-5(consumed byecdsa_p256_siggen.ts; 10 P-256 SHA-256 records in tgId 14; other curves, other hashes, and SP 800-106 conformance groups filtered out at transcription time). - ACVP ECDSA-SigVer-FIPS186-5:
https://github.com/usnistgov/ACVP-Server/tree/15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0/gen-val/json-files/ECDSA-SigVer-FIPS186-5(consumed byecdsa_p256_sigver.ts; 7 P-256 SHA-256 records in tgId 8, mixed pass/fail pertestPassed). - C2SP Wycheproof
ecdsa_secp256r1_sha256_p1363:https://github.com/C2SP/wycheproof/blob/878e5366008753df2064d40c49f8e2f50f9c6af7/testvectors_v1/ecdsa_secp256r1_sha256_p1363_test.json(consumed byecdsa_p256_wycheproof.ts; 262 records flattened from 112 testGroups, raw r‖s wire shape, no DER).
KMAC and cSHAKE (SP 800-185). Verified against:
- tiny-keccak's KMAC and cSHAKE implementations.
tiny-keccak is a separate Keccak permutation lineage from RustCrypto's sha3 (used elsewhere in this verifier) and from leviathan-crypto's WASM Keccak. The byte-oriented kmac.ts corpus comprises 24 records across six variants: cSHAKE128 (4: 2 samples plus 2 ACVP AFT), cSHAKE256 (5: 2 samples plus 3 ACVP AFT), KMAC128 (5: 3 samples plus 2 ACVP MVT), KMAC256 (3 samples only), KMACXOF128 (3 samples only), KMACXOF256 (4: 3 samples plus 1 ACVP MVT). Sources: NIST CSRC sample PDFs (cSHAKE_samples.pdf, KMAC_samples.pdf, KMACXOF_samples.pdf) plus byte-aligned ACVP-Server records from cSHAKE-128-1.0, cSHAKE-256-1.0, KMAC-128-1.0, and KMAC-256-1.0 (vsId=0 in each). The remaining ACVP cases are bit-level (key, message, MAC, or output lengths not divisible by 8) and are out of scope for leviathan-crypto's byte-oriented public API. They are filtered out at corpus build time and not pinned. The verifier independently reproduces every pinned record byte-for-byte against tiny-keccak. The reason for stepping outside the RustCrypto family for this one corpus is crate availability, not preference: RustCrypto's kmac crate is currently a 0.0.0 placeholder, and the pinned sha3 = "=0.11.0" does not yet expose CShake. tiny-keccak covers both in one crate and lands on a separate Keccak lineage, so the independence claim still holds.
The combined invariant. When all targets emit byte-identical output to the pinned KATs, two independent properties hold simultaneously: leviathan-crypto's symmetric seal wire format reproduces from independent RustCrypto primitives (the Tier 2 reproduction property for XChaCha20 v3 and Serpent v3), and the Tier 1 primitive vectors landed in the repo without transcription error against an independent codebase (the Tier 1 transcription property for AES across modes, POLYVAL, ML-KEM, and ML-DSA). RustCrypto and leviathan-crypto have no shared source code, no shared build system, and no shared person who wrote them. A bug in either stack that affects wire bytes, or a bad copy-paste of an ACVP record, would surface as a verifier mismatch.
Spelling out the limits of the audit is part of the audit.
Constant-time properties are not verified. RustCrypto's primitives are documented as constant-time on supported platforms; leviathan-crypto's WASM primitives are designed for constant-time execution. The verifier checks that they produce the same output bytes, which is a necessary condition for correctness. It does not measure timing variation or independently confirm constant-time behavior on either side. The serpent_audit.md, chacha_audit.md, and sha2_audit.md documents cover that scope separately.
Side channels are out of scope. Cache-timing, power analysis, EM emanation, and speculative-execution leaks are not within reach of byte-equality checking. See architecture.md §Where defense ends.
WASM-internal memory safety is out of scope. The verifier confirms that whatever leviathan-crypto produces on its output buffer matches what RustCrypto produces. It says nothing about whether leviathan-crypto's WASM linear memory is correctly wiped after use, whether transient buffers are scrubbed, or whether dispose paths free key material correctly. The unit and e2e test suites cover those properties via test/unit/*/wipe.test.ts and similar.
Tier 1 vector files are not re-fetched. The Tier 1 KAT files themselves are pinned by SHA-256 hash and treated as authoritative; the verifier's RustCrypto re-derivation is a transcription audit (catching copy-paste errors when records were ported into the repo), not a re-verification of NIST. A discrepancy between RustCrypto and the pinned .ts would point at the .ts first, not at the upstream record.
Tier 3 vectors (ratchet, fortuna) are not covered. No external reference exists to verify against; internal consistency tests in the unit suite cover the available correctness properties.
KEM-wrapped seal blobs (Tier 4) are not covered as a single target. The verifier independently covers the ML-KEM primitive against ACVP and covers symmetric Tier 2 wrappers, but it does not yet exercise MlKemSuite-wrapped seal blobs end to end. Their two pieces are verified separately; their composition is not yet a single verifier target.
The canonical inventory of every pinned vector file, Tier 1 and Tier 2 alike, lives in ../test/vectors/README.md and is mirrored in ./test-suite.md. SHA-256 pins for every file are recorded in ../test/vectors/SHA256SUMS, and the hashsums job in ../.github/workflows/verify-vectors.yml fails the build on any mismatch.
The table below is the audit-doc-specific cut: the Tier 2 self-generated files the Rust verifier currently exercises end to end. They are produced by the generator scripts and pinned as KATs; the verifier re-derives every byte from primitives.
| File | Generator | Verifier coverage |
|---|---|---|
seal_xchacha_v3.ts |
scripts/gen-seal-vectors.ts --cipher xchacha |
full |
seal_serpent_v3.ts |
scripts/gen-seal-vectors.ts --cipher serpent |
full |
seal_aes_v3.ts |
scripts/gen-seal-vectors.ts --cipher aes |
full |
sealstream_xchacha_v3.ts |
scripts/gen-sealstream-vectors.ts --cipher xchacha |
full |
sealstream_serpent_v3.ts |
scripts/gen-sealstream-vectors.ts --cipher serpent |
full |
sealstream_aes_v3.ts |
scripts/gen-sealstream-vectors.ts --cipher aes |
full |
sign_ecdsa_p256.ts |
scripts/gen-ecdsa-p256-vectors.ts |
full |
If a Tier 1 file needs to be refreshed (upstream errata, format change), download the new file, replace the local copy, regenerate SHA256SUMS, update the row in ../test/vectors/README.md, and confirm the relevant unit-test job in .github/workflows/ still passes.
The verify-vectors.yml workflow runs two jobs, sequenced.
Job 1: hashsums. Reads test/vectors/SHA256SUMS and runs sha256sum --check against every pinned vector file. Catches accidental edits or supply-chain tampering of the corpus. Runs in under five seconds.
Job 2: rust-verify. Depends on hashsums. Builds the verifier crate at scripts/verify-vectors/ with the pinned Rust toolchain (1.95.0) and the pinned dependency lockfile, then runs the verifier across twelve cipher targets: xchacha, serpent, aes-seal, aes-gcm-siv, polyval, aes, aes-cbc, aes-ctr, aes-gcm, mlkem, mldsa, and kmac. Each target dispatches to its --target scope (seal, sealstream, keygen, siggen, sigver, or all, depending on the cipher). Caches ~/.cargo/registry and target/ between runs via Swatinem/rust-cache. Cold builds take roughly 60 seconds; cached runs complete in under 15.
Both jobs are gated by workflow_call and triggered by the parent test-suite.yml. They run on every PR.
Reading a green result. Both jobs report ✓. The verifier prints ✓ all vectors verified as the final line.
Reading a red result. If hashsums fails, a vector file was modified in the working copy without regenerating SHA256SUMS. Either revert the change or run cd test/vectors && sha256sum *.ts *.txt > SHA256SUMS and commit. If rust-verify fails, the bytes the verifier computed do not match the pinned KATs. This is a real signal, either the generator script changed (review the diff), the vector file was edited by hand (forbidden by AGENTS.md), or the underlying primitive has shifted in a way that breaks reproducibility. Investigate before merging.
AES and ML-DSA both shipped following these recipes. AES landed as a Tier 1 family (block, CBC, CTR, GCM, GCM-SIV) plus POLYVAL, with five primitive verifier targets. ML-DSA landed as a Tier 1 ACVP target reading three vector files (mldsa_keygen.ts, mldsa_siggen.ts, mldsa_sigver.ts). The AES Tier 2 seal and sealstream wrappers shipped on top of the existing aes-gcm-siv primitive target, reusing the pinned aes-gcm-siv crate as the per-chunk AEAD.
For a new Tier 2 wrapper (any future cipher's seal wrapper):
- Add the cipher's RustCrypto crate to
scripts/verify-vectors/Cargo.tomlwith an exact-version pin, or reuse an already-pinned primitive crate. - Create
scripts/verify-vectors/src/<cipher>_seal.rsmodeled afterxchacha.rsorserpent.rs. The shape is fixed: aderive_v<N>function for HKDF key derivation, aseal_chunk_<cipher>helper that calls the per-chunk AEAD, acheck_preamblefor the per-cipher invariants (format byte, 32-byte commitment), andverify_sealplusverify_sealstreamentry points. - Add a
mod <cipher>_seal;tomain.rsand wirerun_<cipher>_seal/run_<cipher>_sealstreaminto the dispatcher and the--cipherCLI flag. - Run
cargo run --release --cipher <cipher>against the pinned KATs. Confirm the verifier reports green against every record. - Update the Tier 2 self-generated files table in this document, flipping coverage from "not yet" to "full".
For a new Tier 1 primitive (block cipher, mode, hash, MAC, KEM, signature scheme):
- Add the RustCrypto crate to
Cargo.tomlwith an exact-version pin and the audit comment block describing what it oracles. - Create
scripts/verify-vectors/src/<primitive>.rsmodeled afteraes.rs(symmetric) ormldsa.rs(signature). Signature schemes need three vector types per ACVP convention:keyGen(compare pk + sk),sigGen(rebuild M' per signatureInterface and preHash, compare signature bytes),sigVer(decode pk + sig, compare boolean against ACVPtestPassed). - Add a
mod <primitive>;and the--cipherCLI flag entry. - Update the Tier 1 provenance table with the new file, source URL, and last-fetched date.
Notes. RustCrypto's aes crate matches NIST CAVP byte-exactly with no convention conversion. For ciphers not in RustCrypto's aes crate, byte-order conventions vary. Check both implementations' public APIs and the spec they cite before assuming a one-to-one byte-level mapping. Serpent is the working example: RustCrypto's serpent crate uses NIST natural byte order at its public API, leviathan-crypto v3's Serpent API uses NIST natural byte order as well, and the verifier feeds keys, IVs, and plaintext blocks through unchanged at the block-cipher boundary. leviathan-crypto v2 used the AES-submission floppy byte order at its public API and required a byte-reversal at the block-cipher boundary; v3 removes that asymmetry. The ml-kem and ml-dsa crates both ship as 0.x pre-release versions; pin the exact version and audit the API surface on every major bump, especially sign_internal / sign_mu_* / decapsulate_slice and any FIPS 204 Appendix D domain-separation changes. Oracle crate selection now spans multiple lineages: most primitives use RustCrypto, KMAC and cSHAKE use tiny-keccak. A future maintainer adding a new cipher should check whether a working RustCrypto crate exists for the primitive and, if not, pick an alternate independent lineage rather than rolling a bespoke oracle.
| Document | Description |
|---|---|
| audits | Per-primitive correctness audits |
| aead | Authenticated encryption wire format and security model |
| signing | Signing envelope wire format and security model |
| signaturesuite |
SignatureSuite interface and the shipped suite catalog (ML-DSA, SLH-DSA, Ed25519, ECDSA-P256, hybrids) |
| test-suite | Full test inventory and gate structure |
| architecture | Repository structure, build and CI, WASM modules, public API, test suite, and security posture |
| stream_audit | Streaming AEAD composition audit |
| SECURITY.md | Security model, threat model, authenticator robustness |
- Sign Tools
-
SignatureSuite
- format-byte catalog, hybrid composite encodings, custom suite contract
- Serpent-256 TypeScript | WASM
-
Serpent,SerpentCtr,SerpentCbc,SerpentGenerator
-
- ChaCha20 TypeScript | WASM
-
ChaCha20,Poly1305,ChaCha20Poly1305,XChaCha20Poly1305,ChaCha20Generator
-
- AES TypeScript | WASM
-
AES,AESCbc,AESCtr,AESGCM,AESGCMSIV,AESGenerator
-
- ML-DSA TypeScript | WASM
- pure (FIPS 204):
MlDsa44,MlDsa65,MlDsa87 - pure-mode suites:
MlDsa44Suite,MlDsa65Suite,MlDsa87Suite - prehash suites:
MlDsa44PreHashSuite,MlDsa65PreHashSuite,MlDsa87PreHashSuite
- pure (FIPS 204):
- SLH-DSA TypeScript | WASM
- pure (FIPS 205):
SlhDsa128f,SlhDsa192f,SlhDsa256f - pure-mode suites:
SlhDsa128fSuite,SlhDsa192fSuite,SlhDsa256fSuite - prehash suites:
SlhDsa128fPreHashSuite,SlhDsa192fPreHashSuite,SlhDsa256fPreHashSuite
- pure (FIPS 205):
- Ed25519 TypeScript | WASM
-
Ed25519(pure + Ed25519ph),Ed25519Suite,Ed25519PreHashSuite
-
- ECDSA-P256 TypeScript | WASM
-
EcdsaP256(hedged + RFC 6979),EcdsaP256Suite - DER codec:
ecdsaSignatureToDer,ecdsaSignatureFromDer,encodeEcPrivateKey,decodeEcPrivateKey,pointDecompress
-
- Hybrid composites PQ-only | Classical+PQ
- PQ-only:
MlDsa44SlhDsa128fSuite,MlDsa65SlhDsa192fSuite,MlDsa87SlhDsa256fSuite - Classical+PQ:
MlDsa44Ed25519Suite,MlDsa65Ed25519Suite,MlDsa44EcdsaP256Suite,MlDsa65EcdsaP256Suite
- PQ-only:
- X25519 TypeScript | WASM
-
X25519,KeyAgreementError(RFC 7748)
-
- ML-KEM TypeScript | WASM
-
MlKem512,MlKem768,MlKem1024
-
-
Ratchet (SPQR)
-
KDFChain,ratchetInit,kemRatchetEncap,kemRatchetDecap,RatchetKeypair,SkippedKeyStore
-
- Hashing overview
- SHA-2 TypeScript | WASM
-
SHA256,SHA384,SHA512,SHA224,SHA512_224,SHA512_256 -
HMAC_SHA256,HMAC_SHA384,HMAC_SHA512,HKDF_SHA256,HKDF_SHA512
-
- SHA-3 TypeScript | WASM
-
SHA3_224,SHA3_256,SHA3_384,SHA3_512,SHAKE128,SHAKE256
-
- BLAKE3 TypeScript | WASM
-
BLAKE3,BLAKE3Stream,BLAKE3KeyedHash,BLAKE3KeyedHashStream -
BLAKE3DeriveKey,BLAKE3DeriveKeyStream,BLAKE3OutputReader,BLAKE3Hash
-
-
KMAC
-
CSHAKE128,CSHAKE256,KMAC128,KMAC256,KMACXOF128,KMACXOF256
-
-
Merkle
-
MerkleVerifier,MerkleLog -
SignedLog,Sha256Tree,Blake3Tree,MemoryStorage
-
-
Fortuna CSPRNG
-
Fortuna,SerpentGenerator,ChaCha20Generator,AESGenerator,SHA256Hash,SHA3_256Hash,BLAKE3Hash
-
- Utils TypeScript | WASM
-
constantTimeEqual,randomBytes,wipe, encoding helpers
-
-
TypeScript interfaces
-
Hash,KeyedHash,Blockcipher,Streamcipher,AEAD,Generator,HashFn
-