wacc 2.0.0

Web Assembly Cryptographic Constructs VM implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
// SPDX-License-Identifier: Apache-2.0
use crate::{
    api::{WASM_FALSE, WASM_TRUE},
    security::allowed_algorithms,
    types::{CheckCount, ContextPath, LogSize},
    Pairs, Stack, Value,
};
use log::info;
use multi_codec::Codec;
use multi_hash::{mh, Multihash};
use multi_key::{Multikey, Views};
use multi_sig::Multisig;
use multi_util::CodecInfo;
use std::{fmt, io::Write};
use wasmtime::{StoreLimits, Val};

/// Returns true if the codec is one of the XMSS multisig codecs (which carry a
/// stateful leaf index that must not be reused).
const fn is_xmss_msig(codec: Codec) -> bool {
    matches!(
        codec,
        Codec::XmssSha210256Msig | Codec::XmssSha216256Msig | Codec::XmssSha220256Msig
    )
}

/// Returns true if the codec is one of the Lamport signature codecs (which use
/// one-time keys that must not sign more than once).
const fn is_lamport_sig(codec: Codec) -> bool {
    matches!(
        codec,
        Codec::LamportSha3256Sig
            | Codec::LamportSha3384Sig
            | Codec::LamportSha3512Sig
            | Codec::LamportSha2256Sig
            | Codec::LamportSha2384Sig
            | Codec::LamportSha2512Sig
            | Codec::LamportBlake2B512Sig
            | Codec::LamportBlake2S256Sig
            | Codec::LamportBlake3256Sig
            | Codec::LamportShake128Sig
            | Codec::LamportShake256Sig
    )
}

/// Represents the application state for each instance of a WACC execution.
pub struct Context {
    /// The key-value store of the current state
    pub current: Box<dyn Pairs>,
    /// The key-value store of the proposed state update
    pub proposed: Box<dyn Pairs>,
    /// The stack of values
    pub pstack: Box<dyn Stack>,
    /// The stack of return values
    pub rstack: Box<dyn Stack>,
    /// The number of times a check_* operation has been executed
    pub check_count: CheckCount,
    /// The top down stack index for writing into linear memory
    pub write_idx: usize,
    /// The context key-path for hierarchical key organization
    pub context: ContextPath,
    /// In-memory buffer to accumulate log messages from scripts
    pub log: Vec<u8>,
    /// The limiter
    pub limiter: StoreLimits,
}

impl fmt::Debug for Context {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "Context {{ check_count: {}, context: {} }}",
            self.check_count.as_usize(),
            self.context
        )
    }
}

impl Context {
    /// Increment the check counter and push a FAILURE marker on the return stack
    ///
    /// This is called when a cryptographic check operation fails.
    /// The `check_count` tracks the number of check operations performed.
    pub fn check_fail(&mut self, err: &str) -> Val {
        // Increment the check_count to track this check operation
        self.check_count = match self.check_count.increment() {
            Some(count) => count,
            None => {
                // Max check count reached - fail immediately
                return self.fail("maximum check count exceeded");
            }
        };
        // Push failure marker
        self.fail(err)
    }

    /// Push a FAILURE marker onto the return stack without incrementing check count
    ///
    /// This is used for non-check failures (e.g., stack underflow, missing keys).
    pub fn fail(&mut self, err: &str) -> Val {
        // push the FAILURE onto the return stack
        self.rstack.push(Value::Failure(err.to_string()));
        // return that we failed
        WASM_FALSE
    }

    /// Push a SUCCESS marker onto the return stack with current check count
    ///
    /// This is called when a cryptographic check operation succeeds.
    /// The Success value contains the current `check_count`.
    pub fn succeed(&mut self) -> Val {
        // Enforce check count limit
        if self.check_count.at_max() {
            return self.fail("maximum check count exceeded");
        }
        // Push SUCCESS marker with the current check count
        // (This matches original behavior - only failures increment the count)
        self.rstack.push(self.check_count.as_usize().into());
        // return that we succeeded
        WASM_TRUE
    }

