use crate::{
error::VerifierError,
headers::Headers,
receipt::{AlgorithmFlags, CompactReceipt},
};
use alloc::string::ToString;
use sha3::{Digest, Sha3_256};
#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerificationResult {
pub body_hash_matches: bool,
pub receipt_well_formed: bool,
pub algorithms_match_flags: bool,
pub timestamps_agree: bool,
pub flags_from_receipt: Option<AlgorithmFlags>,
pub computed_body_hash: [u8; 32],
}
impl VerificationResult {
#[must_use]
pub const fn is_valid(&self) -> bool {
self.body_hash_matches
&& self.receipt_well_formed
&& self.algorithms_match_flags
&& self.timestamps_agree
}
#[must_use]
pub const fn summary(&self) -> &'static str {
if !self.body_hash_matches {
"body hash mismatch — response body does not match X-H33-Substrate"
} else if !self.receipt_well_formed {
"receipt malformed — X-H33-Receipt failed structural parsing"
} else if !self.algorithms_match_flags {
"algorithm disagreement — X-H33-Algorithms does not match receipt flags"
} else if !self.timestamps_agree {
"timestamp disagreement — X-H33-Substrate-Ts does not match receipt verified_at_ms"
} else {
"verified"
}
}
}
pub fn verify_structural(
body: &[u8],
headers: &Headers<'_>,
) -> Result<VerificationResult, VerifierError> {
let mut hasher = Sha3_256::new();
hasher.update(body);
let computed_body_hash: [u8; 32] = hasher.finalize().into();
let claimed_body_hash = headers.decode_substrate()?;
let body_hash_matches = constant_time_eq(&computed_body_hash, &claimed_body_hash);
let receipt_result = CompactReceipt::from_hex(headers.receipt);
let (receipt_well_formed, flags_from_receipt, receipt_timestamp) =
receipt_result.as_ref().map_or((false, None, None), |r| {
(true, Some(r.flags()), Some(r.verified_at_ms()))
});
let algorithms_match_flags = if let Some(flags) = flags_from_receipt {
let header_set = parse_algorithm_set(headers)?;
let receipt_set = AlgorithmSet::from_flags(flags);
header_set == receipt_set
} else {
false
};
let timestamps_agree = matches!(
receipt_timestamp,
Some(ts) if ts == headers.timestamp_ms
);
Ok(VerificationResult {
body_hash_matches,
receipt_well_formed,
algorithms_match_flags,
timestamps_agree,
flags_from_receipt,
computed_body_hash,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AlgorithmSet {
has_dilithium: bool,
has_falcon: bool,
has_sphincs: bool,
}
impl AlgorithmSet {
const fn from_flags(flags: AlgorithmFlags) -> Self {
Self {
has_dilithium: flags.has_dilithium(),
has_falcon: flags.has_falcon(),
has_sphincs: flags.has_sphincs(),
}
}
}
fn parse_algorithm_set(headers: &Headers<'_>) -> Result<AlgorithmSet, VerifierError> {
let mut set = AlgorithmSet {
has_dilithium: false,
has_falcon: false,
has_sphincs: false,
};
for raw in headers.algorithm_identifiers() {
match canonicalize_alg_id(raw) {
Some(CanonicalAlg::Dilithium) => set.has_dilithium = true,
Some(CanonicalAlg::Falcon) => set.has_falcon = true,
Some(CanonicalAlg::Sphincs) => set.has_sphincs = true,
None => return Err(VerifierError::UnknownAlgorithm(raw.to_string())),
}
}
Ok(set)
}
enum CanonicalAlg {
Dilithium,
Falcon,
Sphincs,
}
fn canonicalize_alg_id(raw: &str) -> Option<CanonicalAlg> {
let r = raw.trim();
if r.eq_ignore_ascii_case("ML-DSA-44")
|| r.eq_ignore_ascii_case("ML-DSA-65")
|| r.eq_ignore_ascii_case("ML-DSA-87")
|| r.eq_ignore_ascii_case("Dilithium2")
|| r.eq_ignore_ascii_case("Dilithium3")
|| r.eq_ignore_ascii_case("Dilithium5")
{
return Some(CanonicalAlg::Dilithium);
}
if r.eq_ignore_ascii_case("FALCON-512")
|| r.eq_ignore_ascii_case("FALCON-1024")
|| r.eq_ignore_ascii_case("FN-DSA-512")
|| r.eq_ignore_ascii_case("FN-DSA-1024")
{
return Some(CanonicalAlg::Falcon);
}
if is_slh_dsa_identifier(r) || is_sphincs_plus_identifier(r) {
return Some(CanonicalAlg::Sphincs);
}
None
}
fn is_slh_dsa_identifier(r: &str) -> bool {
matches!(
r.to_ascii_uppercase().as_str(),
"SLH-DSA-SHA2-128F"
| "SLH-DSA-SHA2-128S"
| "SLH-DSA-SHA2-192F"
| "SLH-DSA-SHA2-192S"
| "SLH-DSA-SHA2-256F"
| "SLH-DSA-SHA2-256S"
| "SLH-DSA-SHAKE-128F"
| "SLH-DSA-SHAKE-128S"
| "SLH-DSA-SHAKE-192F"
| "SLH-DSA-SHAKE-192S"
| "SLH-DSA-SHAKE-256F"
| "SLH-DSA-SHAKE-256S"
)
}
fn is_sphincs_plus_identifier(r: &str) -> bool {
let upper = r.to_ascii_uppercase();
let trimmed = upper
.strip_suffix("-SIMPLE")
.or_else(|| upper.strip_suffix("-ROBUST"))
.unwrap_or(&upper);
matches!(
trimmed,
"SPHINCS+-SHA2-128F"
| "SPHINCS+-SHA2-128S"
| "SPHINCS+-SHA2-192F"
| "SPHINCS+-SHA2-192S"
| "SPHINCS+-SHA2-256F"
| "SPHINCS+-SHA2-256S"
| "SPHINCS+-SHAKE-128F"
| "SPHINCS+-SHAKE-128S"
| "SPHINCS+-SHAKE-192F"
| "SPHINCS+-SHAKE-192S"
| "SPHINCS+-SHAKE-256F"
| "SPHINCS+-SHAKE-256S"
)
}
#[inline]
#[must_use]
fn constant_time_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
let mut diff: u8 = 0;
for i in 0..32 {
let left = a.get(i).copied().unwrap_or(0);
let right = b.get(i).copied().unwrap_or(0);
diff |= left ^ right;
}
diff == 0
}
#[cfg(test)]
pub(crate) const KNOWN_SHA3_EMPTY: &str =
"a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a";
#[cfg(test)]
mod tests {
use super::*;
use crate::receipt::{
ALG_ALL_THREE, ALG_DILITHIUM, ALG_FALCON, ALG_SPHINCS, RECEIPT_SIZE,
RECEIPT_VERSION,
};
fn fabricate_receipt_hex(
_body_hash: [u8; 32],
verified_at_ms: u64,
flags: u8,
) -> alloc::string::String {
let mut bytes = [0u8; RECEIPT_SIZE];
bytes[0] = RECEIPT_VERSION;
for b in &mut bytes[1..33] {
*b = 0xCC;
}
bytes[33..41].copy_from_slice(&verified_at_ms.to_be_bytes());
bytes[41] = flags;
hex::encode(bytes)
}
fn sha3_of(body: &[u8]) -> [u8; 32] {
let mut h = Sha3_256::new();
h.update(body);
h.finalize().into()
}
#[test]
fn verifies_a_known_good_response() {
let body = b"{\"tenant\":\"abc\",\"plan\":\"premium\"}";
let body_hash = sha3_of(body);
let substrate_hex = hex::encode(body_hash);
let ts = 1_733_942_731_234_u64;
let receipt_hex = fabricate_receipt_hex(body_hash, ts, ALG_ALL_THREE);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"ML-DSA-65,FALCON-512,SPHINCS+-SHA2-128f",
ts,
);
let result = verify_structural(body, &headers).unwrap();
assert!(result.is_valid(), "expected valid, got: {}", result.summary());
assert!(result.body_hash_matches);
assert!(result.receipt_well_formed);
assert!(result.algorithms_match_flags);
assert!(result.timestamps_agree);
}
#[test]
fn detects_body_tampering() {
let body = b"original body";
let tampered = b"tampered body";
let original_hash = sha3_of(body);
let substrate_hex = hex::encode(original_hash);
let ts = 1_000;
let receipt_hex = fabricate_receipt_hex(original_hash, ts, ALG_ALL_THREE);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"ML-DSA-65,FALCON-512,SPHINCS+-SHA2-128f",
ts,
);
let result = verify_structural(tampered, &headers).unwrap();
assert!(!result.body_hash_matches);
assert!(!result.is_valid());
assert!(result.summary().contains("body hash mismatch"));
}
#[test]
fn detects_algorithm_header_stripping() {
let body = b"body";
let hash = sha3_of(body);
let ts = 2_000;
let receipt_hex = fabricate_receipt_hex(hash, ts, ALG_ALL_THREE);
let substrate_hex = hex::encode(hash);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"ML-DSA-65,FALCON-512",
ts,
);
let result = verify_structural(body, &headers).unwrap();
assert!(result.body_hash_matches);
assert!(result.receipt_well_formed);
assert!(!result.algorithms_match_flags);
assert!(!result.is_valid());
}
#[test]
fn detects_timestamp_disagreement() {
let body = b"body";
let hash = sha3_of(body);
let receipt_hex = fabricate_receipt_hex(hash, 3_000, ALG_ALL_THREE);
let substrate_hex = hex::encode(hash);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"ML-DSA-65,FALCON-512,SPHINCS+-SHA2-128f",
4_000,
);
let result = verify_structural(body, &headers).unwrap();
assert!(!result.timestamps_agree);
assert!(!result.is_valid());
}
#[test]
fn partial_algorithm_sets_verify_when_header_matches() {
let body = b"body";
let hash = sha3_of(body);
let ts = 5_000;
let receipt_hex =
fabricate_receipt_hex(hash, ts, ALG_DILITHIUM | ALG_FALCON);
let substrate_hex = hex::encode(hash);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"ML-DSA-65,FALCON-512",
ts,
);
let result = verify_structural(body, &headers).unwrap();
assert!(result.is_valid());
assert_eq!(result.flags_from_receipt.unwrap().count(), 2);
}
#[test]
fn unknown_algorithm_identifier_is_an_error() {
let body = b"body";
let hash = sha3_of(body);
let ts = 6_000;
let receipt_hex = fabricate_receipt_hex(hash, ts, ALG_DILITHIUM);
let substrate_hex = hex::encode(hash);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"QUANTUM-MAGIC-9000",
ts,
);
let result = verify_structural(body, &headers);
assert!(matches!(
result,
Err(VerifierError::UnknownAlgorithm(_))
));
}
#[test]
fn historical_aliases_are_accepted() {
let body = b"body";
let hash = sha3_of(body);
let ts = 7_000;
let receipt_hex = fabricate_receipt_hex(hash, ts, ALG_ALL_THREE);
let substrate_hex = hex::encode(hash);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"Dilithium3, FN-DSA-512, SLH-DSA-SHA2-128f",
ts,
);
let result = verify_structural(body, &headers).unwrap();
assert!(result.is_valid());
}
#[test]
fn every_known_dilithium_variant_maps_to_the_dilithium_bit() {
for name in [
"ML-DSA-44",
"ML-DSA-65",
"ML-DSA-87",
"Dilithium2",
"Dilithium3",
"Dilithium5",
"ml-dsa-65", ] {
assert!(
matches!(canonicalize_alg_id(name), Some(CanonicalAlg::Dilithium)),
"identifier {name} should map to Dilithium"
);
}
}
#[test]
fn every_known_falcon_variant_maps_to_the_falcon_bit() {
for name in [
"FALCON-512",
"FALCON-1024",
"FN-DSA-512",
"FN-DSA-1024",
"falcon-512",
"fn-dsa-1024",
] {
assert!(
matches!(canonicalize_alg_id(name), Some(CanonicalAlg::Falcon)),
"identifier {name} should map to FALCON"
);
}
}
#[test]
fn every_known_sphincs_plus_variant_maps_to_the_sphincs_bit() {
for name in [
"SLH-DSA-SHA2-128f",
"SLH-DSA-SHA2-128s",
"SLH-DSA-SHA2-192f",
"SLH-DSA-SHA2-192s",
"SLH-DSA-SHA2-256f",
"SLH-DSA-SHA2-256s",
"SLH-DSA-SHAKE-128f",
"SLH-DSA-SHAKE-128s",
"SLH-DSA-SHAKE-192f",
"SLH-DSA-SHAKE-192s",
"SLH-DSA-SHAKE-256f",
"SLH-DSA-SHAKE-256s",
] {
assert!(
matches!(canonicalize_alg_id(name), Some(CanonicalAlg::Sphincs)),
"FIPS 205 identifier {name} should map to SPHINCS+"
);
}
for base in [
"SPHINCS+-SHA2-128f",
"SPHINCS+-SHA2-128s",
"SPHINCS+-SHA2-192f",
"SPHINCS+-SHA2-192s",
"SPHINCS+-SHA2-256f",
"SPHINCS+-SHA2-256s",
"SPHINCS+-SHAKE-128f",
"SPHINCS+-SHAKE-128s",
"SPHINCS+-SHAKE-192f",
"SPHINCS+-SHAKE-192s",
"SPHINCS+-SHAKE-256f",
"SPHINCS+-SHAKE-256s",
] {
for suffix in ["", "-simple", "-robust"] {
let name = alloc::format!("{base}{suffix}");
assert!(
matches!(canonicalize_alg_id(&name), Some(CanonicalAlg::Sphincs)),
"SPHINCS+ identifier {name} should map to SPHINCS+"
);
}
}
}
#[test]
fn level3_upgrade_algorithm_bundle_still_verifies() {
let body = b"body";
let hash = sha3_of(body);
let ts = 9_000;
let receipt_hex = fabricate_receipt_hex(hash, ts, ALG_ALL_THREE);
let substrate_hex = hex::encode(hash);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"ML-DSA-65, FALCON-1024, SLH-DSA-SHA2-192f",
ts,
);
let result = verify_structural(body, &headers).unwrap();
assert!(
result.is_valid(),
"Level 3 upgrade bundle should still verify: {}",
result.summary()
);
}
#[test]
fn sphincs_only_receipt_verifies_with_sphincs_only_header() {
let body = b"body";
let hash = sha3_of(body);
let ts = 8_000;
let receipt_hex = fabricate_receipt_hex(hash, ts, ALG_SPHINCS);
let substrate_hex = hex::encode(hash);
let headers = Headers::from_strs(
&substrate_hex,
&receipt_hex,
"SPHINCS+-SHA2-128f",
ts,
);
let result = verify_structural(body, &headers).unwrap();
assert!(result.is_valid());
}
#[test]
fn constant_time_eq_rejects_last_byte_difference() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
assert!(constant_time_eq(&a, &b));
b[31] = 1;
assert!(!constant_time_eq(&a, &b));
a[0] = 255;
assert!(!constant_time_eq(&a, &b));
}
#[test]
fn empty_body_computes_known_sha3() {
let body: &[u8] = b"";
let hash = sha3_of(body);
assert_eq!(hex::encode(hash), KNOWN_SHA3_EMPTY);
}
}