use sha2::{Digest, Sha256};
use crate::message::hex_encode;
const LF: char = '\n';
pub fn body_hash_hex(body: &[u8]) -> String {
hex_encode(&Sha256::digest(body))
}
pub fn request_canonical(
method: &str,
path: &str,
timestamp: &str,
nonce: &str,
body_hash_hex: &str,
scope: Option<&str>,
) -> String {
let mut s = String::with_capacity(method.len() + path.len() + 160);
s.push_str(method);
s.push(LF);
s.push_str(path);
s.push(LF);
s.push_str(timestamp);
s.push(LF);
s.push_str(nonce);
s.push(LF);
s.push_str(body_hash_hex);
if let Some(scope) = scope {
s.push(LF);
s.push_str(scope);
}
s
}
pub fn response_canonical(
correlation: &str,
timestamp: &str,
nonce: &str,
body_hash_hex: &str,
scope: Option<&str>,
) -> String {
let mut s = String::with_capacity(correlation.len() + 160);
s.push_str("RESPONSE");
s.push(LF);
s.push_str(correlation);
s.push(LF);
s.push_str(timestamp);
s.push(LF);
s.push_str(nonce);
s.push(LF);
s.push_str(body_hash_hex);
if let Some(scope) = scope {
s.push(LF);
s.push_str(scope);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_canonical_matches_paper_worked_example_shape() {
let c = request_canonical(
"POST",
"/esam?channel=SportsFeed-East",
"2026-02-24T18:00:00Z",
"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"deadbeef",
None,
);
assert_eq!(
c,
"POST\n/esam?channel=SportsFeed-East\n2026-02-24T18:00:00Z\na1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\ndeadbeef"
);
}
#[test]
fn tier2_appends_scope_line() {
let c = request_canonical(
"POST",
"/esam",
"2026-02-24T18:00:00Z",
"aa",
"bb",
Some("channel=SportsFeed-East"),
);
assert!(c.ends_with("\nchannel=SportsFeed-East"));
assert_eq!(c.lines().count(), 6);
}
#[test]
fn body_hash_is_lowercase_hex_sha256() {
assert_eq!(
body_hash_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn response_canonical_binds_correlation() {
let c = response_canonical("sig-20260224-001", "2026-02-24T18:00:00Z", "bb", "cc", None);
assert!(c.starts_with("RESPONSE\nsig-20260224-001\n"));
}
}