From c73f7ce5adb7b126b0f99cd67e71f3c2ba394104 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Wed, 23 Feb 2022 09:01:51 +0100 Subject: [PATCH] Add support for digital signatures This adds a new cargo feature flag `digital-signatures` that brings support for signature verification, using the the current proposal for WebAssembly modules signatures. (https://github.com/WebAssembly/tool-conventions/blob/main/Signatures.md) No behavior changes unless the `--experimental-public-keys` option is used with the `run` command. This options accepts one or more public keys, that the entire module must be signed with in order to run. --- .github/workflows/main.yml | 15 ++++++ Cargo.lock | 88 +++++++++++++++++++++++++++---- Cargo.toml | 2 + ci/run-digital-signatures-test.sh | 34 ++++++++++++ crates/wasmtime/Cargo.toml | 4 ++ crates/wasmtime/src/config.rs | 13 +++++ crates/wasmtime/src/module.rs | 10 ++-- src/lib.rs | 31 +++++++++++ 8 files changed, 184 insertions(+), 13 deletions(-) create mode 100755 ci/run-digital-signatures-test.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c636a0856a32..b2dd83447e4a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -348,6 +348,21 @@ jobs: env: RUST_BACKTRACE: 1 + # Build and test digital signatures. + test_digital_signatures: + name: Test digital signatures + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - run: rustup target add wasm32-wasi + - name: Install Rust + run: rustup update stable && rustup default stable + - run: ./ci/run-digital-signatures-test.sh + env: + RUST_BACKTRACE: 1 + bench: name: Run benchmarks runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index bb9c59a7ac3e..bc6da79ec060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.40" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" [[package]] name = "arbitrary" @@ -155,6 +155,16 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + [[package]] name = "base64" version = "0.13.0" @@ -892,6 +902,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ct-codecs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + [[package]] name = "ctr" version = "0.8.0" @@ -1019,6 +1035,16 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e1f30f0312ac83726c1197abeacd91c9557f8a623e904a009ae6bc529ae8d8" +dependencies = [ + "ct-codecs", + "getrandom 0.2.4", +] + [[package]] name = "ed25519-dalek" version = "1.0.1" @@ -1254,13 +1280,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.10.2+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1363,6 +1391,12 @@ dependencies = [ "digest", ] +[[package]] +name = "hmac-sha256" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45e85b74de4f2610b0c832e3a532f3b64cddb5d8923923bc00d70206fb035f7" + [[package]] name = "humantime" version = "1.3.0" @@ -2143,7 +2177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5397335b92875d36fb30f91557c3769517c9cfbc212568a5b8ceafd304eca84" dependencies = [ "cc", - "getrandom 0.2.3", + "getrandom 0.2.4", "libc", ] @@ -2325,7 +2359,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.4", ] [[package]] @@ -2401,7 +2435,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.4", "redox_syscall", ] @@ -2545,6 +2579,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -2702,6 +2742,17 @@ dependencies = [ "der", ] +[[package]] +name = "ssh-keys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2555f9858fe3b961c98100b8be77cbd6a81527bf20d40e7a11dafb8810298e95" +dependencies = [ + "base64 0.9.3", + "byteorder", + "quick-error 1.2.3", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3119,7 +3170,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.4", ] [[package]] @@ -3390,6 +3441,23 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasmsign2" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e82503d0a044f810c4e29d3e59a4b00a7985fa9e6c11dd0cf8878616e325e9" +dependencies = [ + "anyhow", + "ct-codecs", + "ed25519-compact", + "getrandom 0.2.4", + "hmac-sha256", + "log", + "regex", + "ssh-keys", + "thiserror", +] + [[package]] name = "wasmtime" version = "0.34.0" @@ -3414,6 +3482,7 @@ dependencies = [ "tempfile", "wasi-cap-std-sync", "wasmparser", + "wasmsign2", "wasmtime-cache", "wasmtime-cranelift", "wasmtime-environ", @@ -3468,7 +3537,7 @@ name = "wasmtime-cache" version = "0.34.0" dependencies = [ "anyhow", - "base64", + "base64 0.13.0", "bincode", "directories-next", "file-per-thread-logger", @@ -3514,6 +3583,7 @@ dependencies = [ "tokio", "tracing-subscriber", "wasmparser", + "wasmsign2", "wasmtime", "wasmtime-cache", "wasmtime-cranelift", diff --git a/Cargo.toml b/Cargo.toml index 803626376a8e..6670037f70a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ humantime = "2.0.0" wasmparser = "0.82.0" lazy_static = "1.4.0" listenfd = "0.3.5" +wasmsign2 = { version = "0.1.13", optional = true } [target.'cfg(unix)'.dependencies] rustix = "0.33.0" @@ -109,6 +110,7 @@ memory-init-cow = ["wasmtime/memory-init-cow"] pooling-allocator = ["wasmtime/pooling-allocator"] all-arch = ["wasmtime/all-arch"] posix-signals-on-macos = ["wasmtime/posix-signals-on-macos"] +digital-signatures = ["wasmsign2", "wasmtime/digital-signatures"] # Stub feature that does nothing, for Cargo-features compatibility: the new # backend is the default now. diff --git a/ci/run-digital-signatures-test.sh b/ci/run-digital-signatures-test.sh new file mode 100755 index 000000000000..9cbe1b6ce52f --- /dev/null +++ b/ci/run-digital-signatures-test.sh @@ -0,0 +1,34 @@ +#! /bin/bash + +set -e + +TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) +pushd "$TMP_DIR" + +WASMSIGN="${TMP_DIR}/bin/wasmsign2" +WASM_MODULE="${TMP_DIR}/bin/test-module.wasm" +PUBLIC_KEY="${TMP_DIR}/public.key" +SECRET_KEY="${TMP_DIR}/secret.key" + +cargo install --version 0.1.4 --root "$TMP_DIR" wasmsign2-cli +"$WASMSIGN" keygen -K "$PUBLIC_KEY" -k "$SECRET_KEY" + +rm -fr test-module +cargo new test-module +pushd test-module +cargo install --root "$TMP_DIR" --path . --target=wasm32-wasi +popd + +popd + +cargo run --features digital-signatures -- run "$WASM_MODULE" + +if cargo run --features digital-signatures -- run --experimental-public-keys "$PUBLIC_KEY" "$WASM_MODULE"; then + echo "Module ran even though signature verification failed" >&2 + exit 1 +fi + +"$WASMSIGN" sign -k "$SECRET_KEY" -i "$WASM_MODULE" -o "$WASM_MODULE" +cargo run --features digital-signatures -- run --experimental-public-keys "$PUBLIC_KEY" "$WASM_MODULE" + +rm -fr "$TMP_DIR" diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 784c7dbbbcd6..9baa3eeef368 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -38,6 +38,7 @@ rayon = { version = "1.0", optional = true } object = { version = "0.27", default-features = false, features = ['read_core', 'elf'] } async-trait = { version = "0.1.51", optional = true } once_cell = "1.9" +wasmsign2 = { version = "0.1.11", optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = "0.3.7" @@ -108,3 +109,6 @@ posix-signals-on-macos = ["wasmtime-runtime/posix-signals-on-macos"] # Enabling this feature has no effect on unsupported platforms or when the # `uffd` feature is enabled. memory-init-cow = ["wasmtime-runtime/memory-init-cow"] + +# Enables support for signatures verification +digital-signatures = ["wasmsign2"] diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 00689904e920..b45bfd446e59 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -106,6 +106,8 @@ pub struct Config { pub(crate) paged_memory_initialization: bool, pub(crate) memory_init_cow: bool, pub(crate) memory_guaranteed_dense_image_size: u64, + #[cfg(feature = "digital-signatures")] + pub(crate) public_keys: Option>, } impl Config { @@ -133,6 +135,8 @@ impl Config { paged_memory_initialization: cfg!(all(target_os = "linux", feature = "uffd")), memory_init_cow: true, memory_guaranteed_dense_image_size: 16 << 20, + #[cfg(feature = "digital-signatures")] + public_keys: None, }; #[cfg(compiler)] { @@ -1069,6 +1073,13 @@ impl Config { self } + /// Configure the set of public keys used for digital signatures verification. + #[cfg(feature = "digital-signatures")] + pub fn public_keys(&mut self, public_keys: wasmsign2::PublicKeySet) -> &mut Self { + self.public_keys = Some(Arc::new(public_keys)); + self + } + /// Configures the size, in bytes, of the extra virtual memory space /// reserved after a "dynamic" memory for growing into. /// @@ -1337,6 +1348,8 @@ impl Clone for Config { paged_memory_initialization: self.paged_memory_initialization, memory_init_cow: self.memory_init_cow, memory_guaranteed_dense_image_size: self.memory_guaranteed_dense_image_size, + #[cfg(feature = "digital-signatures")] + public_keys: self.public_keys.clone(), } } } diff --git a/crates/wasmtime/src/module.rs b/crates/wasmtime/src/module.rs index e71d3e28d2b7..105aa537a053 100644 --- a/crates/wasmtime/src/module.rs +++ b/crates/wasmtime/src/module.rs @@ -242,10 +242,12 @@ impl Module { #[cfg(compiler)] #[cfg_attr(nightlydoc, doc(cfg(feature = "cranelift")))] // see build.rs pub fn from_file(engine: &Engine, file: impl AsRef) -> Result { - match Self::new( - engine, - &fs::read(&file).with_context(|| "failed to read input file")?, - ) { + let bytes = fs::read(&file).with_context(|| "failed to read input file")?; + #[cfg(feature = "digital-signatures")] + if let Some(public_key_set) = &engine.config().public_keys { + public_key_set.verify(&mut std::io::Cursor::new(&bytes), None)?; + } + match Self::new(engine, &bytes) { Ok(m) => Ok(m), Err(e) => { cfg_if::cfg_if! { diff --git a/src/lib.rs b/src/lib.rs index fc40d9d0d4b8..d63903842d76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,6 +103,11 @@ use wasmtime::{Config, ProfilingStrategy}; #[cfg(feature = "pooling-allocator")] use wasmtime::{InstanceLimits, ModuleLimits, PoolingAllocationStrategy}; +#[cfg(feature = "digital-signatures")] +use wasmsign2::PublicKeySet; +#[cfg(not(feature = "digital-signatures"))] +struct PublicKeySet; + fn pick_profiling_strategy(jitdump: bool, vtune: bool) -> Result { Ok(match (jitdump, vtune) { (true, false) => ProfilingStrategy::JitDump, @@ -263,6 +268,12 @@ struct CommonOptions { #[cfg(feature = "pooling-allocator")] #[structopt(long)] pooling_allocator: bool, + + /// Require modules to have an embedded signature that can be verified with the given set of public keys. + /// The signature format should be considered experimental and is documented at https://github.com/WebAssembly/tool-conventions/blob/main/Signatures.md + #[structopt(long, value_name = "FILE,FILE,...", parse(try_from_str = parse_public_keys))] + #[allow(dead_code)] + experimental_public_keys: Option, } impl CommonOptions { @@ -361,6 +372,11 @@ impl CommonOptions { } } + #[cfg(feature = "digital-signatures")] + if let Some(public_keys) = &self.experimental_public_keys { + config.public_keys(public_keys.clone()); + } + Ok(config) } @@ -518,6 +534,21 @@ fn parse_wasi_modules(modules: &str) -> Result { } } +#[cfg(feature = "digital-signatures")] +fn parse_public_keys(pk_files: &str) -> Result { + let mut public_key_set = PublicKeySet::empty(); + for pk_file in pk_files.split(',') { + let pk_file = pk_file.trim(); + public_key_set.insert_any_file(pk_file)?; + } + Ok(public_key_set) +} + +#[cfg(not(feature = "digital-signatures"))] +fn parse_public_keys(_pk_files: &str) -> Result { + bail!("Digital signatures support is not enabled"); +} + /// Select which WASI modules are available at runtime for use by Wasm programs. #[derive(Debug, Clone, Copy, PartialEq)] pub struct WasiModules {