Skip to main content

touchstone_core/
lib.rs

1//! touchstone-core: the types and verdict rules for the personal AGI
2//! conformance spec. See SPEC.md at the repo root for the normative text.
3//!
4//! This crate deliberately has no filesystem, network, or process
5//! dependencies so it can compile to WASM and be shared by every verifier.
6
7#![forbid(unsafe_code)]
8
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12/// Version of the conformance spec this crate implements. Independent of
13/// crate semver — SPEC.md bumps only when the checklist or verdict rules
14/// change, not on every code release.
15pub const SPEC_VERSION: &str = "0.1";
16
17/// The organs a conformant organism must demonstrate.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Organ {
21    /// Persistent process exists and reports liveness.
22    Awake,
23    /// Organism declares what it is, bound to a device-held key.
24    Identity,
25    /// Opt-in senses (screen, audio, clipboard, etc.) working end-to-end.
26    Perception,
27    /// Store/recall of arbitrary state across time.
28    Memory,
29    /// Recorded structured decision procedure before consequential action.
30    Deliberation,
31    /// Tool executions landing on the audit record.
32    Action,
33    /// Watchers/standing orders firing without a prompt.
34    Vigilance,
35    /// Measurable self-improvement (adapters, strategy memory, etc.).
36    Learning,
37    /// Hash-chained event log verifiable by a second implementation.
38    Audit,
39    /// No required egress; an owner-held stop path exists.
40    Sovereignty,
41}
42
43impl Organ {
44    /// Every required organ, in spec order.
45    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/// Status of a single check, as emitted by an adapter.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum Status {
63    /// Evidence verified.
64    Pass,
65    /// Evidence absent, invalid, or contradictory.
66    Fail,
67    /// Check intentionally skipped / not applicable to this subject.
68    Optional,
69    /// A control check that failed as designed (proving instrumentation works).
70    ControlOk,
71}
72
73/// One check result in an attestation.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CheckResult {
76    /// Dotted id, e.g. "perception.screen".
77    pub id: String,
78    /// Which organ this check measures.
79    pub organ: Organ,
80    /// Outcome of the check.
81    pub status: Status,
82    /// Free-form evidence payload — transcripts, hashes, ledger seq ranges.
83    #[serde(default)]
84    pub evidence: serde_json::Value,
85    /// True when this check is a deliberate-failure control.
86    #[serde(default)]
87    pub control: bool,
88}
89
90/// Overall verdict for a run.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum Verdict {
94    /// Every required organ passed and every control failed as designed.
95    Conformant,
96    /// Some required check failed or is missing.
97    Partial,
98    /// No meaningful evidence / harness integrity failure
99    /// (a control check passed, or zero checks ran).
100    Nonconformant,
101}
102
103/// The subject under test.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Subject {
106    /// Organism/implementation name.
107    pub name: String,
108    /// Subject's own version string.
109    pub version: String,
110    /// Host the run executed on.
111    pub host: String,
112}
113
114/// A signature block on an attestation.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct Signature {
117    /// "ed25519" or "secp256r1-se" (Secure Enclave).
118    pub scheme: String,
119    /// Hex-encoded public key.
120    pub pubkey: String,
121    /// Hex-encoded signature over the canonical document hash.
122    pub sig: String,
123}
124
125/// The signed attestation document.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Attestation {
128    /// Spec tag, e.g. "touchstone/0.1".
129    pub spec: String,
130    /// What was measured.
131    pub subject: Subject,
132    /// ISO-8601 timestamp.
133    pub timestamp: String,
134    /// Every check result the adapter produced.
135    pub checks: Vec<CheckResult>,
136    /// Verdict claimed by the producer — recomputed on verify.
137    pub verdict: Verdict,
138    /// Device-key signature over the canonical document hash.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub signature: Option<Signature>,
141}
142
143/// Compute the verdict for a set of check results.
144///
145/// - `Conformant`: every organ has >=1 `PASS`, no required check `FAIL`ed,
146///   every control check reported `ControlOk` (i.e. it failed as designed).
147/// - `Nonconformant`: a control check passed (instrumentation can't be
148///   trusted) or the run produced no checks at all.
149/// - `Partial`: anything else.
150#[must_use]
151pub fn verdict_for(checks: &[CheckResult]) -> Verdict {
152    if checks.is_empty() {
153        return Verdict::Nonconformant;
154    }
155    // A control that PASSED means the harness can rubber-stamp: nonconformant.
156    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            // A control check failing is the expected outcome — it proves the
171            // harness can report failure — so it must not count as a real fail.
172            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/// Canonical serialization of an attestation for signing: the JSON with the
184/// signature field removed, keys emitted in sorted order.
185#[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/// SHA-256 of the canonical document — what signatures cover.
195#[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/// Structural validation errors an attestation can carry even when it
203/// parses as JSON.
204#[derive(Debug)]
205pub enum ValidationError {
206    /// `spec` field missing or not a `touchstone/<version>` string.
207    BadSpec,
208    /// No checks present — nothing was actually measured.
209    NoChecks,
210    /// Two checks share an id, making the scoreboard ambiguous.
211    DuplicateCheckId(String),
212    /// Claimed verdict disagrees with the verdict recomputed from checks.
213    VerdictMismatch {
214        /// Verdict the document claims.
215        claimed: Verdict,
216        /// Verdict the check results actually produce.
217        actual: Verdict,
218    },
219    /// Signature present but malformed (bad hex, wrong length).
220    BadSignature,
221    /// Timestamp missing or not RFC3339-parseable.
222    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    /// Validate structure independent of signature: spec tag, check
244    /// uniqueness, timestamp shape, and that the claimed verdict equals the
245    /// verdict the checks actually produce. Does NOT verify the signature —
246    /// that is the identity crate's job.
247    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
286/// Serialize a JSON value with all object keys sorted (canonical form).
287fn 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        // signature field excluded from hash
391        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}