Skip to main content

tear_types/
genesis.rs

1//! Genesis + Guid — the *immutable* half of a session's two keys.
2//!
3//! A [`Guid`] is a BLAKE3 commitment to a session's **genesis**: the
4//! program that runs it, the intent it was spawned with, and the
5//! context it was born into. It is the answer to "which session is
6//! this, forever?", and it never changes, because none of its inputs
7//! can change after birth.
8//!
9//! The companion key is [`crate::Address`] — mutable, unique, dotted,
10//! and used for lookup and aggregation. Durable records key on the
11//! `Guid`; nothing keys on the `Address`. That is the whole point:
12//! renaming or re-parenting a session moves the alias and leaves the
13//! identity — and therefore the data — exactly where it was.
14//!
15//! ## What is NOT hashed
16//!
17//! The session's **current** address. There is no field for it here,
18//! so a rename cannot reach the hash even by accident — the omission
19//! is structural, not a convention someone has to remember.
20//!
21//! What *is* hashed is [`Genesis::requested_address`]: the address
22//! asked for at birth. That is a historical fact about the spawn, as
23//! immutable as the cwd it ran in, and including it makes two
24//! otherwise-identical spawns into two distinct sessions.
25//!
26//! ## Why a Merkle root and not one flat hash
27//!
28//! Modelled on tameshi's `CertificationArtifact`: the three inputs are
29//! separate **leaves** composed into a root, with RFC 9162 / Certificate
30//! Transparency domain separation —
31//!
32//! ```text
33//! leaf(x)      = BLAKE3(0x00 || framed-fields)
34//! node(l, r)   = BLAKE3(0x01 || l || r)
35//!
36//!                      root = node(node(L0, L1), L2)
37//!                          /                    \
38//!            node(L0, L1)                        L2  context
39//!             /        \                             (cwd, parent)
40//!   L0 program          L1 intent
41//!                          (requested address, args)
42//! ```
43//!
44//! The distinct `0x00` / `0x01` prefixes mean a leaf hash and an
45//! interior hash can never collide, so no attacker-supplied leaf
46//! content can be re-read as an interior node (the second-preimage
47//! attack RFC 9162 §2.1.1 exists to stop). Within a leaf every field
48//! is length-framed (`u64` little-endian length, then bytes), so
49//! `["a", "b"]` and `["ab"]` are different commitments rather than the
50//! same concatenation.
51//!
52//! Leaf structure also leaves room to hand out an inclusion proof for
53//! one leaf later without revealing the others; nothing here needs
54//! that yet, and nothing here forecloses it.
55//!
56//! ## Full 256 bits, deliberately
57//!
58//! [`Guid`] keeps the whole BLAKE3 root — 32 bytes, 64 hex characters.
59//! It does **not** truncate the way [`crate::SessionId::from_seed`]
60//! does (first 8 little-endian bytes, 64 bits). A 64-bit identifier has
61//! a ~50% collision probability around 5 billion values and is birthday-
62//! attackable by anyone who can influence a seed; an attestable identity
63//! cannot be built on that. `SessionId` stays as it is — this is a new,
64//! wider key alongside it, not a change to the old one.
65//!
66//! ## No way to invent one
67//!
68//! [`Guid`]'s byte array is private to this module, so no other module
69//! — inside this crate or outside it — can name it. The only public
70//! function returning a `Guid` is [`Genesis::guid`]. There is
71//! deliberately no `FromStr`, no `From<[u8; 32]>`, and no
72//! `Deserialize`: a peer cannot hand the daemon an identity, because
73//! there is no code path that turns bytes back into one.
74//!
75//! `Serialize` is implemented (as lowercase hex) for the same reason
76//! [`crate::Shutai`] implements it — identity is *reported* outward to
77//! audit logs, `tear list`, and MCP reads. Information flows out;
78//! authority does not flow in.
79
80use core::fmt;
81
82use serde::{Serialize, Serializer};
83
84use crate::address::Address;
85
86/// RFC 9162 leaf-domain prefix.
87const LEAF_DOMAIN: u8 = 0x00;
88
89/// RFC 9162 interior-node-domain prefix.
90const NODE_DOMAIN: u8 = 0x01;
91
92/// Leaf label: the program that runs the session.
93const LABEL_PROGRAM: &str = "tear.genesis.program.v1";
94
95/// Leaf label: what the spawn asked for.
96const LABEL_INTENT: &str = "tear.genesis.intent.v1";
97
98/// Leaf label: where and under whom it was born.
99const LABEL_CONTEXT: &str = "tear.genesis.context.v1";
100
101/// A session's immutable identity — the full 256-bit BLAKE3 root of
102/// its [`Genesis`].
103///
104/// Obtainable only from [`Genesis::guid`]. See the module docs for why
105/// there is no other constructor and no `Deserialize`.
106#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
107pub struct Guid([u8; 32]);
108
109impl Guid {
110    /// The full 32-byte root.
111    #[must_use]
112    pub fn as_bytes(&self) -> &[u8; 32] {
113        &self.0
114    }
115
116    /// Lowercase hex, 64 characters — the canonical text form.
117    #[must_use]
118    pub fn to_hex(&self) -> String {
119        let mut out = String::with_capacity(64);
120        for b in self.0 {
121            out.push_str(&format!("{b:02x}"));
122        }
123        out
124    }
125
126    /// The first 16 hex characters, for logs and `tear list` columns.
127    /// A display abbreviation only — the identity is always the full
128    /// root, and nothing accepts a short form as input.
129    #[must_use]
130    pub fn short(&self) -> String {
131        self.to_hex()[..16].to_string()
132    }
133}
134
135impl fmt::Display for Guid {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(&self.to_hex())
138    }
139}
140
141impl fmt::Debug for Guid {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(f, "Guid({})", self.to_hex())
144    }
145}
146
147impl Serialize for Guid {
148    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149        serializer.serialize_str(&self.to_hex())
150    }
151}
152
153/// Everything a session is committed to at birth.
154///
155/// Three groups, one per Merkle leaf:
156///
157/// | leaf | fields | what it pins |
158/// |---|---|---|
159/// | 0 | [`program`](Genesis::program) | the image that runs |
160/// | 1 | [`requested_address`](Genesis::requested_address), [`args`](Genesis::args) | what the spawn asked for |
161/// | 2 | [`cwd`](Genesis::cwd), [`parent`](Genesis::parent) | where, and under whom |
162///
163/// There is no field for the session's *current* address — see the
164/// module docs.
165///
166/// Like [`Guid`] and [`crate::Shutai`], `Genesis` is `Serialize` but
167/// not `Deserialize`: it is minted from a spawn the daemon is
168/// performing, never parsed from a payload a peer sent.
169#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
170pub struct Genesis {
171    /// The shell or program image that runs the session — the resolved
172    /// path where one is known (`/run/current-system/sw/bin/zsh`), the
173    /// bare name otherwise.
174    pub program: String,
175    /// The address the spawn asked for. A historical fact about the
176    /// birth, not the session's live alias; the live alias is not an
177    /// input to the hash at all.
178    pub requested_address: Address,
179    /// Arguments the spawn requested, in order, excluding `argv[0]`.
180    pub args: Vec<String>,
181    /// The working directory the session was born in.
182    pub cwd: String,
183    /// The spawning session, when there was one. `None` is a
184    /// top-level spawn. Because a [`Guid`] can only come from a
185    /// [`Genesis`], a parent link is unforgeable by construction: you
186    /// cannot claim a parent you never derived.
187    pub parent: Option<Guid>,
188}
189
190impl Genesis {
191    /// A top-level genesis: program, requested address, cwd. Add args
192    /// with [`Genesis::with_args`] and a parent with
193    /// [`Genesis::with_parent`].
194    #[must_use]
195    pub fn new(program: impl Into<String>, requested_address: Address, cwd: impl Into<String>) -> Self {
196        Self {
197            program: program.into(),
198            requested_address,
199            args: Vec::new(),
200            cwd: cwd.into(),
201            parent: None,
202        }
203    }
204
205    /// Set the spawn arguments.
206    #[must_use]
207    pub fn with_args<I, S>(mut self, args: I) -> Self
208    where
209        I: IntoIterator<Item = S>,
210        S: Into<String>,
211    {
212        self.args = args.into_iter().map(Into::into).collect();
213        self
214    }
215
216    /// Record the spawning session.
217    #[must_use]
218    pub fn with_parent(mut self, parent: Guid) -> Self {
219        self.parent = Some(parent);
220        self
221    }
222
223    /// Derive this genesis's [`Guid`] — the only way a `Guid` comes
224    /// into existence.
225    ///
226    /// Pure and total: the same `Genesis` always yields the same
227    /// `Guid`, on any host, in any process, forever.
228    #[must_use]
229    pub fn guid(&self) -> Guid {
230        let [program, intent, context] = self.leaves();
231        Guid(node(&node(&program, &intent), &context))
232    }
233
234    /// The three domain-separated leaf hashes, in tree order.
235    fn leaves(&self) -> [[u8; 32]; 3] {
236        let program = leaf(LABEL_PROGRAM, |h| {
237            frame(h, self.program.as_bytes());
238        });
239
240        let intent = leaf(LABEL_INTENT, |h| {
241            frame(h, self.requested_address.to_string().as_bytes());
242            frame_len(h, self.args.len());
243            for arg in &self.args {
244                frame(h, arg.as_bytes());
245            }
246        });
247
248        let context = leaf(LABEL_CONTEXT, |h| {
249            frame(h, self.cwd.as_bytes());
250            match &self.parent {
251                None => {
252                    h.update(&[0u8]);
253                }
254                Some(parent) => {
255                    h.update(&[1u8]);
256                    h.update(parent.as_bytes());
257                }
258            }
259        });
260
261        [program, intent, context]
262    }
263}
264
265/// Length-framed field: `u64` little-endian length, then the bytes.
266/// Framing is what makes `["a", "b"]` and `["ab"]` different
267/// commitments.
268fn frame(h: &mut blake3::Hasher, bytes: &[u8]) {
269    frame_len(h, bytes.len());
270    h.update(bytes);
271}
272
273fn frame_len(h: &mut blake3::Hasher, len: usize) {
274    h.update(&(len as u64).to_le_bytes());
275}
276
277/// `BLAKE3(0x00 || framed(label) || body)` — RFC 9162 leaf domain.
278fn leaf(label: &str, body: impl FnOnce(&mut blake3::Hasher)) -> [u8; 32] {
279    let mut h = blake3::Hasher::new();
280    h.update(&[LEAF_DOMAIN]);
281    frame(&mut h, label.as_bytes());
282    body(&mut h);
283    *h.finalize().as_bytes()
284}
285
286/// `BLAKE3(0x01 || left || right)` — RFC 9162 interior-node domain.
287fn node(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
288    let mut h = blake3::Hasher::new();
289    h.update(&[NODE_DOMAIN]);
290    h.update(left);
291    h.update(right);
292    *h.finalize().as_bytes()
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use crate::address::Address;
299
300    fn addr(s: &str) -> Address {
301        Address::parse(s).expect("test fixture must be a legal address")
302    }
303
304    fn sample() -> Genesis {
305        // Compile-checked literal: if `Genesis` gains, loses, or
306        // renames a field this test stops COMPILING. That is the
307        // forcing function behind "the current address is not an input
308        // to the hash" — the only way to break the invariant is to add
309        // a field here, and doing so cannot happen silently.
310        Genesis {
311            program: "/bin/zsh".into(),
312            requested_address: addr("work.akeyless.helm-charts.build"),
313            args: vec!["-l".into()],
314            cwd: "/code/akeylesslabs/helm-charts".into(),
315            parent: None,
316        }
317    }
318
319    // ── reproducibility ────────────────────────────────────────────
320
321    #[test]
322    fn the_same_genesis_always_derives_the_same_guid() {
323        assert_eq!(sample().guid(), sample().guid());
324        // and repeated derivation from one value is stable too
325        let g = sample();
326        assert_eq!(g.guid(), g.guid());
327    }
328
329    #[test]
330    fn the_guid_is_a_full_256_bit_root_not_a_truncation() {
331        let guid = sample().guid();
332        assert_eq!(guid.as_bytes().len(), 32);
333        assert_eq!(guid.to_hex().len(), 64);
334        // The bug being avoided: SessionId::from_seed keeps only the
335        // first 8 bytes. Prove the other 24 carry real entropy.
336        assert!(
337            guid.as_bytes()[8..].iter().any(|b| *b != 0),
338            "bytes past the first 8 must not be dropped"
339        );
340        assert_eq!(guid.short(), &guid.to_hex()[..16]);
341    }
342
343    // ── one leaf differs → a different Guid ────────────────────────
344
345    #[test]
346    fn a_different_program_derives_a_different_guid() {
347        let mut other = sample();
348        other.program = "/bin/bash".into();
349        assert_ne!(sample().guid(), other.guid());
350    }
351
352    #[test]
353    fn a_different_requested_address_derives_a_different_guid() {
354        let mut other = sample();
355        other.requested_address = addr("work.akeyless.helm-charts.test");
356        assert_ne!(sample().guid(), other.guid());
357    }
358
359    #[test]
360    fn different_args_derive_a_different_guid() {
361        let mut other = sample();
362        other.args = vec!["-i".into()];
363        assert_ne!(sample().guid(), other.guid());
364
365        let mut none = sample();
366        none.args = Vec::new();
367        assert_ne!(sample().guid(), none.guid());
368        assert_ne!(other.guid(), none.guid());
369    }
370
371    #[test]
372    fn a_different_cwd_derives_a_different_guid() {
373        let mut other = sample();
374        other.cwd = "/code/akeylesslabs/cli".into();
375        assert_ne!(sample().guid(), other.guid());
376    }
377
378    #[test]
379    fn a_different_parent_derives_a_different_guid() {
380        let parent_a = sample().guid();
381        let parent_b = Genesis::new("/bin/bash", addr("work.other"), "/tmp").guid();
382        assert_ne!(parent_a, parent_b);
383
384        let orphan = sample();
385        let under_a = sample().with_parent(parent_a);
386        let under_b = sample().with_parent(parent_b);
387
388        assert_ne!(orphan.guid(), under_a.guid());
389        assert_ne!(orphan.guid(), under_b.guid());
390        assert_ne!(under_a.guid(), under_b.guid());
391    }
392
393    #[test]
394    fn every_leaf_is_load_bearing() {
395        // One assertion covering the whole claim: perturb each leaf's
396        // fields one at a time; all five perturbations, plus the
397        // baseline, must be six distinct Guids.
398        let base = sample();
399        let mut variants = vec![base.guid()];
400
401        let mut v = base.clone();
402        v.program = "/bin/bash".into();
403        variants.push(v.guid());
404
405        let mut v = base.clone();
406        v.requested_address = addr("work.other");
407        variants.push(v.guid());
408
409        let mut v = base.clone();
410        v.args = vec!["-l".into(), "-i".into()];
411        variants.push(v.guid());
412
413        let mut v = base.clone();
414        v.cwd = "/elsewhere".into();
415        variants.push(v.guid());
416
417        let v = base.clone().with_parent(base.guid());
418        variants.push(v.guid());
419
420        let mut seen = std::collections::BTreeSet::new();
421        for g in &variants {
422            assert!(seen.insert(g.to_hex()), "leaf perturbation collided: {g:?}");
423        }
424        assert_eq!(seen.len(), 6);
425    }
426
427    // ── framing + domain separation ────────────────────────────────
428
429    #[test]
430    fn args_are_length_framed_not_concatenated() {
431        let split = sample().with_args(["a", "b"]);
432        let joined = sample().with_args(["ab"]);
433        let dotted = sample().with_args(["a.b"]);
434        assert_ne!(split.guid(), joined.guid());
435        assert_ne!(split.guid(), dotted.guid());
436        assert_ne!(joined.guid(), dotted.guid());
437
438        // A trailing empty arg is a real difference, not a no-op.
439        let trailing_empty = sample().with_args(["a", ""]);
440        let bare = sample().with_args(["a"]);
441        assert_ne!(trailing_empty.guid(), bare.guid());
442    }
443
444    #[test]
445    fn leaf_and_interior_domains_cannot_collide() {
446        // The RFC 9162 guarantee, tested directly: the same body under
447        // the leaf prefix and under the node prefix are different
448        // hashes, so no leaf can be re-read as an interior node.
449        let l = leaf("x", |h| frame(h, b"left"));
450        let r = leaf("x", |h| frame(h, b"right"));
451        let interior = node(&l, &r);
452
453        // A leaf whose body is byte-identical to the interior body.
454        let leafish = {
455            let mut h = blake3::Hasher::new();
456            h.update(&[LEAF_DOMAIN]);
457            h.update(&l);
458            h.update(&r);
459            *h.finalize().as_bytes()
460        };
461        assert_ne!(interior, leafish, "domain prefixes must separate");
462        assert_ne!(LEAF_DOMAIN, NODE_DOMAIN);
463
464        // And a domain-separated leaf is not the raw BLAKE3 of its body.
465        let raw = *blake3::hash(b"left").as_bytes();
466        assert_ne!(l, raw);
467    }
468
469    #[test]
470    fn the_root_is_the_documented_tree_shape() {
471        let g = sample();
472        let [l0, l1, l2] = g.leaves();
473        let expected = node(&node(&l0, &l1), &l2);
474        assert_eq!(g.guid().as_bytes(), &expected);
475        // Leaves are mutually distinct — the labels do their job.
476        assert_ne!(l0, l1);
477        assert_ne!(l1, l2);
478        assert_ne!(l0, l2);
479    }
480
481    // ── identity vs alias ──────────────────────────────────────────
482
483    #[test]
484    fn a_parent_link_can_only_be_a_derived_guid() {
485        // The chain is closed: the only value `with_parent` accepts is
486        // one that came out of some Genesis, so an invented ancestry is
487        // unrepresentable rather than merely discouraged.
488        let parent = Genesis::new("/bin/zsh", addr("work"), "/code");
489        let child = Genesis::new("/bin/zsh", addr("work.build"), "/code")
490            .with_parent(parent.guid());
491        assert_eq!(child.parent, Some(parent.guid()));
492        assert_ne!(child.guid(), parent.guid());
493    }
494
495    #[test]
496    fn renaming_is_structurally_outside_the_hash() {
497        // The session's live alias is a separate value that Genesis
498        // never sees. Moving it — even to something wildly different —
499        // touches nothing the Guid was derived from.
500        let genesis = sample();
501        let identity = genesis.guid();
502
503        let mut live_alias = genesis.requested_address.clone();
504        assert_eq!(live_alias.to_string(), "work.akeyless.helm-charts.build");
505        live_alias = addr("archive.2026.helm-charts.build");
506
507        assert_eq!(genesis.guid(), identity);
508        assert_ne!(live_alias, genesis.requested_address);
509    }
510
511    // ── outward reporting ──────────────────────────────────────────
512
513    #[test]
514    fn guid_serializes_outward_as_lowercase_hex() {
515        let guid = sample().guid();
516        let json = serde_json::to_string(&guid).unwrap();
517        assert_eq!(json, format!("\"{}\"", guid.to_hex()));
518        assert_eq!(json.len(), 66);
519        assert!(guid.to_hex().chars().all(|c| c.is_ascii_hexdigit()));
520        assert!(!guid.to_hex().chars().any(|c| c.is_ascii_uppercase()));
521    }
522
523    #[test]
524    fn genesis_serializes_with_its_address_as_a_plain_string() {
525        let json = serde_json::to_value(sample()).unwrap();
526        assert_eq!(
527            json["requested_address"],
528            serde_json::json!("work.akeyless.helm-charts.build")
529        );
530        assert_eq!(json["parent"], serde_json::Value::Null);
531    }
532
533    #[test]
534    fn guid_display_and_debug_agree_on_the_hex() {
535        let guid = sample().guid();
536        assert_eq!(guid.to_string(), guid.to_hex());
537        assert_eq!(format!("{guid:?}"), format!("Guid({})", guid.to_hex()));
538    }
539
540    // ── structural forcing functions ───────────────────────────────
541
542    #[test]
543    fn genesis_commits_to_exactly_program_intent_and_context() {
544        // An exhaustive destructure. Adding a field to `Genesis` breaks
545        // this at COMPILE time, which is what keeps "the live address is
546        // not an input" from decaying into a convention someone forgets.
547        let Genesis {
548            program,
549            requested_address,
550            args,
551            cwd,
552            parent,
553        } = sample();
554        assert_eq!(program, "/bin/zsh");
555        assert_eq!(requested_address.to_string(), "work.akeyless.helm-charts.build");
556        assert_eq!(args, vec!["-l".to_string()]);
557        assert_eq!(cwd, "/code/akeylesslabs/helm-charts");
558        assert!(parent.is_none());
559    }
560
561    /// ★ THE STRUCTURAL PROPERTY, as a forcing function.
562    ///
563    /// A `Guid` must never become inventable. Absence of a trait impl
564    /// cannot be asserted at runtime, so this is a comment-stripped
565    /// source scan — the same construction as `shutai.rs`'s
566    /// `shutai_never_becomes_deserializable`.
567    #[test]
568    fn guid_never_gains_a_constructor_other_than_genesis() {
569        let src = include_str!("genesis.rs");
570        let code: String = src
571            .lines()
572            .map(str::trim_start)
573            .filter(|l| !l.starts_with("//"))
574            .collect::<Vec<_>>()
575            .join("\n");
576        let code = code.split("mod tests").next().unwrap_or(&code);
577
578        assert!(
579            !code.contains("Deserialize"),
580            "`Deserialize` appeared in genesis.rs. A peer could then SEND an \
581             identity instead of deriving one, which is the whole thing this \
582             type prevents."
583        );
584        assert!(
585            !code.contains("FromStr"),
586            "`FromStr` would let any string become a Guid"
587        );
588        assert!(
589            !code.contains("impl From<"),
590            "a `From` impl would be a second way to mint a Guid"
591        );
592        assert!(
593            code.contains("pub struct Guid([u8; 32]);"),
594            "Guid's bytes must stay private — a `pub` field is a constructor"
595        );
596        assert_eq!(
597            code.matches("-> Guid").count(),
598            1,
599            "exactly one function may return a Guid, and it is Genesis::guid"
600        );
601
602        // Anti-vacuity: the scan must be looking at real code.
603        assert!(code.contains("pub fn guid(&self) -> Guid"));
604        assert!(
605            code.contains("impl Serialize for Guid"),
606            "a Guid must still report OUTWARD (audit, list, MCP reads)"
607        );
608    }
609}