confium_wasm/
transparency.rs1use confium_transparency::merkle::{Hash, InclusionProof as RustInclusionProof};
4use wasm_bindgen::prelude::*;
5
6#[wasm_bindgen]
12pub struct MerkleTree {
13 inner: std::cell::RefCell<confium_transparency::merkle::MerkleTree>,
14 leaf_hashes: std::cell::RefCell<std::collections::HashMap<u64, Hash>>,
17}
18
19#[wasm_bindgen]
20impl MerkleTree {
21 #[wasm_bindgen(constructor)]
23 pub fn new() -> MerkleTree {
24 Self {
25 inner: std::cell::RefCell::new(confium_transparency::merkle::MerkleTree::new()),
26 leaf_hashes: std::cell::RefCell::new(Default::default()),
27 }
28 }
29
30 pub fn append(&self, artifact_hash: &[u8]) -> Result<u64, JsValue> {
32 if artifact_hash.len() != 32 {
33 return Err(JsValue::from_str(&format!(
34 "artifact_hash must be exactly 32 bytes, got {}",
35 artifact_hash.len()
36 )));
37 }
38 let mut hash_arr = [0u8; 32];
39 hash_arr.copy_from_slice(artifact_hash);
40 let seq = self.inner.borrow().len() as u64;
46 let entry = confium_transparency::entry::MerkleEntry::new(
47 seq,
48 confium_transparency::entry::ArtifactType::CertificateIssuance,
49 hash_arr,
50 );
51 let leaf_hash = hash_leaf(entry.entry_hash());
52 let assigned = self.inner.borrow_mut().append(entry);
53 debug_assert_eq!(assigned, seq, "predicted sequence must match assigned");
54 self.leaf_hashes.borrow_mut().insert(assigned, leaf_hash);
55 Ok(assigned)
56 }
57
58 #[wasm_bindgen(getter)]
60 pub fn length(&self) -> usize {
61 self.inner.borrow().len()
62 }
63
64 #[wasm_bindgen]
66 pub fn root(&self) -> Vec<u8> {
67 self.inner.borrow().root().to_vec()
68 }
69
70 pub fn consistency_proof(&self, old_size: usize) -> Result<Vec<u8>, JsValue> {
74 let proof = self
75 .inner
76 .borrow()
77 .consistency_proof(old_size)
78 .map_err(|e| JsValue::from_str(&e.to_string()))?;
79 let mut flat = Vec::with_capacity(proof.len() * 32);
80 for h in &proof {
81 flat.extend_from_slice(h);
82 }
83 Ok(flat)
84 }
85
86 pub fn inclusion_proof(&self, sequence: u64) -> Result<InclusionProof, JsValue> {
88 let proof = self
89 .inner
90 .borrow()
91 .inclusion_proof(sequence)
92 .map_err(|e| JsValue::from_str(&e.to_string()))?;
93 let leaf = self
94 .leaf_hashes
95 .borrow()
96 .get(&sequence)
97 .copied()
98 .ok_or_else(|| JsValue::from_str("missing leaf hash for sequence"))?;
99 Ok(InclusionProof {
100 inner: proof,
101 leaf_hash: leaf,
102 })
103 }
104}
105
106impl Default for MerkleTree {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112#[wasm_bindgen]
115pub struct InclusionProof {
116 inner: RustInclusionProof,
117 leaf_hash: Hash,
118}
119
120#[wasm_bindgen]
121impl InclusionProof {
122 #[wasm_bindgen(getter)]
124 pub fn sequence(&self) -> u64 {
125 self.inner.sequence
126 }
127
128 #[wasm_bindgen]
131 pub fn verify(&self, root: &[u8]) -> Result<bool, JsValue> {
132 if root.len() != 32 {
133 return Err(JsValue::from_str(&format!(
134 "root must be exactly 32 bytes, got {}",
135 root.len()
136 )));
137 }
138 let mut root_arr = [0u8; 32];
139 root_arr.copy_from_slice(root);
140 let mut current = self.leaf_hash;
141 for step in &self.inner.steps {
142 current = match step.side {
143 confium_transparency::merkle::Side::Left => hash_internal(step.sibling, current),
144 confium_transparency::merkle::Side::Right => hash_internal(current, step.sibling),
145 };
146 }
147 use subtle::ConstantTimeEq;
148 Ok(current.ct_eq(&root_arr).into())
149 }
150}
151
152fn hash_leaf(entry_hash: Hash) -> Hash {
156 use sha2::{Digest, Sha256};
157 let mut h = Sha256::new();
158 h.update([0x01]);
159 h.update(entry_hash);
160 let r = h.finalize();
161 let mut out = [0u8; 32];
162 out.copy_from_slice(&r);
163 out
164}
165
166#[wasm_bindgen]
178pub fn compute_artifact_hash(artifact_bytes: &[u8]) -> Vec<u8> {
179 use sha2::{Digest, Sha256};
180 let mut h = Sha256::new();
181 h.update(artifact_bytes);
182 let r = h.finalize();
183 r.to_vec()
184}
185
186#[wasm_bindgen]
206pub fn compute_leaf_hash(sequence: u64, timestamp_ms: f64, artifact_bytes: &[u8]) -> Vec<u8> {
207 use sha2::{Digest, Sha256};
208 let artifact_hash = {
210 let mut h = Sha256::new();
211 h.update(artifact_bytes);
212 let r = h.finalize();
213 let mut out = [0u8; 32];
214 out.copy_from_slice(&r);
215 out
216 };
217 let mut entry_hasher = Sha256::new();
218 entry_hasher.update(sequence.to_le_bytes());
219 entry_hasher.update((timestamp_ms as i64).to_le_bytes());
220 entry_hasher.update(artifact_hash);
221 let entry_hash: [u8; 32] = {
222 let r = entry_hasher.finalize();
223 let mut out = [0u8; 32];
224 out.copy_from_slice(&r);
225 out
226 };
227 hash_leaf(entry_hash).to_vec()
228}
229
230fn hash_internal(left: Hash, right: Hash) -> Hash {
231 use sha2::{Digest, Sha256};
232 let mut h = Sha256::new();
233 h.update([0x02]);
234 h.update(left);
235 h.update(right);
236 let r = h.finalize();
237 let mut out = [0u8; 32];
238 out.copy_from_slice(&r);
239 out
240}
241
242#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
248pub struct TreeHead {
249 pub size: usize,
251 pub root: Vec<u8>,
253}
254
255#[wasm_bindgen]
259pub fn tree_head_from_json(json: &str) -> Result<String, JsValue> {
260 let head: TreeHead = serde_json::from_str(json)
261 .map_err(|e| JsValue::from_str(&format!("TreeHead JSON parse error: {e}")))?;
262 serde_json::to_string(&serde_json::json!({
263 "size": head.size,
264 "root_hex": head.root.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
265 }))
266 .map_err(|e| JsValue::from_str(&format!("serialize: {e}")))
267}
268
269#[wasm_bindgen]
275pub fn verify_inclusion_with_head(
276 leaf_entry_hash: &[u8],
277 proof_json: &str,
278 head_json: &str,
279) -> Result<bool, JsValue> {
280 if leaf_entry_hash.len() != 32 {
281 return Err(JsValue::from_str(&format!(
282 "leaf_entry_hash must be 32 bytes, got {}",
283 leaf_entry_hash.len()
284 )));
285 }
286 let mut leaf_arr = [0u8; 32];
287 leaf_arr.copy_from_slice(leaf_entry_hash);
288
289 let proof: RustInclusionProof = serde_json::from_str(proof_json)
290 .map_err(|e| JsValue::from_str(&format!("proof JSON parse: {e}")))?;
291 let head: TreeHead = serde_json::from_str(head_json)
292 .map_err(|e| JsValue::from_str(&format!("head JSON parse: {e}")))?;
293 if head.root.len() != 32 {
294 return Err(JsValue::from_str("head.root must be 32 bytes"));
295 }
296 let mut root_arr = [0u8; 32];
297 root_arr.copy_from_slice(&head.root);
298
299 let mut current = hash_leaf(leaf_arr);
300 for step in &proof.steps {
301 current = match step.side {
302 confium_transparency::merkle::Side::Left => hash_internal(step.sibling, current),
303 confium_transparency::merkle::Side::Right => hash_internal(current, step.sibling),
304 };
305 }
306 use subtle::ConstantTimeEq;
307 Ok(current.ct_eq(&root_arr).into())
308}