# "Hazmat"
was removed since `arkworks` security model differs significantly from RustCrypto, and approaching it from the standpoint of the PLUME crate published earlier isn't really a correct way. \
Still I'd like to have a better look into `zeroize` for this crate before calling it `v.0.1`.
# `test_vectors`
were taken from the deprecated crate for SEC1 encoding testing.
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))}
};
```
See issues #67 and #84 .
Few snippets of other paths I considered.
```rust
...
[ProjectivePoint::GENERATOR.to_encoded_point(true).as_bytes(), &pk_bytes, enc!(hashed_to_curve)].map(|b| hashers[0].update(b));
for b in [enc!(nullifier), enc![r_point], enc!(hashed_to_curve_r)] {
hashers[0].update(b);
hashers[1].update(b);
}
let c = hashers.map(|h| h.finalize());
let c_scalar = c.clone().map(|c_i| NonZeroScalar::reduce_nonzero(U256::from_be_byte_array(c_i)));
// Compute s = r + sk ⋅ c
let s_scalar = c_scalar.map(|c_scalar_i| NonZeroScalar::new(*r_scalar + *(self.to_nonzero_scalar() * c_scalar_i))
.expect("something is terribly wrong if the nonce is equal to negated product of the secret and the hash"));
Ok(PlumeSignatureCombined{
message: msg.to_owned(),
pk: pk.into(),
nullifier: nullifier.to_point(),
v2_c: c[1],
v2_s: *s_scalar[1],
v1_c: c[0],
v1_s: *s_scalar[0],
v1_r_point: r_point.into(),
v1_hashed_to_curve_r: hashed_to_curve_r.to_point(),
})
}
}
/// Struct holding mandatory signature data for ... PLUME signature
#[derive(Debug)]
pub struct PlumeSignatureCombined {
/// The message that was signed.
pub message: Vec<u8>,
/// The public key used to verify the signature.
pub pk: ProjectivePoint,
/// The nullifier.
pub nullifier: ProjectivePoint,
/// Part of the signature data.
pub v2_c: Output<Sha256>,
/// Part of the signature data, a scalar value.
pub v2_s: Scalar,
/// Part of the signature data.
pub v1_c: Output<Sha256>,
/// Part of the signature data, a scalar value.
pub v1_s: Scalar,
/// Part of the V1 signature data, a curve point.
pub v1_r_point: ProjectivePoint,
/// Part of the V1 signature data, a curve point.
pub v1_hashed_to_curve_r: ProjectivePoint,
}
impl PlumeSignatureCombined {
pub fn separate(self) -> (PlumeSignature, PlumeSignature) {
let (pk, nullifier) = (self.pk, self.nullifier);
(
PlumeSignature{
message: self.message.clone(), pk, nullifier, c: self.v1_c, s: self.v1_s, v1specific: Some(
PlumeSignatureV1Fields{ r_point: self.v1_r_point, hashed_to_curve_r: self.v1_hashed_to_curve_r }
)
},
PlumeSignature{ message: self.message, pk, nullifier, c: self.v2_c, s: self.v2_s, v1specific: None },
)
}
}
```
_____________________________________
```rust
pub enum PlumeSignature{
V1(PlumeSignatureV1),
V2(PlumeSignatureV2),
}
pub struct PlumeSignatureV1 {
v2: PlumeSignatureV2,
v1: PlumeSignatureV1Fields
}
/// Struct holding mandatory signature data for a PLUME signature.
pub struct PlumeSignatureV2 {
/// The message that was signed.
pub message: Vec<u8>,
/// The public key used to verify the signature.
pub pk: ProjectivePoint,
/// The nullifier.
pub nullifier: ProjectivePoint,
/// Part of the signature data.
pub c: Output<Sha256>,
/// Part of the signature data, a scalar value.
pub s: Scalar,
// /// Optional signature data for variant 1 signatures.
// pub v1: Option<PlumeSignatureV1Fields>,
}
/// struct holding additional signature data used in variant 1 of the protocol.
#[derive(Debug)]
pub struct PlumeSignatureV1Fields {
/// Part of the signature data, a curve point.
pub r_point: ProjectivePoint,
/// Part of the signature data, a curve point.
pub hashed_to_curve_r: ProjectivePoint,
}
impl signature::RandomizedSigner<PlumeSignatureV1> for SecretKey {}
impl signature::RandomizedSigner<PlumeSignatureV2> for SecretKey {}
```
---------
Co-authored-by: skaunov <skaunov@disroot.org>
* current progress
* link to relevant issue
* crate name edit
* Flat the docs entities
* meta information
* Update the crate name in `tests`
* current progress
here I came to <https://github.com/plume-sig/zk-nullifier-sig/issues/84>
* Refactor tests
And reread the thing estimating clarity.
* `fmt` ready
* Tests improvements
Divides tests into unit and integrational.
Clean and structure info it provided.
Disjoints `mod helpers` from the `release`; along the road where pastes and adapts "one-liners" where it relied on the actual code under the testing (preserves used function calls indication on the best efforts as comments, they're good to be removed on a next iteration).
should add that it's also needed to
* convert `tests` to integration,
* clean hex strings assertions for further comprehensibilty. \
(This well might demote `AsRef` issue to a _nice to have_ thing.)