proof-aware-crypto-tooling-.../dogfood/quorum/verify-slhdsa/src/main.rs
mrwulf 16040b79f5 quorum: pacta-verify-slhdsa — the SLH-DSA head-checker built from the proven source
Fifth quorum member, first post-quantum one: verifies an SLH-DSA-SHA2-128s
signature by calling slh_verify_128s, the extraction root the eleven fips205
certificates cover (apex fips205.slh_verify_128s_accepts_iff). Verify-only
like the other four: quorum members judge, they never sign.

Build discipline, because "built from the proven source" is a claim that has
to survive a hostile reader: build-verify-slhdsa.sh REFUSES to build if the
pinned checkout is dirty or at any commit other than a3ce8e8, exports the
pinned commit via git archive (never a working copy), applies
expose-mono.patch to that scratch copy, and then DIFFS the patched
verify_mono.rs against the pinned one, aborting if any existing line changed
rather than being appended. The patch is a visibility keyword plus its doc
comment (the crate denies missing_docs, so pub mod alone does not compile)
and one appended argument-assembly function whose body is the crate's own
test helper. The extraction root is provably untouched. A provenance sidecar
lands beside the binary: source commit, patch hash, main.rs hash, rustc, and
a not_covered field naming what no certificate reaches — M-prime assembly
(including the pure/prehash domain-separator byte), hex/file IO, the
compiler; signing and keygen out of scope entirely.

Demonstrated against OpenSSL 3.5.5 on a throwaway key: valid signature OK
both ways, wrong message INVALID, corrupted signature INVALID. The agreement
is itself a finding — this binary assembles M' = 0x00 || 0x00 || payload
(pure variant, empty context) and OpenSSL evidently does the same.

Convention matches the other members: template + main.rs + patch + build
script tracked; rendered Cargo.toml, lock, target/ and the .build-slhdsa
scratch tree ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 21:52:56 +02:00

101 lines
3.8 KiB
Rust

//! warden quorum member: SLH-DSA-SHA2-128s, the Lean-proven verify path.
//!
//! Built against a copy of the PINNED proven source (`fips205-source` at the
//! commit named in the build provenance sidecar) plus `expose-mono.patch`,
//! which adds a visibility keyword and an argument-assembly function and
//! changes no existing line's semantics.
//!
//! The function this calls, `slh_verify_128s`, is the extraction root the
//! eleven certificates cover, apex `fips205.slh_verify_128s_accepts_iff`.
//! Verify-only on purpose: quorum members judge, they never sign.
//!
//! TWO THINGS THIS BINARY DOES THAT NO CERTIFICATE COVERS, stated here because
//! a reader of the output cannot see them:
//!
//! * It assembles M'. FIPS 205 hashes M' = toByte(0,1) ‖ toByte(|ctx|,1) ‖
//! ctx ‖ M, and Algorithm 20's input is already M'. Everything above the
//! extraction root -- including that leading domain-separator byte, the one
//! thing distinguishing the pure variant from prehash -- is outside every
//! proof (TRUSTED-BASE item 10). This binary implements the PURE variant
//! with EMPTY context, i.e. M' = 0x00 ‖ 0x00 ‖ payload, and refuses to
//! guess at anything else.
//! * It parses hex and reads a file.
//!
//! Usage: <pubkey-hex-32B> <sig-hex-7856B> <payload-file>
//! stdout OK / INVALID; exit 0 = accept, 1 = reject, 2 = input error.
use std::process::ExitCode;
const SIG_LEN: usize = 7856;
const PK_LEN: usize = 32;
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 {
return Err("odd-length hex".into());
}
(0..s.len() / 2)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string()))
.collect()
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
if args.len() != 4 {
eprintln!("usage: {} <pubkey-hex-32B> <sig-hex-7856B> <payload-file>", args[0]);
return ExitCode::from(2);
}
let pk_bytes = match hex_decode(&args[1]) {
Ok(b) if b.len() == PK_LEN => b,
Ok(b) => {
eprintln!("error: public key must be {PK_LEN} bytes, got {}", b.len());
return ExitCode::from(2);
}
Err(e) => {
eprintln!("error: public key hex: {e}");
return ExitCode::from(2);
}
};
let sig_bytes = match hex_decode(&args[2]) {
Ok(b) if b.len() == SIG_LEN => b,
Ok(b) => {
// Size is part of the parameter set. A 7856-byte signature is
// SLH-DSA-SHA2-128s; anything else is a DIFFERENT parameter set and
// outside every certificate this binary exists to exercise. Refuse
// rather than attempt it.
eprintln!("error: signature must be {SIG_LEN} bytes (SLH-DSA-SHA2-128s), got {}", b.len());
return ExitCode::from(2);
}
Err(e) => {
eprintln!("error: signature hex: {e}");
return ExitCode::from(2);
}
};
let payload = match std::fs::read(&args[3]) {
Ok(p) => p,
Err(e) => {
eprintln!("error: cannot read payload file {}: {e}", args[3]);
return ExitCode::from(2);
}
};
// M' for the PURE variant with empty context: two length/domain bytes then
// the message. Built here, not proven anywhere.
let mut mprime = Vec::with_capacity(payload.len() + 2);
mprime.push(0u8); // domain separator: 0 = pure, 1 = prehash
mprime.push(0u8); // |ctx| = 0
mprime.extend_from_slice(&payload);
let mut sig_arr = [0u8; SIG_LEN];
sig_arr.copy_from_slice(&sig_bytes);
let mut pk_arr = [0u8; PK_LEN];
pk_arr.copy_from_slice(&pk_bytes);
if fips205::verify_mono::verify_mono_bytes(&mprime, &sig_arr, &pk_arr) {
println!("OK");
ExitCode::from(0)
} else {
println!("INVALID");
ExitCode::from(1)
}
}