mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-08 20:40:32 +00:00
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>
101 lines
3.8 KiB
Rust
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)
|
|
}
|
|
}
|