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