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 {} not authorized under zero-trust policy",
27 identity
28 );
29 Ok(())
30 }
31
32 pub fn wrap(&self, payload: Value) -> PayloadEnvelope {
33 let integrity = IntegrityTag::new(&payload, &self.integrity_salt);
34 PayloadEnvelope {
35 payload,
36 integrity,
37 issued_at: SystemTime::now(),
38 }
39 }
40}
41
42use base64::Engine;
43use base64::engine::general_purpose::STANDARD;
44use ring::hmac;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct IntegrityTag(String);
49
50impl IntegrityTag {
51 pub fn new(payload: &Value, salt: &str) -> Self {
52 let key = hmac::Key::new(hmac::HMAC_SHA256, salt.as_bytes());
53 let signature = hmac::sign(&key, payload.to_string().as_bytes());
54 IntegrityTag(STANDARD.encode(signature.as_ref()))
55 }
56
57 pub fn verify(&self, payload: &Value, salt: &str) -> bool {
58 let key = hmac::Key::new(hmac::HMAC_SHA256, salt.as_bytes());
59 let expected_signature_bytes = match STANDARD.decode(&self.0) {
60 Ok(bytes) => bytes,
61 Err(_) => return false,
62 };
63
64 hmac::verify(
65 &key,
66 payload.to_string().as_bytes(),
67 &expected_signature_bytes,
68 )
69 .is_ok()
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct PayloadEnvelope {
76 pub payload: Value,
77 pub integrity: IntegrityTag,
78 pub issued_at: SystemTime,
79}
80
81impl PayloadEnvelope {
82 pub fn validate(&self, salt: &str) -> Result<()> {
83 ensure!(
84 self.integrity.verify(&self.payload, salt),
85 "payload integrity check failed"
86 );
87 Ok(())
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use std::iter::FromIterator;
95
96 #[test]
97 fn rejects_unknown_identity() {
98 let ctx = ZeroTrustContext::new(HashSet::from_iter(["node-a".to_string()]), "salt");
99 let err = ctx.authorize("node-b").unwrap_err();
100 assert!(err.to_string().contains("not authorized"));
101 }
102
103 #[test]
104 fn detects_tampering() {
105 let ctx = ZeroTrustContext::new(HashSet::from_iter(["node-a".to_string()]), "salt");
106 let mut envelope = ctx.wrap(serde_json::json!({"a": 1}));
107 envelope.payload = serde_json::json!({"a": 2});
108 let err = envelope.validate("salt").unwrap_err();
109 assert!(err.to_string().contains("integrity"));
110 }
111}