tatara_process/
attestation.rs1use chrono::{DateTime, Utc};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20use crate::three_pillar;
21
22#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
35#[serde(rename_all = "camelCase")]
36pub struct ProcessAttestation {
37 pub artifact_hash: String,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub control_hash: Option<String>,
42 pub intent_hash: String,
44 pub composed_root: String,
46 pub generation: u64,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub previous_root: Option<String>,
51 pub attested_at: DateTime<Utc>,
53}
54
55impl ProcessAttestation {
56 pub fn compose(
58 artifact_hash: String,
59 control_hash: Option<String>,
60 intent_hash: String,
61 previous_root: Option<String>,
62 generation: u64,
63 ) -> Self {
64 let composed_root = three_pillar::compose_root(
65 &artifact_hash,
66 control_hash.as_deref(),
67 &intent_hash,
68 previous_root.as_deref(),
69 );
70 Self {
71 artifact_hash,
72 control_hash,
73 intent_hash,
74 composed_root,
75 generation,
76 previous_root,
77 attested_at: Utc::now(),
78 }
79 }
80
81 pub fn initial(
83 artifact_hash: String,
84 control_hash: Option<String>,
85 intent_hash: String,
86 ) -> Self {
87 Self::compose(artifact_hash, control_hash, intent_hash, None, 0)
88 }
89
90 pub fn next(
92 &self,
93 artifact_hash: String,
94 control_hash: Option<String>,
95 intent_hash: String,
96 ) -> Self {
97 Self::compose(
98 artifact_hash,
99 control_hash,
100 intent_hash,
101 Some(self.composed_root.clone()),
102 self.generation + 1,
103 )
104 }
105
106 pub fn verify(&self) -> bool {
108 let recomputed = three_pillar::compose_root(
109 &self.artifact_hash,
110 self.control_hash.as_deref(),
111 &self.intent_hash,
112 self.previous_root.as_deref(),
113 );
114 three_pillar::constant_time_eq(recomputed.as_bytes(), self.composed_root.as_bytes())
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn initial_has_generation_zero() {
124 let a = ProcessAttestation::initial("a".into(), None, "i".into());
125 assert_eq!(a.generation, 0);
126 assert!(a.previous_root.is_none());
127 assert!(a.verify());
128 }
129
130 #[test]
131 fn chain_extends_previous_root() {
132 let a0 = ProcessAttestation::initial("a0".into(), Some("c0".into()), "i0".into());
133 let a1 = a0.next("a1".into(), Some("c1".into()), "i1".into());
134 assert_eq!(a1.generation, 1);
135 assert_eq!(a1.previous_root.as_deref(), Some(a0.composed_root.as_str()));
136 assert_ne!(a0.composed_root, a1.composed_root);
137 assert!(a1.verify());
138 }
139
140 #[test]
141 fn verify_detects_tamper() {
142 let mut a = ProcessAttestation::initial("a".into(), None, "i".into());
143 assert!(a.verify());
144 a.artifact_hash = "tampered".into();
145 assert!(!a.verify());
146 }
147
148 #[test]
149 fn control_hash_affects_root() {
150 let a = ProcessAttestation::initial("x".into(), None, "y".into());
151 let b = ProcessAttestation::initial("x".into(), Some("c".into()), "y".into());
152 assert_ne!(a.composed_root, b.composed_root);
153 }
154}