diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b529ac5a83..2eb0aa47fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,5 +61,9 @@ jobs: else echo "TEST_MODE=--release" >> $GITHUB_ENV fi + - name: Build (wasm) + if: matrix.arch == 'wasm32-unknown-unknown' + run: cargo build $TEST_MODE --verbose --target ${{ matrix.arch }} -p openmls -F js - name: Build + if: ${{ matrix.arch != 'wasm32-unknown-unknown' }} run: cargo build $TEST_MODE --verbose --target ${{ matrix.arch }} -p openmls diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3448466735..7c03d1e52c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: dtolnay/rust-toolchain@stable with: - targets: i686-pc-windows-msvc, i686-unknown-linux-gnu + targets: i686-pc-windows-msvc, i686-unknown-linux-gnu, wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - name: Toggle rustc mode @@ -44,6 +44,14 @@ jobs: else echo "TEST_MODE=--release" >> $GITHUB_ENV fi + - name: Tests Wasm32 on linux + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt update && sudo apt install nodejs + cargo install wasm-bindgen-cli + export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=$HOME/.cargo/bin/wasm-bindgen-test-runner + cargo test $TEST_MODE -p openmls -vv --target wasm32-unknown-unknown -F js + - name: Tests if: matrix.os != 'windows-latest' run: cargo test $TEST_MODE -p openmls --verbose diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 0e799e577f..ac497f77d8 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -17,6 +17,7 @@ - [Processing incoming messages](user_manual/processing.md) - [Persistence of group state](user_manual/persistence.md) - [Credential validation](user_manual/credential_validation.md) + - [WebAssembly](user_manual/wasm.md) - [Traits & External Types](./traits/README.md) - [Traits](./traits/traits.md) - [Types](./traits/types.md) diff --git a/book/src/user_manual/wasm.md b/book/src/user_manual/wasm.md new file mode 100644 index 0000000000..930d838fb0 --- /dev/null +++ b/book/src/user_manual/wasm.md @@ -0,0 +1,4 @@ +# WebAssembly + +OpenMLS can be built for WebAssembly. However, it does require two features that WebAssembly itself does not provide: access to secure randomness and the current time. Currently, this means that it can only run in a runtime that provides common JavaScript APIs (e.g. in the browser or node.js), accessed through the `web_sys` crate. +You can enable the `js` feature on the `openmls` crate to signal that the APIs are available. diff --git a/openmls/Cargo.toml b/openmls/Cargo.toml index ce6d33c2a8..d3412abb11 100644 --- a/openmls/Cargo.toml +++ b/openmls/Cargo.toml @@ -30,6 +30,9 @@ openmls_basic_credential = { version = "0.2.0", path = "../basic_credential", op ] } rstest = { version = "^0.16", optional = true } rstest_reuse = { version = "0.4", optional = true } +wasm-bindgen-test = {version = "0.3.40", optional = true} +getrandom = {version = "0.2.12", optional = true, features = [ "js" ]} +fluvio-wasm-timer = {version = "0.2.5", optional = true} [features] default = ["backtrace"] @@ -41,14 +44,16 @@ test-utils = [ "dep:rand", "dep:rstest", "dep:rstest_reuse", + "dep:wasm-bindgen-test", "dep:openmls_basic_credential", ] crypto-debug = [] # ☣️ Enable logging of sensitive cryptographic information content-debug = [] # ☣️ Enable logging of sensitive message content +js = ["dep:getrandom", "dep:fluvio-wasm-timer"] # enable js randomness source for provider [dev-dependencies] backtrace = "0.3" -criterion = "^0.5" +criterion = {version = "^0.5", default-features = false} # need to disable default features for wasm hex = { version = "0.4", features = ["serde"] } itertools = "0.10" lazy_static = "1.4" @@ -60,6 +65,8 @@ pretty_env_logger = "0.5" rstest = "^0.16" rstest_reuse = "0.4" tempfile = "3" +wasm-bindgen = "0.2.90" +wasm-bindgen-test = "0.3.40" [[bench]] name = "benchmark" diff --git a/openmls/src/binary_tree/array_representation/kat_treemath.rs b/openmls/src/binary_tree/array_representation/kat_treemath.rs index 2f91954e54..616a012a3c 100644 --- a/openmls/src/binary_tree/array_representation/kat_treemath.rs +++ b/openmls/src/binary_tree/array_representation/kat_treemath.rs @@ -186,7 +186,7 @@ pub fn run_test_vector(test_vector: TreeMathTestVector) -> Result<(), TmTestVect #[test] fn read_test_vectors_tm() { - let tests: Vec = read("test_vectors/tree-math.json"); + let tests: Vec = read_json!("../../../test_vectors/tree-math.json"); for test_vector in tests { match run_test_vector(test_vector) { Ok(_) => {} diff --git a/openmls/src/ciphersuite/tests/kat_crypto_basics.rs b/openmls/src/ciphersuite/tests/kat_crypto_basics.rs index 1fdecc601b..9c42b83e47 100644 --- a/openmls/src/ciphersuite/tests/kat_crypto_basics.rs +++ b/openmls/src/ciphersuite/tests/kat_crypto_basics.rs @@ -400,7 +400,7 @@ fn read_test_vectors() { let provider = OpenMlsRustCrypto::default(); - let tests: Vec = read("test_vectors/crypto-basics.json"); + let tests: Vec = read_json!("../../../test_vectors/crypto-basics.json"); for test in tests { match run_test_vector(test, &provider) { Ok(_) => {} diff --git a/openmls/src/group/core_group/test_core_group.rs b/openmls/src/group/core_group/test_core_group.rs index 3a5e80a74a..f25b249d27 100644 --- a/openmls/src/group/core_group/test_core_group.rs +++ b/openmls/src/group/core_group/test_core_group.rs @@ -53,14 +53,15 @@ pub(crate) fn setup_alice_group( fn test_core_group_persistence(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) { let (alice_group, _, _, _) = setup_alice_group(ciphersuite, provider); - let mut file_out = tempfile::NamedTempFile::new().expect("Could not create file"); + // we need something that implements io::Write + let mut file_out: Vec = vec![]; alice_group .save(&mut file_out) .expect("Could not write group state to file"); - let file_in = file_out - .reopen() - .expect("Error re-opening serialized group state file"); + // make it into a type that implements io::Read + let file_in: &[u8] = &file_out; + let alice_group_deserialized = CoreGroup::load(file_in).expect("Could not deserialize mls group"); diff --git a/openmls/src/group/tests/kat_messages.rs b/openmls/src/group/tests/kat_messages.rs index 3d90344837..ec5acde92b 100644 --- a/openmls/src/group/tests/kat_messages.rs +++ b/openmls/src/group/tests/kat_messages.rs @@ -645,7 +645,7 @@ pub fn run_test_vector(tv: MessagesTestVector) -> Result<(), EncodingMismatch> { #[test] fn read_test_vectors_messages() { - let tests: Vec = read("test_vectors/messages.json"); + let tests: Vec = read_json!("../../../test_vectors/messages.json"); for test_vector in tests { match run_test_vector(test_vector) { diff --git a/openmls/src/kat_vl.rs b/openmls/src/kat_vl.rs index 0a755ac9f6..73975c505b 100644 --- a/openmls/src/kat_vl.rs +++ b/openmls/src/kat_vl.rs @@ -25,8 +25,6 @@ use serde::Deserialize; use tls_codec::{Deserialize as TlsDeserialize, VLBytes}; -use crate::test_utils::read; - #[derive(Deserialize)] struct TestElement { #[serde(with = "hex")] @@ -57,7 +55,7 @@ fn read_test_vectors_deserialize() { let _ = pretty_env_logger::try_init(); log::debug!("Reading test vectors ..."); - let tests: Vec = read("test_vectors/deserialization.json"); + let tests: Vec = read_json!("../test_vectors/deserialization.json"); for test_vector in tests { match run_test_vector(test_vector) { diff --git a/openmls/src/key_packages/lifetime.rs b/openmls/src/key_packages/lifetime.rs index 4318ce50a4..9552de8bcf 100644 --- a/openmls/src/key_packages/lifetime.rs +++ b/openmls/src/key_packages/lifetime.rs @@ -1,3 +1,6 @@ +#[cfg(target_arch = "wasm32")] +use fluvio_wasm_timer::{SystemTime, UNIX_EPOCH}; +#[cfg(not(target_arch = "wasm32"))] use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; diff --git a/openmls/src/lib.rs b/openmls/src/lib.rs index 1d3a2b4414..3d4452fc0e 100644 --- a/openmls/src/lib.rs +++ b/openmls/src/lib.rs @@ -146,6 +146,9 @@ target_pointer_width = "128" ))] +#[cfg(all(target_arch = "wasm32", not(feature = "js")))] +compile_error!("In order for OpenMLS to build for WebAssembly, JavaScript APIs must be available (for access to secure randomness and the current time). This can be signalled by setting the `js` feature on OpenMLS."); + // === Testing === /// Single place, re-exporting all structs and functions needed for integration tests @@ -187,3 +190,9 @@ mod tree; /// Single place, re-exporting the most used public functions. pub mod prelude; + +// this is a workaround, see https://github.com/la10736/rstest/issues/211#issuecomment-1701238125 +#[cfg(any(test, feature = "test-utils"))] +pub mod wasm { + pub use wasm_bindgen_test::wasm_bindgen_test as test; +} diff --git a/openmls/src/schedule/kat_key_schedule.rs b/openmls/src/schedule/kat_key_schedule.rs index 7a726eb686..c93d0218be 100644 --- a/openmls/src/schedule/kat_key_schedule.rs +++ b/openmls/src/schedule/kat_key_schedule.rs @@ -13,7 +13,7 @@ use tls_codec::Serialize as TlsSerializeTrait; use super::{errors::KsTestVectorError, CommitSecret}; #[cfg(test)] -use crate::test_utils::{read, write}; +use crate::test_utils::write; use crate::{ciphersuite::*, extensions::Extensions, group::*, schedule::*, test_utils::*}; #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -258,7 +258,7 @@ fn write_test_vectors() { fn read_test_vectors_key_schedule(provider: &impl OpenMlsProvider) { let _ = pretty_env_logger::try_init(); - let tests: Vec = read("test_vectors/key-schedule.json"); + let tests: Vec = read_json!("../../test_vectors/key-schedule.json"); for test_vector in tests { match run_test_vector(test_vector, provider) { diff --git a/openmls/src/schedule/kat_psk_secret.rs b/openmls/src/schedule/kat_psk_secret.rs index 5177b420b4..191a8ffc86 100644 --- a/openmls/src/schedule/kat_psk_secret.rs +++ b/openmls/src/schedule/kat_psk_secret.rs @@ -108,7 +108,7 @@ fn read_test_vectors_ps(provider: &impl OpenMlsProvider) { let _ = pretty_env_logger::try_init(); log::debug!("Reading test vectors ..."); - let tests: Vec = read("test_vectors/psk_secret.json"); + let tests: Vec = read_json!("../../test_vectors/psk_secret.json"); for test_vector in tests { match run_test_vector(test_vector, provider) { diff --git a/openmls/src/test_utils/mod.rs b/openmls/src/test_utils/mod.rs index d6454f92a5..d4ae407395 100644 --- a/openmls/src/test_utils/mod.rs +++ b/openmls/src/test_utils/mod.rs @@ -41,6 +41,15 @@ pub(crate) fn write(file_name: &str, obj: impl Serialize) { .expect("Error writing test vector file"); } +// the macro is used in other files, suppress false positive +#[allow(unused_macros)] +macro_rules! read_json { + ($file_name:expr) => {{ + let data = include_str!($file_name); + serde_json::from_str(data).expect(&format!("Error reading file {}", $file_name)) + }}; +} + pub(crate) fn read(file_name: &str) -> T { let file = match File::open(file_name) { Ok(f) => f, @@ -212,6 +221,7 @@ pub use openmls_rust_crypto::OpenMlsRustCrypto; ) ] #[allow(non_snake_case)] +#[cfg_attr(target_arch = "wasm32", openmls::wasm::test)] pub fn providers(provider: &impl OpenMlsProvider) {} // === Ciphersuites === @@ -233,6 +243,7 @@ pub fn providers(provider: &impl OpenMlsProvider) {} ) )] #[allow(non_snake_case)] +#[cfg_attr(target_arch = "wasm32", openmls::wasm::test)] pub fn ciphersuites(ciphersuite: Ciphersuite) {} // === Ciphersuites & providers === @@ -246,4 +257,5 @@ pub fn ciphersuites(ciphersuite: Ciphersuite) {} ) ] #[allow(non_snake_case)] +#[cfg_attr(target_arch = "wasm32", openmls::wasm::test)] pub fn ciphersuites_and_providers(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) {} diff --git a/openmls/src/test_utils/test_framework/mod.rs b/openmls/src/test_utils/test_framework/mod.rs index 1e83be5e9b..831dd4f042 100644 --- a/openmls/src/test_utils/test_framework/mod.rs +++ b/openmls/src/test_utils/test_framework/mod.rs @@ -40,10 +40,13 @@ use openmls_traits::{ types::{Ciphersuite, HpkeKeyPair, SignatureScheme}, OpenMlsProvider, }; -use rayon::prelude::*; + use std::{collections::HashMap, sync::RwLock}; use tls_codec::*; +#[cfg(not(target_arch = "wasm32"))] +use rayon::prelude::*; + pub mod client; pub mod errors; @@ -363,9 +366,13 @@ impl MlsGroupTestSetup { authentication_service: AS, ) { let clients = self.clients.read().expect("An unexpected error occurred."); - let messages = group - .members - .par_iter() + + #[cfg(not(target_arch = "wasm32"))] + let group_members = group.members.par_iter(); + #[cfg(target_arch = "wasm32")] + let group_members = group.members.iter(); + + let messages = group_members .filter_map(|(_, m_id)| { let m = clients .get(m_id) diff --git a/openmls/src/tree/tests_and_kats/kats/kat_encryption.rs b/openmls/src/tree/tests_and_kats/kats/kat_encryption.rs index 54b87876f6..c309c1d071 100644 --- a/openmls/src/tree/tests_and_kats/kats/kat_encryption.rs +++ b/openmls/src/tree/tests_and_kats/kats/kat_encryption.rs @@ -841,7 +841,8 @@ fn read_test_vectors_encryption(provider: &impl OpenMlsProvider) { let _ = pretty_env_logger::try_init(); log::debug!("Reading test vectors ..."); - let tests: Vec = read("test_vectors/kat_encryption_openmls.json"); + let tests: Vec = + read_json!("../../../../test_vectors/kat_encryption_openmls.json"); for test_vector in tests { match run_test_vector(test_vector, provider) { diff --git a/openmls/src/tree/tests_and_kats/kats/kat_message_protection.rs b/openmls/src/tree/tests_and_kats/kats/kat_message_protection.rs index b8615ce037..de1ac20e55 100644 --- a/openmls/src/tree/tests_and_kats/kats/kat_message_protection.rs +++ b/openmls/src/tree/tests_and_kats/kats/kat_message_protection.rs @@ -704,7 +704,8 @@ fn read_test_vectors_mp(provider: &impl OpenMlsProvider) { let _ = pretty_env_logger::try_init(); log::debug!("Reading test vectors ..."); - let tests: Vec = read("test_vectors/message-protection.json"); + let tests: Vec = + read_json!("../../../../test_vectors/message-protection.json"); for test_vector in tests { match run_test_vector(test_vector, provider) { diff --git a/openmls/src/tree/tests_and_kats/kats/secret_tree.rs b/openmls/src/tree/tests_and_kats/kats/secret_tree.rs index da1e56c612..585b12eee8 100644 --- a/openmls/src/tree/tests_and_kats/kats/secret_tree.rs +++ b/openmls/src/tree/tests_and_kats/kats/secret_tree.rs @@ -192,7 +192,7 @@ fn read_test_vectors_st(provider: &impl OpenMlsProvider) { let _ = pretty_env_logger::try_init(); log::debug!("Reading test vectors ..."); - let tests: Vec = read("test_vectors/secret-tree.json"); + let tests: Vec = read_json!("../../../../test_vectors/secret-tree.json"); for test_vector in tests { match run_test_vector(test_vector, provider) { diff --git a/openmls/src/treesync/node/parent_node.rs b/openmls/src/treesync/node/parent_node.rs index b1f7ec053b..daa3440190 100644 --- a/openmls/src/treesync/node/parent_node.rs +++ b/openmls/src/treesync/node/parent_node.rs @@ -3,6 +3,7 @@ //! [`UpdatePathNode`] instances. use openmls_traits::crypto::OpenMlsCrypto; use openmls_traits::types::{Ciphersuite, HpkeCiphertext}; +#[cfg(not(target_arch = "wasm32"))] use rayon::prelude::*; use serde::{Deserialize, Serialize}; use thiserror::*; @@ -66,8 +67,12 @@ impl PlainUpdatePathNode { public_keys: &[EncryptionKey], group_context: &[u8], ) -> Result { + #[cfg(target_arch = "wasm32")] + let public_keys = public_keys.iter(); + #[cfg(not(target_arch = "wasm32"))] + let public_keys = public_keys.par_iter(); + public_keys - .par_iter() .map(|pk| { self.path_secret .encrypt(crypto, ciphersuite, pk, group_context) @@ -131,8 +136,13 @@ impl ParentNode { ); // Iterate over the path secrets and derive a key pair + + #[cfg(not(target_arch = "wasm32"))] + let path_secrets = path_secrets.into_par_iter(); + #[cfg(target_arch = "wasm32")] + let path_secrets = path_secrets.into_iter(); + let (path_with_keypairs, update_path_nodes): PathDerivationResults = path_secrets - .into_par_iter() .zip(path_indices) .map(|(path_secret, index)| { // Derive a key pair from the path secret. This includes the diff --git a/openmls/src/treesync/tests_and_kats/kats/kat_tree_operations.rs b/openmls/src/treesync/tests_and_kats/kats/kat_tree_operations.rs index 7726940212..727fd5dd7b 100644 --- a/openmls/src/treesync/tests_and_kats/kats/kat_tree_operations.rs +++ b/openmls/src/treesync/tests_and_kats/kats/kat_tree_operations.rs @@ -139,7 +139,7 @@ fn read_test_vectors_tree_operations(provider: &impl OpenMlsProvider) { let _ = pretty_env_logger::try_init(); log::debug!("Reading test vectors ..."); - let tests: Vec = read("test_vectors/tree-operations.json"); + let tests: Vec = read_json!("../../../../test_vectors/tree-operations.json"); for test_vector in tests { match run_test_vector(test_vector, provider) { diff --git a/openmls/src/treesync/tests_and_kats/kats/kat_tree_validation.rs b/openmls/src/treesync/tests_and_kats/kats/kat_tree_validation.rs index 8a150399cb..b0537466ec 100644 --- a/openmls/src/treesync/tests_and_kats/kats/kat_tree_validation.rs +++ b/openmls/src/treesync/tests_and_kats/kats/kat_tree_validation.rs @@ -141,7 +141,7 @@ fn read_test_vectors_tree_validation(provider: &impl OpenMlsProvider) { let _ = pretty_env_logger::try_init(); log::debug!("Reading test vectors ..."); - let tests: Vec = read("test_vectors/tree-validation.json"); + let tests: Vec = read_json!("../../../../test_vectors/tree-validation.json"); for test_vector in tests { match run_test_vector(test_vector, provider) { diff --git a/openmls/src/treesync/tests_and_kats/kats/kat_treekem.rs b/openmls/src/treesync/tests_and_kats/kats/kat_treekem.rs index 7c6f9a6e26..9df787d8cc 100644 --- a/openmls/src/treesync/tests_and_kats/kats/kat_treekem.rs +++ b/openmls/src/treesync/tests_and_kats/kats/kat_treekem.rs @@ -14,7 +14,7 @@ use crate::{ messages::PathSecret, prelude_test::Secret, schedule::CommitSecret, - test_utils::{hex_to_bytes, read}, + test_utils::hex_to_bytes, treesync::{ node::encryption_keys::EncryptionKeyPair, treekem::{DecryptPathParams, UpdatePath, UpdatePathIn}, @@ -385,7 +385,7 @@ fn apply_update_path( #[test] fn read_test_vectors_treekem() { let _ = pretty_env_logger::try_init(); - let tests: Vec = read("test_vectors/treekem.json"); + let tests: Vec = read_json!("../../../../test_vectors/treekem.json"); let provider = OpenMlsRustCrypto::default(); diff --git a/openmls/src/treesync/treekem.rs b/openmls/src/treesync/treekem.rs index ffdd089542..43edb146e6 100644 --- a/openmls/src/treesync/treekem.rs +++ b/openmls/src/treesync/treekem.rs @@ -10,6 +10,7 @@ use openmls_traits::{ crypto::OpenMlsCrypto, types::{Ciphersuite, HpkeCiphertext}, }; +#[cfg(not(target_arch = "wasm32"))] use rayon::prelude::*; use serde::{Deserialize, Serialize}; use tls_codec::{TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize}; @@ -70,8 +71,13 @@ impl<'a> TreeSyncDiff<'a> { debug_assert_eq!(copath_resolutions.len(), path.len()); // Encrypt the secrets - path.par_iter() - .zip(copath_resolutions.par_iter()) + + #[cfg(not(target_arch = "wasm32"))] + let resolved_path = path.par_iter().zip(copath_resolutions.par_iter()); + #[cfg(target_arch = "wasm32")] + let resolved_path = path.iter().zip(copath_resolutions.iter()); + + resolved_path .map(|(node, resolution)| node.encrypt(crypto, ciphersuite, resolution, group_context)) .collect::, LibraryError>>() }