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