Skip to main content

vtcode_core/
security.rs

1use hashbrown::HashSet;
2
3use std::time::SystemTime;
4
5use anyhow::{Result, ensure};
6use serde_json::Value;
7
8/// Zero-trust context that guards cross-component requests.
9#[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/// Integrity tag that can be recomputed to detect tampering.
42#[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/// Encrypted or integrity-protected payload wrapper.
64#[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}