Skip to main content

rite_model/
commitment.rs

1//! The commitment chain: how a transcript's lines fold into its fingerprint.
2//!
3//! ```text
4//! node_0      = SHA-256(0x02 ‖ JCS(header))
5//! leaf_i      = SHA-256(0x00 ‖ salt_i ‖ JCS(fact_i))
6//! node_i      = SHA-256(0x01 ‖ node_{i-1} ‖ len(at_i) ‖ at_i ‖ level_i ‖ leaf_i)
7//! fingerprint = node_n
8//! ```
9//!
10//! `JCS` is the RFC 8785 canonical form ([`canonical_json`](crate::canonical_json)).
11//! The functions here take it already computed, since the writer puts the same
12//! text on the line. A salt is [`SALT_LEN`] bytes from the OS random source,
13//! fresh for every fact; it is never drawn from the ceremony entropy source,
14//! which is public by design and would let anyone holding a redacted
15//! transcript test guesses against a withheld fact. A line writes the salt as 32 lowercase hex digits, and the
16//! leaf hashes the 16 decoded bytes, so the commitment does not depend on how
17//! the line spells them. `at` is UTF-8, preceded by its length. Lengths and
18//! the level are 8-byte big-endian integers.
19//!
20//! The leaf commits to the fact; the node commits to the leaf, the time and
21//! the level, and to everything before it. A line whose fact is withheld keeps
22//! its leaf, so it folds to the same node, and a transcript with withheld lines
23//! has the same fingerprint as the complete one. The one-byte prefixes keep a
24//! leaf, a node and the header from ever hashing the same input.
25//!
26//! # Example
27//!
28//! Folding a header and one fact, as a verifier does line by line; the
29//! values are the construction's test vectors.
30//!
31//! ```
32//! use rite_model::commitment::{chain_node, fact_leaf, header_node};
33//! use rite_model::{Level, Sha256Digest, canonical_json};
34//! use serde_json::json;
35//!
36//! let header = json!({
37//!     "rite_transcript": 1,
38//!     "vocabulary": 1,
39//!     "producer": "rite 0.6.0",
40//!     "run_id": "00000000000000000000000000000000",
41//!     "dry_run": false,
42//!     "levels": { "public": 10, "restricted": 20, "confidential": 30 },
43//! });
44//! let node_0 = header_node(&canonical_json(&header)?);
45//!
46//! let fact = json!({ "type": "act_started", "id": "setup", "label": "Setup" });
47//! let leaf = fact_leaf(&[0x11; 16], &canonical_json(&fact)?);
48//! let node_1 = chain_node(&node_0, "2026-09-22T10:00:00.000000Z", Level::PUBLIC, &leaf);
49//!
50//! assert_eq!(
51//!     Sha256Digest::from_bytes(&node_1).as_str(),
52//!     "sha256:e1942a1ab439cad63eb64ed52692d425d898c2c16b0fac24c08e5c04f71a1af5"
53//! );
54//! # Ok::<(), rite_model::CanonicalJsonError>(())
55//! ```
56
57use sha2::{Digest, Sha256};
58
59use crate::transcript::Level;
60
61/// Length of a fact's salt, in bytes.
62pub const SALT_LEN: usize = 16;
63
64const LEAF_PREFIX: u8 = 0x00;
65const NODE_PREFIX: u8 = 0x01;
66const HEADER_PREFIX: u8 = 0x02;
67
68/// `node_0`: the commitment to the header, given as its canonical JSON.
69#[must_use]
70pub fn header_node(canonical_header: &str) -> [u8; 32] {
71    let mut hasher = Sha256::new();
72    hasher.update([HEADER_PREFIX]);
73    hasher.update(canonical_header.as_bytes());
74    hasher.finalize().into()
75}
76
77/// `leaf_i`: the commitment to one fact, given as its canonical JSON, under
78/// its salt.
79#[must_use]
80pub fn fact_leaf(salt: &[u8; SALT_LEN], canonical_fact: &str) -> [u8; 32] {
81    let mut hasher = Sha256::new();
82    hasher.update([LEAF_PREFIX]);
83    hasher.update(salt);
84    hasher.update(canonical_fact.as_bytes());
85    hasher.finalize().into()
86}
87
88/// `node_i`: the chain value after one line.
89#[must_use]
90pub fn chain_node(previous: &[u8; 32], at: &str, level: Level, leaf: &[u8; 32]) -> [u8; 32] {
91    let mut hasher = Sha256::new();
92    hasher.update([NODE_PREFIX]);
93    hasher.update(previous);
94    update_with_length(&mut hasher, at.as_bytes());
95    hasher.update(u64::from(level.value()).to_be_bytes());
96    hasher.update(leaf);
97    hasher.finalize().into()
98}
99
100fn update_with_length(hasher: &mut Sha256, bytes: &[u8]) {
101    let len = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
102    hasher.update(len.to_be_bytes());
103    hasher.update(bytes);
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::canonical_json;
110    use serde_json::{Value, json};
111
112    fn hex(bytes: &[u8; 32]) -> String {
113        base16ct::lower::encode_string(bytes)
114    }
115
116    fn jcs(value: &Value) -> String {
117        canonical_json(value).expect("canonical")
118    }
119
120    /// Fixed vectors: a change to the construction shows up here, and a
121    /// third-party implementation can check itself against them.
122    #[test]
123    fn construction_vectors() {
124        let header = json!({
125            "rite_transcript": 1,
126            "vocabulary": 1,
127            "producer": "rite 0.6.0",
128            "run_id": "00000000000000000000000000000000",
129            "dry_run": false,
130            "levels": { "public": 10, "restricted": 20, "confidential": 30 },
131        });
132        let node_0 = header_node(&jcs(&header));
133        assert_eq!(
134            hex(&node_0),
135            "acd329c9b34b4e9443db1b47b4a5908b3f66affd630005935e5c5201c3f123ec"
136        );
137
138        let salt = [0x11; SALT_LEN];
139        let fact = json!({ "type": "act_started", "id": "setup", "label": "Setup" });
140        let leaf = fact_leaf(&salt, &jcs(&fact));
141        assert_eq!(
142            hex(&leaf),
143            "60d495f6e5117daeaa777bcac4b9aa5360c43aff76bdcf5461380864e0c053d0"
144        );
145
146        let node_1 = chain_node(&node_0, "2026-09-22T10:00:00.000000Z", Level::PUBLIC, &leaf);
147        assert_eq!(
148            hex(&node_1),
149            "e1942a1ab439cad63eb64ed52692d425d898c2c16b0fac24c08e5c04f71a1af5"
150        );
151    }
152
153    #[test]
154    fn every_committed_input_changes_the_node() {
155        let base = chain_node(&[0; 32], "t", Level::PUBLIC, &[1; 32]);
156        assert_ne!(base, chain_node(&[9; 32], "t", Level::PUBLIC, &[1; 32]));
157        assert_ne!(base, chain_node(&[0; 32], "u", Level::PUBLIC, &[1; 32]));
158        assert_ne!(base, chain_node(&[0; 32], "t", Level::RESTRICTED, &[1; 32]));
159        assert_ne!(base, chain_node(&[0; 32], "t", Level::PUBLIC, &[2; 32]));
160    }
161
162    #[test]
163    fn the_salt_and_the_fact_both_change_the_leaf() {
164        let fact = jcs(&json!({ "type": "x" }));
165        let leaf = fact_leaf(&[0; SALT_LEN], &fact);
166        assert_ne!(leaf, fact_leaf(&[1; SALT_LEN], &fact));
167        assert_ne!(
168            leaf,
169            fact_leaf(&[0; SALT_LEN], &jcs(&json!({ "type": "y" })))
170        );
171    }
172
173    #[test]
174    fn member_order_does_not_change_the_leaf() {
175        let a: Value = serde_json::from_str(r#"{"a":1,"b":2}"#).expect("parse");
176        let b: Value = serde_json::from_str(r#"{"b":2,"a":1}"#).expect("parse");
177        assert_eq!(
178            fact_leaf(&[0; SALT_LEN], &jcs(&a)),
179            fact_leaf(&[0; SALT_LEN], &jcs(&b))
180        );
181    }
182}