From b603aeeccbeefe979d0212546bc0cfc72581fdb9 Mon Sep 17 00:00:00 2001 From: arvidn Date: Fri, 7 Oct 2022 16:04:26 +0200 Subject: [PATCH 1/4] add new create chia-keys, with support for all Chia key operations, from BIP39, hardened and unhardened key derivation, signatures, aggregation and verification exclude the python wheel from the cargo workspace. PyO3 doesn't like cargo test or cargo bench --- .github/workflows/benchmark.yml | 22 +- .github/workflows/build-test.yml | 4 +- Cargo.toml | 5 +- chia-bls/Cargo.toml | 31 ++ chia-bls/README.md | 76 +++++ chia-bls/benches/derive_key.rs | 46 +++ chia-bls/benches/sign.rs | 29 ++ chia-bls/benches/verify.rs | 33 ++ chia-bls/src/derivable_key.rs | 3 + chia-bls/src/derive_keys.rs | 44 +++ chia-bls/src/lib.rs | 6 + chia-bls/src/mnemonic.rs | 97 ++++++ chia-bls/src/public_key.rs | 112 +++++++ chia-bls/src/secret_key.rs | 305 +++++++++++++++++++ chia-bls/src/signature.rs | 505 +++++++++++++++++++++++++++++++ 15 files changed, 1314 insertions(+), 4 deletions(-) create mode 100644 chia-bls/Cargo.toml create mode 100644 chia-bls/README.md create mode 100644 chia-bls/benches/derive_key.rs create mode 100644 chia-bls/benches/sign.rs create mode 100644 chia-bls/benches/verify.rs create mode 100644 chia-bls/src/derivable_key.rs create mode 100644 chia-bls/src/derive_keys.rs create mode 100644 chia-bls/src/lib.rs create mode 100644 chia-bls/src/mnemonic.rs create mode 100644 chia-bls/src/public_key.rs create mode 100644 chia-bls/src/secret_key.rs create mode 100644 chia-bls/src/signature.rs diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 48a33db74..1a05eecb1 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -70,7 +70,7 @@ jobs: run: | pytest tests - benchmarks: + generator-benchmarks: name: Generator performance runs-on: benchmark strategy: @@ -121,3 +121,23 @@ jobs: cd tests ./generate-programs.py ./run-programs.py + + benchmarks: + name: rust benchmarks + runs-on: benchmark + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Set up rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: cargo bench + run: | + cargo bench --all diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6c23d9d82..a78961ac0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -310,7 +310,7 @@ jobs: - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features --all + args: --all-features --workspace fuzz_targets: runs-on: ubuntu-latest @@ -338,7 +338,7 @@ jobs: with: toolchain: stable - name: cargo test - run: cargo test + run: cargo test --workspace --all-features upload: name: Upload to PyPI - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} diff --git a/Cargo.toml b/Cargo.toml index c7b9aef93..bb702d0ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ +# the "wheel" crate is excluded from the workspace because pyo3 has problems with +# "cargo test" and "cargo bench" [workspace] -members = ["wasm", "wheel", "chia_streamable_macro"] +members = ["wasm", "chia_streamable_macro", "chia-bls"] +exclude = ["wheel"] [package] name = "chia" diff --git a/chia-bls/Cargo.toml b/chia-bls/Cargo.toml new file mode 100644 index 000000000..fbd6a85b0 --- /dev/null +++ b/chia-bls/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "chia-bls" +version = "0.1.0" +edition = "2021" + +[dependencies] +tiny-bip39 = "=1.0.0" +anyhow = "=1.0.65" +# the newer sha2 crate doesn't implement the digest traits required by hkdf +sha2 = "=0.9.9" +bls12_381_plus = "=0.7.0" +num-bigint = "=0.4.3" +hkdf = "=0.11.0" +group = "=0.12.0" + +[dev-dependencies] +hex = "^0.4.3" +rand = "^0.8.5" +criterion = "^0.4" + +[[bench]] +name = "derive_key" +harness = false + +[[bench]] +name = "sign" +harness = false + +[[bench]] +name = "verify" +harness = false diff --git a/chia-bls/README.md b/chia-bls/README.md new file mode 100644 index 000000000..96e1e2d8a --- /dev/null +++ b/chia-bls/README.md @@ -0,0 +1,76 @@ +Library providing building blocks for a Chia wallet. + +BIP39 mnemonic handling: + +``` +fn entropy_to_mnemonic(entropy: &[u8; 32]) -> String +fn mnemonic_to_entropy(mnemonic: &str) -> Result<[u8; 32], Error> +fn entropy_to_seed(entropy: &[u8; 32]) -> [u8; 64] +``` + +SecretKey + +``` +impl SecretKey { + pub fn from_seed(seed: &[u8; 64]) -> SecretKey + pub fn from_bytes(bytes: &[u8; 32]) -> Option + pub fn to_bytes(&self) -> [u8; 32] + + pub fn public_key(&self) -> PublicKey + + pub fn derive_unhardened(&self, idx: u32) -> SecretKey + pub fn derive_hardened(&self, idx: u32) -> SecretKey +} +``` + +PublicKey + +``` +impl PublicKey { + pub fn from_bytes(bytes: &[u8; 48]) -> Option + pub fn to_bytes(&self) -> [u8; 48] + pub fn derive_unhardened(&self, idx: u32) -> PublicKey +} +``` + +Unhardened Key derivation (`Key` can be both a secret- or public key) + +``` +fn master_to_wallet_unhardened_intermediate(key: &Key) -> Key +fn master_to_wallet_unhardened(key: &Key, idx: u32) -> Key + +``` + +Hardened key derivation (only SecretKey) + +``` +fn master_to_wallet_hardened_intermediate(key: &SecretKey) -> SecretKey +fn master_to_wallet_hardened(key: &SecretKey, idx: u32) -> SecretKey +fn master_to_pool_singleton(key: &SecretKey, pool_wallet_idx: u32) -> SecretKey +fn master_to_pool_authentication(key: &SecretKey, pool_wallet_idx: u32, idx: u32) -> SecretKey +``` + +Signature + +``` +impl Signature { + pub fn from_bytes(buf: &[u8; 96]) -> Option + pub fn to_bytes(&self) -> [u8; 96] + pub fn aggregate(&mut self, sig: &Signature) +} + +impl Default for Signature { + fn default() -> Self +} +``` + +sign and verify (using the Augmented scheme) + +``` +pub fn sign>(sk: &SecretKey, msg: Msg) -> Signature +pub fn aggregate, I>(sigs: I) -> Signature + where I: IntoIterator +pub fn verify>(sig: &Signature, key: &PublicKey, msg: Msg) -> bool +pub fn aggregate_verify, Msg: Borrow<[u8]>, I>(sig: &Signature, data: I) -> bool + where I: IntoIterator +``` diff --git a/chia-bls/benches/derive_key.rs b/chia-bls/benches/derive_key.rs new file mode 100644 index 000000000..16749ccb0 --- /dev/null +++ b/chia-bls/benches/derive_key.rs @@ -0,0 +1,46 @@ +use chia_bls::derivable_key::DerivableKey; +use chia_bls::secret_key::SecretKey; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; +use std::time::Instant; + +fn key_derivation_benchmark(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + rng.fill(data.as_mut_slice()); + + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + + c.bench_function("secret key, unhardened", |b| { + b.iter_custom(|iters| { + let start = Instant::now(); + for i in 0..iters { + black_box(sk.derive_unhardened(i as u32)); + } + start.elapsed() + }) + }); + c.bench_function("secret key, hardened", |b| { + b.iter_custom(|iters| { + let start = Instant::now(); + for i in 0..iters { + black_box(sk.derive_hardened(i as u32)); + } + start.elapsed() + }) + }); + c.bench_function("public key, unhardened", |b| { + b.iter_custom(|iters| { + let start = Instant::now(); + for i in 0..iters { + black_box(pk.derive_unhardened(i as u32)); + } + start.elapsed() + }) + }); +} + +criterion_group!(key_derivation, key_derivation_benchmark); +criterion_main!(key_derivation); diff --git a/chia-bls/benches/sign.rs b/chia-bls/benches/sign.rs new file mode 100644 index 000000000..0b0ca381f --- /dev/null +++ b/chia-bls/benches/sign.rs @@ -0,0 +1,29 @@ +use chia_bls::secret_key::SecretKey; +use chia_bls::signature::sign; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +fn sign_benchmark(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + rng.fill(data.as_mut_slice()); + + let sk = SecretKey::from_seed(&data); + let small_msg = b"The quick brown fox jumps over the lazy dog"; + let large_msg = [42_u8; 4096]; + + c.bench_function("sign, small msg", |b| { + b.iter(|| { + sign(&sk, black_box(&small_msg)); + }); + }); + c.bench_function("sign, 4kiB msg", |b| { + b.iter(|| { + sign(&sk, black_box(&large_msg)); + }); + }); +} + +criterion_group!(signing, sign_benchmark); +criterion_main!(signing); diff --git a/chia-bls/benches/verify.rs b/chia-bls/benches/verify.rs new file mode 100644 index 000000000..bd27706bf --- /dev/null +++ b/chia-bls/benches/verify.rs @@ -0,0 +1,33 @@ +use chia_bls::secret_key::SecretKey; +use chia_bls::signature; +use chia_bls::signature::sign; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +fn verify_benchmark(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + rng.fill(data.as_mut_slice()); + + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + let msg_small = b"The quick brown fox jumps over the lazy dog"; + let msg_large = [42_u8; 4096]; + let sig_small = sign(&sk, &msg_small); + let sig_large = sign(&sk, &msg_large); + + c.bench_function("verify, small msg", |b| { + b.iter(|| { + signature::verify(&sig_small, &pk, black_box(&msg_small)); + }); + }); + c.bench_function("verify, 4kiB msg", |b| { + b.iter(|| { + signature::verify(&sig_large, &pk, black_box(&msg_large)); + }); + }); +} + +criterion_group!(verify, verify_benchmark); +criterion_main!(verify); diff --git a/chia-bls/src/derivable_key.rs b/chia-bls/src/derivable_key.rs new file mode 100644 index 000000000..0d4d5645b --- /dev/null +++ b/chia-bls/src/derivable_key.rs @@ -0,0 +1,3 @@ +pub trait DerivableKey { + fn derive_unhardened(&self, idx: u32) -> Self; +} diff --git a/chia-bls/src/derive_keys.rs b/chia-bls/src/derive_keys.rs new file mode 100644 index 000000000..ea789975d --- /dev/null +++ b/chia-bls/src/derive_keys.rs @@ -0,0 +1,44 @@ +use crate::derivable_key::DerivableKey; +use crate::secret_key::SecretKey; + +fn derive_path_unhardened(key: &Key, path: &[u32]) -> Key { + let mut derived = key.derive_unhardened(path[0]); + for idx in &path[1..] { + derived = derived.derive_unhardened(*idx); + } + derived +} + +fn derive_path_hardened(key: &SecretKey, path: &[u32]) -> SecretKey { + let mut derived = key.derive_hardened(path[0]); + for idx in &path[1..] { + derived = derived.derive_hardened(*idx); + } + derived +} + +pub fn master_to_wallet_unhardened_intermediate(key: &Key) -> Key { + derive_path_unhardened(key, &[12381_u32, 8444, 2]) +} + +pub fn master_to_wallet_unhardened(key: &Key, idx: u32) -> Key { + derive_path_unhardened(key, &[12381_u32, 8444, 2, idx]) +} + +pub fn master_to_wallet_hardened_intermediate(key: &SecretKey) -> SecretKey { + derive_path_hardened(key, &[12381_u32, 8444, 2]) +} + +pub fn master_to_wallet_hardened(key: &SecretKey, idx: u32) -> SecretKey { + derive_path_hardened(key, &[12381_u32, 8444, 2, idx]) +} + +pub fn master_to_pool_singleton(key: &SecretKey, pool_wallet_idx: u32) -> SecretKey { + derive_path_hardened(key, &[12381_u32, 8444, 5, pool_wallet_idx]) +} + +pub fn master_to_pool_authentication(key: &SecretKey, pool_wallet_idx: u32, idx: u32) -> SecretKey { + assert!(pool_wallet_idx < 10000); + assert!(idx < 10000); + derive_path_hardened(key, &[12381_u32, 8444, 6, pool_wallet_idx * 10000 + idx]) +} diff --git a/chia-bls/src/lib.rs b/chia-bls/src/lib.rs new file mode 100644 index 000000000..e60d4db5f --- /dev/null +++ b/chia-bls/src/lib.rs @@ -0,0 +1,6 @@ +pub mod derivable_key; +pub mod derive_keys; +pub mod mnemonic; +pub mod public_key; +pub mod secret_key; +pub mod signature; diff --git a/chia-bls/src/mnemonic.rs b/chia-bls/src/mnemonic.rs new file mode 100644 index 000000000..a83ebd96d --- /dev/null +++ b/chia-bls/src/mnemonic.rs @@ -0,0 +1,97 @@ +use anyhow::Error; +use bip39::{Language, Mnemonic, Seed}; +use std::array::TryFromSliceError; +use std::result::Result; + +pub fn entropy_to_mnemonic(entropy: &[u8; 32]) -> String { + Mnemonic::from_entropy(entropy, Language::English) + .unwrap() + .into_phrase() +} + +pub fn mnemonic_to_entropy(mnemonic: &str) -> Result<[u8; 32], Error> { + let m = Mnemonic::from_phrase(mnemonic, Language::English)?; + let ent = m.entropy(); + ent.try_into().map_err(|e: TryFromSliceError| { + Error::from(e).context("incorrect number of words in mnemonic") + }) +} + +pub fn entropy_to_seed(entropy: &[u8; 32]) -> [u8; 64] { + let m = Mnemonic::from_entropy(entropy, Language::English).unwrap(); + Seed::new(&m, "").as_bytes().try_into().unwrap() +} + +#[cfg(test)] +use hex::encode; +#[cfg(test)] +use hex::FromHex; + +#[test] +fn test_parse_mnemonic() { + // test vectors from BIP39 + // https://github.com/trezor/python-mnemonic/blob/master/vectors.json + // The seeds are changed to account for chia using an empty passphrase + // (whereas the trezor test vectors use "TREZOR") + + // phrase, entropy, seed + let test_cases = &[ + ("all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", + "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", + "fc795be0c3f18c50dddb34e72179dc597d64055497ecc1e69e2e56a5409651bc139aae8070d4df0ea14d8d2a518a9a00bb1cc6e92e053fe34051f6821df9164c" + ), + ("void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", + "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", + "b873212f885ccffbf4692afcb84bc2e55886de2dfa07d90f5c3c239abc31c0a6ce047e30fd8bf6a281e71389aa82d73df74c7bbfb3b06b4639a5cee775cccd3c" + ), + ("panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", + "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", + "3e066d7dee2dbf8fcd3fe240a3975658ca118a8f6f4ca81cf99104944604b05a5090a79d99e545704b914ca0397fedb82fd00fd6a72098703709c891a065ee49") + ]; + + for (phrase, entropy, seed) in test_cases { + println!("{}", phrase); + assert_eq!( + encode(mnemonic_to_entropy(phrase).unwrap()), + entropy.to_string() + ); + assert_eq!( + entropy_to_mnemonic(&<[u8; 32]>::from_hex(entropy).unwrap()).as_str(), + *phrase + ); + assert_eq!( + encode(entropy_to_seed(&<[u8; 32]>::from_hex(entropy).unwrap())), + seed.to_string() + ) + } +} + +#[test] +fn test_invalid_mnemonic() { + assert_eq!( + format!( + "{}", + mnemonic_to_entropy("camp survey warrior").unwrap_err() + ), + "invalid number of words in phrase: 3" + ); + assert_eq!( + format!( + "{}", + mnemonic_to_entropy( + "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich" + ) + .unwrap_err() + ), + "invalid checksum" + ); + assert_eq!(format!("{}", mnemonic_to_entropy("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap_err()), "incorrect number of words in mnemonic"); + assert_eq!(mnemonic_to_entropy("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art").unwrap(), <[u8; 32]>::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap()); + + assert_eq!(mnemonic_to_entropy("letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless").unwrap(), + <[u8; 32]>::from_hex("8080808080808080808080808080808080808080808080808080808080808080").unwrap()); + + // make sure all whitespace is ignored + assert_eq!(mnemonic_to_entropy("letter advice \t cage\t absurd \tamount doctor acoustic \n avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless").unwrap(), + <[u8; 32]>::from_hex("8080808080808080808080808080808080808080808080808080808080808080").unwrap()); +} diff --git a/chia-bls/src/public_key.rs b/chia-bls/src/public_key.rs new file mode 100644 index 000000000..8029a81f1 --- /dev/null +++ b/chia-bls/src/public_key.rs @@ -0,0 +1,112 @@ +use crate::derivable_key::DerivableKey; +use bls12_381_plus::{G1Affine, G1Projective, Scalar}; +use group::Curve; +use num_bigint::BigUint; +use sha2::{Digest, Sha256}; + +#[derive(PartialEq, Eq, Debug)] +pub struct PublicKey(pub G1Projective); + +impl PublicKey { + pub fn from_bytes(bytes: &[u8; 48]) -> Option { + G1Affine::from_compressed(bytes) + .map(|p| Self(G1Projective::from(&p))) + .into() + } + + pub fn to_bytes(&self) -> [u8; 48] { + self.0.to_affine().to_compressed() + } + + pub fn is_valid(&self) -> bool { + self.0.is_identity().unwrap_u8() == 0 && self.0.is_on_curve().unwrap_u8() == 1 + } +} + +impl DerivableKey for PublicKey { + fn derive_unhardened(&self, idx: u32) -> Self { + let mut hasher = Sha256::new(); + hasher.update(self.to_bytes()); + hasher.update(idx.to_be_bytes()); + let digest = hasher.finalize(); + + // in an ideal world, we would not need to reach for the sledge-hammer of + // num-bigint here. This would most likely be faster if implemented in + // Scalar directly. + + // interpret the hash as an unsigned big-endian number + let mut nounce = BigUint::from_bytes_be(digest.as_slice()); + + let q = BigUint::from_bytes_be(&[ + 0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1, + 0xd8, 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x01, + ]); + + // mod by G1 Order + nounce %= q; + + let raw_bytes = nounce.to_bytes_be(); + let mut bytes = [0_u8; 32]; + bytes[32 - raw_bytes.len()..].copy_from_slice(&raw_bytes); + bytes.reverse(); + + let nounce = Scalar::from_bytes(&bytes).unwrap(); + + PublicKey(self.0 + G1Projective::generator() * nounce) + } +} + +#[cfg(test)] +use hex::FromHex; + +#[cfg(test)] +use crate::secret_key::SecretKey; + +#[test] +fn test_derive_unhardened() { + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + let pk = sk.public_key(); + + // make sure deriving the secret keys produce the same public keys as + // deriving the public key + for idx in 0..4_usize { + let derived_sk = sk.derive_unhardened(idx as u32); + let derived_pk = pk.derive_unhardened(idx as u32); + assert_eq!(derived_pk.to_bytes(), derived_sk.public_key().to_bytes()); + } +} + +#[cfg(test)] +use rand::{Rng, SeedableRng}; + +#[cfg(test)] +use rand::rngs::StdRng; + +#[test] +fn test_from_bytes() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 48]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + // just any random bytes are not a valid key and should fail + assert_eq!(PublicKey::from_bytes(&data), None); + } +} + +#[test] +fn test_roundtrip() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + let bytes = pk.to_bytes(); + let pk2 = PublicKey::from_bytes(&bytes).unwrap(); + assert_eq!(pk, pk2); + } +} + +// ERROR test is_valid diff --git a/chia-bls/src/secret_key.rs b/chia-bls/src/secret_key.rs new file mode 100644 index 000000000..0674036d5 --- /dev/null +++ b/chia-bls/src/secret_key.rs @@ -0,0 +1,305 @@ +use crate::derivable_key::DerivableKey; +use crate::public_key::PublicKey; +use bls12_381_plus::{G1Projective, Scalar}; +use hkdf::HkdfExtract; +use num_bigint::BigUint; +use sha2::{Digest, Sha256}; + +#[derive(PartialEq, Eq, Debug)] +pub struct SecretKey(pub(crate) Scalar); + +fn flip_bits(input: [u8; 32]) -> [u8; 32] { + let mut ret = [0; 32]; + for i in 0..32 { + ret[i] = input[i] ^ 0xff; + } + ret +} + +fn ikm_to_lamport_sk(ikm: &[u8; 32], salt: &[u8; 4]) -> [u8; 255 * 32] { + let mut extracter = HkdfExtract::::new(Some(salt)); + extracter.input_ikm(ikm); + let (_, h) = extracter.finalize(); + + let mut output = [0_u8; 255 * 32]; + h.expand(&[], &mut output).unwrap(); + output +} + +fn sha256(bytes: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hasher.finalize().try_into().unwrap() +} + +impl SecretKey { + pub fn from_seed(seed: &[u8]) -> SecretKey { + // described here: + // https://eips.ethereum.org/EIPS/eip-2333#derive_master_sk + assert!(seed.len() >= 32); + + const SALT: &[u8] = b"BLS-SIG-KEYGEN-SALT-"; + let mut extracter = HkdfExtract::::new(Some(SALT)); + extracter.input_ikm(seed); + extracter.input_ikm(&[0_u8]); + let h = extracter.finalize().1; + + // TODO: = uninitialized + let mut sk = [0_u8; 48]; + h.expand(&[0_u8, 48], &mut sk) + .expect("failed to generate secret key"); + SecretKey(Scalar::from_okm(&sk)) + } + + pub fn from_bytes(b: &[u8; 32]) -> Option { + let t = [ + b[31], b[30], b[29], b[28], b[27], b[26], b[25], b[24], b[23], b[22], b[21], b[20], + b[19], b[18], b[17], b[16], b[15], b[14], b[13], b[12], b[11], b[10], b[9], b[8], b[7], + b[6], b[5], b[4], b[3], b[2], b[1], b[0], + ]; + Scalar::from_bytes(&t).map(SecretKey).into() + } + + pub fn to_bytes(&self) -> [u8; 32] { + let mut bytes = self.0.to_bytes(); + bytes.reverse(); + bytes + } + + pub fn public_key(&self) -> PublicKey { + PublicKey(G1Projective::generator() * self.0) + } + + fn to_lamport_pk(&self, idx: u32) -> [u8; 32] { + let ikm = self.to_bytes(); + let not_ikm = flip_bits(ikm); + let salt = idx.to_be_bytes(); + + let mut lamport0 = ikm_to_lamport_sk(&ikm, &salt); + let mut lamport1 = ikm_to_lamport_sk(¬_ikm, &salt); + + for i in (0..32 * 255).step_by(32) { + let hash = sha256(&lamport0[i..i + 32]); + lamport0[i..i + 32].copy_from_slice(&hash); + } + for i in (0..32 * 255).step_by(32) { + let hash = sha256(&lamport1[i..i + 32]); + lamport1[i..i + 32].copy_from_slice(&hash); + } + + let mut hasher = Sha256::new(); + hasher.update(lamport0); + hasher.update(lamport1); + hasher.finalize().try_into().unwrap() + } + + pub fn derive_hardened(&self, idx: u32) -> SecretKey { + // described here: + // https://eips.ethereum.org/EIPS/eip-2333#derive_child_sk + SecretKey::from_seed(&self.to_lamport_pk(idx)) + } +} + +impl DerivableKey for SecretKey { + fn derive_unhardened(&self, idx: u32) -> Self { + let pk = self.public_key(); + + let mut hasher = Sha256::new(); + hasher.update(pk.to_bytes()); + hasher.update(idx.to_be_bytes()); + let digest = hasher.finalize(); + + // in an ideal world, we would not need to reach for the sledge-hammer of + // num-bigint here. This would most likely be faster if implemented in + // Scalar directly. + + // interpret the hash as an unsigned big-endian number + let mut scalar = BigUint::from_bytes_be(digest.as_slice()); + + let q = BigUint::from_bytes_be(&[ + 0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1, + 0xd8, 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x01, + ]); + + // mod by G1 Order + scalar %= q; + + // Now, convert BigUint -> Scalar, that we can use to create the new secret key with + let mut raw_limbs = [0_u64; 4]; + for (it, limb) in raw_limbs.iter_mut().zip(scalar.to_u64_digits()) { + *it = limb; + } + + let mut new_sk = Scalar::from_raw(raw_limbs); + + // aggregate the new secret with the existing secret + // The Scalar type uses modulus arithmetic in the Q order, so the modulus Q + // is implied in these addition operations + new_sk += self.0; + SecretKey(new_sk) + } +} + +#[cfg(test)] +use hex::FromHex; + +#[test] +fn test_make_key() { + // test vectors from: + // from chia.util.keychain import KeyDataSecrets + // print(KeyDataSecrets.from_mnemonic(phrase)["privatekey"]) + + // (seed, secret-key) + let test_cases = &[ + ("fc795be0c3f18c50dddb34e72179dc597d64055497ecc1e69e2e56a5409651bc139aae8070d4df0ea14d8d2a518a9a00bb1cc6e92e053fe34051f6821df9164c", + "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"), + ("b873212f885ccffbf4692afcb84bc2e55886de2dfa07d90f5c3c239abc31c0a6ce047e30fd8bf6a281e71389aa82d73df74c7bbfb3b06b4639a5cee775cccd3c", + "35d65c35d926f62ba2dd128754ddb556edb4e2c926237ab9e02a23e7b3533613"), + ("3e066d7dee2dbf8fcd3fe240a3975658ca118a8f6f4ca81cf99104944604b05a5090a79d99e545704b914ca0397fedb82fd00fd6a72098703709c891a065ee49", + "59095c391107936599b7ee6f09067979b321932bd62e23c7f53ed5fb19f851f6") + ]; + + for (seed, sk) in test_cases { + assert_eq!( + SecretKey::from_seed(&<[u8; 64]>::from_hex(seed).unwrap()) + .to_bytes() + .to_vec(), + Vec::::from_hex(sk).unwrap() + ); + } +} + +#[test] +fn test_derive_unhardened() { + // test vectors from: + // from blspy import AugSchemeMPL + // from blspy import PrivateKey + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 0) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 1) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 2) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 3) + // + // + // + // + + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let derived_hex = [ + "399638f99d446500f3c3a363f24c2b0634ad7caf646f503455093f35f29290bd", + "3dcb4098ad925d8940e2f516d2d5a4dbab393db928a8c6cb06b93066a09a843a", + "13115c8fb68a3d667938dac2ffc6b867a4a0f216bbb228aa43d6bdde14245575", + "52e7e9f2fb51f2c5705aea8e11ac82737b95e664ae578f015af22031d956f92b", + ]; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + + for idx in 0..4_usize { + let derived = sk.derive_unhardened(idx as u32); + assert_eq!( + derived.to_bytes(), + <[u8; 32]>::from_hex(derived_hex[idx]).unwrap() + ) + } +} + +#[test] +fn test_public_key() { + // test vectors from: + // from blspy import PrivateKey + // from blspy import AugSchemeMPL + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // for i in [100, 52312, 352350, 316]: + // sk0 = AugSchemeMPL.derive_child_sk_unhardened(sk, i) + // print(bytes(sk0).hex()) + // print(bytes(sk0.get_g1()).hex()) + + // secret key, public key + let test_cases = [ + ("5aac8405befe4cb3748a67177c56df26355f1f98d979afdb0b2f97858d2f71c3", + "b9de000821a610ef644d160c810e35113742ff498002c2deccd8f1a349e423047e9b3fc17ebfc733dbee8fd902ba2961"), + ("23f1fb291d3bd7434282578b842d5ea4785994bb89bd2c94896d1b4be6c70ba2", + "96f304a5885e67abdeab5e1ed0576780a1368777ea7760124834529e8694a1837a20ffea107b9769c4f92a1f6c167e69"), + ("2bc1d6d6efe58d365c29ccb7ad12c8457c0eec70a29003073692ac4cb1cd7ba2", + "b10568446def64b17fc9b6d614ae036deaac3f2d654e12e45ea04b19208246e0d760e8826426e97f9f0666b7ce340d75"), + ("2bfc8672d859700e30aa6c8edc24a8ce9e6dc53bb1ef936f82de722847d05b9e", + "9641472acbd6af7e5313d2500791b87117612af43eef929cf7975aaaa5a203a32698a8ef53763a84d90ad3f00b86ad66"), + ("3311f883dad1e39c52bf82d5870d05371c0b1200576287b5160808f55568151b", + "928ea102b5a3e3efe4f4c240d3458a568dfeb505e02901a85ed70a384944b0c08c703a35245322709921b8f2b7f5e54a"), + ]; + + for (sk_hex, pk_hex) in test_cases { + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + let pk = sk.public_key(); + assert_eq!( + pk, + PublicKey::from_bytes(&<[u8; 48]>::from_hex(pk_hex).unwrap()).unwrap() + ); + } +} + +#[test] +fn test_derive_hardened() { + // test vectors from: + // from blspy import AugSchemeMPL + // from blspy import PrivateKey + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // AugSchemeMPL.derive_child_sk(sk, 0) + // AugSchemeMPL.derive_child_sk(sk, 1) + // AugSchemeMPL.derive_child_sk(sk, 2) + // AugSchemeMPL.derive_child_sk(sk, 3) + // + // + // + // + + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let derived_hex = [ + "05eccb2d70e814f51a30d8b9965505605c677afa97228fa2419db583a8121db9", + "612ae96bdce2e9bc01693ac579918fbb559e04ec365cce9b66bb80e328f62c46", + "5df14a0a34fd6c30a80136d4103f0a93422ce82d5c537bebbecbc56e19fee5b9", + "3ea55db88d9a6bf5f1d9c9de072e3c9a56b13f4156d72fca7880cd39b4bd4fdc", + ]; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + + for idx in 0..derived_hex.len() { + let derived = sk.derive_hardened(idx as u32); + assert_eq!( + derived.to_bytes(), + <[u8; 32]>::from_hex(derived_hex[idx]).unwrap() + ) + } +} + +#[cfg(test)] +use rand::{Rng, SeedableRng}; + +#[cfg(test)] +use rand::rngs::StdRng; + +#[test] +fn test_from_bytes() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + // make the bytes exceed q + data[0] |= 0x80; + // just any random bytes are not a valid key and should fail + assert_eq!(SecretKey::from_bytes(&data), None); + } +} + +#[test] +fn test_roundtrip() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let bytes = sk.to_bytes(); + let sk2 = SecretKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk, sk2); + assert_eq!(sk.public_key(), sk2.public_key()); + } +} diff --git a/chia-bls/src/signature.rs b/chia-bls/src/signature.rs new file mode 100644 index 000000000..380bb793c --- /dev/null +++ b/chia-bls/src/signature.rs @@ -0,0 +1,505 @@ +use crate::public_key::PublicKey; +use crate::secret_key::SecretKey; +use bls12_381_plus::{ + multi_miller_loop, ExpandMsgXmd, G1Affine, G2Affine, G2Prepared, G2Projective, +}; +use group::{Curve, Group}; +use std::borrow::Borrow; +use std::convert::AsRef; +use std::ops::Neg; + +#[derive(PartialEq, Eq, Debug)] +pub struct Signature(pub(crate) G2Projective); + +impl Signature { + pub fn from_bytes(buf: &[u8; 96]) -> Option { + G2Affine::from_compressed(buf) + .map(|p| Self(G2Projective::from(&p))) + .into() + } + + pub fn to_bytes(&self) -> [u8; 96] { + self.0.to_affine().to_compressed() + } + + pub fn aggregate(&mut self, sig: &Signature) { + self.0 += sig.0; + } + + pub fn is_valid(&self) -> bool { + self.0.is_on_curve().unwrap_u8() == 1 + } +} + +impl Default for Signature { + fn default() -> Self { + Signature(G2Projective::identity()) + } +} + +fn hash_msg>(pk: &PublicKey, msg: Msg) -> G2Projective { + let mut prepended_msg = pk.to_bytes().to_vec(); + prepended_msg.extend_from_slice(msg.as_ref()); + // domain separation tag + const CIPHER_SUITE: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_"; + G2Projective::hash::>(&prepended_msg, CIPHER_SUITE) +} + +pub fn aggregate, I>(sigs: I) -> Signature +where + I: IntoIterator, +{ + let mut ret = Signature::default(); + + for s in sigs.into_iter() { + ret.aggregate(s.borrow()); + } + ret +} + +pub fn verify>(sig: &Signature, key: &PublicKey, msg: Msg) -> bool { + if !key.is_valid() || !sig.is_valid() { + return false; + } + let a = hash_msg(key, msg); + let g1 = G1Affine::generator().neg(); + + multi_miller_loop(&[ + (&key.0.to_affine(), &G2Prepared::from(a.to_affine())), + (&g1, &G2Prepared::from(sig.0.to_affine())), + ]) + .final_exponentiation() + .is_identity() + .into() +} + +pub fn aggregate_verify, Msg: Borrow<[u8]>, I>( + sig: &Signature, + data: I, +) -> bool +where + I: IntoIterator, +{ + if !sig.is_valid() { + return false; + } + let mut store = Vec::<(G1Affine, G2Prepared)>::new(); + + for (key, msg) in data.into_iter() { + let key = key.borrow(); + if !key.is_valid() { + return false; + } + store.push(( + key.0.to_affine(), + G2Prepared::from(hash_msg(key, msg.borrow()).to_affine()), + )); + } + + if store.is_empty() { + // if we have exactly zero messages to verify, the only correct + // signature is the identity + // This is an optimization for the edge case of having 0 messages + return sig == &Signature::default(); + } + + store.push(( + G1Affine::generator().neg(), + G2Prepared::from(sig.0.to_affine()), + )); + + let mut terms = Vec::<(&G1Affine, &G2Prepared)>::new(); + for (g1, g2) in &store { + terms.push((g1, g2)); + } + + // multi_miller_loop takes a slice of *references*, which means we need to build + // both a vector owning the elements (G1Affine and G2Prepared) in addition to a + // vector holding references into it. + multi_miller_loop(terms.as_slice()) + .final_exponentiation() + .is_identity() + .into() +} + +pub fn sign>(sk: &SecretKey, msg: Msg) -> Signature { + let g2 = hash_msg(&sk.public_key(), msg); + Signature(g2 * sk.0) +} + +#[cfg(test)] +use rand::{Rng, SeedableRng}; + +#[cfg(test)] +use rand::rngs::StdRng; + +#[test] +fn test_from_bytes() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 96]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + // just any random bytes are not a valid signature and should fail + assert_eq!(Signature::from_bytes(&data), None); + } +} + +#[test] +fn test_roundtrip() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + let mut msg = [0u8; 32]; + rng.fill(msg.as_mut_slice()); + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let sig = sign(&sk, msg); + let bytes = sig.to_bytes(); + let sig2 = Signature::from_bytes(&bytes).unwrap(); + assert_eq!(sig, sig2); + } +} + +#[test] +fn test_random_verify() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + let mut msg = [0u8; 32]; + rng.fill(msg.as_mut_slice()); + for _i in 0..20 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + let sig = sign(&sk, &msg); + assert!(verify(&sig, &pk, msg)); + + let bytes = sig.to_bytes(); + let sig2 = Signature::from_bytes(&bytes).unwrap(); + assert!(verify(&sig2, &pk, msg)); + } +} + +#[cfg(test)] +use hex::FromHex; + +#[test] +fn test_verify() { + // test case from: + // from blspy import PrivateKey + // from blspy import AugSchemeMPL + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // data = b"foobar" + // print(AugSchemeMPL.sign(sk, data)) + let msg = b"foobar"; + let sk = SecretKey::from_bytes( + &<[u8; 32]>::from_hex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb") + .unwrap(), + ) + .unwrap(); + + let sig = sign(&sk, &msg); + assert!(verify(&sig, &sk.public_key(), msg)); + + assert_eq!(sig.to_bytes(), <[u8; 96]>::from_hex("b45825c0ee7759945c0189b4c38b7e54231ebadc83a851bec3bb7cf954a124ae0cc8e8e5146558332ea152f63bf8846e04826185ef60e817f271f8d500126561319203f9acb95809ed20c193757233454be1562a5870570941a84605bd2c9c9a").unwrap()); +} + +#[test] +fn test_aggregate_signature() { + // from blspy import PrivateKey + // from blspy import AugSchemeMPL + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // data = b"foobar" + // sk0 = AugSchemeMPL.derive_child_sk(sk, 0) + // sk1 = AugSchemeMPL.derive_child_sk(sk, 1) + // sk2 = AugSchemeMPL.derive_child_sk(sk, 2) + // sk3 = AugSchemeMPL.derive_child_sk(sk, 3) + + // sig0 = AugSchemeMPL.sign(sk0, data) + // sig1 = AugSchemeMPL.sign(sk1, data) + // sig2 = AugSchemeMPL.sign(sk2, data) + // sig3 = AugSchemeMPL.sign(sk3, data) + + // agg = AugSchemeMPL.aggregate([sig0, sig1, sig2, sig3]) + + // 87bce2c588f4257e2792d929834548c7d3af679272cb4f8e1d24cf4bf584dd287aa1d9f5e53a86f288190db45e1d100d0a5e936079a66a709b5f35394cf7d52f49dd963284cb5241055d54f8cf48f61bc1037d21cae6c025a7ea5e9f4d289a18 + + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + let msg = b"foobar"; + let mut agg = Signature::default(); + let mut data = Vec::<(PublicKey, &[u8])>::new(); + for idx in 0..4 { + let derived = sk.derive_hardened(idx as u32); + data.push((derived.public_key(), msg)); + agg.aggregate(&sign(&derived, msg)); + } + assert_eq!(agg.to_bytes(), <[u8; 96]>::from_hex("87bce2c588f4257e2792d929834548c7d3af679272cb4f8e1d24cf4bf584dd287aa1d9f5e53a86f288190db45e1d100d0a5e936079a66a709b5f35394cf7d52f49dd963284cb5241055d54f8cf48f61bc1037d21cae6c025a7ea5e9f4d289a18").unwrap()); + + // ensure the aggregate signature verifies OK + assert!(aggregate_verify(&agg, data)); +} + +#[cfg(test)] +fn random_sk(rng: &mut R) -> SecretKey { + let mut data = [0u8; 64]; + rng.fill(data.as_mut_slice()); + SecretKey::from_seed(&data) +} + +#[test] +fn test_aggregate_signature_separate_msg() { + let mut rng = StdRng::seed_from_u64(1337); + let sk = [random_sk(&mut rng), random_sk(&mut rng)]; + let pk = [sk[0].public_key(), sk[1].public_key()]; + let msg: [&'static [u8]; 2] = [b"foo", b"foobar"]; + let sig = [sign(&sk[0], msg[0]), sign(&sk[1], msg[1])]; + let mut agg = Signature::default(); + agg.aggregate(&sig[0]); + agg.aggregate(&sig[1]); + + assert!(aggregate_verify(&agg, pk.iter().zip(msg))); + // order does not matter + assert!(aggregate_verify(&agg, pk.iter().zip(msg).rev())); +} + +#[test] +fn test_aggregate_signature_identity() { + // when verifying 0 messages, an identity signature is considered valid + let empty = Vec::<(PublicKey, &[u8])>::new(); + assert!(aggregate_verify(&Signature::default(), empty)); +} + +#[test] +fn test_invalid_aggregate_signature() { + let mut rng = StdRng::seed_from_u64(1337); + let sk = [random_sk(&mut rng), random_sk(&mut rng)]; + let pk = [sk[0].public_key(), sk[1].public_key()]; + let msg: [&'static [u8]; 2] = [b"foo", b"foobar"]; + let sig = [sign(&sk[0], msg[0]), sign(&sk[1], msg[1])]; + let mut agg = Signature::default(); + agg.aggregate(&sig[0]); + agg.aggregate(&sig[1]); + + assert!(aggregate_verify(&agg, [(&pk[0], msg[0])]) == false); + assert!(aggregate_verify(&agg, [(&pk[1], msg[1])]) == false); + // public keys mixed with the wrong message + assert!(aggregate_verify(&agg, [(&pk[0], msg[1]), (&pk[1], msg[0])]) == false); + assert!(aggregate_verify(&agg, [(&pk[1], msg[0]), (&pk[0], msg[1])]) == false); +} + +#[test] +fn test_vector_2_aggregate_of_aggregates() { + // test case from: bls-signatures/src/test.cpp + // "Chia test vector 2 (Augmented, aggregate of aggregates)" + let message1 = [1_u8, 2, 3, 40]; + let message2 = [5_u8, 6, 70, 201]; + let message3 = [9_u8, 10, 11, 12, 13]; + let message4 = [15_u8, 63, 244, 92, 0, 1]; + + let sk1 = SecretKey::from_seed(&[2_u8; 32]); + let sk2 = SecretKey::from_seed(&[3_u8; 32]); + + let pk1 = sk1.public_key(); + let pk2 = sk2.public_key(); + + let sig1 = sign(&sk1, &message1); + let sig2 = sign(&sk2, &message2); + let sig3 = sign(&sk2, &message1); + let sig4 = sign(&sk1, &message3); + let sig5 = sign(&sk1, &message1); + let sig6 = sign(&sk1, &message4); + + let agg_sig_l = aggregate(&[sig1, sig2]); + let agg_sig_r = aggregate(&[sig3, sig4, sig5]); + let aggsig = aggregate(&[agg_sig_l, agg_sig_r, sig6]); + + assert!(aggregate_verify( + &aggsig, + [ + (&pk1, &message1 as &[u8]), + (&pk2, &message2), + (&pk2, &message1), + (&pk1, &message3), + (&pk1, &message1), + (&pk1, &message4) + ] + )); + + assert_eq!( + aggsig.to_bytes(), + <[u8; 96]>::from_hex( + "a1d5360dcb418d33b29b90b912b4accde535cf0e52caf467a005dc632d9f7af44b6c4e9acd4\ + 6eac218b28cdb07a3e3bc087df1cd1e3213aa4e11322a3ff3847bbba0b2fd19ddc25ca964871\ + 997b9bceeab37a4c2565876da19382ea32a962200" + ) + .unwrap() + ); +} + +#[test] +fn test_signature_zero_key() { + // test case from: bls-signatures/src/test.cpp + // "Should sign with the zero key" + let sk = SecretKey::from_bytes(&[0; 32]).unwrap(); + assert_eq!(sign(&sk, &[1_u8, 2, 3]), Signature::default()); +} + +#[test] +fn test_aggregate_many_g2_elements_diff_message() { + // test case from: bls-signatures/src/test.cpp + // "Should Aug aggregate many G2Elements, diff message" + + let mut rng = StdRng::seed_from_u64(1337); + + let mut pairs = Vec::<(PublicKey, Vec)>::new(); + let mut sigs = Vec::::new(); + + for i in 0..80 { + let message = vec![0_u8, 100, 2, 45, 64, 12, 12, 63, i]; + let sk = random_sk(&mut rng); + let sig = sign(&sk, &message); + pairs.push((sk.public_key(), message)); + sigs.push(sig); + } + + let aggsig = aggregate(sigs); + + assert!(aggregate_verify(&aggsig, pairs)); +} + +#[test] +fn test_aggregate_identity() { + // test case from: bls-signatures/src/test.cpp + // "Aggregate Verification of zero items with infinity should pass" + let sig = Signature::default(); + let aggsig = aggregate([&sig]); + assert_eq!(aggsig, sig); + assert_eq!(aggsig, Signature::default()); + + assert!(aggregate_verify(&aggsig, [] as [(&PublicKey, &[u8]); 0])); +} + +#[test] +fn test_aggregate_multiple_levels_degenerate() { + // test case from: bls-signatures/src/test.cpp + // "Should aggregate with multiple levels, degenerate" + + let mut rng = StdRng::seed_from_u64(1337); + + let message1 = [100_u8, 2, 254, 88, 90, 45, 23]; + let sk1 = random_sk(&mut rng); + let pk1 = sk1.public_key(); + let mut agg_sig = sign(&sk1, &message1); + let mut pairs: Vec<(PublicKey, &[u8])> = vec![(pk1, &message1)]; + + for _i in 0..10 { + let sk = random_sk(&mut rng); + let pk = sk.public_key(); + pairs.push((pk, &message1)); + let sig = sign(&sk, &message1); + agg_sig.aggregate(&sig); + } + assert!(aggregate_verify(&agg_sig, pairs)); +} + +#[test] +fn test_aggregate_multiple_levels_different_messages() { + // test case from: bls-signatures/src/test.cpp + // "Should aggregate with multiple levels, different messages" + + let mut rng = StdRng::seed_from_u64(1337); + + let message1 = [100_u8, 2, 254, 88, 90, 45, 23]; + let message2 = [192_u8, 29, 2, 0, 0, 45, 23]; + let message3 = [52_u8, 29, 2, 0, 0, 45, 102]; + let message4 = [99_u8, 29, 2, 0, 0, 45, 222]; + + let sk1 = random_sk(&mut rng); + let sk2 = random_sk(&mut rng); + + let pk1 = sk1.public_key(); + let pk2 = sk2.public_key(); + + let sig1 = sign(&sk1, &message1); + let sig2 = sign(&sk2, &message2); + let sig3 = sign(&sk2, &message3); + let sig4 = sign(&sk1, &message4); + + let agg_sig_l = aggregate([sig1, sig2]); + let agg_sig_r = aggregate([sig3, sig4]); + let agg_sig = aggregate([agg_sig_l, agg_sig_r]); + + let all_pairs: [(&PublicKey, &[u8]); 4] = [ + (&pk1, &message1), + (&pk2, &message2), + (&pk2, &message3), + (&pk1, &message4), + ]; + assert!(aggregate_verify(&agg_sig, all_pairs)); +} + +#[test] +fn test_aug_scheme() { + // test case from: bls-signatures/src/test.cpp + // "Aug Scheme" + + let msg1 = [7_u8, 8, 9]; + let msg2 = [10_u8, 11, 12]; + + let sk1 = SecretKey::from_seed(&[4_u8; 32]); + let pk1 = sk1.public_key(); + let pk1v = pk1.to_bytes(); + let sig1 = sign(&sk1, &msg1); + let sig1v = sig1.to_bytes(); + + assert!(verify(&sig1, &pk1, &msg1)); + assert!(verify( + &Signature::from_bytes(&sig1v).unwrap(), + &PublicKey::from_bytes(&pk1v).unwrap(), + &msg1 + )); + + let sk2 = SecretKey::from_seed(&[5_u8; 32]); + let pk2 = sk2.public_key(); + let pk2v = pk2.to_bytes(); + let sig2 = sign(&sk2, &msg2); + let sig2v = sig2.to_bytes(); + + assert!(verify(&sig2, &pk2, &msg2)); + assert!(verify( + &Signature::from_bytes(&sig2v).unwrap(), + &PublicKey::from_bytes(&pk2v).unwrap(), + &msg2 + )); + + // Wrong G2Element + assert!(!verify(&sig2, &pk1, &msg1)); + assert!(!verify( + &Signature::from_bytes(&sig2v).unwrap(), + &PublicKey::from_bytes(&pk1v).unwrap(), + &msg1 + )); + // Wrong msg + assert!(!verify(&sig1, &pk1, &msg2)); + assert!(!verify( + &Signature::from_bytes(&sig1v).unwrap(), + &PublicKey::from_bytes(&pk1v).unwrap(), + &msg2 + )); + // Wrong pk + assert!(!verify(&sig1, &pk2, &msg1)); + assert!(!verify( + &Signature::from_bytes(&sig1v).unwrap(), + &PublicKey::from_bytes(&pk2v).unwrap(), + &msg1 + )); + + let aggsig = aggregate([sig1, sig2]); + let aggsigv = aggsig.to_bytes(); + let pairs: [(&PublicKey, &[u8]); 2] = [(&pk1, &msg1), (&pk2, &msg2)]; + assert!(aggregate_verify(&aggsig, pairs)); + assert!(aggregate_verify( + &Signature::from_bytes(&aggsigv).unwrap(), + pairs + )); +} From 9c717c43271590c86acdd7ccb06028405217070f Mon Sep 17 00:00:00 2001 From: arvidn Date: Fri, 28 Oct 2022 12:55:01 +0200 Subject: [PATCH 2/4] add fuzz target for unhardened key derivation, signing and verification --- chia-bls/fuzz/.gitignore | 3 +++ chia-bls/fuzz/Cargo.toml | 25 ++++++++++++++++++++++ chia-bls/fuzz/fuzz_targets/derive.rs | 32 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 chia-bls/fuzz/.gitignore create mode 100644 chia-bls/fuzz/Cargo.toml create mode 100644 chia-bls/fuzz/fuzz_targets/derive.rs diff --git a/chia-bls/fuzz/.gitignore b/chia-bls/fuzz/.gitignore new file mode 100644 index 000000000..a0925114d --- /dev/null +++ b/chia-bls/fuzz/.gitignore @@ -0,0 +1,3 @@ +target +corpus +artifacts diff --git a/chia-bls/fuzz/Cargo.toml b/chia-bls/fuzz/Cargo.toml new file mode 100644 index 000000000..6141322da --- /dev/null +++ b/chia-bls/fuzz/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "chia-bls-fuzz" +version = "0.0.0" +authors = ["Automatically generated"] +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.chia-bls] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "derive" +path = "fuzz_targets/derive.rs" +test = false +doc = false diff --git a/chia-bls/fuzz/fuzz_targets/derive.rs b/chia-bls/fuzz/fuzz_targets/derive.rs new file mode 100644 index 000000000..8912f021e --- /dev/null +++ b/chia-bls/fuzz/fuzz_targets/derive.rs @@ -0,0 +1,32 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use chia_bls::secret_key::SecretKey; +use chia_bls::public_key::PublicKey; +use chia_bls::signature::{sign, verify}; +use chia_bls::derivable_key::DerivableKey; + +fuzz_target!(|data: &[u8]| { + if data.len() < 32 { + return; + } + + let sk = SecretKey::from_seed(data); + let pk = sk.public_key(); + + // round-trip SecretKey + let bytes = sk.to_bytes(); + assert_eq!(sk, SecretKey::from_bytes(&bytes).unwrap()); + + // round-trip PublicKey + let bytes = pk.to_bytes(); + assert_eq!(pk, PublicKey::from_bytes(&bytes).unwrap()); + + // unhardened derivation + let sk1 = sk.derive_unhardened(1337); + let pk1 = pk.derive_unhardened(1337); + + let sig = sign(&sk1, b"foobar"); + assert!(verify(&sig, &pk1, b"foobar")); + +}); From 28fac7629e71fa9c68600bc2c36c412f0479514d Mon Sep 17 00:00:00 2001 From: arvidn Date: Fri, 28 Oct 2022 19:41:36 +0200 Subject: [PATCH 3/4] add fuzzer to compare against blspy --- chia-bls/fuzz/Cargo.toml | 7 ++ chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs | 88 ++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs diff --git a/chia-bls/fuzz/Cargo.toml b/chia-bls/fuzz/Cargo.toml index 6141322da..f6fa826e9 100644 --- a/chia-bls/fuzz/Cargo.toml +++ b/chia-bls/fuzz/Cargo.toml @@ -10,6 +10,7 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" +pyo3 = { version = ">=0.17.2", features = ["auto-initialize"]} [dependencies.chia-bls] path = ".." @@ -23,3 +24,9 @@ name = "derive" path = "fuzz_targets/derive.rs" test = false doc = false + +[[bin]] +name = "blspy-fidelity" +path = "fuzz_targets/blspy-fidelity.rs" +test = false +doc = false diff --git a/chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs b/chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs new file mode 100644 index 000000000..0f7c08307 --- /dev/null +++ b/chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs @@ -0,0 +1,88 @@ +// PyO3 might use an unexpected version of python. So make sure blspy is +// installed in the one that's actually being used. +// On M1 MacOS, for example, pyO3 doesn't appear to be using the active venv, +// but the system Python3.9 executable (even though the default system python is +// 3.8.9). To install blspy: + +// python3.9 -m pip install blspy + +#![no_main] +use libfuzzer_sys::fuzz_target; +use pyo3::prelude::*; +use std::convert::TryFrom; + +use chia_bls::secret_key::SecretKey; +use chia_bls::signature::{sign, aggregate}; +use chia_bls::derivable_key::DerivableKey; +use pyo3::types::{PyTuple, PyBytes, PyList}; + +fn to_bytes<'a>(obj: &'a PyAny) -> &'a [u8] { + obj.call_method0("__bytes__").unwrap().downcast::().unwrap().as_bytes() +} + +fuzz_target!(|data: &[u8]| { + if data.len() < 32 { + return; + } + + Python::with_gil(|py| { + + let blspy = py.import("blspy").unwrap(); + let aug = blspy.getattr("AugSchemeMPL").unwrap(); + + // Generate key pair from seed + let rust_sk = SecretKey::from_seed(data); + let py_sk = aug.call_method1("key_gen", PyTuple::new(py, &[PyBytes::new(py, data)])).unwrap(); + + // convert to bytes + let rust_sk_bytes = rust_sk.to_bytes(); + let py_sk_bytes = to_bytes(py_sk); + assert_eq!(py_sk_bytes, rust_sk_bytes); + + // get the public key + let rust_pk = rust_sk.public_key(); + let py_pk = py_sk.call_method0("get_g1").unwrap(); + + // convert to bytes + let rust_pk_bytes = rust_pk.to_bytes(); + let py_pk_bytes = to_bytes(py_pk); + assert_eq!(py_pk_bytes, rust_pk_bytes); + + let idx = u32::from_be_bytes(<[u8; 4]>::try_from(&data[0..4]).unwrap()); + let rust_sk1 = rust_sk.derive_unhardened(idx); + let py_sk1 = aug.call_method1("derive_child_sk_unhardened", + PyTuple::new(py, &[py_sk, idx.to_object(py).as_ref(py)])).unwrap(); + assert_eq!(to_bytes(py_sk1), rust_sk1.to_bytes()); + + let rust_pk1 = rust_pk.derive_unhardened(idx); + let py_pk1 = aug.call_method1("derive_child_pk_unhardened", + PyTuple::new(py, &[py_pk, idx.to_object(py).as_ref(py)])).unwrap(); + assert_eq!(to_bytes(py_pk1), rust_pk1.to_bytes()); + + // sign with the derived keys + let rust_sig1 = sign(&rust_sk1, data); + let py_sig1 = aug.call_method1("sign", + PyTuple::new(py, &[py_sk1, PyBytes::new(py, data)])).unwrap(); + assert_eq!(to_bytes(py_sig1), rust_sig1.to_bytes()); + + // derive hardened + let idx = u32::from_be_bytes(<[u8; 4]>::try_from(&data[4..8]).unwrap()); + let rust_sk2 = rust_sk.derive_hardened(idx); + let py_sk2 = aug.call_method1("derive_child_sk", + PyTuple::new(py, &[py_sk, idx.to_object(py).as_ref(py)])).unwrap(); + assert_eq!(to_bytes(py_sk2), rust_sk2.to_bytes()); + + // sign with the derived keys + let rust_sig2 = sign(&rust_sk2, data); + let py_sig2 = aug.call_method1("sign", + PyTuple::new(py, &[py_sk2, PyBytes::new(py, data)])).unwrap(); + assert_eq!(to_bytes(py_sig2), rust_sig2.to_bytes()); + + // aggregate + let rust_agg = aggregate(&[rust_sig1, rust_sig2]); + let py_agg = aug.call_method1("aggregate", PyTuple::new(py, + &[PyList::new(py, &[py_sig1, py_sig2])])).unwrap(); + assert_eq!(to_bytes(py_agg), rust_agg.to_bytes()); + }); + +}); From 928e443c8bd973d14b72e588be6c8cb4759b817f Mon Sep 17 00:00:00 2001 From: arvidn Date: Wed, 9 Nov 2022 10:32:00 +0100 Subject: [PATCH 4/4] fix upload-artifact directory --- .github/workflows/build-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index a78961ac0..73ac3204c 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -220,7 +220,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: packages - path: ./target/wheels/ + path: ./wheel/target/wheels/ check-typestubs: name: Check chia_rs.pyi @@ -280,7 +280,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: packages - path: ./target/wheels/ + path: ./wheel/target/wheels/ fmt: runs-on: ubuntu-latest