1use base64::engine::general_purpose::STANDARD;
5use base64::Engine;
6use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use sha2::{Digest, Sha256};
10
11use crate::canonicalize;
12
13#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
14pub struct SignatureEnvelope {
15 pub algorithm: String,
16 pub signer: String,
17 pub signature: String,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
21pub struct EvidenceIncident {
22 pub label: String,
23 pub started_at: String,
24 #[serde(skip_serializing_if = "Option::is_none", default)]
25 pub ended_at: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none", default)]
27 pub domains: Option<Vec<String>>,
28 #[serde(skip_serializing_if = "Option::is_none", default)]
29 pub description: Option<String>,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
33pub struct EvidenceBundle {
34 pub evidence_version: String,
35 pub bundle_id: String,
36 pub trust_domain: String,
37 pub incident: EvidenceIncident,
38 #[serde(skip_serializing_if = "Option::is_none", default)]
39 pub actors: Option<Vec<String>>,
40 pub events: Vec<Value>,
41 pub policy_decisions: Vec<Value>,
42 pub approvals: Vec<Value>,
43 #[serde(skip_serializing_if = "Option::is_none", default)]
44 pub ceremonies: Option<Vec<Value>>,
45 #[serde(skip_serializing_if = "Option::is_none", default)]
46 pub quorum_outcomes: Option<Vec<Value>>,
47 #[serde(skip_serializing_if = "Option::is_none", default)]
48 pub anchors: Option<Vec<EvidenceAnchor>>,
49 #[serde(skip_serializing_if = "Option::is_none", default)]
50 pub encrypted_payload: Option<Value>,
51 #[serde(skip_serializing_if = "Option::is_none", default)]
52 pub level: Option<String>,
53 pub issued_at: String,
54 pub issuer: String,
55 pub signature: SignatureEnvelope,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
59pub struct EvidenceAnchor {
60 pub kind: String,
61 #[serde(skip_serializing_if = "Option::is_none", default)]
62 pub url: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none", default)]
64 pub inclusion_proof: Option<Value>,
65}
66
67pub fn evidence_signing_bytes(b: &EvidenceBundle) -> [u8; 32] {
68 let mut value = serde_json::to_value(b).unwrap_or(Value::Null);
69 if let Value::Object(map) = &mut value {
70 map.remove("signature");
71 }
72 let canonical = canonicalize(&value).unwrap_or_default();
73 Sha256::digest(canonical.as_bytes()).into()
74}
75
76#[derive(Clone, Debug, Default)]
77pub struct AssembleArgs {
78 pub bundle_id: String,
79 pub trust_domain: String,
80 pub label: String,
81 pub started_at: String,
82 pub ended_at: Option<String>,
83 pub domains: Option<Vec<String>>,
84 pub description: Option<String>,
85 pub actor_filter: Option<Vec<String>>,
86 pub event_type_pattern: Option<String>,
87 pub policy_decisions: Vec<Value>,
88 pub approvals: Vec<Value>,
89 pub ceremonies: Option<Vec<Value>>,
90 pub quorum_outcomes: Option<Vec<Value>>,
91 pub issuer: String,
92 pub private_key: [u8; 32],
93}
94
95#[derive(Debug)]
96pub struct AssembleResult {
97 pub bundle: EvidenceBundle,
98 pub skipped: usize,
99}
100
101pub fn assemble_evidence_bundle(
102 events: &[Value],
103 args: AssembleArgs,
104) -> Result<AssembleResult, String> {
105 let actor_set: Option<std::collections::HashSet<String>> = args
106 .actor_filter
107 .as_ref()
108 .map(|a| a.iter().cloned().collect());
109 let regex = match args.event_type_pattern.as_deref() {
110 Some(p) => Some(regex::Regex::new(p).map_err(|e| format!("type pattern: {}", e))?),
111 None => None,
112 };
113 let mut skipped = 0usize;
114 let mut filtered = Vec::new();
115 for ev in events {
116 let ts = ev.get("timestamp").and_then(|v| v.as_str()).unwrap_or("");
117 if ts < args.started_at.as_str() {
118 skipped += 1;
119 continue;
120 }
121 if let Some(end) = &args.ended_at {
122 if ts > end.as_str() {
123 skipped += 1;
124 continue;
125 }
126 }
127 if let Some(set) = &actor_set {
128 let actor = ev.get("actor_id").and_then(|v| v.as_str()).unwrap_or("");
129 if !set.contains(actor) {
130 skipped += 1;
131 continue;
132 }
133 }
134 if let Some(re) = ®ex {
135 let typ = ev.get("type").and_then(|v| v.as_str()).unwrap_or("");
136 if !re.is_match(typ) {
137 skipped += 1;
138 continue;
139 }
140 }
141 filtered.push(ev.clone());
142 }
143 if filtered.is_empty() {
144 return Err("evidence bundle requires at least one matching event".into());
145 }
146 let mut actors: Vec<String> = filtered
147 .iter()
148 .filter_map(|ev| {
149 ev.get("actor_id")
150 .and_then(|v| v.as_str())
151 .map(str::to_string)
152 })
153 .collect();
154 actors.sort();
155 actors.dedup();
156 let level = highest_level(&filtered);
157
158 let mut bundle = EvidenceBundle {
159 evidence_version: "1".into(),
160 bundle_id: args.bundle_id,
161 trust_domain: args.trust_domain,
162 incident: EvidenceIncident {
163 label: args.label,
164 started_at: args.started_at,
165 ended_at: args.ended_at,
166 domains: args.domains,
167 description: args.description,
168 },
169 actors: Some(actors),
170 events: filtered,
171 policy_decisions: args.policy_decisions,
172 approvals: args.approvals,
173 ceremonies: args.ceremonies,
174 quorum_outcomes: args.quorum_outcomes,
175 anchors: None,
176 encrypted_payload: None,
177 level: Some(level),
178 issued_at: now_iso8601(),
179 issuer: args.issuer.clone(),
180 signature: SignatureEnvelope {
181 algorithm: "ed25519".into(),
182 signer: args.issuer,
183 signature: String::new(),
184 },
185 };
186 let digest = evidence_signing_bytes(&bundle);
187 let signing = SigningKey::from_bytes(&args.private_key);
188 let sig: Signature = signing.sign(&digest);
189 bundle.signature.signature = STANDARD.encode(sig.to_bytes());
190 Ok(AssembleResult { bundle, skipped })
191}
192
193fn highest_level(events: &[Value]) -> String {
194 let order = ["L0", "L1", "L2", "L3", "L4", "L5"];
195 let mut max = 0usize;
196 for ev in events {
197 let lvl = ev.get("level").and_then(|v| v.as_str()).unwrap_or("L0");
198 if let Some(idx) = order.iter().position(|x| *x == lvl) {
199 if idx > max {
200 max = idx;
201 }
202 }
203 }
204 order[max].into()
205}
206
207#[derive(Debug, Default)]
208pub struct VerifyResult {
209 pub ok: bool,
210 pub reason: Option<String>,
211 pub outer_signature_ok: bool,
212 pub events_verified: usize,
213 pub events_skipped: usize,
214}
215
216pub fn verify_evidence_bundle(
217 bundle: &EvidenceBundle,
218 issuer_public_key: &[u8; 32],
219) -> VerifyResult {
220 let mut result = VerifyResult::default();
221 let digest = evidence_signing_bytes(bundle);
222 let sig_bytes = match STANDARD.decode(&bundle.signature.signature) {
223 Ok(b) => b,
224 Err(e) => {
225 result.reason = Some(format!("signature base64: {}", e));
226 return result;
227 }
228 };
229 let sig = match Signature::from_slice(&sig_bytes) {
230 Ok(s) => s,
231 Err(e) => {
232 result.reason = Some(format!("signature parse: {}", e));
233 return result;
234 }
235 };
236 let vk = match VerifyingKey::from_bytes(issuer_public_key) {
237 Ok(v) => v,
238 Err(e) => {
239 result.reason = Some(format!("verifying key: {}", e));
240 return result;
241 }
242 };
243 if vk.verify(&digest, &sig).is_err() {
244 result.reason = Some("outer signature did not verify".into());
245 return result;
246 }
247 result.outer_signature_ok = true;
248 result.ok = true;
249 result
250}
251
252fn now_iso8601() -> String {
253 let secs = std::time::SystemTime::now()
254 .duration_since(std::time::UNIX_EPOCH)
255 .unwrap_or_default()
256 .as_secs() as i64;
257 let (y, m, d, h, mi, s) = secs_to_ymdhms(secs);
258 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, h, mi, s)
259}
260
261fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
262 let days = secs.div_euclid(86_400);
263 let time = secs.rem_euclid(86_400);
264 let hour = (time / 3600) as u32;
265 let minute = ((time % 3600) / 60) as u32;
266 let second = (time % 60) as u32;
267 let z = days + 719_468;
268 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
269 let doe = (z - era * 146_097) as u64;
270 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
271 let y = yoe as i64 + era * 400;
272 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
273 let mp = (5 * doy + 2) / 153;
274 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
275 let m = if mp < 10 {
276 (mp + 3) as u32
277 } else {
278 (mp - 9) as u32
279 };
280 let year = if m <= 2 { y + 1 } else { y };
281 (year as i32, m, d, hour, minute, second)
282}