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 {} 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/// Integrity tag that can be recomputed to detect tampering.
47#[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/// Encrypted or integrity-protected payload wrapper.
74#[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}