Skip to main content

appcore_security/
request_hash.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: request_hash.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/20 12:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/20 12:00:00 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Canonical request framing for request-bound Runtime credentials.
12
13use 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/// Details of an incoming query or command request used to verify its integrity.
19#[derive(Debug, Clone)]
20pub struct RequestValidationDetails {
21    /// Request purpose.
22    pub purpose: String,
23    /// Command or query name.
24    pub name: String,
25    /// Request identity.
26    pub id: String,
27    /// Optional idempotency key.
28    pub idempotency_key: Option<String>,
29    /// Canonical serialized payload.
30    pub payload: String,
31    /// Optional authenticated subject.
32    pub subject: Option<String>,
33    /// Optional target audience.
34    pub audience: Option<String>,
35}
36
37/// Computes the deterministic V2 SHA-256 hash of a canonically framed request.
38pub 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}