appcore_security/
request_hash.rs1use sha2::{Digest, Sha256};
14
15const REQUEST_HASH_DOMAIN_V2: &[u8] = b"appcore.request-hash.v2\0";
16const REQUEST_HASH_PREFIX_V2: &str = "v2:";
17
18#[derive(Debug, Clone)]
20pub struct RequestValidationDetails {
21 pub purpose: String,
23 pub name: String,
25 pub id: String,
27 pub idempotency_key: Option<String>,
29 pub payload: String,
31 pub subject: Option<String>,
33 pub audience: Option<String>,
35}
36
37pub fn compute_request_hash(details: &RequestValidationDetails) -> String {
39 let mut hasher = Sha256::new();
40 hasher.update(REQUEST_HASH_DOMAIN_V2);
41 update_required(&mut hasher, 1, &details.purpose);
42 update_required(&mut hasher, 2, &details.name);
43 update_required(&mut hasher, 3, &details.id);
44 update_optional(&mut hasher, 4, details.idempotency_key.as_deref());
45 update_required(&mut hasher, 5, &details.payload);
46 update_optional(&mut hasher, 6, details.subject.as_deref());
47 update_optional(&mut hasher, 7, details.audience.as_deref());
48
49 let digest = hasher.finalize();
50 let mut output = String::with_capacity(REQUEST_HASH_PREFIX_V2.len() + digest.len() * 2);
51 output.push_str(REQUEST_HASH_PREFIX_V2);
52 push_hex(&mut output, &digest);
53 output
54}
55
56fn update_required(hasher: &mut Sha256, tag: u8, value: &str) {
57 hasher.update([tag]);
58 hasher.update((value.len() as u64).to_be_bytes());
59 hasher.update(value.as_bytes());
60}
61
62fn update_optional(hasher: &mut Sha256, tag: u8, value: Option<&str>) {
63 hasher.update([tag]);
64 match value {
65 Some(value) => {
66 hasher.update([1]);
67 hasher.update((value.len() as u64).to_be_bytes());
68 hasher.update(value.as_bytes());
69 }
70 None => hasher.update([0]),
71 }
72}
73
74fn push_hex(output: &mut String, bytes: &[u8]) {
75 const HEX: &[u8; 16] = b"0123456789abcdef";
76 for byte in bytes {
77 output.push(HEX[(byte >> 4) as usize] as char);
78 output.push(HEX[(byte & 0x0f) as usize] as char);
79 }
80}