diff --git a/Cargo.lock b/Cargo.lock index 6b51664..87b3ea5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,12 @@ dependencies = [ "inout", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cpp_demangle" version = "0.3.5" @@ -480,6 +486,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "debugid" version = "0.8.0" @@ -489,6 +523,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -541,6 +585,43 @@ dependencies = [ "winapi", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "edwards-wit" +version = "0.1.0" +dependencies = [ + "cargo-component-bindings", + "ed25519-dalek", + "serde", + "serde_json", + "thiserror", + "wasmtime", + "wasmtime-wasi", +] + [[package]] name = "either" version = "1.9.0" @@ -589,6 +670,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1037,12 +1124,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "platforms" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1191,6 +1294,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.28" @@ -1319,6 +1431,15 @@ dependencies = [ "dirs", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slice-group-by" version = "0.3.1" @@ -1341,6 +1462,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sptr" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 07fbf5e..44a0b15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "crates/*"] +members = [".", "crates/*", "examples/edwards-wit"] resolver = "2" [workspace.dependencies] diff --git a/examples/edwards-wit/.vscode/settings.json b/examples/edwards-wit/.vscode/settings.json new file mode 100644 index 0000000..35e43fe --- /dev/null +++ b/examples/edwards-wit/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.check.overrideCommand": ["cargo", "component", "check", "--message-format=json"] +} diff --git a/examples/edwards-wit/Cargo.toml b/examples/edwards-wit/Cargo.toml new file mode 100644 index 0000000..ea8d95e --- /dev/null +++ b/examples/edwards-wit/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "edwards-wit" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +cargo-component-bindings = "0.5.0" +ed25519-dalek = "2.1" + +[dev-dependencies] +wasmtime = { version = "15", features = ['component-model'] } +wasmtime-wasi = "15.0.0" +thiserror = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[package.metadata.component] +package = "component:edwards-wit" + +[package.metadata.component.dependencies] + +[package.metadata.component.target.dependencies] +"seed-keeper:wallet" = { path = "../../crates/seed-keeper-wit/wit" } # directory containing the WIT package diff --git a/examples/edwards-wit/README.md b/examples/edwards-wit/README.md new file mode 100644 index 0000000..08a1ac0 --- /dev/null +++ b/examples/edwards-wit/README.md @@ -0,0 +1,44 @@ +# Example Plugin + +Usign Edwards curve to sign a message and verify the signature. + +## Create a Plugin WIT Component + +1. Create a component using [`cargo component`](https://github.com/bytecodealliance/cargo-component): + +```bash +cargo component new --reactor +``` + +2. Import the seed keeper get seed interface + +```wit +world yourworld { + + import seed-keeper:wallet/seed-getter@0.1.0; + + // the rest of your WIT world + export operations; +} +``` + +3. Add the path to the wallet wit file in your Cargo.toml. This is just the interface, not the implementation of it. + +```toml +[package.metadata.component.target.dependencies] +"seed-keeper:wallet" = { path = "../path/to/seed-keeper-wit/wit" } # directory containing the WIT package +``` + +4. Export an interface which defines a `sign` func which takes a message and returns a signature. + +```wit +/// WIT interface operations exported by yourworld +interface operations { + sign: func(message: list) -> list; + verify: func(message: list, signature: list) -> bool; +} +``` + +## Compose + +Compose this plugin together with the `seed-keeper-wit` and the `seed-keeper-wit-ui` components. diff --git a/examples/edwards-wit/src/lib.rs b/examples/edwards-wit/src/lib.rs new file mode 100644 index 0000000..385ce9d --- /dev/null +++ b/examples/edwards-wit/src/lib.rs @@ -0,0 +1,63 @@ +cargo_component_bindings::generate!(); + +use bindings::exports::component::edwards_wit::operations::Guest; +// use bindings::seed_keeper::wallet::seed_getter::get_seed; + +use ed25519_dalek::SECRET_KEY_LENGTH; +use ed25519_dalek::{Signature, Signer, SigningKey}; + +struct Component; + +impl Guest for Component { + /// Say hello! + /// sign: func(message: list) -> list; + fn sign(message: Vec) -> Result, String> { + let seed = [1u8; 32]; // get_seed()?; + let seed: [u8; SECRET_KEY_LENGTH] = seed + .clone() + .try_into() + .map_err(|_| format!("Seed length is not 32 bytes, got {}", seed.len()).to_owned())?; + let signer = SigningKey::from_bytes(&seed); + let signature: Signature = signer.sign(&message); + Ok(signature.to_bytes().to_vec()) + } + + /// Verify + /// verify: func(message: list, signature: list) -> bool; + fn verify(message: Vec, signature: Vec) -> Result { + let seed = [1u8; 32]; // get_seed()?; + let seed: [u8; SECRET_KEY_LENGTH] = seed + .clone() + .try_into() + .map_err(|_| format!("Seed length is not 32 bytes, got {}", seed.len()).to_owned())?; + let signer = SigningKey::from_bytes(&seed); + let signature = Signature::from_bytes( + &(signature.clone().try_into().map_err(|_| { + format!("Signature length is not 64 bytes, got {}", signature.len()).to_owned() + })?), + ); + Ok(signer.verify(&message, &signature).is_ok()) + } +} + +#[cfg(test)] +mod test_edwards_wit { + use super::*; + + fn assert_keypair(keypair: &SigningKey) -> bool { + let message: &[u8] = b"This is a test of the tsunami alert system."; + let signature: Signature = keypair.sign(message); + keypair.verify(message, &signature).is_ok() + } + + #[test] + fn test_keypair_from_seed() { + let seed: [u8; 32] = [ + 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, + 0x2c, 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x34, 0x67, 0x2b, 0x6a, 0x2f, 0x8d, 0x1d, 0x7b, + 0x10, 0xac, 0x39, 0x23, + ]; + let keypair = SigningKey::from_bytes(&seed); + assert_keypair(&keypair); + } +} diff --git a/examples/edwards-wit/tests/mod.rs b/examples/edwards-wit/tests/mod.rs new file mode 100644 index 0000000..024b943 --- /dev/null +++ b/examples/edwards-wit/tests/mod.rs @@ -0,0 +1,153 @@ +mod bindgen { + wasmtime::component::bindgen!("example"); // name of the world in the .wit file +} + +use serde::{Deserialize, Serialize}; +use std::{ + env, + path::{Path, PathBuf}, +}; +use thiserror::Error; +use wasmtime::component::{Component, Linker}; +use wasmtime::{Config, Engine, Store}; +use wasmtime_wasi::preview2::{Table, WasiCtx, WasiCtxBuilder, WasiView}; + +struct MyCtx { + wasi_ctx: Context, +} + +struct Context { + table: Table, + wasi: WasiCtx, +} +impl WasiView for MyCtx { + fn table(&self) -> &Table { + &self.wasi_ctx.table + } + fn table_mut(&mut self) -> &mut Table { + &mut self.wasi_ctx.table + } + fn ctx(&self) -> &WasiCtx { + &self.wasi_ctx.wasi + } + fn ctx_mut(&mut self) -> &mut WasiCtx { + &mut self.wasi_ctx.wasi + } +} + +/// Implementing this trait gives us +/// - the ability to add_to_linker using SeedKeeper::add_to_linker +/// - call get_seed from inside out component +/// +/// Normally this would be implemented by another WIT component that is composed with this +/// component, but for testing we mock it up below. +impl bindgen::seed_keeper::wallet::seed_getter::Host for MyCtx { + fn get_seed(&mut self) -> Result, String>, wasmtime::Error> { + let seed = vec![1u8; 32]; + Ok(Ok(seed)) + } +} + +#[derive(Error, Debug)] +pub enum TestError { + /// From String + #[error("Error message {0}")] + Stringified(String), + + /// From Wasmtime + #[error("Wasmtime: {0}")] + Wasmtime(#[from] wasmtime::Error), + + /// From VarError + #[error("VarError: {0}")] + VarError(#[from] std::env::VarError), + + /// From io + #[error("IO: {0}")] + Io(#[from] std::io::Error), + + /// From serde_json + #[error("Serde JSON: {0}")] + SerdeJson(#[from] serde_json::Error), +} + +/// Impl From for TestError +impl From for TestError { + fn from(s: String) -> Self { + TestError::Stringified(s) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct TestFixtures { + seed: Vec, + encrypted: Vec, + username: Vec, + password: Vec, +} + +/// Utility function to get the workspace dir +pub fn workspace_dir() -> PathBuf { + let output = std::process::Command::new(env!("CARGO")) + .arg("locate-project") + .arg("--workspace") + .arg("--message-format=plain") + .output() + .unwrap() + .stdout; + let cargo_path = Path::new(std::str::from_utf8(&output).unwrap().trim()); + cargo_path.parent().unwrap().to_path_buf() +} + +#[cfg(test)] +mod edwards_example_wit_tests { + + use super::*; + + #[test] + fn test_roundtrip_sign_and_verify() -> wasmtime::Result<(), TestError> { + // get the target/wasm32-wasi/debug/CARGO_PKG_NAME.wasm file + let pkg_name = std::env::var("CARGO_PKG_NAME")?.replace('-', "_"); + let workspace = workspace_dir(); + let wasm_path = format!("target/wasm32-wasi/debug/{}.wasm", pkg_name); + let wasm_path = workspace.join(wasm_path); + + let mut config = Config::new(); + config.cache_config_load_default()?; + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + config.wasm_component_model(true); + + let engine = Engine::new(&config)?; + let component = Component::from_file(&engine, &wasm_path)?; + + let mut linker = Linker::new(&engine); + // link imports like get_seed to our instantiation + bindgen::Example::add_to_linker(&mut linker, |state: &mut MyCtx| state)?; + // link the WASI imports to our instantiation + wasmtime_wasi::preview2::command::sync::add_to_linker(&mut linker)?; + + let table = Table::new(); + let wasi: WasiCtx = WasiCtxBuilder::new().inherit_stdout().args(&[""]).build(); + let state = MyCtx { + wasi_ctx: Context { table, wasi }, + }; + let mut store = Store::new(&engine, state); + + let (bindings, _) = bindgen::Example::instantiate(&mut store, &component, &linker)?; + + // use bindings to sign a message + let message = b"hello world"; + let signature = bindings + .component_edwards_wit_operations() + .call_sign(&mut store, message)??; + + // use bindings to verify the signature + let is_valid = bindings + .component_edwards_wit_operations() + .call_verify(&mut store, message, &signature)??; + + assert!(is_valid); + + Ok(()) + } +} diff --git a/examples/edwards-wit/wit/deps/seed-keeper/wallet.wit b/examples/edwards-wit/wit/deps/seed-keeper/wallet.wit new file mode 100644 index 0000000..481f7d4 --- /dev/null +++ b/examples/edwards-wit/wit/deps/seed-keeper/wallet.wit @@ -0,0 +1,48 @@ +// This file needs to be in a directory with the same path as the package name! seed-keeper/wallet.wit +package seed-keeper:wallet@0.1.0; + +interface types { + /// The confuration of the seed keeper + record credentials { + /// The username to use for the seed keeper + username: list, + + /// The password to use for the seed keeper + password: list, + + /// Optional prevously generated encrypted seed to use for the seed keeper + encrypted: option> + } +} + +interface config { + /// Import the types interface + use types.{credentials}; + + /// Load into the component from an encrypted seed, password, and salt (username) + /// Returns the encrypted seed or an error + set-config: func(config: credentials) -> result<_, string>; +} + +interface encrypted { + /// Returns the encrypted seed or None if it doesn't exist + get-encrypted: func() -> result, string>; +} + +/// Exported interfaces of dependencies like seed keeper are not exported by the primary component +/// This keeps out seed safely out of the public API +interface seed-getter { + /// Get the plaintext seed + get-seed: func() -> result, string>; +} + +/// An example world for the component to target. +world keeper { + /// Export the ability for users to set the config of the seed keeper + export config; + + /// Export the seed interface + export seed-getter; + export encrypted; +} + diff --git a/examples/edwards-wit/wit/world.wit b/examples/edwards-wit/wit/world.wit new file mode 100644 index 0000000..c225028 --- /dev/null +++ b/examples/edwards-wit/wit/world.wit @@ -0,0 +1,13 @@ +package component:edwards-wit; + +/// WIT interface exported by yourworld +interface operations { + sign: func(message: list) -> result, string>; + verify: func(message: list, signature: list) -> result; +} + +/// An example world for the component to target. +world example { + import seed-keeper:wallet/seed-getter@0.1.0; + export operations; +}