1use hashbrown::HashSet;
2
3use std::time::SystemTime;
4
5use anyhow::{Result, ensure};
6use serde_json::Value;
7
8#[derive(Debug, Clone)]
10pub struct ZeroTrustContext {
11 allowed_identities: HashSet<String>,
12 integrity_salt: String,
13}
14
15impl ZeroTrustContext {
16 pub fn new(allowed_identities: HashSet<String>, integrity_salt: impl Into<String>) -> Self {
17 Self {
18 allowed_identities,
19 integrity_salt: integrity_salt.into(),
20 }
21 }
22
23 pub fn authorize(&self, identity: &str) -> Result<()> {
24 ensure!(
25 self.allowed_identities.contains(identity),
26 "principal {identity} not authorized under zero-trust policy"
27 );
28 Ok(())
29 }
30
31 pub fn wrap(&self, payload: Value) -> PayloadEnvelope {
32 let integrity = IntegrityTag::new(&payload, &self.integrity_salt);
33 PayloadEnvelope { payload, integrity, issued_at: SystemTime::now() }
34 }
35}
36
37use base64::Engine;
38use base64::engine::general_purpose::STANDARD;
39use ring::hmac;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct IntegrityTag(String);
44
45impl IntegrityTag {
46 pub fn new(payload: &Value, salt: &str) -> Self {
47 let key = hmac::Key::new(hmac::HMAC_SHA256, salt.as_bytes());
48 let signature = hmac::sign(&key, payload.to_string().as_bytes());
49 IntegrityTag(STANDARD.encode(signature.as_ref()))
50 }
51
52 pub fn verify(&self, payload: &Value, salt: &str) -> bool {
53 let key = hmac::Key::new(hmac::HMAC_SHA256, salt.as_bytes());
54 let expected_signature_bytes = match STANDARD.decode(&self.0) {
55 Ok(bytes) => bytes,
56 Err(_) => return false,
57 };
58
59 hmac::verify(&key, payload.to_string().as_bytes(), &expected_signature_bytes).is_ok()
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct PayloadEnvelope {
66 pub payload: Value,
67 pub integrity: IntegrityTag,
68 pub issued_at: SystemTime,
69}
70
71impl PayloadEnvelope {
72 pub fn validate(&self, salt: &str) -> Result<()> {
73 ensure!(self.integrity.verify(&self.payload, salt), "payload integrity check failed");
74 Ok(())
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use std::iter::FromIterator;
82
83 #[test]
84 fn rejects_unknown_identity() {
85 let ctx = ZeroTrustContext::new(HashSet::from_iter(["node-a".to_string()]), "salt");
86 let err = ctx.authorize("node-b").unwrap_err();
87 assert!(err.to_string().contains("not authorized"));
88 }
89
90 #[test]
91 fn detects_tampering() {
92 let ctx = ZeroTrustContext::new(HashSet::from_iter(["node-a".to_string()]), "salt");
93 let mut envelope = ctx.wrap(serde_json::json!({"a": 1}));
94 envelope.payload = serde_json::json!({"a": 2});
95 let err = envelope.validate("salt").unwrap_err();
96 assert!(err.to_string().contains("integrity"));
97 }
98}