eth_id/attestation/
mod.rs1use crate::error::{Result, EthIdError};
2use crate::parser::ParsedDocument;
3use crate::verifier::VerificationResult;
4use serde::{Deserialize, Serialize};
5use sha2::{Sha256, Digest};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AttestationBundle {
10 pub version: String,
11 pub session_id: String,
12 pub timestamp: chrono::DateTime<chrono::Utc>,
13 pub document_hash: String,
14 pub claim: String,
15 pub result: AttestationResult,
16 pub proof_type: ProofType,
17 pub bundle_hash: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AttestationResult {
22 pub answer: bool,
23 pub confidence: f32,
24 pub reasoning: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub enum ProofType {
29 ZeroKnowledge { circuit: String, proof: String },
30 LLM { provider: String, model: String },
31}
32
33impl AttestationBundle {
34 pub fn create(
35 session_id: &str,
36 document: &ParsedDocument,
37 claim: &str,
38 result: &VerificationResult,
39 use_zk: bool,
40 ) -> Result<Self> {
41 let mut hasher = Sha256::new();
42 hasher.update(document.raw_text().as_bytes());
43 let document_hash = format!("{:x}", hasher.finalize());
44
45 let proof_type = if use_zk {
46 ProofType::ZeroKnowledge {
47 circuit: "age_check".to_string(),
48 proof: result.proof.clone().unwrap_or_default(),
49 }
50 } else {
51 ProofType::LLM {
52 provider: "openai".to_string(),
53 model: "gpt-4o".to_string(),
54 }
55 };
56
57 let mut bundle = Self {
58 version: "1.0.0".to_string(),
59 session_id: session_id.to_string(),
60 timestamp: chrono::Utc::now(),
61 document_hash,
62 claim: claim.to_string(),
63 result: AttestationResult {
64 answer: result.answer,
65 confidence: result.confidence,
66 reasoning: result.reasoning.clone(),
67 },
68 proof_type,
69 bundle_hash: String::new(),
70 };
71
72 bundle.bundle_hash = bundle.compute_hash();
73
74 Ok(bundle)
75 }
76
77 fn compute_hash(&self) -> String {
78 let mut hasher = Sha256::new();
79 hasher.update(self.version.as_bytes());
80 hasher.update(self.session_id.as_bytes());
81 hasher.update(self.timestamp.to_rfc3339().as_bytes());
82 hasher.update(self.document_hash.as_bytes());
83 hasher.update(self.claim.as_bytes());
84 hasher.update(self.result.answer.to_string().as_bytes());
85 format!("{:x}", hasher.finalize())
86 }
87
88 pub fn save(&self) -> Result<PathBuf> {
89 let home_dir = dirs::home_dir()
90 .ok_or_else(|| EthIdError::Attestation("Cannot find home directory".to_string()))?;
91
92 let attestations_dir = home_dir.join(".eth-id").join("attestations");
93 std::fs::create_dir_all(&attestations_dir)?;
94
95 let filename = format!("attestation_{}.json", self.session_id);
96 let path = attestations_dir.join(filename);
97
98 let json = serde_json::to_string_pretty(self)?;
99 std::fs::write(&path, json)?;
100
101 Ok(path)
102 }
103
104 pub fn load(session_id: &str) -> Result<Self> {
105 let home_dir = dirs::home_dir()
106 .ok_or_else(|| EthIdError::Attestation("Cannot find home directory".to_string()))?;
107
108 let path = home_dir
109 .join(".eth-id")
110 .join("attestations")
111 .join(format!("attestation_{}.json", session_id));
112
113 if !path.exists() {
114 return Err(EthIdError::Attestation(
115 format!("Attestation not found: {}", session_id)
116 ));
117 }
118
119 let json = std::fs::read_to_string(path)?;
120 let bundle: Self = serde_json::from_str(&json)?;
121
122 Ok(bundle)
123 }
124
125 pub fn verify_integrity(&self) -> bool {
126 let computed_hash = self.compute_hash();
127 computed_hash == self.bundle_hash
128 }
129}