1#![forbid(unsafe_code)]
8
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12pub const SPEC_VERSION: &str = "0.1";
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Organ {
21 Awake,
23 Identity,
25 Perception,
27 Memory,
29 Deliberation,
31 Action,
33 Vigilance,
35 Learning,
37 Audit,
39 Sovereignty,
41}
42
43impl Organ {
44 pub const ALL: [Organ; 10] = [
46 Organ::Awake,
47 Organ::Identity,
48 Organ::Perception,
49 Organ::Memory,
50 Organ::Deliberation,
51 Organ::Action,
52 Organ::Vigilance,
53 Organ::Learning,
54 Organ::Audit,
55 Organ::Sovereignty,
56 ];
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum Status {
63 Pass,
65 Fail,
67 Optional,
69 ControlOk,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CheckResult {
76 pub id: String,
78 pub organ: Organ,
80 pub status: Status,
82 #[serde(default)]
84 pub evidence: serde_json::Value,
85 #[serde(default)]
87 pub control: bool,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum Verdict {
94 Conformant,
96 Partial,
98 Nonconformant,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Subject {
106 pub name: String,
108 pub version: String,
110 pub host: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct Signature {
117 pub scheme: String,
119 pub pubkey: String,
121 pub sig: String,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Attestation {
128 pub spec: String,
130 pub subject: Subject,
132 pub timestamp: String,
134 pub checks: Vec<CheckResult>,
136 pub verdict: Verdict,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub signature: Option<Signature>,
141}
142
143#[must_use]
151pub fn verdict_for(checks: &[CheckResult]) -> Verdict {
152 if checks.is_empty() {
153 return Verdict::Nonconformant;
154 }
155 for c in checks.iter().filter(|c| c.control) {
157 if c.status == Status::Pass {
158 return Verdict::Nonconformant;
159 }
160 }
161 let mut organs_passed = [false; 10];
162 let mut any_fail = false;
163 for c in checks {
164 match c.status {
165 Status::Pass => {
166 if let Some(i) = Organ::ALL.iter().position(|o| *o == c.organ) {
167 organs_passed[i] = true;
168 }
169 }
170 Status::Fail if !c.control => any_fail = true,
173 _ => {}
174 }
175 }
176 if organs_passed.iter().all(|p| *p) && !any_fail {
177 Verdict::Conformant
178 } else {
179 Verdict::Partial
180 }
181}
182
183#[must_use]
186pub fn canonical_bytes(doc: &Attestation) -> Vec<u8> {
187 let mut v = serde_json::to_value(doc).expect("attestation serializes");
188 if let Some(obj) = v.as_object_mut() {
189 obj.remove("signature");
190 }
191 canonical_json(&v).into_bytes()
192}
193
194#[must_use]
196pub fn document_hash(doc: &Attestation) -> [u8; 32] {
197 let mut h = Sha256::new();
198 h.update(canonical_bytes(doc));
199 h.finalize().into()
200}
201
202#[derive(Debug)]
205pub enum ValidationError {
206 BadSpec,
208 NoChecks,
210 DuplicateCheckId(String),
212 VerdictMismatch {
214 claimed: Verdict,
216 actual: Verdict,
218 },
219 BadSignature,
221 BadTimestamp,
223}
224
225impl std::fmt::Display for ValidationError {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 match self {
228 Self::BadSpec => write!(f, "spec field missing or not touchstone/<version>"),
229 Self::NoChecks => write!(f, "attestation has no checks"),
230 Self::DuplicateCheckId(id) => write!(f, "duplicate check id: {id}"),
231 Self::VerdictMismatch { claimed, actual } => {
232 write!(f, "claimed verdict {claimed:?} != recomputed {actual:?}")
233 }
234 Self::BadSignature => write!(f, "signature malformed"),
235 Self::BadTimestamp => write!(f, "timestamp missing or not RFC3339"),
236 }
237 }
238}
239
240impl std::error::Error for ValidationError {}
241
242impl Attestation {
243 pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
248 let mut errs = Vec::new();
249 if !self.spec.starts_with("touchstone/") || self.spec.len() <= "touchstone/".len() {
250 errs.push(ValidationError::BadSpec);
251 }
252 if self.checks.is_empty() {
253 errs.push(ValidationError::NoChecks);
254 }
255 let mut seen = std::collections::HashSet::new();
256 for c in &self.checks {
257 if !seen.insert(&c.id) {
258 errs.push(ValidationError::DuplicateCheckId(c.id.clone()));
259 }
260 }
261 let actual = verdict_for(&self.checks);
262 if actual != self.verdict {
263 errs.push(ValidationError::VerdictMismatch {
264 claimed: self.verdict,
265 actual,
266 });
267 }
268 if let Some(sig) = &self.signature {
269 let hex_ok =
270 |s: &str, n: usize| s.len() == n && s.chars().all(|c| c.is_ascii_hexdigit());
271 if !hex_ok(&sig.pubkey, 64) || !hex_ok(&sig.sig, 128) {
272 errs.push(ValidationError::BadSignature);
273 }
274 }
275 if chrono::DateTime::parse_from_rfc3339(&self.timestamp).is_err() {
276 errs.push(ValidationError::BadTimestamp);
277 }
278 if errs.is_empty() {
279 Ok(())
280 } else {
281 Err(errs)
282 }
283 }
284}
285
286fn canonical_json(v: &serde_json::Value) -> String {
288 match v {
289 serde_json::Value::Object(map) => {
290 let mut keys: Vec<&String> = map.keys().collect();
291 keys.sort();
292 let inner: Vec<String> = keys
293 .into_iter()
294 .map(|k| {
295 format!(
296 "{}:{}",
297 serde_json::to_string(k).unwrap(),
298 canonical_json(&map[k])
299 )
300 })
301 .collect();
302 format!("{{{}}}", inner.join(","))
303 }
304 serde_json::Value::Array(a) => {
305 let inner: Vec<String> = a.iter().map(canonical_json).collect();
306 format!("[{}]", inner.join(","))
307 }
308 other => serde_json::to_string(other).unwrap(),
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 fn check(id: &str, organ: Organ, status: Status) -> CheckResult {
317 CheckResult {
318 id: id.into(),
319 organ,
320 status,
321 evidence: serde_json::Value::Null,
322 control: false,
323 }
324 }
325
326 #[test]
327 fn conformant_when_all_organs_pass() {
328 let checks: Vec<CheckResult> = Organ::ALL
329 .iter()
330 .map(|o| check("x", *o, Status::Pass))
331 .collect();
332 assert_eq!(verdict_for(&checks), Verdict::Conformant);
333 }
334
335 #[test]
336 fn partial_when_an_organ_missing() {
337 let checks: Vec<CheckResult> = Organ::ALL[..9]
338 .iter()
339 .map(|o| check("x", *o, Status::Pass))
340 .collect();
341 assert_eq!(verdict_for(&checks), Verdict::Partial);
342 }
343
344 #[test]
345 fn nonconformant_when_control_passes() {
346 let mut checks: Vec<CheckResult> = Organ::ALL
347 .iter()
348 .map(|o| check("x", *o, Status::Pass))
349 .collect();
350 checks.push(CheckResult {
351 control: true,
352 ..check("planted", Organ::Audit, Status::Pass)
353 });
354 assert_eq!(verdict_for(&checks), Verdict::Nonconformant);
355 }
356
357 #[test]
358 fn control_fail_is_fine() {
359 let mut checks: Vec<CheckResult> = Organ::ALL
360 .iter()
361 .map(|o| check("x", *o, Status::Pass))
362 .collect();
363 checks.push(CheckResult {
364 control: true,
365 ..check("planted", Organ::Audit, Status::Fail)
366 });
367 assert_eq!(verdict_for(&checks), Verdict::Conformant);
368 }
369
370 #[test]
371 fn empty_is_nonconformant() {
372 assert_eq!(verdict_for(&[]), Verdict::Nonconformant);
373 }
374
375 #[test]
376 fn canonical_hash_stable() {
377 let doc = Attestation {
378 spec: "touchstone/0.1".into(),
379 subject: Subject {
380 name: "x".into(),
381 version: "0".into(),
382 host: "h".into(),
383 },
384 timestamp: "t".into(),
385 checks: vec![check("a.b", Organ::Awake, Status::Pass)],
386 verdict: Verdict::Partial,
387 signature: None,
388 };
389 assert_eq!(document_hash(&doc), document_hash(&doc));
390 let mut signed = doc.clone();
392 signed.signature = Some(Signature {
393 scheme: "ed25519".into(),
394 pubkey: "00".into(),
395 sig: "ff".into(),
396 });
397 assert_eq!(document_hash(&doc), document_hash(&signed));
398 }
399}