Skip to main content

tf_types/
evidence.rs

1//! Compliance evidence pipeline (TF-0012) — Rust mirror of
2//! `tools/tf-types-ts/src/core/evidence.ts`.
3
4use 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) = &regex {
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}