    /// Add a line to the log
    pub fn log(&mut self, log_line: &str) -> Val {
        // Check if adding this line would exceed log size limit
        let current_size = LogSize::new(self.log.len());
        let line_size = log_line.len() + 1; // +1 for newline

        if current_size.would_exceed(line_size) {
            return self.fail("log buffer size limit exceeded");
        }

        // add the log line to the log
        match writeln!(&mut self.log, "{log_line}") {
            Ok(()) => WASM_TRUE,
            Err(e) => self.fail(&e.to_string()),
        }
    }

    /// Push raw bytes directly onto the parameter stack
    ///
    /// Unlike [`Self::push`], this does not look up a value from the KVP store.
    /// It pushes the provided bytes directly as a `Value::Bin`.
    /// Used by the `_push_value` WASM import to push data from linear memory.
    pub fn push_value(&mut self, bytes: Vec<u8>) -> Val {
        self.pstack.push(Value::Bin {
            hint: String::new(),
            data: bytes.into(),
        });
        WASM_TRUE
    }

    /// Push the value associated with the key onto the parameter stack
    pub fn push(&mut self, key: &str) -> Val {
        // try to look up the key-value pair by key and push the result onto the stack
        match self.current.get(key) {
            Some(v) => {
                self.pstack.push(v); // pushes Value::Bin(Vec<u8>)
                WASM_TRUE
            }
            None => self.fail(&format!("kvp missing key: {key}")),
        }
    }

    /// Pop a value from the parameter stack
    pub fn pop(&mut self) -> Val {
        // make sure we have at least one parameter on the stack
        if self.pstack.is_empty() {
            return self.fail(&format!(
                "not enough parameters on the stack for pop ({})",
                self.pstack.len()
            ));
        }

        // pop the value from the stack
        let _ = self.pstack.pop();
        WASM_TRUE
    }

    /// Calculate the full key given the context by branching
    ///
    /// For backward compatibility, this simply concatenates the context and key.
    /// The context path already includes separators as needed.
    #[must_use]
    pub fn branch(&self, key: &str) -> String {
        let s = format!("{}{}", self.context.as_str(), key);
        info!("branch({key}) -> {s}");
        s
    }

    /// Verifies the top of the stack matches the value associated with the key
    pub fn check_eq(&mut self, key: &str) -> Val {
        info!("check_eq: loading from current {key}");
        // look up the value
        let value = {
            match self.current.get(key) {
                Some(v @ Value::Bin { .. }) => v,
                Some(v @ Value::Str { .. }) => v,
                Some(_) => {
                    return self.check_fail(&format!("unexpected value type associated with {key}"))
                }
                None => return self.check_fail(&format!("no value associated with {key}")),
            }
        };

        // make sure we have at least one parameter on the stack
        if self.pstack.is_empty() {
            return self.check_fail(&format!(
                "not enough parameters on the stack for check_eq ({})",
                self.pstack.len()
            ));
        }

        // peek at the top item
        info!("check_eq: loading value from stack");
        let stack_value = {
            match self.pstack.top() {
                Some(v @ Value::Bin { .. }) => v,
                Some(v @ Value::Str { .. }) => v,
                _ => return self.check_fail("no value on stack"),
            }
        };

        // check that the values
        if value == stack_value {
            info!("check_eq({key}) -> {value:?} == {stack_value:?} -> true");
            // the eq check passed so pop the argument from the stack
            let _ = self.pstack.pop();
            self.succeed()
        } else {
            info!("check_eq({key}) -> {value:?} == {stack_value:?} -> false");
            // the hashes don't match
            self.check_fail("values don't match")
        }
    }

