1use super::vep::{
2 AuthoritySegment, EvidenceCapsuleV0, IdentitySegment, IntentSegment, RequestCommitment,
3 WitnessSegment,
4};
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum VerifierError {
9 #[error("Invalid magic: expected VEP, got {0:?}")]
10 InvalidMagic([u8; 3]),
11 #[error("Unsupported version: expected {0}, got {1}")]
12 UnsupportedVersion(u8, u8),
13 #[error("Header size mismatch")]
14 HeaderTooSmall,
15 #[error("JSON parsing error: {0}")]
16 Json(#[from] serde_json::Error),
17 #[error("Integrity error: {0}")]
18 Integrity(String),
19 #[error("Cryptographic error: {0}")]
20 Crypto(String),
21}
22
23pub struct VepVerifier;
25
26impl VepVerifier {
27 pub fn verify_binary(
29 data: &[u8],
30 public_key: Option<&[u8]>,
31 ) -> Result<EvidenceCapsuleV0, VerifierError> {
32 use vex_core::vep::VepPacket;
33
34 let packet = VepPacket::new(data).map_err(|e| VerifierError::Integrity(e.to_string()))?;
36
37 if let Some(pk) = public_key {
39 packet
40 .verify(pk)
41 .map_err(|e| VerifierError::Crypto(e.to_string()))?;
42 }
43
44 let core_capsule = packet
46 .to_capsule()
47 .map_err(|e| VerifierError::Integrity(e.to_string()))?;
48
49 let intent = match &core_capsule.intent {
51 vex_core::segment::IntentData::Transparent {
52 request_sha256,
53 confidence,
54 capabilities,
55 magpie_source,
56 continuation_token: _,
57 metadata,
58 } => IntentSegment {
59 request_sha256: request_sha256.clone(),
60 confidence: *confidence,
61 capabilities: capabilities.clone(),
62 magpie_source: magpie_source.clone(),
63 circuit_id: None, intent_data: Some(core_capsule.intent.clone()),
65 metadata: vex_core::segment::SchemaValue(metadata.0.clone()),
66 },
67 vex_core::segment::IntentData::Shadow { .. } => {
68 return Err(VerifierError::Integrity(
69 "SHADOW_INTENT_NOT_SUPPORTED_IN_V0".to_string(),
70 ))
71 }
72 };
73
74 let authority = AuthoritySegment {
75 capsule_id: core_capsule.authority.capsule_id,
76 outcome: core_capsule.authority.outcome,
77 reason_code: core_capsule.authority.reason_code,
78 trace_root: core_capsule.authority.trace_root,
79 nonce: core_capsule.authority.nonce,
80 escalation_id: core_capsule.authority.escalation_id,
81 binding_status: core_capsule.authority.binding_status,
82 continuation_token: core_capsule.authority.continuation_token,
83 authority_class: core_capsule.authority.authority_class,
84 gate_sensors: core_capsule.authority.gate_sensors,
85 metadata: core_capsule.authority.metadata,
86 };
87
88 let identity = IdentitySegment {
89 aid: core_capsule.identity.aid,
90 identity_type: core_capsule.identity.identity_type,
91 pcrs: core_capsule.identity.pcrs,
92 metadata: core_capsule.identity.metadata,
93 };
94
95 let witness = WitnessSegment {
96 chora_node_id: core_capsule.witness.chora_node_id,
97 receipt_hash: core_capsule.witness.receipt_hash,
98 timestamp: core_capsule.witness.timestamp,
99 metadata: core_capsule.witness.metadata,
100 };
101
102 let request_commitment = core_capsule.request_commitment.map(|rc| RequestCommitment {
103 canonicalization: rc.canonicalization,
104 payload_sha256: rc.payload_sha256,
105 payload_encoding: rc.payload_encoding,
106 });
107
108 let mut v0 =
109 EvidenceCapsuleV0::new(intent, authority, identity, witness, request_commitment)
110 .map_err(|e| VerifierError::Integrity(e.to_string()))?;
111
112 v0.crypto.signature_b64 = core_capsule.crypto.signature_b64;
113
114 Ok(v0)
115 }
116
117 pub async fn reverify_formal_intent(&self, capsule: &EvidenceCapsuleV0) -> Result<(), String> {
119 let source = capsule
120 .intent
121 .magpie_source
122 .as_ref()
123 .ok_or("VEP_MISSING_SOURCE: No bundled Magpie AST found")?;
124
125 let tmp_path = std::env::temp_dir().join(format!("verify_{}.mp", capsule.capsule_id));
126 tokio::fs::write(&tmp_path, source)
127 .await
128 .map_err(|e| format!("IO_ERROR: {}", e))?;
129
130 struct Cleanup(std::path::PathBuf);
131 impl Drop for Cleanup {
132 fn drop(&mut self) {
133 let _ = std::fs::remove_file(&self.0);
134 }
135 }
136 let _cleanup = Cleanup(tmp_path.clone());
137
138 use tokio::process::Command;
139 let mut cmd = Command::new(crate::utils::find_magpie_binary());
140 cmd.arg("--output")
141 .arg("json")
142 .arg("--entry")
143 .arg(&tmp_path)
144 .arg("parse");
145
146 let output = cmd
147 .output()
148 .await
149 .map_err(|e| format!("MAGPIE_EXEC_ERROR: {}", e))?;
150
151 if output.status.success() {
152 Ok(())
153 } else {
154 let stderr = String::from_utf8_lossy(&output.stderr);
155 Err(format!("FORMAL_VERIFICATION_FAILED: {}", stderr))
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::super::vep::{AuthoritySegment, IdentitySegment, IntentSegment, WitnessSegment};
163 use super::*;
164 use ed25519_dalek::SigningKey;
165 use rand::rngs::OsRng;
166
167 #[test]
168 fn test_vep_end_to_end_verification() {
169 let mut csprng = OsRng;
170 let signing_key = SigningKey::generate(&mut csprng);
171 let verifying_key = signing_key.verifying_key();
172
173 let intent = IntentSegment {
174 request_sha256: "aabbcc".to_string(),
175 confidence: 0.9,
176 capabilities: vec!["test".to_string()],
177 magpie_source: None,
178 circuit_id: None,
179 intent_data: None,
180 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
181 };
182 let authority = AuthoritySegment {
183 capsule_id: "test-capsule".to_string(),
184 outcome: "ALLOW".to_string(),
185 reason_code: "OK".to_string(),
186 trace_root: "tr".to_string(),
187 nonce: "42".to_string(),
188 escalation_id: None,
189 binding_status: None,
190 continuation_token: None,
191 authority_class: Some("ALLOW_PATH".to_string()),
192 gate_sensors: vex_core::segment::SchemaValue(serde_json::Value::Null),
193 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
194 };
195 let identity = IdentitySegment {
196 aid: hex::encode([0u8; 32]), identity_type: "mock".to_string(),
198 pcrs: None,
199 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
200 };
201 let witness = WitnessSegment {
202 chora_node_id: "node1".to_string(),
203 receipt_hash: "rh".to_string(),
204 timestamp: 1710396000,
205 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
206 };
207
208 let mut capsule =
209 EvidenceCapsuleV0::new(intent, authority, identity, witness, None).unwrap();
210 capsule.sign(&signing_key).unwrap();
211
212 let binary = capsule.to_vep_binary().unwrap();
213
214 let verified = VepVerifier::verify_binary(&binary, Some(verifying_key.as_bytes())).unwrap();
216 assert_eq!(verified.capsule_id, "test-capsule");
217
218 let integrity_only = VepVerifier::verify_binary(&binary, None).unwrap();
220 assert_eq!(integrity_only.capsule_id, "test-capsule");
221
222 let mut tampered = binary.clone();
224 tampered[40] ^= 0xFF;
225 let err = VepVerifier::verify_binary(&tampered, None).unwrap_err();
226 assert!(matches!(err, VerifierError::Integrity(_)));
227 }
228
229 #[tokio::test]
230 async fn test_audit_store_vep_integration() {
231 use crate::audit::vep::{AuthoritySegment, IdentitySegment, IntentSegment, WitnessSegment};
232 use std::sync::Arc;
233 use vex_core::audit::AuditEventType;
234 use vex_persist::backend::MemoryBackend;
235 use vex_persist::AuditStore;
236
237 let backend = Arc::new(MemoryBackend::new());
238 let store = AuditStore::new(backend);
239 let tenant = "handshake-test";
240
241 let mut csprng = OsRng;
242 let signing_key = SigningKey::generate(&mut csprng);
243
244 let intent = IntentSegment {
246 request_sha256: "deadbeef".to_string(),
247 confidence: 1.0,
248 capabilities: vec!["audit".to_string()],
249 magpie_source: None,
250 circuit_id: None,
251 intent_data: None,
252 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
253 };
254 let authority = AuthoritySegment {
255 capsule_id: "capsule-xyz-789".to_string(),
256 outcome: "ALLOW".to_string(),
257 reason_code: "VERIFIED".to_string(),
258 trace_root: "tr".to_string(),
259 nonce: "101".to_string(),
260 escalation_id: None,
261 binding_status: None,
262 continuation_token: None,
263 authority_class: Some("ALLOW_PATH".to_string()),
264 gate_sensors: vex_core::segment::SchemaValue(serde_json::Value::Null),
265 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
266 };
267 let identity = IdentitySegment {
268 aid: hex::encode([1u8; 32]),
269 identity_type: "hardware".to_string(),
270 pcrs: None,
271 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
272 };
273 let witness = WitnessSegment {
274 chora_node_id: "nodeB".to_string(),
275 receipt_hash: "receiptB".to_string(),
276 timestamp: 1710396000,
277 metadata: vex_core::segment::SchemaValue(serde_json::Value::Null),
278 };
279
280 let mut capsule =
281 EvidenceCapsuleV0::new(intent, authority, identity, witness, None).unwrap();
282 capsule.sign(&signing_key).unwrap();
283 let binary = capsule.to_vep_binary().unwrap();
284
285 store
287 .log(
288 tenant,
289 AuditEventType::GateDecision,
290 vex_core::audit::ActorType::System("verifier".to_string()),
291 None,
292 serde_json::json!({
293 "authority": {
294 "capsule_id": "capsule-xyz-789",
295 "outcome": "ALLOW",
296 "reason_code": "VERIFIED",
297 "nonce": "101"
298 }
299 }),
300 None,
301 Some("receiptB".to_string()),
302 Some(binary.clone()),
303 )
304 .await
305 .unwrap();
306
307 let retrieved_blob = store
309 .get_vep_by_capsule_id(tenant, "capsule-xyz-789")
310 .await
311 .unwrap();
312 assert!(retrieved_blob.is_some());
313 let blob = retrieved_blob.unwrap();
314 assert_eq!(blob, binary);
315
316 let result =
318 VepVerifier::verify_binary(&blob, Some(signing_key.verifying_key().as_bytes()))
319 .unwrap();
320 assert_eq!(result.capsule_id, "capsule-xyz-789");
321 assert_eq!(result.authority.nonce, "101");
322 }
323}