use crate::error::VerifierError;
use alloc::string::ToString;
pub const HEADER_SUBSTRATE: &str = "x-h33-substrate";
pub const HEADER_RECEIPT: &str = "x-h33-receipt";
pub const HEADER_ALGORITHMS: &str = "x-h33-algorithms";
pub const HEADER_SUBSTRATE_TS: &str = "x-h33-substrate-ts";
pub const HEADER_ATTEST_OPT_OUT: &str = "x-h33-attest";
pub const SUBSTRATE_HEADER_HEX_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Headers<'a> {
pub substrate: &'a str,
pub receipt: &'a str,
pub algorithms: &'a str,
pub timestamp_ms: u64,
}
impl<'a> Headers<'a> {
#[must_use]
pub const fn from_strs(
substrate: &'a str,
receipt: &'a str,
algorithms: &'a str,
timestamp_ms: u64,
) -> Self {
Self {
substrate,
receipt,
algorithms,
timestamp_ms,
}
}
pub fn decode_substrate(&self) -> Result<[u8; 32], VerifierError> {
if self.substrate.len() != SUBSTRATE_HEADER_HEX_LEN {
return Err(VerifierError::InvalidSubstrateHeaderLength {
actual: self.substrate.len(),
});
}
let bytes = hex::decode(self.substrate)
.map_err(|e| VerifierError::InvalidSubstrateHeaderHex(e.to_string()))?;
let mut out = [0u8; 32];
if bytes.len() != 32 {
return Err(VerifierError::InvalidSubstrateHeaderLength {
actual: self.substrate.len(),
});
}
out.copy_from_slice(&bytes);
Ok(out)
}
pub fn algorithm_identifiers(&self) -> impl Iterator<Item = &'a str> {
self.algorithms.split(',').map(str::trim).filter(|s| !s.is_empty())
}
}
#[cfg(feature = "reqwest-support")]
pub fn headers_from_reqwest(
response: &reqwest::Response,
) -> Result<OwnedHeaders, VerifierError> {
use alloc::string::String;
fn get(
response: &reqwest::Response,
name: &str,
) -> Result<String, VerifierError> {
response
.headers()
.get(name)
.and_then(|v| v.to_str().ok())
.map(ToString::to_string)
.ok_or_else(|| {
VerifierError::PublicKeysParse(alloc::format!(
"missing required header: {name}"
))
})
}
let substrate = get(response, HEADER_SUBSTRATE)?;
let receipt = get(response, HEADER_RECEIPT)?;
let algorithms = get(response, HEADER_ALGORITHMS)?;
let ts_str = get(response, HEADER_SUBSTRATE_TS)?;
let timestamp_ms = ts_str.parse::<u64>().map_err(|e| {
VerifierError::PublicKeysParse(alloc::format!(
"X-H33-Substrate-Ts is not a valid u64: {e}"
))
})?;
Ok(OwnedHeaders {
substrate,
receipt,
algorithms,
timestamp_ms,
})
}
#[cfg(feature = "reqwest-support")]
#[derive(Debug, Clone)]
pub struct OwnedHeaders {
pub substrate: alloc::string::String,
pub receipt: alloc::string::String,
pub algorithms: alloc::string::String,
pub timestamp_ms: u64,
}
#[cfg(feature = "reqwest-support")]
impl OwnedHeaders {
#[must_use]
pub fn borrow(&self) -> Headers<'_> {
Headers::from_strs(
&self.substrate,
&self.receipt,
&self.algorithms,
self.timestamp_ms,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_substrate_round_trips() {
let hex_str = "f3a8b2c1deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
let h = Headers::from_strs(hex_str, "00", "ML-DSA-65", 0);
let decoded = h.decode_substrate().unwrap();
assert_eq!(decoded.len(), 32);
assert_eq!(decoded[0], 0xF3);
assert_eq!(decoded[1], 0xA8);
}
#[test]
fn decode_substrate_rejects_wrong_length() {
let h = Headers::from_strs("f3a8", "00", "ML-DSA-65", 0);
assert!(matches!(
h.decode_substrate(),
Err(VerifierError::InvalidSubstrateHeaderLength { actual: 4 })
));
}
#[test]
fn decode_substrate_rejects_bad_hex() {
let bad = "z".repeat(SUBSTRATE_HEADER_HEX_LEN);
let h = Headers::from_strs(&bad, "00", "ML-DSA-65", 0);
assert!(matches!(
h.decode_substrate(),
Err(VerifierError::InvalidSubstrateHeaderHex(_))
));
}
#[test]
fn algorithm_identifiers_splits_and_trims() {
let h = Headers::from_strs(
"",
"",
"ML-DSA-65, FALCON-512 , SPHINCS+-SHA2-128f",
0,
);
let ids: alloc::vec::Vec<&str> = h.algorithm_identifiers().collect();
assert_eq!(ids, ["ML-DSA-65", "FALCON-512", "SPHINCS+-SHA2-128f"]);
}
#[test]
fn algorithm_identifiers_drops_empty_segments() {
let h = Headers::from_strs("", "", "ML-DSA-65,,FALCON-512,", 0);
let ids: alloc::vec::Vec<&str> = h.algorithm_identifiers().collect();
assert_eq!(ids, ["ML-DSA-65", "FALCON-512"]);
}
}