From 902938609dc0a16fd7409d75034c56017e273454 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Thu, 19 Feb 2026 12:04:38 -0600 Subject: [PATCH] feat: add wasm-mps Ticket: HSM-1425 --- .github/workflows/build-and-test.yaml | 18 +- CODEOWNERS | 1 + package-lock.json | 59 +- packages/wasm-mps/.gitignore | 8 + packages/wasm-mps/.mocharc.json | 5 + packages/wasm-mps/.prettierignore | 4 + packages/wasm-mps/Cargo.lock | 991 ++++++++++++++++++++++++++ packages/wasm-mps/Cargo.toml | 30 + packages/wasm-mps/Makefile | 70 ++ packages/wasm-mps/README.md | 3 + packages/wasm-mps/eslint.config.js | 43 ++ packages/wasm-mps/js/index.ts | 1 + packages/wasm-mps/package.json | 55 ++ packages/wasm-mps/src/lib.rs | 394 ++++++++++ packages/wasm-mps/test/mps.ts | 187 +++++ packages/wasm-mps/tsconfig.cjs.json | 10 + packages/wasm-mps/tsconfig.json | 18 + packages/wasm-mps/tsconfig.test.json | 9 + 18 files changed, 1904 insertions(+), 2 deletions(-) create mode 100644 packages/wasm-mps/.gitignore create mode 100644 packages/wasm-mps/.mocharc.json create mode 100644 packages/wasm-mps/.prettierignore create mode 100644 packages/wasm-mps/Cargo.lock create mode 100644 packages/wasm-mps/Cargo.toml create mode 100644 packages/wasm-mps/Makefile create mode 100644 packages/wasm-mps/README.md create mode 100644 packages/wasm-mps/eslint.config.js create mode 100644 packages/wasm-mps/js/index.ts create mode 100644 packages/wasm-mps/package.json create mode 100644 packages/wasm-mps/src/lib.rs create mode 100644 packages/wasm-mps/test/mps.ts create mode 100644 packages/wasm-mps/tsconfig.cjs.json create mode 100644 packages/wasm-mps/tsconfig.json create mode 100644 packages/wasm-mps/tsconfig.test.json diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index e77ea7e3..7f9f43da 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -41,6 +41,7 @@ jobs: workspaces: | packages/wasm-utxo packages/wasm-bip32 + packages/wasm-mps packages/wasm-solana cache-on-failure: true @@ -91,6 +92,8 @@ jobs: packages/wasm-utxo/js/wasm/ packages/wasm-bip32/dist/ packages/wasm-bip32/js/wasm/ + packages/wasm-mps/dist/ + packages/wasm-mps/js/wasm/ packages/wasm-solana/dist/ packages/wasm-solana/js/wasm/ retention-days: 1 @@ -108,7 +111,7 @@ jobs: strategy: fail-fast: false matrix: - package: [wasm-bip32, wasm-utxo, wasm-solana] + package: [wasm-bip32, wasm-mps, wasm-utxo, wasm-solana] include: - package: wasm-utxo needs-wasm-pack: true @@ -116,6 +119,9 @@ jobs: - package: wasm-bip32 needs-wasm-pack: false has-wasm-pack-tests: false + - package: wasm-mps + needs-wasm-pack: false + has-wasm-pack-tests: false - package: wasm-solana needs-wasm-pack: false has-wasm-pack-tests: false @@ -211,6 +217,16 @@ jobs: run: npm test working-directory: packages/webui + - name: Upload wasm-mps build artifacts + if: inputs.upload-artifacts + uses: actions/upload-artifact@v4 + with: + name: wasm-mps-build + path: | + packages/wasm-mps/pkg/ + packages/wasm-mps/dist/ + retention-days: 1 + - name: Upload wasm-utxo build artifacts if: inputs.upload-artifacts uses: actions/upload-artifact@v4 diff --git a/CODEOWNERS b/CODEOWNERS index c263739f..6616de1d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,3 +4,4 @@ # These owners and first officers will be the default owners for everything in the repo. * @BitGo/btc-team +packages/wasm-mps/* @BitGo/hsm diff --git a/package-lock.json b/package-lock.json index 9a194783..f1686b3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6240,6 +6240,15 @@ "bigi": "^1.4.2" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11612,6 +11621,23 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/libsodium-sumo": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.2.tgz", + "integrity": "sha512-uMgnjphJ717jLN+jFG1HUgNrK/gOVVfaO1DGZ1Ig/fKLKLVhvaH/sM1I1v784JFvmkJDaczDpi7xSYC4Jvdo1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.2.tgz", + "integrity": "sha512-wd1xAY++Kr6VMikSaa4EPRAHJmFvNlGWiiwU3Jh3GR1zRYF3/I3vy/wYsr4k3LVsNzwb9sqfEQ4LdVQ6zEebyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -21484,6 +21510,10 @@ "node": "20 || >=22" } }, + "node_modules/wasm-mps": { + "resolved": "packages/wasm-mps", + "link": true + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -22203,6 +22233,33 @@ "node": ">=8.0.0" } }, + "packages/wasm-mps": { + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "chai": "^6.2.2" + }, + "devDependencies": { + "libsodium-wrappers-sumo": "0.8.2", + "mocha": "10.6.0", + "tsx": "4.20.6", + "typescript": "5.5.3" + } + }, + "packages/wasm-mps/node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "packages/wasm-solana": { "name": "@bitgo/wasm-solana", "version": "0.0.1", @@ -22278,7 +22335,7 @@ "html-webpack-plugin": "^5.6.0", "style-loader": "^4.0.0", "ts-loader": "^9.1.2", - "webpack": "^5.104.1", + "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" } diff --git a/packages/wasm-mps/.gitignore b/packages/wasm-mps/.gitignore new file mode 100644 index 00000000..31693ad9 --- /dev/null +++ b/packages/wasm-mps/.gitignore @@ -0,0 +1,8 @@ +target/ +node_modules/ +# we actually only track the .ts files +dist/ +test/*.js +test/*.d.ts +js/wasm +.vscode diff --git a/packages/wasm-mps/.mocharc.json b/packages/wasm-mps/.mocharc.json new file mode 100644 index 00000000..f585fb0e --- /dev/null +++ b/packages/wasm-mps/.mocharc.json @@ -0,0 +1,5 @@ +{ + "extensions": ["ts", "tsx", "js", "jsx"], + "spec": ["test/**/*.ts"], + "node-option": ["import=tsx/esm", "experimental-wasm-modules"] +} diff --git a/packages/wasm-mps/.prettierignore b/packages/wasm-mps/.prettierignore new file mode 100644 index 00000000..715040be --- /dev/null +++ b/packages/wasm-mps/.prettierignore @@ -0,0 +1,4 @@ +dist/ +target/ +pkg/ +node_modules/ diff --git a/packages/wasm-mps/Cargo.lock b/packages/wasm-mps/Cargo.lock new file mode 100644 index 00000000..6844b55d --- /dev/null +++ b/packages/wasm-mps/Cargo.lock @@ -0,0 +1,991 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto_box" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" +dependencies = [ + "aead", + "crypto_secretbox", + "curve25519-dalek", + "salsa20", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "group", + "rand_core", + "rustc_version", + "serde", + "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", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "serdect", + "signature", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "serdect", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "multi-party-schnorr" +version = "1.2.0-pre.1" +source = "git+https://github.com/silence-laboratories/multi-party-schnorr.git?rev=7511971e757a2260afa797283cf239c9cdfd5f19#7511971e757a2260afa797283cf239c9cdfd5f19" +dependencies = [ + "bytemuck", + "ciborium", + "crypto-bigint", + "crypto_box", + "curve25519-dalek", + "derivation-path", + "ed25519-dalek", + "elliptic-curve", + "ff", + "hmac", + "k256", + "pasta_curves", + "rand", + "rand_chacha", + "serde", + "serde_bytes", + "sha2", + "signature", + "sl-mpc-mate", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "ff", + "group", + "hex", + "rand", + "serde", + "static_assertions", + "subtle", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "sl-mpc-mate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a84618a45d32e654109a17118dc963305fe8177d045af1c818a92289c5382e2" +dependencies = [ + "base64", + "bs58", + "derivation-path", + "elliptic-curve", + "hex", + "hmac", + "k256", + "rand", + "rand_core", + "ripemd", + "serde", + "sha2", + "subtle", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-mps" +version = "0.1.0" +dependencies = [ + "bincode", + "crypto_box", + "getrandom", + "js-sys", + "multi-party-schnorr", + "rand", + "serde", + "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/packages/wasm-mps/Cargo.toml b/packages/wasm-mps/Cargo.toml new file mode 100644 index 00000000..fd07d114 --- /dev/null +++ b/packages/wasm-mps/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "wasm-mps" +version = "0.1.0" +edition = "2021" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[lib] +crate-type = ["cdylib"] + +[lints.clippy] +all = "warn" + +[dependencies] +bincode = "1.3" +crypto_box = "0.9" +getrandom = { version = "0.2", features = ["js"] } +js-sys = "0.3" +multi-party-schnorr = { git = "https://github.com/silence-laboratories/multi-party-schnorr.git", rev = "7511971e757a2260afa797283cf239c9cdfd5f19", features = ["serde"]} +serde = { version = "1.0", features = ["derive"] } +thiserror = "2.0.18" +wasm-bindgen = "0.2" +web-sys = "0.3" + +[dev-dependencies] +rand = "0.8" + +[profile.release] +strip = true diff --git a/packages/wasm-mps/Makefile b/packages/wasm-mps/Makefile new file mode 100644 index 00000000..6b8f1f95 --- /dev/null +++ b/packages/wasm-mps/Makefile @@ -0,0 +1,70 @@ +WASM_PACK = wasm-pack +WASM_OPT = wasm-opt +WASM_PACK_FLAGS = --no-pack --weak-refs + +ifdef WASM_PACK_DEV + WASM_PACK_FLAGS += --dev +endif + +# Auto-detect Mac and use Homebrew LLVM for WASM compilation +# Apple's Clang doesn't support wasm32-unknown-unknown target +UNAME_S := $(shell uname -s) + +ifeq ($(UNAME_S),Darwin) + # Mac detected - check for Homebrew LLVM installation + HOMEBREW_LLVM := $(shell brew --prefix llvm 2>/dev/null) + + ifdef HOMEBREW_LLVM + export CC = $(HOMEBREW_LLVM)/bin/clang + export AR = $(HOMEBREW_LLVM)/bin/llvm-ar + $(info Using Homebrew LLVM: $(HOMEBREW_LLVM)) + else + $(warning Homebrew LLVM not found. Install with: brew install llvm) + $(warning Continuing with system clang - may fail on Apple Silicon) + endif +endif + +define WASM_PACK_COMMAND + $(WASM_PACK) build --no-opt --out-dir $(1) $(WASM_PACK_FLAGS) --target $(2) +endef + +# run wasm-opt separately so we can pass `--enable-bulk-memory` +define WASM_OPT_COMMAND + $(WASM_OPT) --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext -Oz $(1)/*.wasm -o $(1)/*.wasm +endef + +define REMOVE_GITIGNORE + find $(1) -name .gitignore -delete +endef + +define SHOW_WASM_SIZE + @find $(1) -name "*.wasm" -exec gzip -k {} \; + @find $(1) -name "*.wasm" -exec du -h {} \; + @find $(1) -name "*.wasm.gz" -exec du -h {} \; + @find $(1) -name "*.wasm.gz" -delete +endef + +define BUILD + rm -rf $(1) + $(call WASM_PACK_COMMAND,$(1),$(2)) + $(call WASM_OPT_COMMAND,$(1)) + $(call REMOVE_GITIGNORE,$(1)) + $(call SHOW_WASM_SIZE,$(1)) +endef + +.PHONY: js/wasm +js/wasm: + $(call BUILD,$@,bundler) + +.PHONY: dist/esm/js/wasm +dist/esm/js/wasm: + $(call BUILD,$@,bundler) + +.PHONY: dist/cjs/js/wasm +dist/cjs/js/wasm: + $(call BUILD,$@,nodejs) + +.PHONY: lint +lint: + cargo fmt --check + cargo clippy --all-targets --all-features -- -D warnings diff --git a/packages/wasm-mps/README.md b/packages/wasm-mps/README.md new file mode 100644 index 00000000..cde39da9 --- /dev/null +++ b/packages/wasm-mps/README.md @@ -0,0 +1,3 @@ +# wasm-mps + +WASM bindings for clients using Silence Lab's [multi-party-schnorr](https://github.com/silence-laboratories/multi-party-schnorr) protocol. diff --git a/packages/wasm-mps/eslint.config.js b/packages/wasm-mps/eslint.config.js new file mode 100644 index 00000000..ac914ad7 --- /dev/null +++ b/packages/wasm-mps/eslint.config.js @@ -0,0 +1,43 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.test.json"], + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + ignores: ["dist/", "pkg/", "target/", "node_modules/", "js/wasm/", "*.config.js"], + }, + // Ban Node.js globals in production code + { + files: ["js/**/*.ts"], + rules: { + "no-restricted-globals": [ + "error", + { + name: "Buffer", + message: "Use Uint8Array instead of Buffer for ESM compatibility.", + }, + { + name: "process", + message: "Avoid Node.js process global for ESM compatibility.", + }, + { + name: "__dirname", + message: "Use import.meta.url instead of __dirname for ESM.", + }, + { + name: "__filename", + message: "Use import.meta.url instead of __filename for ESM.", + }, + ], + }, + }, +); diff --git a/packages/wasm-mps/js/index.ts b/packages/wasm-mps/js/index.ts new file mode 100644 index 00000000..f70c9e0d --- /dev/null +++ b/packages/wasm-mps/js/index.ts @@ -0,0 +1 @@ +export * from "./wasm/wasm_mps"; diff --git a/packages/wasm-mps/package.json b/packages/wasm-mps/package.json new file mode 100644 index 00000000..8154ac3a --- /dev/null +++ b/packages/wasm-mps/package.json @@ -0,0 +1,55 @@ +{ + "version": "0.0.1", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/BitGo/BitGoWASM" + }, + "license": "MIT", + "files": [ + "dist/esm/js/**/*", + "dist/cjs/js/**/*", + "dist/cjs/package.json" + ], + "exports": { + ".": { + "import": { + "types": "./dist/esm/js/index.d.ts", + "default": "./dist/esm/js/index.js" + }, + "require": { + "types": "./dist/cjs/js/index.d.ts", + "default": "./dist/cjs/js/index.js" + } + } + }, + "main": "./dist/cjs/js/index.js", + "module": "./dist/esm/js/index.js", + "types": "./dist/esm/js/index.d.ts", + "sideEffects": [ + "./dist/esm/js/wasm/wasm_mps.js", + "./dist/cjs/js/wasm/wasm_mps.js" + ], + "scripts": { + "test": "npm run test:mocha", + "test:mocha": "mocha --recursive test", + "build:wasm": "make js/wasm && make dist/esm/js/wasm && make dist/cjs/js/wasm", + "build:ts-esm": "tsc", + "build:ts-cjs": "tsc --project tsconfig.cjs.json", + "build:ts": "npm run build:ts-esm && npm run build:ts-cjs", + "build:package-json": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", + "build": "npm run build:wasm && npm run build:ts && npm run build:package-json", + "check-fmt": "prettier --check . && cargo fmt -- --check", + "lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings" + }, + "devDependencies": { + "libsodium-wrappers-sumo": "0.8.2", + "mocha": "10.6.0", + "tsx": "4.20.6", + "typescript": "5.5.3" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/wasm-mps/src/lib.rs b/packages/wasm-mps/src/lib.rs new file mode 100644 index 00000000..89ff8c6a --- /dev/null +++ b/packages/wasm-mps/src/lib.rs @@ -0,0 +1,394 @@ +//! Session-based Multi-Party Schnorr frontend + +mod mps { + + use multi_party_schnorr::common::traits::Round; + use multi_party_schnorr::curve25519_dalek::EdwardsPoint; + use multi_party_schnorr::keygen::{KeygenMsg1, KeygenMsg2, KeygenParty, R0, R1, R2}; + use std::sync::Arc; + use thiserror::Error; + + /// Errors that can be returned as results. + #[derive(Debug, Error)] + pub enum DkgError { + #[error("Serialization Error")] + SerializationError, + + #[error("Deserialization Error")] + DeserializationError, + + #[error("Invalid Input")] + InvalidInput, + + #[error("Protocol Error")] + ProtocolError, + } + + /// Internal state used for round 1. + #[derive(serde::Serialize, serde::Deserialize)] + struct StateR1 { + pub msg: KeygenMsg1, + pub party: KeygenParty, EdwardsPoint>, + } + + /// Internal state used for round 2. + #[derive(serde::Serialize, serde::Deserialize)] + struct StateR2 { + pub msg: KeygenMsg2, + pub party: KeygenParty, + } + + /// Result from processing that includes a public messages for other + /// parties and a private state to be stored in memory. + pub struct MsgState { + pub msg: Vec, + pub state: Vec, + } + + /// Signing share returned from round 2. + pub struct Share { + pub share: Vec, + pub pk: [u8; 32], + } + + /// Process round 0 of protocol. + /// party_id: Party indentifier / index. + /// decryption_key: Private Curve25519 key. + /// encryption_keys: Public Curve25519 keys of other parties. + /// seed: PRNG seed for entropy. + pub fn round0_process( + party_id: u8, + decryption_key: &[u8; 32], + encryption_keys: &[Vec; 2], + seed: &[u8; 32], + ) -> Result { + if party_id >= 3 { + return Err(DkgError::InvalidInput); + } + + // Parse decryption key + let secret_key = crypto_box::SecretKey::from(*decryption_key); + + // Parse all party encryption keys + let i0_pk = crypto_box::PublicKey::from( + <[u8; 32]>::try_from(encryption_keys[0].clone()).map_err(|_| DkgError::InvalidInput)?, + ); + let i1_pk = crypto_box::PublicKey::from( + <[u8; 32]>::try_from(encryption_keys[1].clone()).map_err(|_| DkgError::InvalidInput)?, + ); + let mut public_keys = Vec::new(); + if party_id == 0 { + public_keys.push((1u8, i0_pk)); + public_keys.push((2u8, i1_pk)); + } else if party_id == 1 { + public_keys.push((0u8, i0_pk)); + public_keys.push((2u8, i1_pk)); + } else { + public_keys.push((0u8, i0_pk)); + public_keys.push((1u8, i1_pk)); + } + public_keys.push((party_id, secret_key.public_key())); + + // Create KeygenParty + let p0 = KeygenParty::::new( + 2, // threshold + 3, // total parties + party_id, + Arc::new(secret_key), + public_keys, + None, // refresh_data + None, // key_id + *seed, + None, // extra_data + ) + .map_err(|_| DkgError::ProtocolError)?; + + // Generate message + let (p1, msg1) = p0.process(()).map_err(|_| DkgError::ProtocolError)?; + + // Create the state for storage between rounds + let state = StateR1 { + msg: msg1, + party: p1, + }; + + Ok(MsgState { + msg: bincode::serialize(&msg1).map_err(|_| DkgError::SerializationError)?, + state: bincode::serialize(&state).map_err(|_| DkgError::SerializationError)?, + }) + } + + /// Process round 1 of protocol. + /// round1_messages: Public messages from other parties. + /// state: Private state result from from round 0. + pub fn round1_process( + round1_messages: &[Vec; 2], + state: &[u8], + ) -> Result { + // Parse state + let state: StateR1 = + bincode::deserialize(state).map_err(|_| DkgError::DeserializationError)?; + + // Parse messages + let i0_msg1: KeygenMsg1 = bincode::deserialize(round1_messages[0].as_slice()) + .map_err(|_| DkgError::DeserializationError)?; + let i1_msg1: KeygenMsg1 = bincode::deserialize(round1_messages[1].as_slice()) + .map_err(|_| DkgError::DeserializationError)?; + let msgs = vec![i0_msg1, i1_msg1, state.msg]; + + // Process all round0 messages together + let (p2, msg2) = state + .party + .process(msgs) + .map_err(|_| DkgError::ProtocolError)?; + + // Create the state for storage between rounds + let state = StateR2 { + msg: msg2.clone(), + party: p2, + }; + + Ok(MsgState { + msg: bincode::serialize(&msg2).map_err(|_| DkgError::SerializationError)?, + state: bincode::serialize(&state).map_err(|_| DkgError::SerializationError)?, + }) + } + + /// Process round 2 of protocol. + /// round2_messages: Public messages from other parties. + /// state: Private state result from round 1. + pub fn round2_process(round2_messages: &[Vec; 2], state: &[u8]) -> Result { + // Deserialize round2 messages from other parties + let i0_msg2: KeygenMsg2 = bincode::deserialize(round2_messages[0].as_slice()) + .map_err(|_| DkgError::DeserializationError)?; + let i1_msg2: KeygenMsg2 = bincode::deserialize(round2_messages[1].as_slice()) + .map_err(|_| DkgError::DeserializationError)?; + + // Deserialize state + let state: StateR2 = + bincode::deserialize(state).map_err(|_| DkgError::DeserializationError)?; + + // Generate share + let share = state + .party + .process(vec![i0_msg2, i1_msg2, state.msg.clone()]) + .map_err(|_| DkgError::ProtocolError)?; + + Ok(Share { + share: bincode::serialize(&share).map_err(|_| DkgError::SerializationError)?, + pk: share.public_key.compress().to_bytes(), + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use rand::{self, Rng}; + + #[test] + fn test_dkg() { + // Generate key pairs and seeds for all parties + let mut prv_keys = Vec::new(); + let mut pub_keys = Vec::new(); + let mut seeds = Vec::new(); + for i in 0..3 { + let secret_key = crypto_box::SecretKey::generate(&mut rand::thread_rng()); + let public_key = secret_key.public_key(); + prv_keys.push(secret_key); + pub_keys.push((i, public_key)); + let seed: [u8; 32] = rand::thread_rng().gen(); + seeds.push(seed); + } + + // Parties generate their round 0 messages + let p0_0 = mps::round0_process( + 0, + &prv_keys[0].to_bytes(), + &[ + pub_keys[1].1.to_bytes().to_vec(), + pub_keys[2].1.to_bytes().to_vec(), + ], + &seeds[0], + ) + .unwrap(); + let p1_0 = mps::round0_process( + 1, + &prv_keys[1].to_bytes(), + &[ + pub_keys[0].1.to_bytes().to_vec(), + pub_keys[2].1.to_bytes().to_vec(), + ], + &seeds[1], + ) + .unwrap(); + let p2_0 = mps::round0_process( + 2, + &prv_keys[2].to_bytes(), + &[ + pub_keys[0].1.to_bytes().to_vec(), + pub_keys[1].1.to_bytes().to_vec(), + ], + &seeds[2], + ) + .unwrap(); + + // Parties generate their round 1 messages + let p0_1 = + mps::round1_process(&[p1_0.msg.clone(), p2_0.msg.clone()], p0_0.state.as_slice()) + .unwrap(); + let p1_1 = + mps::round1_process(&[p0_0.msg.clone(), p2_0.msg.clone()], p1_0.state.as_slice()) + .unwrap(); + let p2_1 = + mps::round1_process(&[p0_0.msg.clone(), p1_0.msg.clone()], p2_0.state.as_slice()) + .unwrap(); + + // Parties generate their key shares + let p0_share = + mps::round2_process(&[p1_1.msg.clone(), p2_1.msg.clone()], p0_1.state.as_slice()) + .unwrap(); + let p1_share = + mps::round2_process(&[p0_1.msg.clone(), p2_1.msg.clone()], p1_1.state.as_slice()) + .unwrap(); + let p2_share = + mps::round2_process(&[p0_1.msg.clone(), p1_1.msg.clone()], p2_1.state.as_slice()) + .unwrap(); + + // Assert generated public keys are equal + assert_eq!( + p2_share.pk, p0_share.pk, + "Party 0 share differs from party 2 share" + ); + assert_eq!( + p2_share.pk, p1_share.pk, + "Party 1 share differs from party 2 share" + ); + } +} + +use js_sys::Array; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct MsgState { + msg: Vec, + state: Vec, +} + +#[wasm_bindgen] +impl MsgState { + #[wasm_bindgen(getter)] + pub fn msg(&self) -> Vec { + self.msg.clone() + } + + #[wasm_bindgen(getter)] + pub fn state(&self) -> Vec { + self.state.clone() + } +} + +#[wasm_bindgen] +pub struct Share { + share: Vec, + pk: Vec, +} + +#[wasm_bindgen] +impl Share { + #[wasm_bindgen(getter)] + pub fn share(&self) -> Vec { + self.share.clone() + } + + #[wasm_bindgen(getter)] + pub fn pk(&self) -> Vec { + self.pk.clone() + } +} + +#[wasm_bindgen] +pub struct MsgShare { + msg: Vec, + share: Share, +} + +#[wasm_bindgen] +impl MsgShare { + #[wasm_bindgen(getter)] + pub fn msg(&self) -> Vec { + self.msg.clone() + } + + #[wasm_bindgen(getter)] + pub fn share(&self) -> Share { + Share { + share: self.share.share.clone(), + pk: self.share.pk.clone(), + } + } +} + +#[wasm_bindgen] +pub fn round0_process( + party_id: u8, + decryption_key: &[u8], + encryption_keys: Array, + seed: &[u8], +) -> Result { + let decryption_key_32: [u8; 32] = decryption_key[..32] + .try_into() + .map_err(|_| "Deserialization Error")?; + let seed_32: [u8; 32] = seed[..32].try_into().map_err(|_| "Deserialization Error")?; + let result = mps::round0_process( + party_id, + &decryption_key_32, + &[ + js_sys::Uint8Array::from(encryption_keys.get(0)).to_vec(), + js_sys::Uint8Array::from(encryption_keys.get(1)).to_vec(), + ], + &seed_32, + ) + .map_err(|e| e.to_string())?; + + Ok(MsgState { + msg: result.msg, + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn round1_process(round1_messages: Array, state: &[u8]) -> Result { + let result = mps::round1_process( + &[ + js_sys::Uint8Array::from(round1_messages.get(0)).to_vec(), + js_sys::Uint8Array::from(round1_messages.get(1)).to_vec(), + ], + state, + ) + .map_err(|e| e.to_string())?; + + Ok(MsgState { + msg: result.msg, + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn round2_process(round2_messages: Array, state: &[u8]) -> Result { + let result = mps::round2_process( + &[ + js_sys::Uint8Array::from(round2_messages.get(0)).to_vec(), + js_sys::Uint8Array::from(round2_messages.get(1)).to_vec(), + ], + state, + ) + .map_err(|e| e.to_string())?; + + Ok(Share { + share: result.share, + pk: result.pk.to_vec(), + }) +} diff --git a/packages/wasm-mps/test/mps.ts b/packages/wasm-mps/test/mps.ts new file mode 100644 index 00000000..b327f2fb --- /dev/null +++ b/packages/wasm-mps/test/mps.ts @@ -0,0 +1,187 @@ +import assert from "assert"; +import crypto from "crypto"; +import * as mps from "../js"; +import sodium from "libsodium-wrappers-sumo"; + +await sodium.ready; + +describe("mps", function () { + const otherIndices = [ + [1, 2], + [0, 2], + [0, 1], + ]; + const keypairs: Array<{ privateKey: Uint8Array; publicKey: Uint8Array }> = []; + + before("generates keypairs", function () { + for (let i = 0; i < 3; i++) { + keypairs.push(sodium.crypto_box_keypair()); + } + }); + + it("performs round 0", function () { + for (let i = 0; i < 3; i++) { + mps.round0_process( + i, + keypairs[i].privateKey, + otherIndices[i].map((i) => keypairs[i].publicKey), + crypto.randomBytes(32), + ); + } + }); + + let results1: Array; + + before("performs round 0", function () { + results1 = [0, 1, 2].map((i) => + mps.round0_process( + i, + keypairs[i].privateKey, + otherIndices[i].map((i) => keypairs[i].publicKey), + crypto.randomBytes(32), + ), + ); + }); + + it("performs round 1", function () { + for (let i = 0; i < 3; i++) { + mps.round1_process( + otherIndices[i].map((i) => results1[i].msg), + results1[i].state, + ); + } + }); + + let results2: Array; + + before("performs round 1", function () { + results2 = [0, 1, 2].map((i) => + mps.round1_process( + otherIndices[i].map((i) => results1[i].msg), + results1[i].state, + ), + ); + }); + + it("performs round 2", function () { + const results3 = [0, 1, 2].map((i) => + mps.round2_process( + otherIndices[i].map((i) => results2[i].msg), + results2[i].state, + ), + ); + for (let i = 0; i < 2; i++) { + assert.ok(results3[i].pk.every((value, index) => value === results3[2].pk[index])); + } + }); + + describe("input handling", function () { + function shouldThrow(fn: () => unknown): unknown { + try { + fn(); + } catch (e: unknown) { + return e; + } + throw new Error("Expected function to throw an error"); + } + + describe("round0_process", function () { + it("does not panic on bad party size", function () { + shouldThrow(() => + mps.round0_process( + "255", + Buffer.alloc(32), + [Buffer.alloc(32), Buffer.alloc(32)], + crypto.randomBytes(32), + ), + ); + }); + + it("does not panic on bad encryption key", function () { + shouldThrow(() => + mps.round0_process( + 0, + "encryption key", + [Buffer.alloc(32), Buffer.alloc(32)], + crypto.randomBytes(32), + ), + ); + shouldThrow(() => + mps.round0_process( + 0, + Buffer.alloc(0), + [Buffer.alloc(32), Buffer.alloc(32)], + crypto.randomBytes(32), + ), + ); + }); + + it("does not panic on bad decryption keys", function () { + shouldThrow(() => + mps.round0_process(0, Buffer.alloc(0), "decryption keys", crypto.randomBytes(32)), + ); + shouldThrow(() => mps.round0_process(0, Buffer.alloc(0), [], crypto.randomBytes(32))); + shouldThrow(() => + mps.round0_process(0, Buffer.alloc(0), ["decryption key"], crypto.randomBytes(32)), + ); + shouldThrow(() => + mps.round0_process(0, Buffer.alloc(0), [Buffer.alloc(0)], crypto.randomBytes(32)), + ); + shouldThrow(() => + mps.round0_process( + 0, + Buffer.alloc(0), + [Buffer.alloc(32), Buffer.alloc(0)], + crypto.randomBytes(32), + ), + ); + }); + + it("does not panic on bad seed", function () { + shouldThrow(() => + mps.round0_process(0, Buffer.alloc(0), [Buffer.alloc(32), Buffer.alloc(32)], "seed"), + ); + shouldThrow(() => + mps.round0_process( + 0, + Buffer.alloc(0), + [Buffer.alloc(32), Buffer.alloc(32)], + Buffer.alloc(0), + ), + ); + }); + }); + + describe("round1_process", function () { + it("does not panic on bad messages", function () { + shouldThrow(() => mps.round1_process("messages", Buffer.alloc(1224))); + shouldThrow(() => mps.round1_process([], Buffer.alloc(1224))); + shouldThrow(() => mps.round1_process(["message"], Buffer.alloc(1224))); + shouldThrow(() => mps.round1_process([Buffer.alloc(0), Buffer.alloc(1224)])); + }); + + it("does not panic on bad state", function () { + shouldThrow(() => mps.round1_process([Buffer.alloc(65), Buffer.alloc(65)], "state")); + shouldThrow(() => + mps.round1_process([Buffer.alloc(65), Buffer.alloc(65)], Buffer.alloc(0)), + ); + }); + }); + + describe("round2_process", function () { + it("does not panic on bad messages", function () { + shouldThrow(() => mps.round2_process("messages", Buffer.alloc(1224))); + shouldThrow(() => mps.round2_process([], Buffer.alloc(1224))); + shouldThrow(() => mps.round2_process(["message"], Buffer.alloc(1224))); + shouldThrow(() => mps.round2_process([Buffer.alloc(0), Buffer.alloc(1224)])); + }); + + it("does not panic on bad state", function () { + shouldThrow(() => mps.round2_process([Buffer.alloc(65), Buffer.alloc(65)], "state")); + shouldThrow(() => + mps.round2_process([Buffer.alloc(65), Buffer.alloc(65)], Buffer.alloc(0)), + ); + }); + }); + }); +}); diff --git a/packages/wasm-mps/tsconfig.cjs.json b/packages/wasm-mps/tsconfig.cjs.json new file mode 100644 index 00000000..390db86d --- /dev/null +++ b/packages/wasm-mps/tsconfig.cjs.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "rootDir": ".", + "outDir": "./dist/cjs" + }, + "exclude": ["node_modules", "./js/wasm/**/*", "test/**/*"] +} diff --git a/packages/wasm-mps/tsconfig.json b/packages/wasm-mps/tsconfig.json new file mode 100644 index 00000000..499b65c4 --- /dev/null +++ b/packages/wasm-mps/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "ES2022", + "target": "ES2022", + "moduleResolution": "bundler", + "esModuleInterop": true, + "allowJs": true, + "skipLibCheck": true, + "declaration": true, + "composite": true, + "rootDir": ".", + "outDir": "./dist/esm", + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["./js/**/*.ts"], + "exclude": ["node_modules", "./js/wasm/**/*", "test/**/*"] +} diff --git a/packages/wasm-mps/tsconfig.test.json b/packages/wasm-mps/tsconfig.test.json new file mode 100644 index 00000000..7ec46d32 --- /dev/null +++ b/packages/wasm-mps/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node", "mocha"], + "noEmit": true + }, + "include": ["./js/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "./js/wasm/**/*"] +}