h33_substrate_verifier/
headers.rs1use crate::error::VerifierError;
20use alloc::string::ToString;
21
22pub const HEADER_SUBSTRATE: &str = "x-h33-substrate";
24pub const HEADER_RECEIPT: &str = "x-h33-receipt";
26pub const HEADER_ALGORITHMS: &str = "x-h33-algorithms";
28pub const HEADER_SUBSTRATE_TS: &str = "x-h33-substrate-ts";
30pub const HEADER_ATTEST_OPT_OUT: &str = "x-h33-attest";
32
33pub const SUBSTRATE_HEADER_HEX_LEN: usize = 64;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Headers<'a> {
45 pub substrate: &'a str,
47 pub receipt: &'a str,
49 pub algorithms: &'a str,
51 pub timestamp_ms: u64,
53}
54
55impl<'a> Headers<'a> {
56 #[must_use]
76 pub const fn from_strs(
77 substrate: &'a str,
78 receipt: &'a str,
79 algorithms: &'a str,
80 timestamp_ms: u64,
81 ) -> Self {
82 Self {
83 substrate,
84 receipt,
85 algorithms,
86 timestamp_ms,
87 }
88 }
89
90 pub fn decode_substrate(&self) -> Result<[u8; 32], VerifierError> {
94 if self.substrate.len() != SUBSTRATE_HEADER_HEX_LEN {
95 return Err(VerifierError::InvalidSubstrateHeaderLength {
96 actual: self.substrate.len(),
97 });
98 }
99 let bytes = hex::decode(self.substrate)
100 .map_err(|e| VerifierError::InvalidSubstrateHeaderHex(e.to_string()))?;
101 let mut out = [0u8; 32];
102 if bytes.len() != 32 {
103 return Err(VerifierError::InvalidSubstrateHeaderLength {
104 actual: self.substrate.len(),
105 });
106 }
107 out.copy_from_slice(&bytes);
108 Ok(out)
109 }
110
111 pub fn algorithm_identifiers(&self) -> impl Iterator<Item = &'a str> {
114 self.algorithms.split(',').map(str::trim).filter(|s| !s.is_empty())
115 }
116}
117
118#[cfg(feature = "reqwest-support")]
122pub fn headers_from_reqwest(
123 response: &reqwest::Response,
124) -> Result<OwnedHeaders, VerifierError> {
125 use alloc::string::String;
126
127 fn get(
128 response: &reqwest::Response,
129 name: &str,
130 ) -> Result<String, VerifierError> {
131 response
132 .headers()
133 .get(name)
134 .and_then(|v| v.to_str().ok())
135 .map(ToString::to_string)
136 .ok_or_else(|| {
137 VerifierError::PublicKeysParse(alloc::format!(
138 "missing required header: {name}"
139 ))
140 })
141 }
142
143 let substrate = get(response, HEADER_SUBSTRATE)?;
144 let receipt = get(response, HEADER_RECEIPT)?;
145 let algorithms = get(response, HEADER_ALGORITHMS)?;
146 let ts_str = get(response, HEADER_SUBSTRATE_TS)?;
147 let timestamp_ms = ts_str.parse::<u64>().map_err(|e| {
148 VerifierError::PublicKeysParse(alloc::format!(
149 "X-H33-Substrate-Ts is not a valid u64: {e}"
150 ))
151 })?;
152
153 Ok(OwnedHeaders {
154 substrate,
155 receipt,
156 algorithms,
157 timestamp_ms,
158 })
159}
160
161#[cfg(feature = "reqwest-support")]
166#[derive(Debug, Clone)]
167pub struct OwnedHeaders {
168 pub substrate: alloc::string::String,
170 pub receipt: alloc::string::String,
172 pub algorithms: alloc::string::String,
174 pub timestamp_ms: u64,
176}
177
178#[cfg(feature = "reqwest-support")]
179impl OwnedHeaders {
180 #[must_use]
183 pub fn borrow(&self) -> Headers<'_> {
184 Headers::from_strs(
185 &self.substrate,
186 &self.receipt,
187 &self.algorithms,
188 self.timestamp_ms,
189 )
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn decode_substrate_round_trips() {
199 let hex_str = "f3a8b2c1deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
200 let h = Headers::from_strs(hex_str, "00", "ML-DSA-65", 0);
201 let decoded = h.decode_substrate().unwrap();
202 assert_eq!(decoded.len(), 32);
203 assert_eq!(decoded[0], 0xF3);
204 assert_eq!(decoded[1], 0xA8);
205 }
206
207 #[test]
208 fn decode_substrate_rejects_wrong_length() {
209 let h = Headers::from_strs("f3a8", "00", "ML-DSA-65", 0);
210 assert!(matches!(
211 h.decode_substrate(),
212 Err(VerifierError::InvalidSubstrateHeaderLength { actual: 4 })
213 ));
214 }
215
216 #[test]
217 fn decode_substrate_rejects_bad_hex() {
218 let bad = "z".repeat(SUBSTRATE_HEADER_HEX_LEN);
219 let h = Headers::from_strs(&bad, "00", "ML-DSA-65", 0);
220 assert!(matches!(
221 h.decode_substrate(),
222 Err(VerifierError::InvalidSubstrateHeaderHex(_))
223 ));
224 }
225
226 #[test]
227 fn algorithm_identifiers_splits_and_trims() {
228 let h = Headers::from_strs(
229 "",
230 "",
231 "ML-DSA-65, FALCON-512 , SPHINCS+-SHA2-128f",
232 0,
233 );
234 let ids: alloc::vec::Vec<&str> = h.algorithm_identifiers().collect();
235 assert_eq!(ids, ["ML-DSA-65", "FALCON-512", "SPHINCS+-SHA2-128f"]);
236 }
237
238 #[test]
239 fn algorithm_identifiers_drops_empty_segments() {
240 let h = Headers::from_strs("", "", "ML-DSA-65,,FALCON-512,", 0);
241 let ids: alloc::vec::Vec<&str> = h.algorithm_identifiers().collect();
242 assert_eq!(ids, ["ML-DSA-65", "FALCON-512"]);
243 }
244}