tatara_process/three_pillar.rs
1//! Three-pillar BLAKE3 composition — the ONE substrate owner for the
2//! typed hashing chain every `tatara-process/v1alpha1` attestation +
3//! receipt-envelope consumer walks.
4//!
5//! # Why it exists
6//!
7//! Two peer consumers in this crate walked the SAME domain-tagged
8//! BLAKE3 chain pre-lift, each with its own private `DOMAIN_TAG`
9//! constant, its own 4-argument `compose_*` fn, AND its own
10//! `constant_time_eq` byte-comparator:
11//!
12//! * [`crate::attestation::ProcessAttestation::compose`] + `verify` —
13//! the on-chain attestation writer. Composes a new
14//! `attestation.composed_root` from the four pillars + a chained
15//! `previous_root`, and verifies a persisted attestation matches
16//! its own claim.
17//! * [`crate::receipt::ReceiptEnvelope::build`] + `verify_root` — the
18//! fleet-wide receipt-envelope writer + reader. Composes a new
19//! `envelope.composed_root` from the four pillars + the operator's
20//! expected `previous_root`, and verifies a wire-parsed envelope
21//! matches its own claim.
22//!
23//! Both consumers restated the identical BLAKE3 chain byte-for-byte
24//! (`DOMAIN_TAG` | `artifact` | `\n` | `control?` | `\n` | `intent` |
25//! `\n` | `previous?`), the identical `hex::encode(h.finalize().
26//! as_bytes())` cast, AND the identical eight-line
27//! `constant_time_eq` byte-comparator. The receipt-side even documented
28//! the duplication in a pre-lift comment ("Same composition as
29//! `ProcessAttestation::composed_hex` — kept local so
30//! `tatara_process::receipt::compose_root(...)` is a single line in
31//! downstream code without re-importing the attestation module").
32//!
33//! Silent divergence between the two chains would break receipt
34//! verification with **no compile-time signal** — the reconciler's
35//! `ConditionKind::ClosedLoopAuth` evaluator would false-negative
36//! every closed-loop probe receipt against a Process attestation
37//! whose composed_root uses the drifted rule. Silent divergence on
38//! `DOMAIN_TAG` (a version bump on ONE side, or a typo on either)
39//! would silently invalidate every persisted receipt against the
40//! attestation chain that reads it back. Silent divergence on the
41//! `constant_time_eq` bit-mask fold (a `!=` typo, an early `return
42//! true` on empty inputs, a short-circuit `&&`) would open a
43//! timing-side-channel corner AT ONE consumer without touching the
44//! peer's proof.
45//!
46//! # What the lift owns
47//!
48//! One typed owner per shape:
49//!
50//! * [`DOMAIN_TAG`] — the `tatara-process/v1alpha1\n` prefix bytes.
51//! The prefix "tatara-process" is the *crate name*, not the K8s
52//! API group (which is `tatara.pleme.io`) — the receipt schema
53//! version + the attestation domain-separation tag are keyed off
54//! the crate that owns the wire type, deliberately independent of
55//! how kube-rs projects the CRD group. A pin below binds the const
56//! to `format!("tatara-process/{}\n", crate::VERSION)` so a future
57//! CRD-version bump lands at the substrate owner AND the domain
58//! tag together, not at the tag alone (silent invalidation of
59//! every persisted composed_root) or at the version alone (silent
60//! attestation of a stale tag past a wire-format break).
61//! * [`compose_root`] — the 4-pillar BLAKE3 → hex projection.
62//! * [`constant_time_eq`] — the length-checked, bit-mask-folded
63//! byte-comparator. Peer to the `subtle` crate's `ConstantTimeEq`
64//! trait but pure Rust, no dep.
65//!
66//! # Why it compounds
67//!
68//! A future normalization at the substrate owner reaches BOTH
69//! consumers (attestation + receipt) mechanically — no per-site
70//! edit at either callsite:
71//!
72//! * A CRD-version bump (`v1alpha1` → `v1beta1` → `v1`) lands as ONE
73//! `DOMAIN_TAG` byte-string edit at the substrate owner; both
74//! consumers pick it up at the same commit or neither does.
75//! * A domain-tag structural change (a length-prefix, a version-
76//! independent stable tag, a per-pillar sub-tag) lands at ONE
77//! composer body.
78//! * A move to a subtler constant-time comparator (a `subtle`-crate
79//! dep, an intrinsics-backed comparator on nightly, an
80//! architecture-conditional short-circuit ban) lands at ONE
81//! comparator body.
82//!
83//! # Not a `constant_time_eq` crate substitution
84//!
85//! The workspace's Cargo.lock already carries the `constant_time_eq`
86//! crate as a transitive dep of the BLAKE3 backend, but pulling it in
87//! as a direct dep here would add a compile-time-tunable direct dep
88//! for a comparator whose body is literally eight lines and whose
89//! typed contract this module already owns. Kept pure Rust; a future
90//! swap onto `subtle::ConstantTimeEq` or an intrinsics-backed
91//! comparator lands at [`constant_time_eq`] below without changing
92//! any caller.
93
94use blake3::Hasher;
95
96/// The domain-separation tag every three-pillar composition rides.
97///
98/// The prefix `tatara-process` is the *crate name* that owns the
99/// wire type, deliberately independent of the CRD's K8s API group
100/// (`tatara.pleme.io`). The version suffix binds to
101/// [`crate::VERSION`] via the pin at
102/// [`tests::domain_tag_matches_crate_name_and_version_bytes`] so a
103/// future CRD-version bump either lands at both or fails-loudly at
104/// the pin.
105pub const DOMAIN_TAG: &[u8] = b"tatara-process/v1alpha1\n";
106
107/// Compose the three-pillar BLAKE3 → hex composed_root from the four
108/// pillars. `control` and `previous` are `Option<&str>` because the
109/// receipt-envelope + attestation surfaces both treat an absent
110/// slot as "no control step" / "no chain predecessor", encoded on
111/// the wire as either an empty string (the receipt-envelope
112/// `control_hash: ""` posture) or an absent slot (the attestation
113/// `previous_root: None` posture). The composer normalizes both onto
114/// the same "empty-bytes chunk between the `\n` separators" wire
115/// shape — matching every pre-lift consumer byte-for-byte.
116///
117/// A byte-identity pin at [`tests::compose_root_matches_pre_lift_
118/// hand_authored_chain`] fixes the composition against the
119/// hand-authored chain both pre-lift consumers walked, so a
120/// regression at the composer's body (a reordered pillar, a swapped
121/// separator, a missing `hex::encode`) surfaces at ONE substrate
122/// pin rather than as silent invalidation of every downstream
123/// composed_root read.
124#[must_use]
125pub fn compose_root(
126 artifact: &str,
127 control: Option<&str>,
128 intent: &str,
129 previous: Option<&str>,
130) -> String {
131 let mut h = Hasher::new();
132 h.update(DOMAIN_TAG);
133 h.update(artifact.as_bytes());
134 h.update(b"\n");
135 h.update(control.unwrap_or("").as_bytes());
136 h.update(b"\n");
137 h.update(intent.as_bytes());
138 h.update(b"\n");
139 h.update(previous.unwrap_or("").as_bytes());
140 // Terminal `hex::encode(<hash>.as_bytes())` step rides through
141 // the substrate primitive [`crate::hash::hex_blake3_hash`] — the
142 // ONE owner of the streaming-digest hex encoding. Pre-lift this
143 // site restated `hex::encode(h.finalize().as_bytes())` inline,
144 // sibling to the same 1-link chain hand-authored at
145 // `tatara-reconciler::phase_machine::handle_running` (the per-ref
146 // artifact-hash fold on the ATTEST step) past the ★★ PRIME-
147 // DIRECTIVE ≥ 2 duplication threshold; post-lift both consumers
148 // route through ONE substrate function, and a future re-encoding
149 // reaches both mechanically.
150 crate::hash::hex_blake3_hash(&h.finalize())
151}
152
153/// Length-checked, bit-mask-folded constant-time byte comparator.
154///
155/// Returns `true` iff `a` and `b` are equal in length AND in every
156/// byte. On unequal lengths short-circuits `false` without touching
157/// the payload — matches every pre-lift comparator byte-for-byte
158/// (the length short-circuit at both attestation.rs + receipt.rs
159/// pre-lift is a load-bearing "different lengths CAN NEVER be
160/// equal" fast path, not a leak). On equal lengths folds a bit-mask
161/// across the full payload before deciding, so a per-byte timing
162/// leak does not surface at ONE consumer without touching the peer.
163#[must_use]
164pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
165 if a.len() != b.len() {
166 return false;
167 }
168 let mut acc: u8 = 0;
169 for (x, y) in a.iter().zip(b.iter()) {
170 acc |= x ^ y;
171 }
172 acc == 0
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 // ── DOMAIN_TAG shape pins ─────────────────────────────────────
180
181 #[test]
182 fn domain_tag_matches_crate_name_and_version_bytes() {
183 // Binds the substrate `DOMAIN_TAG` const to the crate-name
184 // prefix "tatara-process" + the workspace-wide
185 // `crate::VERSION` spelling. A future CRD-version bump that
186 // lands at ONE side (say, `VERSION` becomes `v1beta1` but
187 // `DOMAIN_TAG` stays `v1alpha1`) fails loudly HERE rather
188 // than as silent invalidation of every persisted
189 // composed_root on the wire.
190 //
191 // Note the prefix is the CRATE name, not the K8s API GROUP
192 // (`tatara.pleme.io`) — the receipt schema version + the
193 // attestation domain-separation tag are keyed off the crate
194 // that owns the wire type, deliberately independent of how
195 // kube-rs projects the CRD group.
196 let expected = format!("tatara-process/{}\n", crate::VERSION);
197 assert_eq!(DOMAIN_TAG, expected.as_bytes());
198 }
199
200 #[test]
201 fn domain_tag_ends_with_newline_separator() {
202 // The pre-lift chain relied on `DOMAIN_TAG`'s trailing `\n`
203 // to double as the first field separator (no explicit `h.
204 // update(b"\n")` between the tag and the artifact chunk).
205 // A regression that dropped the trailing newline would
206 // silently produce a different composed_root for every
207 // downstream receipt, so bind the shape here.
208 assert_eq!(DOMAIN_TAG.last(), Some(&b'\n'));
209 }
210
211 // ── compose_root byte-identity pins ───────────────────────────
212
213 /// The hand-authored chain both pre-lift consumers walked —
214 /// `attestation::composed_hex` and `receipt::compose_root` had
215 /// identical bodies to this. The substrate `compose_root` MUST
216 /// match this byte-for-byte for every input on every consumer.
217 fn hand_authored_chain(
218 artifact: &str,
219 control: Option<&str>,
220 intent: &str,
221 previous: Option<&str>,
222 ) -> String {
223 let mut h = Hasher::new();
224 h.update(DOMAIN_TAG);
225 h.update(artifact.as_bytes());
226 h.update(b"\n");
227 h.update(control.unwrap_or("").as_bytes());
228 h.update(b"\n");
229 h.update(intent.as_bytes());
230 h.update(b"\n");
231 h.update(previous.unwrap_or("").as_bytes());
232 hex::encode(h.finalize().as_bytes())
233 }
234
235 #[test]
236 fn compose_root_matches_pre_lift_hand_authored_chain() {
237 // Sweeps every corner of the (control, previous) Option pair
238 // — both consumers' pre-lift chains treated `None` as
239 // empty-bytes, so the substrate composer MUST too.
240 let cases: &[(&str, Option<&str>, &str, Option<&str>)] = &[
241 ("aaaa", None, "iiii", None),
242 ("aaaa", Some("cccc"), "iiii", None),
243 ("aaaa", None, "iiii", Some("pppp")),
244 ("aaaa", Some("cccc"), "iiii", Some("pppp")),
245 ("", None, "", None),
246 ("", Some(""), "", Some("")),
247 ];
248 for (artifact, control, intent, previous) in cases {
249 assert_eq!(
250 compose_root(artifact, *control, intent, *previous),
251 hand_authored_chain(artifact, *control, intent, *previous),
252 "compose_root drifted from pre-lift hand-authored chain \
253 for inputs (artifact={artifact:?}, control={control:?}, \
254 intent={intent:?}, previous={previous:?})",
255 );
256 }
257 }
258
259 #[test]
260 fn compose_root_treats_empty_control_and_none_control_identically() {
261 // Load-bearing invariant the receipt-envelope + attestation
262 // consumers both rely on: an absent `control_hash` slot
263 // (attestation's `Option<String>::None`) and an empty-string
264 // `control_hash` slot (the receipt-envelope wire posture
265 // where the writer stamps `""` for "no control step") MUST
266 // compose to the SAME composed_root. Otherwise a receipt
267 // written with `""` would false-negative against an
268 // attestation chained with `None` even on identical pillars.
269 let with_none = compose_root("art", None, "int", None);
270 let with_empty = compose_root("art", Some(""), "int", Some(""));
271 assert_eq!(with_none, with_empty);
272 }
273
274 #[test]
275 fn compose_root_is_deterministic_across_calls() {
276 // BLAKE3 is deterministic; the composer is pure. Pin it so
277 // a future refactor that accidentally seeds a nonce or
278 // reads a clock fails-loudly HERE.
279 let a = compose_root("art", Some("ctl"), "int", Some("prev"));
280 let b = compose_root("art", Some("ctl"), "int", Some("prev"));
281 assert_eq!(a, b);
282 }
283
284 #[test]
285 fn compose_root_differs_across_every_pillar() {
286 // Each of the four pillars is load-bearing — a swap between
287 // any two MUST produce a distinct composed_root, else the
288 // domain-separation between pillars collapsed.
289 let base = compose_root("aaaa", Some("cccc"), "iiii", Some("pppp"));
290 assert_ne!(
291 base,
292 compose_root("BBBB", Some("cccc"), "iiii", Some("pppp")),
293 "artifact pillar swap failed to alter composed_root"
294 );
295 assert_ne!(
296 base,
297 compose_root("aaaa", Some("CCCC"), "iiii", Some("pppp")),
298 "control pillar swap failed to alter composed_root"
299 );
300 assert_ne!(
301 base,
302 compose_root("aaaa", Some("cccc"), "IIII", Some("pppp")),
303 "intent pillar swap failed to alter composed_root"
304 );
305 assert_ne!(
306 base,
307 compose_root("aaaa", Some("cccc"), "iiii", Some("PPPP")),
308 "previous pillar swap failed to alter composed_root"
309 );
310 }
311
312 #[test]
313 fn compose_root_output_is_lowercase_hex_of_blake3_length() {
314 // BLAKE3 produces 32-byte digests; hex-encoded → 64 lowercase
315 // characters. Pin the output shape so a downstream reader's
316 // width assumption (a 26-char base32 slot in the wire form,
317 // for instance) surfaces here rather than as a wire-parse
318 // failure.
319 let out = compose_root("a", None, "i", None);
320 assert_eq!(out.len(), 64);
321 assert!(out.chars().all(|c| c.is_ascii_hexdigit()));
322 assert!(out.chars().all(|c| !c.is_ascii_uppercase()));
323 }
324
325 // ── constant_time_eq byte-identity + corner pins ──────────────
326
327 fn hand_authored_ct_eq(a: &[u8], b: &[u8]) -> bool {
328 if a.len() != b.len() {
329 return false;
330 }
331 let mut acc: u8 = 0;
332 for (x, y) in a.iter().zip(b.iter()) {
333 acc |= x ^ y;
334 }
335 acc == 0
336 }
337
338 #[test]
339 fn constant_time_eq_matches_pre_lift_hand_authored_body() {
340 // Sweeps both length axes AND both equality axes so the
341 // substrate comparator matches both pre-lift bodies byte-
342 // for-byte on every corner.
343 let cases: &[(&[u8], &[u8])] = &[
344 (b"", b""),
345 (b"", b"a"),
346 (b"a", b""),
347 (b"a", b"a"),
348 (b"a", b"b"),
349 (b"abcd", b"abcd"),
350 (b"abcd", b"abce"),
351 (b"abcd", b"abc"),
352 (b"abc", b"abcd"),
353 (b"\x00\x00\x00", b"\x00\x00\x00"),
354 (b"\xff\xff\xff", b"\xff\xff\xff"),
355 (b"\xff\xff\xff", b"\xff\xff\x00"),
356 ];
357 for (a, b) in cases {
358 assert_eq!(
359 constant_time_eq(a, b),
360 hand_authored_ct_eq(a, b),
361 "constant_time_eq drifted from pre-lift hand-authored \
362 body for inputs (a={a:?}, b={b:?})",
363 );
364 }
365 }
366
367 #[test]
368 fn constant_time_eq_short_circuits_on_length_mismatch() {
369 // The pre-lift length short-circuit at both consumers is a
370 // load-bearing "different lengths CAN NEVER be equal" fast
371 // path, not a leak. Pin the corner explicitly.
372 assert!(!constant_time_eq(b"", b"a"));
373 assert!(!constant_time_eq(b"abc", b"abcd"));
374 assert!(!constant_time_eq(b"abcd", b"abc"));
375 }
376
377 #[test]
378 fn constant_time_eq_returns_true_only_on_full_byte_equality() {
379 assert!(constant_time_eq(b"", b""));
380 assert!(constant_time_eq(b"abc", b"abc"));
381 assert!(!constant_time_eq(b"abc", b"abd"));
382 // Distinct only at the final byte — verifies the fold
383 // reaches the end rather than short-circuiting on the
384 // first mismatch.
385 assert!(!constant_time_eq(b"abcdef", b"abcdeg"));
386 // Distinct only at the first byte — verifies the fold
387 // does NOT short-circuit on the first byte (the "constant"
388 // in "constant time" — full payload gets folded before
389 // deciding).
390 assert!(!constant_time_eq(b"Abcdef", b"abcdef"));
391 }
392}