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:
Sergey Kaunov
2024-07-17 03:15:22 +03:00
committed by GitHub
parent bb24ab0ecf
commit 83e3ed91f0
25 changed files with 417 additions and 3421 deletions

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = ["rust-arkworks", "rust-k256"]
members = ["rust-arkworks", "rust-k256", "javascript"]
[patch.crates-io]
ark-ec = { git = "https://github.com/FindoraNetwork/ark-algebra" }

11
javascript/.appveyor.yml Normal file
View 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
View 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
View File

@@ -1,9 +1,6 @@
# 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
/dist
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

View File

@@ -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
View 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
View 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"

View File

@@ -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
View 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>.

View File

@@ -1,5 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
export { computeAllInputs, PlumeVersion } from "./signals";

197
javascript/src/lib.rs Normal file
View 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",
))
}

View File

@@ -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,
};
}

View File

@@ -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)));
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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",
);
});
});

View File

@@ -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",
);
});
});
});

View File

@@ -1,5 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["test/**/*.test.ts"]
}

View File

@@ -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"]
}