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