    /// Checks the preimage proof against the hash already committed to
    ///
    /// This function validates that the hash algorithm is on the security whitelist
    /// to prevent the use of broken or weak hash functions.
    pub fn check_preimage(&mut self, key: &str) -> Val {
        // look up the hash and try to decode it
        let hash = {
            match self.current.get(key) {
                Some(Value::Bin { hint: _, data }) => match Multihash::try_from(data.as_ref()) {
                    Ok(hash) => {
                        // Security: Validate hash algorithm is allowed
                        let codec_value = hash.codec().code();
                        if !allowed_algorithms::is_hash_allowed(codec_value) {
                            return self.check_fail(&format!(
                                "hash algorithm {} is not allowed (codec: 0x{:x})",
                                allowed_algorithms::hash_name(codec_value),
                                codec_value
                            ));
                        }
                        hash
                    }
                    Err(e) => return self.check_fail(&e.to_string()),
                },
                Some(_) => {
                    return self.check_fail(&format!("unexpected value type associated with {key}"))
                }
                None => return self.check_fail(&format!("kvp missing key: {key}")),
            }
        };

        // make sure we have at least one parameter on the stack
        if self.pstack.is_empty() {
            return self.check_fail(&format!(
                "not enough parameters on the stack for check_preimage: {}",
                self.pstack.len()
            ));
        }

        // get the preimage data from the stack
        let preimage = {
            match self.pstack.top() {
                Some(Value::Bin { hint: _, data }) => {
                    match mh::Builder::new_from_bytes(hash.codec(), data.as_ref()) {
                        Ok(builder) => match builder.try_build() {
                            Ok(hash) => hash,
                            Err(e) => return self.check_fail(&e.to_string()),
                        },
                        Err(e) => return self.check_fail(&e.to_string()),
                    }
                }
                Some(Value::Str { hint: _, data }) => {
                    match mh::Builder::new_from_bytes(hash.codec(), data.as_ref().as_bytes()) {
                        Ok(builder) => match builder.try_build() {
                            Ok(hash) => hash,
                            Err(e) => return self.check_fail(&e.to_string()),
                        },
                        Err(e) => return self.check_fail(&e.to_string()),
                    }
                }
                _ => return self.check_fail("no multihash data on stack"),
            }
        };

        // check that the hashes match
        if hash == preimage {
            info!("check_preimage({key}) -> true");
            // the hash check passed so pop the argument from the stack
            let _ = self.pstack.pop();
            self.succeed()
        } else {
            info!("check_preimage({key}) -> false");
            // the hashes don't match
            self.check_fail("preimage doesn't match")
        }
    }

    /// Checks that a public key's fingerprint matches a baked-in hash.
    ///
    /// Takes hash bytes (from WASM memory) and a KVP path string.
    /// Decodes the hash bytes as a Multihash, validates the hash algorithm,
    /// reads the Multikey from `current.get(key)`, uses its `fingerprint_view`
    /// to compute the fingerprint with the same hash codec, and compares.
    /// No pstack interaction.
    pub fn check_preimage_value(&mut self, hash_bytes: &[u8], key: &str) -> Val {
        // Decode hash bytes as Multihash
        let expected = match Multihash::try_from(hash_bytes) {
            Ok(h) => {
                let codec_value = h.codec().code();
                if !allowed_algorithms::is_hash_allowed(codec_value) {
                    return self.check_fail(&format!(
                        "hash algorithm {} is not allowed (codec: 0x{:x})",
                        allowed_algorithms::hash_name(codec_value),
                        codec_value
                    ));
                }
                h
            }
            Err(e) => return self.check_fail(&e.to_string()),
        };

        // Read Multikey from current KVP
        let pubkey = match self.current.get(key) {
            Some(Value::Bin { data, .. }) => match Multikey::try_from(data.as_ref()) {
                Ok(mk) => mk,
                Err(e) => return self.check_fail(&e.to_string()),
            },
            Some(_) => return self.check_fail(&format!("unexpected value type at {key}")),
            None => return self.check_fail(&format!("kvp missing key: {key}")),
        };

        // Compute fingerprint using Multikey's fingerprint view
        let fp_view = match pubkey.fingerprint_view() {
            Ok(v) => v,
            Err(e) => return self.check_fail(&e.to_string()),
        };
        let actual = match fp_view.fingerprint(expected.codec()) {
            Ok(h) => h,
            Err(e) => return self.check_fail(&e.to_string()),
        };

        // Compare
        if expected == actual {
            info!("check_preimage_value({key}) -> true");
            self.succeed()
        } else {
            info!("check_preimage_value({key}) -> false");
            self.check_fail("key fingerprint doesn't match")
        }
    }

