mirror of
https://github.com/plume-sig/zk-nullifier-sig.git
synced 2026-01-09 21:08:00 -05:00
Replaces NPM package with Wasm wrapper
Resolves #114 removes respective GA this one should be tested like a package I guess, ideas for such tests are welcome as issues Couple of things left out of the committed code. # Subtle I was really late to understand that Subtle crypto supports the different curve `secp256r`, *and* it doesn't provide a facility to store secret values. So implementation for `web_sys::SecretKey` turned out to be just extra miles leading nowhere. ```toml web-sys = { version = "0.3", features = ["CryptoKey", "SubtleCrypto", "Crypto", "EcKeyImportParams"] } wasm-bindgen-futures = "0.4" ``` ```rust #[wasm_bindgen] extern "C" { // Return type of js_sys::global() type Global; // // Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/) // type WebCrypto; // Getters for the WebCrypto API #[wasm_bindgen(method, getter)] fn crypto(this: &Global) -> web_sys::Crypto; } // `fn sign` if sk.type_() != "secret" {return Err(JsError::new("`sk` must be secret key"))} if !js_sys::Object::values(&sk.algorithm().map_err( |er| JsError::new(er.as_string().expect("TODO check this failing").as_str()) )?).includes(&JsValue::from_str("P-256"), 0) {return Err(JsError::new("`sk` must be from `secp256`"))} // this was my approach, but seems I got what they did at <https://github.com/rust-random/getrandom/blob/master/src/js.rs> // js_sys::global().entries().find(); // TODO throw if no Crypto in global let global_the: Global = js_sys::global().unchecked_into(); let crypto_the: web_sys::Crypto = global_the.crypto(); let subtle_the = crypto_the.subtle(); let sk = JsFuture::from(subtle_the.export_key("pkcs8", &sk)?).await?; // ... ::from_pkcs8_der(js_sys::ArrayBuffer::from(sk).try_into()?)?; zeroize::Zeroizing::new(js_sys::Uint8Array::from(JsFuture::from(subtle_the.export_key("pkcs8", &sk).map_err( |er| Err(JsError::new(er.as_string().expect("TODO check this failing").as_str())) )?).await?).to_vec()); // ... // `fn try_into` // ... // zeroization protection ommitted here due to deprecation // <https://github.com/plume-sig/zk-nullifier-sig/issues/112> // mostly boilerplate from signing; also some excessive ops left for the same reason // TODO align error-handling in this part if self.c.type_() != "secret" {return Err(JsError::new("`c` must be secret key"))} if !js_sys::Object::values(&self.c.algorithm()?).includes(js_sys::JsString::from("P-256").into(), 0) {return Err(JsError::new("`c` must be from `secp256`"))} this was my approach, but seems I got what they did at <https://github.com/rust-random/getrandom/blob/master/src/js.rs> js_sys::global().entries().find(); // TODO throw if no Crypto in global let global_the: Global = js_sys::global().unchecked_into(); let crypto_the: web_sys::Crypto = global_the.crypto(); let subtle_the = crypto_the.subtle(); let c_pkcs = //zeroize::Zeroizing::new( js_sys::Uint8Array::from(JsFuture::from(subtle_the.export_key("pkcs8", &self.c)?).await?).to_vec(); // ); let c_scalar = &plume_rustcrypto::SecretKey::from_pkcs8_der(&c_pkcs)?.to_nonzero_scalar(); sk_z.zeroize(); // ... ``` # randomness Somehow I thought Wasm doesn't have access to RNG, so I used a seedable one and required the seed. Here's how `sign` `fn` was different. ```rust // Wasm environment doesn't have a suitable way to get randomness for the signing process, so this instantiates ChaCha20 RNG with the provided seed. // @throws a "crypto error" in case of a problem with the secret key, and a verbal error on a problem with `seed` // @param {Uint8Array} seed - must be exactly 32 bytes. pub fn sign(seed: &mut [u8], v1: bool, sk: &mut [u8], msg: &[u8]) -> Result<PlumeSignature, JsError> { // ... let seed_z: zeroize::Zeroizing<[u8; 32]> = zeroize::Zeroizing::new(seed.try_into()?); seed.zeroize(); // TODO switch to `wasi-random` when that will be ready for crypto let sig = match v1 { true => plume_rustcrypto::PlumeSignature::sign_v1( &sk_z, msg, &mut rand_chacha::ChaCha20Rng::from_seed(seed_z) ), false => plume_rustcrypto::PlumeSignature::sign_v2( &sk_z, msg, &mut rand_chacha::ChaCha20Rng::from_seed(seed_z) ), }; let sig = signer.sign_with_rng( &mut rand_chacha::ChaCha20Rng::from_seed(*seed_z), msg ); // ... } ``` # `BigInt` conversion It was appealing to leave `s` as `BigInt` (see the comments), but that seems to be confusing and hinder downstream code reusage. There's an util function left for anybody who would want to have it as `BigInt`, but leaving the contraty function makes less sense and also makes the thing larger. So let me left it here for reference. ```rust let scalar_from_bigint = |n: js_sys::BigInt| -> Result<plume_rustcrypto::NonZeroScalar, anyhow::Error> { let result = plume_rustcrypto::NonZeroScalar::from_repr(k256::FieldBytes::from_slice( hex::decode({ let hexstring_freelen = n.to_string(16).map_err( |er| anyhow::Error::msg(er.as_string().expect("`RangeError` can be printed out")) )?.as_string().expect("on `JsString` this always produce a `String`"); let l = hexstring_freelen.len(); if l > 32*2 {return Err(anyhow::Error::msg("too many digits"))} else {["0".repeat(64-l), hexstring_freelen].concat()} })?.as_slice() ).to_owned()); if result.is_none().into() {Err(anyhow::Error::msg("isn't valid `secp256` non-zero scalar"))} else {Ok(result.expect(EXPECT_NONEALREADYCHECKED))} }; ```
This commit is contained in:
41
.github/workflows/javascript.yml
vendored
41
.github/workflows/javascript.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
name: Javascript checks
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
checks:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
node-version: [18]
|
|
||||||
command: ["prettier", "types", "test:coverage"]
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
|
|
||||||
- name: Check javascript
|
|
||||||
run: |
|
|
||||||
pnpm install --no-frozen-lockfile --prefer-offline
|
|
||||||
pnpm run build
|
|
||||||
pnpm run ${{ matrix.command }}
|
|
||||||
working-directory: "./javascript"
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
members = ["rust-arkworks", "rust-k256"]
|
members = ["rust-arkworks", "rust-k256", "javascript"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
ark-ec = { git = "https://github.com/FindoraNetwork/ark-algebra" }
|
ark-ec = { git = "https://github.com/FindoraNetwork/ark-algebra" }
|
||||||
|
|||||||
11
javascript/.appveyor.yml
Normal file
11
javascript/.appveyor.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
install:
|
||||||
|
- appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
||||||
|
- if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly
|
||||||
|
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||||
|
- rustc -V
|
||||||
|
- cargo -V
|
||||||
|
|
||||||
|
build: false
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- cargo test --locked
|
||||||
8
javascript/.github/dependabot.yml
vendored
Normal file
8
javascript/.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: cargo
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "08:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
15
javascript/.gitignore
vendored
15
javascript/.gitignore
vendored
@@ -1,9 +1,6 @@
|
|||||||
# Swap the comments on the following lines if you don't wish to use zero-installs
|
/target
|
||||||
# Documentation here: https://yarnpkg.com/features/zero-installs
|
**/*.rs.bk
|
||||||
# !.yarn/cache
|
Cargo.lock
|
||||||
.pnp.*
|
bin/
|
||||||
|
pkg/
|
||||||
# node.js
|
wasm-pack.log
|
||||||
/node_modules
|
|
||||||
/dist
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# Swap the comments on the following lines if you don't wish to use zero-installs
|
|
||||||
# Documentation here: https://yarnpkg.com/features/zero-installs
|
|
||||||
# !.yarn/cache
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
# node.js
|
|
||||||
/node_modules
|
|
||||||
69
javascript/.travis.yml
Normal file
69
javascript/.travis.yml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
language: rust
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
cache: cargo
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
|
||||||
|
# Builds with wasm-pack.
|
||||||
|
- rust: beta
|
||||||
|
env: RUST_BACKTRACE=1
|
||||||
|
addons:
|
||||||
|
firefox: latest
|
||||||
|
chrome: stable
|
||||||
|
before_script:
|
||||||
|
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||||
|
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
|
||||||
|
- cargo install-update -a
|
||||||
|
- curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
|
||||||
|
script:
|
||||||
|
- cargo generate --git . --name testing
|
||||||
|
# Having a broken Cargo.toml (in that it has curlies in fields) anywhere
|
||||||
|
# in any of our parent dirs is problematic.
|
||||||
|
- mv Cargo.toml Cargo.toml.tmpl
|
||||||
|
- cd testing
|
||||||
|
- wasm-pack build
|
||||||
|
- wasm-pack test --chrome --firefox --headless
|
||||||
|
|
||||||
|
# Builds on nightly.
|
||||||
|
- rust: nightly
|
||||||
|
env: RUST_BACKTRACE=1
|
||||||
|
before_script:
|
||||||
|
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||||
|
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
|
||||||
|
- cargo install-update -a
|
||||||
|
- rustup target add wasm32-unknown-unknown
|
||||||
|
script:
|
||||||
|
- cargo generate --git . --name testing
|
||||||
|
- mv Cargo.toml Cargo.toml.tmpl
|
||||||
|
- cd testing
|
||||||
|
- cargo check
|
||||||
|
- cargo check --target wasm32-unknown-unknown
|
||||||
|
- cargo check --no-default-features
|
||||||
|
- cargo check --target wasm32-unknown-unknown --no-default-features
|
||||||
|
- cargo check --no-default-features --features console_error_panic_hook
|
||||||
|
- cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
|
||||||
|
- cargo check --no-default-features --features "console_error_panic_hook wee_alloc"
|
||||||
|
- cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc"
|
||||||
|
|
||||||
|
# Builds on beta.
|
||||||
|
- rust: beta
|
||||||
|
env: RUST_BACKTRACE=1
|
||||||
|
before_script:
|
||||||
|
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||||
|
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
|
||||||
|
- cargo install-update -a
|
||||||
|
- rustup target add wasm32-unknown-unknown
|
||||||
|
script:
|
||||||
|
- cargo generate --git . --name testing
|
||||||
|
- mv Cargo.toml Cargo.toml.tmpl
|
||||||
|
- cd testing
|
||||||
|
- cargo check
|
||||||
|
- cargo check --target wasm32-unknown-unknown
|
||||||
|
- cargo check --no-default-features
|
||||||
|
- cargo check --target wasm32-unknown-unknown --no-default-features
|
||||||
|
- cargo check --no-default-features --features console_error_panic_hook
|
||||||
|
- cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
|
||||||
|
# Note: no enabling the `wee_alloc` feature here because it requires
|
||||||
|
# nightly for now.
|
||||||
35
javascript/Cargo.toml
Normal file
35
javascript/Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[package]
|
||||||
|
name = "plume-sig"
|
||||||
|
version = "3.0.0-rc.1"
|
||||||
|
authors = ["skaunov"]
|
||||||
|
edition = "2018"
|
||||||
|
keywords = ["nullifier", "zero-knowledge", "ECDSA", "PLUME"]
|
||||||
|
repository = "https://github.com/plume-sig/zk-nullifier-sig/"
|
||||||
|
description = "wrapper around `plume_rustcrypto` crate to produce PLUME signatures in JS contexts using Wasm"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# I'd alias this to `sec1` if that won't be tricky
|
||||||
|
verify = ["dep:sec1"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasm-bindgen = "~0.2.84"
|
||||||
|
js-sys = "0.3"
|
||||||
|
|
||||||
|
plume_rustcrypto = {version = "~0.2.1", default-features = false}
|
||||||
|
sec1 = {version = "~0.7.3", optional = true} # match with `k256`
|
||||||
|
elliptic-curve = {version = "~0.13.8"}
|
||||||
|
zeroize = "1.8"
|
||||||
|
signature = "^2.2.0"
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wasm-bindgen-test = "~0.3.34"
|
||||||
|
|
||||||
|
[profile.release] # This comes from template; didn't touch this yet - docs doesn't tell much about it.
|
||||||
|
# Tell `rustc` to optimize for small code size.
|
||||||
|
# opt-level = "s"
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
`plume-sig`
|
|
||||||
==============
|
|
||||||
TypeScript implementation of the ERC-7524 PLUME signature scheme.
|
|
||||||
|
|
||||||
A new type of cryptographic signature that would allow for anonymous and unique digital identities on the Ethereum blockchain in a verifiable way.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
`npm install plume-sig`
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { computeAllInputs, PlumeVersion } from 'plume-sig';
|
|
||||||
|
|
||||||
return computeAllInputs(message, secretKey);
|
|
||||||
```
|
|
||||||
|
|
||||||
The function returns the signature w.r.t. to given arguments as the object of the following structure.
|
|
||||||
### `plume`
|
|
||||||
`secp256k1` point
|
|
||||||
### `s`
|
|
||||||
`secp256k1` scalar hexstring
|
|
||||||
### `pk`
|
|
||||||
Public key of the signer; SEC1 encoded.
|
|
||||||
### `c`
|
|
||||||
SHA-256 hash. It's value depends on `PlumeVersion` of the signature.
|
|
||||||
### `rPoint`
|
|
||||||
`secp256k1` point representing the unique random scalar used for signing. V1 specific.
|
|
||||||
### `hashedToCurveR`
|
|
||||||
`secp256k1` point. V1 specific.
|
|
||||||
|
|
||||||
## Signature variants
|
|
||||||
The scheme comes in two variants. V2 is default for this implementation.
|
|
||||||
|
|
||||||
### Version 1: Verifier Optimized
|
|
||||||
|
|
||||||
In a situation where there is a verifier who must *not* know the signer's `pk`, but the signer must nevertheless prove that they know `secretKey` corresponding to the signature given `message`, a zero-knowledge proof is required.
|
|
||||||
|
|
||||||
The following verification function may be described via a circuit as part of a non-interactive zero-knowledge proving system, such as Groth16. To create a proof, the prover supplies the following inputs:
|
|
||||||
|
|
||||||
### Version 2: Prover Optimized
|
|
||||||
|
|
||||||
Currently, SHA-256 hashing operations are particularly expensive with zk proofs in the browser. In the context of PLUME, the computation of $c$ is a bottleneck for efficient proof times, so one modification suggested by the Poseidon team was to move this hash computation outside the circuit, into the verifier.
|
|
||||||
|
|
||||||
Due to SHA-256 being a native precompile on Ethereum, this operation will still be efficient for smart contract verifiers.
|
|
||||||
|
|
||||||
## License
|
|
||||||
MIT
|
|
||||||
90
javascript/README.md
Normal file
90
javascript/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
This is wrapper around `plume_rustcrypto` crate to produce PLUME signatures in JS contexts using Wasm.
|
||||||
|
|
||||||
|
TODO add here couple of examples from systems which uses this.
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
Get the package from NPM. The repository contains Rust code for generating Wasm and packaging it.
|
||||||
|
|
||||||
|
The package usage outline; see the details in subsections.
|
||||||
|
```js
|
||||||
|
// ...
|
||||||
|
let result = plume.sign(isV1, secretKeySec1Der, msg);
|
||||||
|
console.log(result.nullifier);
|
||||||
|
result.zeroizePrivateParts();
|
||||||
|
```
|
||||||
|
|
||||||
|
Please, refer to the JS-doc for types description, function signatures, and exceptions notes.
|
||||||
|
|
||||||
|
Values in the following examples are in line with tests in the wrapped crate.
|
||||||
|
## producing the signature
|
||||||
|
```js
|
||||||
|
import * as plume from 'plume-sig';
|
||||||
|
|
||||||
|
let result = plume.sign(
|
||||||
|
false,
|
||||||
|
new Uint8Array([48, 107, 2, 1, 1, 4, 32, 81, 155, 66, 61, 113, 95, 139, 88, 31, 79, 168, 238, 89, 244, 119, 26, 91, 68, 200, 19, 11, 78, 62, 172, 202, 84, 165, 109, 218, 114, 180, 100, 161, 68, 3, 66, 0, 4, 12, 236, 2, 142, 224, 141, 9, 224, 38, 114, 166, 131, 16, 129, 67, 84, 249, 234, 191, 255, 13, 230, 218, 204, 28, 211, 167, 116, 73, 96, 118, 174, 239, 244, 113, 251, 160, 64, 152, 151, 182, 164, 142, 136, 1, 173, 18, 249, 93, 0, 9, 183, 83, 207, 143, 81, 193, 40, 191, 107, 11, 210, 127, 189]),
|
||||||
|
new Uint8Array([
|
||||||
|
65, 110, 32, 101, 120, 97, 109, 112, 108, 101, 32, 97, 112, 112, 32, 109, 101, 115, 115, 97, 103, 101, 32, 115, 116, 114, 105, 110, 103
|
||||||
|
])
|
||||||
|
);
|
||||||
|
```
|
||||||
|
## getters
|
||||||
|
`PlumeSignature` provide getters for each property of it, so you have access to any of them upon signing.
|
||||||
|
```js
|
||||||
|
// ...
|
||||||
|
console.log(result.nullifier);
|
||||||
|
/* Uint8Array(33) [
|
||||||
|
3, 87, 188, 62, 210, 129, 114, 239,
|
||||||
|
138, 221, 228, 185, 224, 194, 204, 231,
|
||||||
|
69, 252, 197, 166, 100, 115, 164, 92,
|
||||||
|
30, 98, 111, 29, 12, 103, 229, 88,
|
||||||
|
48
|
||||||
|
] */
|
||||||
|
console.log(result.s);
|
||||||
|
/* Uint8Array(109) [
|
||||||
|
48, 107, 2, 1, 1, 4, 32, 73, 27, 195, 183, 106,
|
||||||
|
202, 136, 167, 50, 193, 119, 152, 153, 233, 56, 176, 58,
|
||||||
|
221, 183, 4, 126, 189, 69, 201, 173, 102, 98, 248, 36,
|
||||||
|
112, 183, 176, 161, 68, 3, 66, 0, 4, 13, 18, 115,
|
||||||
|
220, 215, 120, 156, 20, 128, 225, 106, 29, 255, 16, 218,
|
||||||
|
5, 19, 179, 80, 204, 25, 144, 61, 150, 121, 83, 76,
|
||||||
|
174, 21, 232, 58, 153, 97, 227, 239, 78, 114, 199, 53,
|
||||||
|
138, 93, 108, 150, 98, 141, 89, 159, 219, 243, 182, 188,
|
||||||
|
22, 224, 154, 171,
|
||||||
|
... 9 more items
|
||||||
|
] */
|
||||||
|
console.log(result.c);
|
||||||
|
console.log(result.pk);
|
||||||
|
console.log(result.message);
|
||||||
|
console.log(result.v1specific);
|
||||||
|
// undefined
|
||||||
|
```
|
||||||
|
Note that variant is specified by `v1specific`; if it's `undefined` then the object contains V2, otherwise it's V1.
|
||||||
|
```js
|
||||||
|
// ...
|
||||||
|
if (result.v1specific) {
|
||||||
|
console.log(result.v1specific.r_point);
|
||||||
|
console.log(result.v1specific.hashed_to_curve_r);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Also there's #convertion utility provided.
|
||||||
|
## zeroization
|
||||||
|
Depending on your context you might want to clear values of the result from Wasm memory after getting the values.
|
||||||
|
```js
|
||||||
|
// ...
|
||||||
|
result.zeroizePrivateParts();
|
||||||
|
result.zeroizeAll();
|
||||||
|
```
|
||||||
|
|
||||||
|
# #convertion of `s` to `BigInt`
|
||||||
|
JS most native format for scalar is `BigInt`, but it's not really transportable or secure, so for uniformity of approach `s` in `PlumeSignature` is defined similar to `c`; but if you want to have it as a `BigInt` there's `sec1DerScalarToBigint` helper funtion.
|
||||||
|
|
||||||
|
# Working with source files
|
||||||
|
|
||||||
|
This package is built with the tech provided by <https://github.com/rustwasm> which contains everything needed to work with it. Also the wrapper crate was initiated with `wasm-pack-template`.
|
||||||
|
|
||||||
|
Note that the wrapper crate has `verify` feature which can check the resulting signature.
|
||||||
|
|
||||||
|
# License
|
||||||
|
See <https://github.com/plume-sig/zk-nullifier-sig/blob/main/LICENSE>.
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
||||||
module.exports = {
|
|
||||||
preset: "ts-jest",
|
|
||||||
testEnvironment: "node",
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "plume-sig",
|
|
||||||
"version": "2.0.9",
|
|
||||||
"keywords": ["nullifier", "zero-knowledge", "ECDSA", "PLUME"],
|
|
||||||
"repository": "https://github.com/plume-sig/zk-nullifier-sig/",
|
|
||||||
"pnpm": {
|
|
||||||
"overrides": {
|
|
||||||
"@noble/secp256k1": "$@noble/secp256k1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 <19"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jest": "^29.5.0",
|
|
||||||
"@types/js-sha512": "^0",
|
|
||||||
"@types/node": "^18.11.9",
|
|
||||||
"@types/nodemon": "^1.19.2",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"nodemon": "^2.0.20",
|
|
||||||
"prettier": "^3.0.3",
|
|
||||||
"ts-jest": "^29.0.3",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typescript": "^4.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@noble/secp256k1": "^1.7.0",
|
|
||||||
"amcl-js": "^3.1.0",
|
|
||||||
"js-sha256": "^0.10.1"
|
|
||||||
},
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"preinstall": "npx only-allow pnpm",
|
|
||||||
"start": "nodemon",
|
|
||||||
"build": "tsc -p tsconfig.build.json",
|
|
||||||
"prettier": "prettier -c . --config ../.prettierrc --ignore-path ../.prettierignore",
|
|
||||||
"prettier:fix": "prettier -w . --config ../.prettierrc --ignore-path ../.prettierignore",
|
|
||||||
"types": "tsc -p tsconfig.json --noEmit",
|
|
||||||
"test": "jest",
|
|
||||||
"test:coverage": "pnpm run test --coverage",
|
|
||||||
"publish": "pnpm run build && pnpm publish"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2695
javascript/pnpm-lock.yaml
generated
2695
javascript/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
export { computeAllInputs, PlumeVersion } from "./signals";
|
|
||||||
197
javascript/src/lib.rs
Normal file
197
javascript/src/lib.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
//! sadly `wasm-bindgen` doesn't support top-level @module docs yet
|
||||||
|
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
use elliptic_curve::sec1::FromEncodedPoint;
|
||||||
|
use elliptic_curve::sec1::ToEncodedPoint;
|
||||||
|
use signature::RandomizedSigner;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
|
/// @typedef {Object} PlumeSignature - Wrapper around [`plume_rustcrypto::PlumeSignature`](https://docs.rs/plume_rustcrypto/latest/plume_rustcrypto/struct.PlumeSignature.html).
|
||||||
|
/// [`plume_rustcrypto::AffinePoint`](https://docs.rs/plume_rustcrypto/latest/plume_rustcrypto/struct.AffinePoint.html) is represented as a `Uint8Array` containing SEC1 encoded point.
|
||||||
|
/// [`plume_rustcrypto::NonZeroScalar`](https://docs.rs/plume_rustcrypto/latest/plume_rustcrypto/type.NonZeroScalar.html) is represented as a `Uint8Array` containing SEC1 DER secret key.
|
||||||
|
/// `Option` can be `undefined` or instance of [`PlumeSignatureV1Fields`].
|
||||||
|
pub struct PlumeSignature {
|
||||||
|
pub message: Vec<u8>,
|
||||||
|
pub pk: Vec<u8>,
|
||||||
|
pub nullifier: Vec<u8>,
|
||||||
|
pub c: Vec<u8>,
|
||||||
|
pub s: Vec<u8>,
|
||||||
|
pub v1specific: Option<PlumeSignatureV1Fields>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
|
/// @typedef {Object} PlumeSignatureV1Fields - Wrapper around [`plume_rustcrypto::PlumeSignatureV1Fields`](https://docs.rs/plume_rustcrypto/latest/plume_rustcrypto/struct.PlumeSignatureV1Fields.html).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PlumeSignatureV1Fields {
|
||||||
|
pub r_point: Vec<u8>,
|
||||||
|
pub hashed_to_curve_r: Vec<u8>,
|
||||||
|
}
|
||||||
|
#[wasm_bindgen()]
|
||||||
|
impl PlumeSignatureV1Fields {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(r_point: Vec<u8>, hashed_to_curve_r: Vec<u8>) -> PlumeSignatureV1Fields {
|
||||||
|
PlumeSignatureV1Fields {
|
||||||
|
r_point,
|
||||||
|
hashed_to_curve_r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl PlumeSignature {
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
/// @deprecated Use this only for testing purposes.
|
||||||
|
/// @throws an error if the data in the object doesn't let it to properly run verification; message contains nature of the problem and indicates relevant property of the object. In case of other (crypto) problems returns `false`.
|
||||||
|
pub fn verify(self) -> Result<bool, JsError> {
|
||||||
|
Ok(plume_rustcrypto::PlumeSignature::verify(&self.try_into()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// there's no case for constructing it from values, so this only used internally and for testing
|
||||||
|
/// `v1specific` discriminates if it's V1 or V2 scheme used. Pls, see wrapped docs for details.
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(
|
||||||
|
message: Vec<u8>,
|
||||||
|
pk: Vec<u8>,
|
||||||
|
nullifier: Vec<u8>,
|
||||||
|
c: Vec<u8>,
|
||||||
|
s: Vec<u8>,
|
||||||
|
v1specific: Option<PlumeSignatureV1Fields>,
|
||||||
|
) -> PlumeSignature {
|
||||||
|
PlumeSignature {
|
||||||
|
/* I really wonder how good is this pattern. But taking so much of args isn't good, and builder pattern seems redundant as all
|
||||||
|
of the fields are required, and setters are just assignments. */
|
||||||
|
// Actually there's no case for constructing it from values, so this only used internally and for testing.
|
||||||
|
message,
|
||||||
|
pk,
|
||||||
|
nullifier,
|
||||||
|
c,
|
||||||
|
s,
|
||||||
|
v1specific, //: if v1specific.is_falsy() {None} else {Some(v1specific)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// js_sys::Object::from_entries(&values)?
|
||||||
|
// values.get
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = zeroizePrivateParts)]
|
||||||
|
/// Zeroize private values of the object from Wasm memory.
|
||||||
|
pub fn zeroize_privateparts(&mut self) {
|
||||||
|
self.c.zeroize();
|
||||||
|
self.pk.zeroize();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = zeroizeAll)]
|
||||||
|
/// Zeroize all values of the object from Wasm memory.
|
||||||
|
pub fn zeroize_all(&mut self) {
|
||||||
|
self.zeroize_privateparts();
|
||||||
|
self.message.zeroize();
|
||||||
|
self.nullifier.zeroize();
|
||||||
|
self.s.zeroize();
|
||||||
|
if let Some(v1) = self.v1specific.as_mut() {
|
||||||
|
v1.hashed_to_curve_r.zeroize();
|
||||||
|
v1.r_point.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(skip_jsdoc)]
|
||||||
|
/// @throws a "crypto error" in case of a problem with the secret key
|
||||||
|
/// @param {boolean} v1 - is the flag to choose between V1 and V2 output.
|
||||||
|
/// @param {Uint8Array} sk - secret key in SEC1 DER format.
|
||||||
|
/// @param {Uint8Array} msg
|
||||||
|
/// @returns {PlumeSignature}
|
||||||
|
pub fn sign(v1: bool, sk: &mut [u8], msg: &[u8]) -> Result<PlumeSignature, JsError> {
|
||||||
|
let sk_z = plume_rustcrypto::SecretKey::from_sec1_der(sk)?;
|
||||||
|
sk.zeroize();
|
||||||
|
let signer = plume_rustcrypto::randomizedsigner::PlumeSigner::new(&sk_z, v1);
|
||||||
|
|
||||||
|
Ok(signer
|
||||||
|
.sign_with_rng(&mut signature::rand_core::OsRng, msg)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO deprecate when `verify` gone
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
impl TryInto<plume_rustcrypto::PlumeSignature> for PlumeSignature {
|
||||||
|
type Error = JsError;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<plume_rustcrypto::PlumeSignature, Self::Error> {
|
||||||
|
let point_check = |point_bytes: Vec<u8>| -> Result<AffinePoint, anyhow::Error> {
|
||||||
|
let point_encoded = sec1::point::EncodedPoint::from_bytes(point_bytes)?; // TODO improve formatting (quotes e.g.)
|
||||||
|
let result = plume_rustcrypto::AffinePoint::from_encoded_point(&point_encoded);
|
||||||
|
if result.is_none().into() {
|
||||||
|
Err(anyhow::Error::msg("the point isn't on the curve"))
|
||||||
|
} else {
|
||||||
|
Ok(result.expect("`None` is processed the line above"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let err_field_wrap = |name_field: &str, er: anyhow::Error| -> JsError {
|
||||||
|
JsError::new(
|
||||||
|
("while proccessing ".to_owned() + name_field + " :" + er.to_string().as_str())
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(plume_rustcrypto::PlumeSignature {
|
||||||
|
message: self.message,
|
||||||
|
pk: point_check(self.pk).map_err(|er| err_field_wrap("`pk`", er))?,
|
||||||
|
// plume_rustcrypto::AffinePoint::try_from(self.pk)?, //.try_into<[u8; 33]>()?.into(),
|
||||||
|
nullifier: point_check(self.nullifier)
|
||||||
|
.map_err(|er| err_field_wrap("`nullifier`", er))?,
|
||||||
|
c: plume_rustcrypto::SecretKey::from_sec1_der(&self.c)?.into(),
|
||||||
|
s: plume_rustcrypto::SecretKey::from_sec1_der(&self.s)?.into(), //scalar_from_bigint(self.s).map_err(|er| err_field_wrap("`s`", er))?,
|
||||||
|
v1specific: if let Some(v1) = self.v1specific {
|
||||||
|
Some(plume_rustcrypto::PlumeSignatureV1Fields {
|
||||||
|
r_point: point_check(v1.r_point)
|
||||||
|
.map_err(|er| err_field_wrap("`r_point`", er))?,
|
||||||
|
hashed_to_curve_r: point_check(v1.hashed_to_curve_r)
|
||||||
|
.map_err(|er| err_field_wrap("`hashed_to_curve_r`", er))?,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<plume_rustcrypto::PlumeSignature> for PlumeSignature {
|
||||||
|
fn from(value: plume_rustcrypto::PlumeSignature) -> Self {
|
||||||
|
PlumeSignature {
|
||||||
|
message: value.message,
|
||||||
|
pk: value.pk.to_encoded_point(true).as_bytes().to_vec(),
|
||||||
|
nullifier: value.nullifier.to_encoded_point(true).as_bytes().to_vec(),
|
||||||
|
c: plume_rustcrypto::SecretKey::from(value.c).to_sec1_der().expect("`k256` restricts this type to proper keys, so it's serialized representation shouldn't have a chance to fail")
|
||||||
|
.to_vec(),
|
||||||
|
s: plume_rustcrypto::SecretKey::from(value.s).to_sec1_der().expect("`k256` restricts this type to proper keys, so it's serialized representation shouldn't have a chance to fail")
|
||||||
|
.to_vec(),
|
||||||
|
v1specific: value.v1specific.map(|v1| {PlumeSignatureV1Fields {
|
||||||
|
r_point: v1.r_point.to_encoded_point(true).as_bytes().to_vec(),
|
||||||
|
hashed_to_curve_r: v1.hashed_to_curve_r.to_encoded_point(true).as_bytes().to_vec(),
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = sec1DerScalarToBigint)]
|
||||||
|
/// This might leave values in memory! Don't use for private values.
|
||||||
|
/// JS most native format for scalar is `BigInt`, but it's not really transportable or secure, so for uniformity of approach `s` in `PlumeSignature` is defined similar to `c`;
|
||||||
|
/// but if you want to have it as a `BigInt` this util is left here.
|
||||||
|
pub fn sec1derscalar_to_bigint(scalar: &[u8]) -> Result<js_sys::BigInt, JsError> {
|
||||||
|
Ok(js_sys::BigInt::new(&JsValue::from_str(
|
||||||
|
("0x".to_owned()
|
||||||
|
+ plume_rustcrypto::SecretKey::from_sec1_der(scalar)?
|
||||||
|
.to_nonzero_scalar()
|
||||||
|
.to_string()
|
||||||
|
.as_str())
|
||||||
|
.as_str(),
|
||||||
|
))
|
||||||
|
.expect(
|
||||||
|
"`BigInt` always can be created from hex string, and `v.to_string()` always produce that",
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import { CURVE, getPublicKey, Point, utils } from "@noble/secp256k1";
|
|
||||||
import {
|
|
||||||
concatUint8Arrays,
|
|
||||||
hexToBigInt,
|
|
||||||
hexToUint8Array,
|
|
||||||
messageToUint8Array,
|
|
||||||
uint8ArrayToBigInt,
|
|
||||||
} from "./utils/encoding";
|
|
||||||
import hashToCurve from "./utils/hashToCurve";
|
|
||||||
import { HashedPoint, multiplyPoint } from "./utils/curve";
|
|
||||||
import { sha256 } from "js-sha256";
|
|
||||||
|
|
||||||
// PLUME version
|
|
||||||
export enum PlumeVersion {
|
|
||||||
V1 = 1,
|
|
||||||
V2 = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeHashToCurve(
|
|
||||||
message: Uint8Array,
|
|
||||||
pk: Uint8Array,
|
|
||||||
): HashedPoint {
|
|
||||||
// Concatenate message and publicKey
|
|
||||||
const preimage = new Uint8Array(message.length + pk.length);
|
|
||||||
preimage.set(message);
|
|
||||||
preimage.set(pk, message.length);
|
|
||||||
return hashToCurve(Array.from(preimage));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeC_V2(
|
|
||||||
nullifier: Point,
|
|
||||||
rPoint: Point,
|
|
||||||
hashedToCurveR: Point,
|
|
||||||
) {
|
|
||||||
const nullifierBytes = nullifier.toRawBytes(true);
|
|
||||||
const preimage = concatUint8Arrays([
|
|
||||||
nullifierBytes,
|
|
||||||
rPoint.toRawBytes(true),
|
|
||||||
hashedToCurveR.toRawBytes(true),
|
|
||||||
]);
|
|
||||||
return sha256.create().update(preimage).hex();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeC_V1(
|
|
||||||
pkBytes: Uint8Array,
|
|
||||||
hashedToCurve: HashedPoint,
|
|
||||||
nullifier: Point,
|
|
||||||
rPoint: Point,
|
|
||||||
hashedToCurveR: Point,
|
|
||||||
) {
|
|
||||||
const nullifierBytes = nullifier.toRawBytes(true);
|
|
||||||
const preimage = concatUint8Arrays([
|
|
||||||
Point.BASE.toRawBytes(true),
|
|
||||||
pkBytes,
|
|
||||||
new Point(
|
|
||||||
hexToBigInt(hashedToCurve.x.toString()),
|
|
||||||
hexToBigInt(hashedToCurve.y.toString()),
|
|
||||||
).toRawBytes(true),
|
|
||||||
nullifierBytes,
|
|
||||||
rPoint.toRawBytes(true),
|
|
||||||
hashedToCurveR.toRawBytes(true),
|
|
||||||
]);
|
|
||||||
return sha256.create().update(preimage).hex();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeNullifer(hashedToCurve: HashedPoint, sk: Uint8Array) {
|
|
||||||
return multiplyPoint(hashedToCurve, sk);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeRPoint(rScalar: Uint8Array) {
|
|
||||||
return Point.fromPrivateKey(rScalar);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeHashToCurveR(
|
|
||||||
hashedToCurve: HashedPoint,
|
|
||||||
rScalar: Uint8Array,
|
|
||||||
) {
|
|
||||||
return multiplyPoint(hashedToCurve, rScalar);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeS(rScalar: Uint8Array, sk: Uint8Array, c: string) {
|
|
||||||
return (
|
|
||||||
(((uint8ArrayToBigInt(sk) * hexToBigInt(c)) % CURVE.n) +
|
|
||||||
uint8ArrayToBigInt(rScalar)) %
|
|
||||||
CURVE.n
|
|
||||||
).toString(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes and returns the Plume and other signals for the prover.
|
|
||||||
* @param {string | Uint8Array} message - Message to sign, in either string or UTF-8 array format.
|
|
||||||
* @param {string | Uint8Array} sk - ECDSA secret key to sign with.
|
|
||||||
* @param {string| Uint8Array} rScalar - Optional seed for randomness.
|
|
||||||
* @returns Object containing Plume and other signals - public key, s, c, gPowR, and hashMPKPowR.
|
|
||||||
*/
|
|
||||||
export function computeAllInputs(
|
|
||||||
message: string | Uint8Array,
|
|
||||||
sk: string | Uint8Array,
|
|
||||||
rScalar?: string | Uint8Array,
|
|
||||||
version: PlumeVersion = PlumeVersion.V2,
|
|
||||||
) {
|
|
||||||
const skBytes = typeof sk === "string" ? hexToUint8Array(sk) : sk;
|
|
||||||
const messageBytes =
|
|
||||||
typeof message === "string" ? messageToUint8Array(message) : message;
|
|
||||||
const pkBytes = getPublicKey(skBytes, true);
|
|
||||||
|
|
||||||
let rScalarBytes: Uint8Array;
|
|
||||||
if (rScalar) {
|
|
||||||
rScalarBytes =
|
|
||||||
typeof rScalar === "string" ? hexToUint8Array(rScalar) : rScalar;
|
|
||||||
} else {
|
|
||||||
rScalarBytes = utils.randomPrivateKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedToCurve = computeHashToCurve(messageBytes, pkBytes);
|
|
||||||
const nullifier = computeNullifer(hashedToCurve, skBytes);
|
|
||||||
const hashedToCurveR = computeHashToCurveR(hashedToCurve, rScalarBytes);
|
|
||||||
const rPoint = computeRPoint(rScalarBytes);
|
|
||||||
const c =
|
|
||||||
version == PlumeVersion.V1
|
|
||||||
? computeC_V1(pkBytes, hashedToCurve, nullifier, rPoint, hashedToCurveR)
|
|
||||||
: computeC_V2(nullifier, rPoint, hashedToCurveR);
|
|
||||||
const s = computeS(rScalarBytes, skBytes, c);
|
|
||||||
|
|
||||||
return {
|
|
||||||
plume: nullifier,
|
|
||||||
s,
|
|
||||||
pk: pkBytes,
|
|
||||||
c,
|
|
||||||
rPoint,
|
|
||||||
hashedToCurveR,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Point } from "@noble/secp256k1";
|
|
||||||
import { uint8ArrayToHex } from "./encoding";
|
|
||||||
|
|
||||||
export interface HashedPoint {
|
|
||||||
x: {
|
|
||||||
toString(): string;
|
|
||||||
};
|
|
||||||
y: {
|
|
||||||
toString(): string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function multiplyPoint(h: HashedPoint, secretKey: Uint8Array) {
|
|
||||||
const hashPoint = new Point(
|
|
||||||
BigInt("0x" + h.x.toString()),
|
|
||||||
BigInt("0x" + h.y.toString()),
|
|
||||||
);
|
|
||||||
|
|
||||||
return hashPoint.multiply(BigInt("0x" + uint8ArrayToHex(secretKey)));
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
const utf8Encoder = new TextEncoder();
|
|
||||||
|
|
||||||
export function messageToUint8Array(message: string): Uint8Array {
|
|
||||||
return utf8Encoder.encode(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hexToUint8Array(hexString: string): Uint8Array {
|
|
||||||
// Source: https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript/50868276#50868276
|
|
||||||
return Uint8Array.from(
|
|
||||||
hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uint8ArrayToHex(uint8Array: Uint8Array) {
|
|
||||||
// Source: https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript/50868276#50868276
|
|
||||||
return uint8Array.reduce(
|
|
||||||
(str, byte) => str + byte.toString(16).padStart(2, "0"),
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hexToBigInt(hex: string): bigint {
|
|
||||||
return BigInt("0x" + hex);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uint8ArrayToBigInt(buffer: Uint8Array): bigint {
|
|
||||||
return hexToBigInt(uint8ArrayToHex(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function asciitobytes(s: string): number[] {
|
|
||||||
const b: number[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < s.length; i++) {
|
|
||||||
b.push(s.charCodeAt(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function concatUint8Arrays(arrays: Uint8Array[]) {
|
|
||||||
// sum of individual array lengths
|
|
||||||
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
|
||||||
|
|
||||||
const result = new Uint8Array(totalLength);
|
|
||||||
|
|
||||||
if (!arrays.length) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// for each array - copy it over result
|
|
||||||
// next array is copied right after the previous one
|
|
||||||
let length = 0;
|
|
||||||
for (let array of arrays) {
|
|
||||||
result.set(array, length);
|
|
||||||
length += array.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { CTX } from "amcl-js";
|
|
||||||
import { asciitobytes } from "./encoding";
|
|
||||||
|
|
||||||
// Refactored from miracl-core
|
|
||||||
const ctx = new CTX("SECP256K1") as any;
|
|
||||||
const ro = "QUUX-V01-CS02-with-secp256k1_XMD:SHA-256_SSWU_RO_";
|
|
||||||
const hlen = ctx.ECP.HASH_TYPE;
|
|
||||||
|
|
||||||
function ceil(a, b) {
|
|
||||||
return Math.floor((a - 1) / b + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashToField(ctx, hash, hlen, DST, M, ctr) {
|
|
||||||
const u = [];
|
|
||||||
const q = new ctx.BIG(0);
|
|
||||||
q.rcopy(ctx.ROM_FIELD.Modulus);
|
|
||||||
const k = q.nbits();
|
|
||||||
const r = new ctx.BIG(0);
|
|
||||||
r.rcopy(ctx.ROM_CURVE.CURVE_Order);
|
|
||||||
const m = r.nbits();
|
|
||||||
const L = ceil(k + ceil(m, 2), 8);
|
|
||||||
const OKM = ctx.HMAC.XMD_Expand(hash, hlen, L * ctr, DST, M);
|
|
||||||
const fd = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < ctr; i++) {
|
|
||||||
for (let j = 0; j < L; j++) {
|
|
||||||
fd[j] = OKM[i * L + j];
|
|
||||||
}
|
|
||||||
|
|
||||||
const dx = ctx.DBIG.fromBytes(fd);
|
|
||||||
const w = new ctx.FP(dx.mod(q));
|
|
||||||
u[i] = new ctx.FP(w);
|
|
||||||
}
|
|
||||||
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from https://github.com/miracl/core/blob/master/javascript/examples/node/TestHTP.js#L37
|
|
||||||
function hashToPairing(ctx, M, ro, hlen) {
|
|
||||||
const DSTRO = asciitobytes(ro);
|
|
||||||
const u = hashToField(ctx, ctx.HMAC.MC_SHA2, hlen, DSTRO, M, 2);
|
|
||||||
const P = ctx.ECP.map2point(u[0]);
|
|
||||||
const P1 = ctx.ECP.map2point(u[1]);
|
|
||||||
P.add(P1);
|
|
||||||
P.cfp();
|
|
||||||
P.affine();
|
|
||||||
|
|
||||||
return P;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function hashToCurve(bytes: number[]) {
|
|
||||||
return hashToPairing(ctx, bytes, ro, hlen);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { getPublicKey, Point } from "@noble/secp256k1";
|
|
||||||
import {
|
|
||||||
computeC_V1,
|
|
||||||
computeC_V2,
|
|
||||||
computeRPoint,
|
|
||||||
computeHashToCurve,
|
|
||||||
computeHashToCurveR,
|
|
||||||
computeNullifer,
|
|
||||||
computeS,
|
|
||||||
} from "../src/signals";
|
|
||||||
import { hexToUint8Array, messageToUint8Array } from "../src/utils/encoding";
|
|
||||||
|
|
||||||
export const testSecretKey = hexToUint8Array(
|
|
||||||
"519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464",
|
|
||||||
);
|
|
||||||
|
|
||||||
export const testPublicKeyPoint = Point.fromPrivateKey(testSecretKey);
|
|
||||||
export const testPublicKey = getPublicKey(testSecretKey, true);
|
|
||||||
|
|
||||||
export const testR = hexToUint8Array(
|
|
||||||
"93b9323b629f251b8f3fc2dd11f4672c5544e8230d493eceea98a90bda789808",
|
|
||||||
);
|
|
||||||
export const testMessageString = "An example app message string";
|
|
||||||
export const testMessage = messageToUint8Array(testMessageString);
|
|
||||||
export const hashMPk = computeHashToCurve(
|
|
||||||
testMessage,
|
|
||||||
Buffer.from(testPublicKey),
|
|
||||||
);
|
|
||||||
export const nullifier = computeNullifer(hashMPk, testSecretKey);
|
|
||||||
export const hashedToCurveR = computeHashToCurveR(hashMPk, testR);
|
|
||||||
export const rPoint = computeRPoint(testR);
|
|
||||||
export const c_v1 = computeC_V1(
|
|
||||||
testPublicKey,
|
|
||||||
hashMPk,
|
|
||||||
nullifier as unknown as Point,
|
|
||||||
rPoint,
|
|
||||||
hashedToCurveR,
|
|
||||||
);
|
|
||||||
export const s_v1 = computeS(testR, testSecretKey, c_v1);
|
|
||||||
|
|
||||||
export const c_v2 = computeC_V2(nullifier, rPoint, hashedToCurveR);
|
|
||||||
export const s_v2 = computeS(testR, testSecretKey, c_v2);
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import {
|
|
||||||
hexToBigInt,
|
|
||||||
hexToUint8Array,
|
|
||||||
uint8ArrayToBigInt,
|
|
||||||
uint8ArrayToHex,
|
|
||||||
} from "../src/utils/encoding";
|
|
||||||
|
|
||||||
const TEST_VALS = [
|
|
||||||
{
|
|
||||||
hex: "a413bc5f",
|
|
||||||
uint8: Uint8Array.from([164, 19, 188, 95]),
|
|
||||||
bigint: 2752756831n,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "f09f8fb3efb88fe2808df09f8c88",
|
|
||||||
uint8: Uint8Array.from([
|
|
||||||
240, 159, 143, 179, 239, 184, 143, 226, 128, 141, 240, 159, 140, 136,
|
|
||||||
]),
|
|
||||||
bigint: 4880420056602345253094210752449672n,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("encoding", () => {
|
|
||||||
it("hexToUint8Array", () => {
|
|
||||||
expect(hexToUint8Array(TEST_VALS[0].hex)).toEqual(TEST_VALS[0].uint8);
|
|
||||||
expect(hexToUint8Array(TEST_VALS[1].hex)).toEqual(TEST_VALS[1].uint8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uint8ArrayToHex", () => {
|
|
||||||
expect(uint8ArrayToHex(TEST_VALS[0].uint8)).toEqual(TEST_VALS[0].hex);
|
|
||||||
expect(uint8ArrayToHex(TEST_VALS[1].uint8)).toEqual(TEST_VALS[1].hex);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hexToBigInt", () => {
|
|
||||||
expect(hexToBigInt(TEST_VALS[0].hex)).toEqual(TEST_VALS[0].bigint);
|
|
||||||
expect(hexToBigInt(TEST_VALS[1].hex)).toEqual(TEST_VALS[1].bigint);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uint8ArrayToBigInt", () => {
|
|
||||||
expect(uint8ArrayToBigInt(TEST_VALS[0].uint8)).toEqual(TEST_VALS[0].bigint);
|
|
||||||
expect(uint8ArrayToBigInt(TEST_VALS[1].uint8)).toEqual(TEST_VALS[1].bigint);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import hashToCurve from "../src/utils/hashToCurve";
|
|
||||||
|
|
||||||
describe("hashToCurve", () => {
|
|
||||||
it("successfully hashes correct values", () => {
|
|
||||||
const testPreimage = [
|
|
||||||
65, 110, 32, 101, 120, 97, 109, 112, 108, 101, 32, 97, 112, 112, 32, 109,
|
|
||||||
101, 115, 115, 97, 103, 101, 32, 115, 116, 114, 105, 110, 103, 3, 12, 236,
|
|
||||||
2, 142, 224, 141, 9, 224, 38, 114, 166, 131, 16, 129, 67, 84, 249, 234,
|
|
||||||
191, 255, 13, 230, 218, 204, 28, 211, 167, 116, 73, 96, 118, 174,
|
|
||||||
];
|
|
||||||
|
|
||||||
const hash = hashToCurve(testPreimage);
|
|
||||||
|
|
||||||
expect(hash.x.toString()).toEqual(
|
|
||||||
"bcac2d0e12679f23c218889395abcdc01f2affbc49c54d1136a2190db0800b65",
|
|
||||||
);
|
|
||||||
expect(hash.y.toString()).toEqual(
|
|
||||||
"3bcfb339c974c0e757d348081f90a123b0a91a53e32b3752145d87f0cd70966e",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import {
|
|
||||||
hashMPk,
|
|
||||||
nullifier,
|
|
||||||
hashedToCurveR,
|
|
||||||
rPoint,
|
|
||||||
c_v1,
|
|
||||||
s_v1,
|
|
||||||
c_v2,
|
|
||||||
s_v2,
|
|
||||||
testPublicKey,
|
|
||||||
testSecretKey,
|
|
||||||
testMessage,
|
|
||||||
testR,
|
|
||||||
} from "./consts";
|
|
||||||
|
|
||||||
import { computeAllInputs } from "../src";
|
|
||||||
import { PlumeVersion } from "../src/signals";
|
|
||||||
|
|
||||||
describe("signals", () => {
|
|
||||||
it("generates hash(m, pk)", () => {
|
|
||||||
expect(hashMPk.x.toString()).toEqual(
|
|
||||||
"bcac2d0e12679f23c218889395abcdc01f2affbc49c54d1136a2190db0800b65",
|
|
||||||
);
|
|
||||||
expect(hashMPk.y.toString()).toEqual(
|
|
||||||
"3bcfb339c974c0e757d348081f90a123b0a91a53e32b3752145d87f0cd70966e",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates nullifier (hash(m, pk))^sk", () => {
|
|
||||||
expect(nullifier.x.toString(16)).toEqual(
|
|
||||||
"57bc3ed28172ef8adde4b9e0c2cce745fcc5a66473a45c1e626f1d0c67e55830",
|
|
||||||
);
|
|
||||||
expect(nullifier.y.toString(16)).toEqual(
|
|
||||||
"6a2f41488d58f33ae46edd2188e111609f9f3ae67ea38fa891d6087fe59ecb73",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Plume V1", () => {
|
|
||||||
it("generates c and intermediate values correctly", () => {
|
|
||||||
expect(hashedToCurveR.x.toString(16)).toEqual(
|
|
||||||
"6d017c6f63c59fa7a5b1e9a654e27d2869579f4d152131db270558fccd27b97c",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(hashedToCurveR.y.toString(16)).toEqual(
|
|
||||||
"586c43fb5c99818c564a8f80a88a65f83e3f44d3c6caf5a1a4e290b777ac56ed",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(rPoint.x.toString(16)).toEqual(
|
|
||||||
"9d8ca4350e7e2ad27abc6d2a281365818076662962a28429590e2dc736fe9804",
|
|
||||||
);
|
|
||||||
expect(rPoint.y.toString(16)).toEqual(
|
|
||||||
"ff08c30b8afd4e854623c835d9c3aac6bcebe45112472d9b9054816a7670c5a1",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(c_v1).toEqual(
|
|
||||||
"c6a7fc2c926ddbaf20731a479fb6566f2daa5514baae5223fe3b32edbce83254",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates an s signal", () => {
|
|
||||||
expect(s_v1).toEqual(
|
|
||||||
"e69f027d84cb6fe5f761e333d12e975fb190d163e8ea132d7de0bd6079ba28ca",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates all signals", () => {
|
|
||||||
const { plume, s, pk, c, rPoint, hashedToCurveR } = computeAllInputs(
|
|
||||||
testMessage,
|
|
||||||
testSecretKey,
|
|
||||||
testR,
|
|
||||||
PlumeVersion.V1,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(pk).toEqual(testPublicKey);
|
|
||||||
expect(rPoint.x.toString(16)).toEqual(
|
|
||||||
"9d8ca4350e7e2ad27abc6d2a281365818076662962a28429590e2dc736fe9804",
|
|
||||||
);
|
|
||||||
expect(rPoint.y.toString(16)).toEqual(
|
|
||||||
"ff08c30b8afd4e854623c835d9c3aac6bcebe45112472d9b9054816a7670c5a1",
|
|
||||||
);
|
|
||||||
expect(plume.x.toString(16)).toEqual(
|
|
||||||
"57bc3ed28172ef8adde4b9e0c2cce745fcc5a66473a45c1e626f1d0c67e55830",
|
|
||||||
);
|
|
||||||
expect(plume.y.toString(16)).toEqual(
|
|
||||||
"6a2f41488d58f33ae46edd2188e111609f9f3ae67ea38fa891d6087fe59ecb73",
|
|
||||||
);
|
|
||||||
expect(hashedToCurveR.x.toString(16)).toEqual(
|
|
||||||
"6d017c6f63c59fa7a5b1e9a654e27d2869579f4d152131db270558fccd27b97c",
|
|
||||||
);
|
|
||||||
expect(c).toEqual(
|
|
||||||
"c6a7fc2c926ddbaf20731a479fb6566f2daa5514baae5223fe3b32edbce83254",
|
|
||||||
);
|
|
||||||
expect(s).toEqual(
|
|
||||||
"e69f027d84cb6fe5f761e333d12e975fb190d163e8ea132d7de0bd6079ba28ca",
|
|
||||||
);
|
|
||||||
expect(hashedToCurveR.y.toString(16)).toEqual(
|
|
||||||
"586c43fb5c99818c564a8f80a88a65f83e3f44d3c6caf5a1a4e290b777ac56ed",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Plume V2", () => {
|
|
||||||
it("generates c and intermediate values correctly", () => {
|
|
||||||
expect(hashedToCurveR.x.toString(16)).toEqual(
|
|
||||||
"6d017c6f63c59fa7a5b1e9a654e27d2869579f4d152131db270558fccd27b97c",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(hashedToCurveR.y.toString(16)).toEqual(
|
|
||||||
"586c43fb5c99818c564a8f80a88a65f83e3f44d3c6caf5a1a4e290b777ac56ed",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(rPoint.x.toString(16)).toEqual(
|
|
||||||
"9d8ca4350e7e2ad27abc6d2a281365818076662962a28429590e2dc736fe9804",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(rPoint.y.toString(16)).toEqual(
|
|
||||||
"ff08c30b8afd4e854623c835d9c3aac6bcebe45112472d9b9054816a7670c5a1",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(c_v2).toEqual(
|
|
||||||
"3dbfb717705010d4f44a70720c95e74b475bd3a783ab0b9e8a6b3b363434eb96",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates an s signal", () => {
|
|
||||||
expect(s_v2).toEqual(
|
|
||||||
"528e8fbb6452f82200797b1a73b2947a92524bd611085a920f1177cb8098136b",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("generates all signals", () => {
|
|
||||||
const { plume, s, pk, c, rPoint, hashedToCurveR } = computeAllInputs(
|
|
||||||
testMessage,
|
|
||||||
testSecretKey,
|
|
||||||
testR,
|
|
||||||
PlumeVersion.V2,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(pk).toEqual(testPublicKey);
|
|
||||||
expect(rPoint.x.toString(16)).toEqual(
|
|
||||||
"9d8ca4350e7e2ad27abc6d2a281365818076662962a28429590e2dc736fe9804",
|
|
||||||
);
|
|
||||||
expect(rPoint.y.toString(16)).toEqual(
|
|
||||||
"ff08c30b8afd4e854623c835d9c3aac6bcebe45112472d9b9054816a7670c5a1",
|
|
||||||
);
|
|
||||||
expect(plume.x.toString(16)).toEqual(
|
|
||||||
"57bc3ed28172ef8adde4b9e0c2cce745fcc5a66473a45c1e626f1d0c67e55830",
|
|
||||||
);
|
|
||||||
expect(plume.y.toString(16)).toEqual(
|
|
||||||
"6a2f41488d58f33ae46edd2188e111609f9f3ae67ea38fa891d6087fe59ecb73",
|
|
||||||
);
|
|
||||||
expect(hashedToCurveR.x.toString(16)).toEqual(
|
|
||||||
"6d017c6f63c59fa7a5b1e9a654e27d2869579f4d152131db270558fccd27b97c",
|
|
||||||
);
|
|
||||||
expect(c).toEqual(
|
|
||||||
"3dbfb717705010d4f44a70720c95e74b475bd3a783ab0b9e8a6b3b363434eb96",
|
|
||||||
);
|
|
||||||
expect(s).toEqual(
|
|
||||||
"528e8fbb6452f82200797b1a73b2947a92524bd611085a920f1177cb8098136b",
|
|
||||||
);
|
|
||||||
expect(hashedToCurveR.y.toString(16)).toEqual(
|
|
||||||
"586c43fb5c99818c564a8f80a88a65f83e3f44d3c6caf5a1a4e290b777ac56ed",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["src"],
|
|
||||||
"exclude": ["test/**/*.test.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2021",
|
|
||||||
"allowJs": true,
|
|
||||||
"declaration": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"typeRoots": ["./node_modules/@types"],
|
|
||||||
"outDir": "./dist"
|
|
||||||
},
|
|
||||||
"compileOnSave": true,
|
|
||||||
"include": ["src", "test"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user