Files
zk-nullifier-sig/javascript/README.md
Sergey Kaunov 83e3ed91f0 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))}
    };
```
2024-07-17 03:15:22 +03:00

3.6 KiB

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.

// ...
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

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.

// ...
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.

// ...
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.

// ...
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.