    /// Verifies the digital signature proof with the public key and message already committed to
    pub fn check_signature(&mut self, key: &str, msg: &str) -> Val {
        info!("check_signature: loading from current {key}");
        // look up the pubkey and try to decode it
        let pubkey = {
            match self.current.get(key) {
                Some(Value::Bin { hint: _, data }) => match Multikey::try_from(data.as_ref()) {
                    Ok(mk) => mk,
                    Err(e) => return self.check_fail(&e.to_string()),
                },
                Some(_) => {
                    return self.check_fail(&format!("unexpected value type associated with {key}"))
                }
                None => return self.check_fail(&format!("no multikey associated with {key}")),
            }
        };

        // look up the message that was signed
        info!("check_signature: loading from proposed {msg}");
        let message_value = {
            match self.proposed.get(msg) {
                Some(v @ Value::Bin { .. }) => v,
                Some(v @ Value::Str { .. }) => v,
                Some(_) => {
                    return self.check_fail(&format!("unexpected value type associated with {msg}"))
                }
                None => return self.check_fail(&format!("no message associated with {msg}")),
            }
        };

        // make sure we have at least one parameters on the stack
        if self.pstack.is_empty() {
            return self.check_fail(&format!(
                "not enough parameters ({}) on the stack for check_signature ({key}, {msg})",
                self.pstack.len()
            ));
        }

        // peek at the top item and verify that it is a Multisig
        info!("check_signature: loading sig from stack");
        let sig = {
            match self.pstack.top() {
                Some(Value::Bin { hint: _, data }) => match Multisig::try_from(data.as_ref()) {
                    Ok(sig) => sig,
                    Err(e) => return self.check_fail(&e.to_string()),
                },
                _ => return self.check_fail("no multisig on stack"),
            }
        };

        let verify_view = match pubkey.verify_view() {
            Ok(v) => v,
            Err(e) => return self.check_fail(&e.to_string()),
        };

        // verify the signature - extract message bytes based on type
        let verification_result = match &message_value {
            Value::Bin { hint: _, data } => verify_view.verify(&sig, Some(data.as_ref())),
            Value::Str { hint: _, data } => verify_view.verify(&sig, Some(data.as_bytes())),
            _ => unreachable!("message_value already validated as Bin or Str"),
        };

        match verification_result {
            Ok(()) => {
                info!("check_signature({key}, {msg}) -> true");
                // XMSS is stateful: enforce that the consumed leaf index has not
                // been reused or rolled back for this public key across the log.
                if is_xmss_msig(sig.codec()) {
                    let index = match sig.sig_index() {
                        Some(i) => i,
                        None => return self.check_fail("XMSS multisig missing sig-index"),
                    };
                    let pubkey_bytes = match pubkey.data_view().and_then(|dv| dv.key_bytes()) {
                        Ok(b) => b,
                        Err(e) => return self.check_fail(&e.to_string()),
                    };
                    if let Err(e) =
                        crate::vm::xmss_guard::enforce_xmss_index(pubkey_bytes.as_slice(), index)
                    {
                        return self.check_fail(&e);
                    }
                }
                // Lamport keys are one-time: enforce that this public key has not
                // already signed an earlier entry in the log.
                if is_lamport_sig(sig.codec()) {
                    let pubkey_bytes = match pubkey.data_view().and_then(|dv| dv.key_bytes()) {
                        Ok(b) => b,
                        Err(e) => return self.check_fail(&e.to_string()),
                    };
                    if let Err(e) =
                        crate::vm::xmss_guard::enforce_lamport_once(pubkey_bytes.as_slice())
                    {
                        return self.check_fail(&e);
                    }
                }
                // the signature verification worked so pop the signature argument off
                // of the stack before continuing
                self.pstack.pop();
                self.succeed()
            }
            Err(e) => {
                info!("check_signature({key}, {msg}) -> false");
                self.check_fail(&e.to_string())
            }
        }
    }
}