├── .github └── workflows │ └── ci.yml ├── .gitignore ├── COPYRIGHT ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches ├── serialize.rs ├── sign.rs └── verify.rs ├── examples └── verify.rs ├── rust-toolchain ├── src ├── error.rs ├── key.rs ├── lib.rs └── signature.rs └── tests └── data.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: master 7 | 8 | env: 9 | RUSTFLAGS: "-Dwarnings" 10 | CARGO_INCREMENTAL: 0 11 | RUST_BACKTRACE: 1 12 | 13 | jobs: 14 | set-msrv: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | msrv: ${{ steps.msrv.outputs.MSRV }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Extract MSRV from Cargo.toml 21 | id: msrv 22 | run: | 23 | MSRV=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[0].rust_version') 24 | echo "MSRV=$MSRV" >> "$GITHUB_OUTPUT" 25 | 26 | # NOTE vmx 2022-06-14: currently doesn't work, hence run it on CircleCI 27 | # for now. 28 | linux_foreign: 29 | strategy: 30 | matrix: 31 | include: 32 | # 64-bit Linux/arm64 33 | - target: aarch64-unknown-linux-gnu 34 | toolchain: nightly 35 | arch: aarch64 36 | 37 | runs-on: ubuntu-22.04 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: uraimo/run-on-arch-action@v2.5.1 41 | name: Run commands 42 | id: runcmd 43 | with: 44 | arch: aarch64 45 | distro: ubuntu18.04 46 | 47 | # Not required, but speeds up builds by storing container images in 48 | # a GitHub package registry. 49 | githubToken: ${{ github.token }} 50 | 51 | install: | 52 | apt-get update -q -y 53 | apt-get install -q -y ocl-icd-opencl-dev curl build-essential git 54 | curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y 55 | source $HOME/.cargo/env 56 | 57 | run: | 58 | $HOME/.cargo/bin/cargo test --config net.git-fetch-with-cli=true --release --no-default-features --features pairing,multicore --target ${{ matrix.target }} 59 | $HOME/.cargo/bin/cargo test --config net.git-fetch-with-cli=true --release --no-default-features --features blst,multicore --target ${{ matrix.target }} 60 | $HOME/.cargo/bin/cargo test --config net.git-fetch-with-cli=true --release --no-default-features --features blst,multicore,blst-portable --target ${{ matrix.target }} 61 | 62 | # Linux tests 63 | linux: 64 | needs: set-msrv 65 | strategy: 66 | matrix: 67 | include: 68 | # 32-bit Linux/x86 69 | - target: i686-unknown-linux-gnu 70 | toolchain: ${{needs.set-msrv.outputs.msrv}} 71 | deps: sudo apt update && sudo apt install gcc-multilib 72 | - target: i686-unknown-linux-gnu 73 | toolchain: stable 74 | deps: sudo apt update && sudo apt install gcc-multilib 75 | 76 | # 64-bit Linux/x86_64 77 | - target: x86_64-unknown-linux-gnu 78 | toolchain: ${{needs.set-msrv.outputs.msrv}} 79 | - target: x86_64-unknown-linux-gnu 80 | toolchain: stable 81 | 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@v4 85 | - name: Install the Rust toolchain 86 | run: | 87 | rustup toolchain install ${{ matrix.toolchain }} --target ${{ matrix.target }} --profile minimal --no-self-update 88 | rustup override set ${{ matrix.toolchain }} 89 | - name: Install opencl 90 | run: sudo apt-get install -y ocl-icd-opencl-dev 91 | - run: ${{ matrix.deps }} 92 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features pairing,multicore 93 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features pairing 94 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst 95 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst,multicore 96 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst,blst-portable 97 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst,multicore,blst-portable 98 | 99 | 100 | # macOS tests 101 | macos: 102 | needs: set-msrv 103 | strategy: 104 | matrix: 105 | toolchain: 106 | - ${{needs.set-msrv.outputs.msrv}} 107 | - stable 108 | 109 | runs-on: macos-latest 110 | steps: 111 | - uses: actions/checkout@v4 112 | - name: Install the Rust toolchain 113 | run: | 114 | rustup toolchain install ${{ matrix.toolchain }} --target x86_64-apple-darwin --profile minimal --no-self-update 115 | rustup override set ${{ matrix.toolchain }} 116 | - run: cargo test --no-default-features --features pairing,multicore 117 | - run: cargo test --no-default-features --features pairing 118 | - run: cargo test --no-default-features --features blst 119 | - run: cargo test --no-default-features --features blst,multicore 120 | - run: cargo test --no-default-features --features blst,blst-portable 121 | - run: cargo test --no-default-features --features blst,multicore,blst-portable 122 | 123 | # Windows tests 124 | windows: 125 | needs: set-msrv 126 | strategy: 127 | matrix: 128 | include: 129 | # 64-bit Windows (MSVC) 130 | - target: x86_64-pc-windows-msvc 131 | toolchain: ${{needs.set-msrv.outputs.msrv}} 132 | 133 | runs-on: windows-latest 134 | steps: 135 | - uses: actions/checkout@v4 136 | - name: Install the Rust toolchain 137 | run: | 138 | rustup toolchain install ${{ matrix.toolchain }} --target ${{ matrix.target }} --profile minimal --no-self-update 139 | rustup override set ${{ matrix.toolchain }} 140 | - uses: msys2/setup-msys2@v2 141 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features pairing,multicore 142 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features pairing 143 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst 144 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst,multicore 145 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst,blst-portable 146 | - run: cargo test --target ${{ matrix.target }} --no-default-features --features blst,multicore,blst-portable 147 | 148 | clippy_check_blst: 149 | runs-on: ubuntu-latest 150 | steps: 151 | - uses: actions/checkout@v4 152 | - run: rustup component add clippy 153 | - name: Run Clippy 154 | run: cargo clippy --workspace --no-default-features --features blst,multicore -- -D warnings 155 | 156 | clippy_check_pairing: 157 | runs-on: ubuntu-latest 158 | steps: 159 | - uses: actions/checkout@v4 160 | - run: rustup component add clippy 161 | - name: Run Clippy 162 | run: cargo clippy --workspace -- -D warnings 163 | 164 | check_fmt_and_docs: 165 | name: Checking fmt and docs 166 | runs-on: ubuntu-latest 167 | steps: 168 | - uses: actions/checkout@v4 169 | - run: rustup component add rustfmt 170 | - name: Run cargo fmt 171 | run: cargo fmt --all -- --check 172 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | bls-signatures-ffi/target 4 | *.profile 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | This library is dual-licensed under Apache 2.0 and MIT terms. 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bls-signatures" 3 | version = "0.15.0" 4 | authors = ["dignifiedquire "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | resolver = "2" 8 | rust-version = "1.67.1" 9 | 10 | description = "Aggregate BLS Signatures" 11 | documentation = "https://docs.rs/bls-signatures" 12 | homepage = "https://github.com/filecoin-project/bls-signatures" 13 | repository = "https://github.com/filecoin-project/bls-signatures" 14 | readme = "README.md" 15 | 16 | [dependencies] 17 | rayon = { version = "1", optional = true } 18 | rand_core = "0.9" 19 | thiserror = "2.0" 20 | subtle = "2.6.1" 21 | 22 | ff = "0.13" 23 | group = "0.13" 24 | pairing_lib = { version = "0.23.0", package = "pairing" } 25 | 26 | bls12_381 = { version = "0.8.0", optional = true, features = ["experimental"] } 27 | sha2 = { version = "0.9", optional = true } 28 | hkdf = { version = "0.11.0", optional = true } 29 | 30 | blst_lib = { version = "0.3.10", optional = true, package = "blst" } 31 | blstrs = { version = "0.7.0", optional = true } 32 | 33 | [features] 34 | default = ["pairing", "multicore"] 35 | multicore = ["rayon"] 36 | pairing = ["bls12_381", "sha2", "hkdf"] 37 | blst = ["blst_lib", "blstrs"] 38 | blst-portable = ["blst_lib", "blst_lib/portable", "blstrs/portable"] 39 | 40 | [dev-dependencies] 41 | rand = "0.9" 42 | base64 = "0.22.1" 43 | serde = { version = "1.0.106", features = ["derive"] } 44 | serde_json = "1.0.52" 45 | base64-serde = "0.8.0" 46 | rand_chacha = "0.9" 47 | 48 | [[example]] 49 | name = "verify" 50 | required-features = ["multicore"] 51 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2019 by the Filecoin contributors. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BLS Signatures 2 | 3 | [![CircleCI][circleci-shield]][circleci] [![License][license-shield]][license] 4 | 5 | > Implementation of BLS signatures in pure Rust. 6 | 7 | 8 | ## Development 9 | 10 | ### BLST Portability 11 | 12 | To enable the portable feature when building blst dependencies, use the 'blst-portable' feature: `--features blst-portable`. 13 | 14 | ### Tests 15 | 16 | ``` 17 | > cargo test 18 | ``` 19 | 20 | ### Benchmarks 21 | 22 | ``` 23 | > cargo bench 24 | ``` 25 | 26 | ### Examples 27 | 28 | ``` 29 | # Verify 10,000 aggregated signatures 30 | > cargo run --example verify --release 31 | ``` 32 | 33 | ## LICENSE 34 | 35 | MIT or Apache 2.0 36 | 37 | ## Contribution 38 | 39 | Unless you explicitly state otherwise, any contribution intentionally submitted 40 | for inclusion in bls-signatures by you, as defined in the Apache-2.0 license, shall be 41 | dual licensed as above, without any additional terms or conditions. 42 | 43 | [circleci-shield]: https://img.shields.io/circleci/project/github/filecoin-project/bls-signatures.svg?style=flat-square 44 | [circleci]: https://circleci.com/gh/filecoin-project/bls-signatures 45 | [license-shield]: https://img.shields.io/badge/License-MIT%2FApache2.0-green.svg?style=flat-square 46 | [license]: https://github.com//filecoin-project/bls-signatures/blob/master/README.md#LICENSE 47 | [crate-shield]: https://img.shields.io/crates/v/accumulators.svg?style=flat-square 48 | [crate]: https://crates.io/crates/accumulators 49 | -------------------------------------------------------------------------------- /benches/serialize.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use test::{black_box, Bencher}; 5 | 6 | use bls_signatures::*; 7 | use rand::Rng; 8 | 9 | #[bench] 10 | fn bench_serialize_private_key_as_bytes(b: &mut Bencher) { 11 | let private_key = PrivateKey::generate(&mut rand::thread_rng()); 12 | 13 | b.iter(|| black_box(private_key.as_bytes())); 14 | } 15 | 16 | #[bench] 17 | fn bench_serialize_private_key_from_bytes(b: &mut Bencher) { 18 | let private_key = PrivateKey::generate(&mut rand::thread_rng()); 19 | let bytes = private_key.as_bytes(); 20 | 21 | b.iter(|| black_box(PrivateKey::from_bytes(&bytes).unwrap())); 22 | } 23 | 24 | #[bench] 25 | fn bench_serialize_public_key_as_bytes(b: &mut Bencher) { 26 | let public_key = PrivateKey::generate(&mut rand::thread_rng()).public_key(); 27 | 28 | b.iter(|| black_box(public_key.as_bytes())); 29 | } 30 | 31 | #[bench] 32 | fn bench_serialize_public_key_from_bytes(b: &mut Bencher) { 33 | let public_key = PrivateKey::generate(&mut rand::thread_rng()).public_key(); 34 | let bytes = public_key.as_bytes(); 35 | 36 | b.iter(|| black_box(PublicKey::from_bytes(&bytes).unwrap())); 37 | } 38 | 39 | #[bench] 40 | fn bench_serialize_signature_as_bytes(b: &mut Bencher) { 41 | let mut rng = rand::thread_rng(); 42 | let private_key = PrivateKey::generate(&mut rng); 43 | let msg = (0..64).map(|_| rng.gen()).collect::>(); 44 | let signature = private_key.sign(&msg); 45 | 46 | b.iter(|| black_box(signature.as_bytes())); 47 | } 48 | 49 | #[bench] 50 | fn bench_serialize_signature_from_bytes(b: &mut Bencher) { 51 | let mut rng = rand::thread_rng(); 52 | let private_key = PrivateKey::generate(&mut rng); 53 | let msg = (0..64).map(|_| rng.gen()).collect::>(); 54 | let signature = private_key.sign(&msg); 55 | let bytes = signature.as_bytes(); 56 | 57 | b.iter(|| black_box(Signature::from_bytes(&bytes).unwrap())); 58 | } 59 | -------------------------------------------------------------------------------- /benches/sign.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use test::{black_box, Bencher}; 5 | 6 | use bls_signatures::*; 7 | use rand::Rng; 8 | 9 | #[bench] 10 | fn sign_64b(b: &mut Bencher) { 11 | let rng = &mut rand::thread_rng(); 12 | 13 | let private_key = PrivateKey::generate(rng); 14 | let msg: Vec = (0..64).map(|_| rng.gen()).collect(); 15 | 16 | b.iter(|| black_box(private_key.sign(&msg))) 17 | } 18 | -------------------------------------------------------------------------------- /benches/verify.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use test::{black_box, Bencher}; 5 | 6 | use bls_signatures::*; 7 | use rand::Rng; 8 | 9 | macro_rules! bench_verify { 10 | ($name:ident, $num:expr) => { 11 | #[bench] 12 | fn $name(b: &mut Bencher) { 13 | let rng = &mut rand::thread_rng(); 14 | // generate private keys 15 | let private_keys: Vec<_> = (0..$num).map(|_| PrivateKey::generate(rng)).collect(); 16 | 17 | // generate messages 18 | let messages: Vec> = (0..$num) 19 | .map(|_| (0..64).map(|_| rng.gen()).collect()) 20 | .collect(); 21 | 22 | // sign messages 23 | let sigs = messages 24 | .iter() 25 | .zip(&private_keys) 26 | .map(|(message, pk)| pk.sign(message)) 27 | .collect::>(); 28 | 29 | let aggregated_signature = aggregate(&sigs).unwrap(); 30 | 31 | let hashes = messages 32 | .iter() 33 | .map(|message| hash(message)) 34 | .collect::>(); 35 | let public_keys = private_keys 36 | .iter() 37 | .map(|pk| pk.public_key()) 38 | .collect::>(); 39 | 40 | b.iter(|| black_box(verify(&aggregated_signature, &hashes, &public_keys))) 41 | } 42 | }; 43 | } 44 | 45 | bench_verify!(bench_verify_1, 1); 46 | bench_verify!(bench_verify_10, 10); 47 | bench_verify!(bench_verify_100, 100); 48 | -------------------------------------------------------------------------------- /examples/verify.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | #[cfg(feature = "pairing")] 4 | use bls12_381::G2Projective; 5 | #[cfg(feature = "blst")] 6 | use blstrs::G2Projective; 7 | 8 | use bls_signatures::*; 9 | use rand::{Rng, SeedableRng}; 10 | use rand_chacha::ChaCha8Rng; 11 | use rayon::prelude::*; 12 | 13 | macro_rules! measure { 14 | ($name:expr, $code:block) => { 15 | println!("\t{}", $name); 16 | let start = Instant::now(); 17 | let mut duration = Duration::new(0, 0); 18 | 19 | $code; 20 | 21 | duration += start.elapsed(); 22 | let total = 23 | { f64::from(duration.subsec_nanos()) / 1_000_000_000f64 + (duration.as_secs() as f64) }; 24 | 25 | println!("\t took {:.6}s", total); 26 | }; 27 | ($name:expr, $num:expr, $code:block) => { 28 | println!("\t{}", $name); 29 | let start = Instant::now(); 30 | let mut duration = Duration::new(0, 0); 31 | 32 | $code; 33 | 34 | duration += start.elapsed(); 35 | 36 | let total = 37 | { f64::from(duration.subsec_nanos()) / 1_000_000_000f64 + (duration.as_secs() as f64) }; 38 | let per_msg = { 39 | let avg = duration / $num as u32; 40 | f64::from(avg.subsec_nanos()) / 1_000_000f64 + (avg.as_secs() as f64 * 1000f64) 41 | }; 42 | 43 | println!("\t took {:.6}s ({:.3}ms per message)", total, per_msg); 44 | }; 45 | } 46 | 47 | fn run(num_messages: usize) { 48 | println!("dancing with {} messages", num_messages); 49 | 50 | let mut rng = ChaCha8Rng::seed_from_u64(12); 51 | 52 | // generate private keys 53 | let private_keys: Vec<_> = (0..num_messages) 54 | .map(|_| PrivateKey::generate(&mut rng)) 55 | .collect(); 56 | 57 | // generate messages 58 | let messages: Vec> = (0..num_messages) 59 | .map(|_| (0..64).map(|_| rng.random()).collect()) 60 | .collect(); 61 | 62 | // sign messages 63 | let sigs: Vec; 64 | measure!("signing", num_messages, { 65 | sigs = messages 66 | .par_iter() 67 | .zip(private_keys.par_iter()) 68 | .map(|(message, pk)| pk.sign(message)) 69 | .collect::>(); 70 | }); 71 | 72 | let aggregated_signature: Signature; 73 | measure!("aggregate signatures", num_messages, { 74 | aggregated_signature = aggregate(&sigs).expect("failed to aggregate"); 75 | }); 76 | 77 | let serialized_signature: Vec<_>; 78 | measure!("serialize signature", { 79 | serialized_signature = aggregated_signature.as_bytes(); 80 | }); 81 | 82 | let hashes: Vec; 83 | measure!("hashing messages", num_messages, { 84 | hashes = messages 85 | .par_iter() 86 | .map(|message| hash(message)) 87 | .collect::>(); 88 | }); 89 | let public_keys: Vec; 90 | measure!("extracting public keys", num_messages, { 91 | public_keys = private_keys 92 | .par_iter() 93 | .map(|pk| pk.public_key()) 94 | .collect::>(); 95 | }); 96 | 97 | let agg_sig: Signature; 98 | measure!("deserialize signature", { 99 | agg_sig = Signature::from_bytes(&serialized_signature).unwrap(); 100 | }); 101 | 102 | measure!("verification", num_messages, { 103 | assert!(verify(&agg_sig, &hashes, &public_keys)); 104 | }); 105 | 106 | measure!("verification messages", num_messages, { 107 | let messages = messages.iter().map(|r| &r[..]).collect::>(); 108 | assert!(verify_messages(&agg_sig, &messages[..], &public_keys)); 109 | }); 110 | } 111 | 112 | fn main() { 113 | run(10_000); 114 | } 115 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.67.1 2 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum Error { 5 | #[error("Size mismatch")] 6 | SizeMismatch, 7 | #[error("Io error: {0}")] 8 | Io(#[from] std::io::Error), 9 | #[error("Group decode error")] 10 | GroupDecode, 11 | #[error("Curve decode error")] 12 | CurveDecode, 13 | #[error("Prime field decode error")] 14 | FieldDecode, 15 | #[error("Invalid Private Key")] 16 | InvalidPrivateKey, 17 | #[error("Zero sized input")] 18 | ZeroSizedInput, 19 | } 20 | -------------------------------------------------------------------------------- /src/key.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use ff::{PrimeField, PrimeFieldBits}; 4 | use group::Curve; 5 | use rand_core::{CryptoRng, TryRngCore}; 6 | 7 | #[cfg(feature = "pairing")] 8 | use bls12_381::{hash_to_curve::HashToField, G1Affine, G1Projective, Scalar}; 9 | #[cfg(feature = "pairing")] 10 | use hkdf::Hkdf; 11 | #[cfg(feature = "pairing")] 12 | use sha2::{digest::generic_array::typenum::U48, digest::generic_array::GenericArray, Sha256}; 13 | 14 | #[cfg(feature = "blst")] 15 | use blstrs::{G1Affine, G1Projective, G2Affine, Scalar}; 16 | #[cfg(feature = "blst")] 17 | use group::prime::PrimeCurveAffine; 18 | 19 | use crate::error::Error; 20 | use crate::signature::*; 21 | 22 | pub(crate) const G1_COMPRESSED_SIZE: usize = 48; 23 | 24 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 25 | pub struct PublicKey(pub(crate) G1Projective); 26 | 27 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 28 | pub struct PrivateKey(pub(crate) Scalar); 29 | 30 | impl From for PublicKey { 31 | fn from(val: G1Projective) -> Self { 32 | PublicKey(val) 33 | } 34 | } 35 | impl From for G1Projective { 36 | fn from(val: PublicKey) -> Self { 37 | val.0 38 | } 39 | } 40 | 41 | impl From for PrivateKey { 42 | fn from(val: Scalar) -> Self { 43 | PrivateKey(val) 44 | } 45 | } 46 | 47 | impl From for Scalar { 48 | fn from(val: PrivateKey) -> Self { 49 | val.0 50 | } 51 | } 52 | 53 | pub trait Serialize: ::std::fmt::Debug + Sized { 54 | /// Writes the key to the given writer. 55 | fn write_bytes(&self, dest: &mut impl io::Write) -> io::Result<()>; 56 | 57 | /// Recreate the key from bytes in the same form as `write_bytes` produced. 58 | fn from_bytes(raw: &[u8]) -> Result; 59 | 60 | fn as_bytes(&self) -> Vec { 61 | let mut res = Vec::with_capacity(8 * 4); 62 | self.write_bytes(&mut res).expect("preallocated"); 63 | res 64 | } 65 | } 66 | 67 | impl PrivateKey { 68 | /// Generate a deterministic private key from the given bytes. 69 | /// 70 | /// They must be at least 32 bytes long to be secure, will panic otherwise. 71 | pub fn new>(msg: T) -> Self { 72 | PrivateKey(key_gen(msg)) 73 | } 74 | 75 | /// Generate a new private key. 76 | pub fn generate(rng: &mut R) -> Self { 77 | // IKM must be at least 32 bytes long: 78 | // https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-00#section-2.3 79 | let mut ikm = [0u8; 32]; 80 | rng.try_fill_bytes(&mut ikm) 81 | .expect("unable to produce secure randomness"); 82 | 83 | Self::new(ikm) 84 | } 85 | 86 | /// Sign the given message. 87 | /// Calculated by `signature = hash_into_g2(message) * sk` 88 | #[cfg(feature = "pairing")] 89 | pub fn sign>(&self, message: T) -> Signature { 90 | let mut p = hash(message.as_ref()); 91 | p *= self.0; 92 | 93 | p.into() 94 | } 95 | 96 | /// Sign the given message. 97 | /// Calculated by `signature = hash_into_g2(message) * sk` 98 | #[cfg(feature = "blst")] 99 | pub fn sign>(&self, message: T) -> Signature { 100 | let p = hash(message.as_ref()); 101 | let mut sig = G2Affine::identity(); 102 | 103 | unsafe { 104 | blst_lib::blst_sign_pk2_in_g1( 105 | std::ptr::null_mut(), 106 | sig.as_mut(), 107 | p.as_ref(), 108 | &self.0.into(), 109 | ); 110 | } 111 | 112 | sig.into() 113 | } 114 | 115 | /// Get the public key for this private key. 116 | /// Calculated by `pk = g1 * sk`. 117 | #[cfg(feature = "pairing")] 118 | pub fn public_key(&self) -> PublicKey { 119 | let mut pk = G1Projective::generator(); 120 | pk *= self.0; 121 | 122 | PublicKey(pk) 123 | } 124 | 125 | /// Get the public key for this private key. 126 | /// Calculated by `pk = g1 * sk`. 127 | #[cfg(feature = "blst")] 128 | pub fn public_key(&self) -> PublicKey { 129 | let mut pk = G1Affine::identity(); 130 | 131 | unsafe { 132 | blst_lib::blst_sk_to_pk2_in_g1(std::ptr::null_mut(), pk.as_mut(), &self.0.into()); 133 | } 134 | 135 | PublicKey(pk.into()) 136 | } 137 | 138 | /// Deserializes a private key from the field element as a decimal number. 139 | pub fn from_string>(s: T) -> Result { 140 | match Scalar::from_str_vartime(s.as_ref()) { 141 | Some(f) => Ok(f.into()), 142 | None => Err(Error::InvalidPrivateKey), 143 | } 144 | } 145 | } 146 | 147 | impl Serialize for PrivateKey { 148 | fn write_bytes(&self, dest: &mut impl io::Write) -> io::Result<()> { 149 | for digit in &self.0.to_le_bits().data { 150 | dest.write_all(&digit.to_le_bytes())?; 151 | } 152 | 153 | Ok(()) 154 | } 155 | 156 | fn from_bytes(raw: &[u8]) -> Result { 157 | const FR_SIZE: usize = (Scalar::NUM_BITS as usize + 8 - 1) / 8; 158 | if raw.len() != FR_SIZE { 159 | return Err(Error::SizeMismatch); 160 | } 161 | 162 | let mut res = [0u8; FR_SIZE]; 163 | res.copy_from_slice(&raw[..FR_SIZE]); 164 | 165 | // TODO: once zero keys are rejected, insert check for zero. 166 | 167 | Scalar::from_repr_vartime(res) 168 | .map(Into::into) 169 | .ok_or(Error::InvalidPrivateKey) 170 | } 171 | } 172 | 173 | impl PublicKey { 174 | pub fn as_affine(&self) -> G1Affine { 175 | self.0.to_affine() 176 | } 177 | 178 | pub fn verify>(&self, sig: Signature, message: T) -> bool { 179 | verify_messages(&sig, &[message.as_ref()], &[*self]) 180 | } 181 | } 182 | 183 | impl Serialize for PublicKey { 184 | fn write_bytes(&self, dest: &mut impl io::Write) -> io::Result<()> { 185 | let t = self.0.to_affine(); 186 | let tmp = t.to_compressed(); 187 | dest.write_all(tmp.as_ref())?; 188 | 189 | Ok(()) 190 | } 191 | 192 | fn from_bytes(raw: &[u8]) -> Result { 193 | if raw.len() != G1_COMPRESSED_SIZE { 194 | return Err(Error::SizeMismatch); 195 | } 196 | 197 | let mut res = [0u8; G1_COMPRESSED_SIZE]; 198 | res.as_mut().copy_from_slice(raw); 199 | let affine: G1Affine = 200 | Option::from(G1Affine::from_compressed(&res)).ok_or(Error::GroupDecode)?; 201 | 202 | Ok(PublicKey(affine.into())) 203 | } 204 | } 205 | 206 | /// Generates a secret key as defined in 207 | /// https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-2.3 208 | #[cfg(feature = "pairing")] 209 | fn key_gen>(data: T) -> Scalar { 210 | // "BLS-SIG-KEYGEN-SALT-" 211 | const SALT: &[u8] = b"BLS-SIG-KEYGEN-SALT-"; 212 | 213 | let data = data.as_ref(); 214 | assert!(data.len() >= 32, "IKM must be at least 32 bytes"); 215 | 216 | // HKDF-Extract 217 | let mut msg = data.as_ref().to_vec(); 218 | // append zero byte 219 | msg.push(0); 220 | let prk = Hkdf::::new(Some(SALT), &msg); 221 | 222 | // HKDF-Expand 223 | // `result` has enough length to hold the output from HKDF expansion 224 | let mut result = GenericArray::::default(); 225 | assert!(prk.expand(&[0, 48], &mut result).is_ok()); 226 | 227 | Scalar::from_okm(&result) 228 | } 229 | 230 | /// Generates a secret key as defined in 231 | /// https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-2.3 232 | #[cfg(feature = "blst")] 233 | fn key_gen>(data: T) -> Scalar { 234 | let data = data.as_ref(); 235 | assert!(data.len() >= 32, "IKM must be at least 32 bytes"); 236 | 237 | let key_info = &[]; 238 | let mut out = blst_lib::blst_scalar::default(); 239 | unsafe { 240 | blst_lib::blst_keygen( 241 | &mut out, 242 | data.as_ptr(), 243 | data.len(), 244 | key_info.as_ptr(), 245 | key_info.len(), 246 | ) 247 | }; 248 | 249 | out.try_into().expect("invalid key generated") 250 | } 251 | 252 | #[cfg(test)] 253 | mod tests { 254 | use super::*; 255 | 256 | use rand::SeedableRng; 257 | use rand_chacha::ChaCha8Rng; 258 | 259 | #[test] 260 | fn test_bytes_roundtrip() { 261 | let rng = &mut ChaCha8Rng::seed_from_u64(12); 262 | let sk = PrivateKey::generate(rng); 263 | let sk_bytes = sk.as_bytes(); 264 | 265 | assert_eq!(sk_bytes.len(), 32); 266 | assert_eq!(PrivateKey::from_bytes(&sk_bytes).unwrap(), sk); 267 | 268 | let pk = sk.public_key(); 269 | let pk_bytes = pk.as_bytes(); 270 | 271 | assert_eq!(pk_bytes.len(), 48); 272 | assert_eq!(PublicKey::from_bytes(&pk_bytes).unwrap(), pk); 273 | } 274 | 275 | #[test] 276 | fn test_key_gen() { 277 | let key_material = "hello world (it's a secret!) very secret stuff"; 278 | let fr_val = key_gen(key_material); 279 | #[cfg(feature = "blst")] 280 | let expect = Scalar::from_u64s_le(&[ 281 | 0x8a223b0f9e257f7d, 282 | 0x2d80f7b7f5ea6cc4, 283 | 0xcc9e063a0ea0009c, 284 | 0x4a73baed5cb75109, 285 | ]) 286 | .unwrap(); 287 | 288 | #[cfg(feature = "pairing")] 289 | let expect = Scalar::from_raw([ 290 | 0xa9f8187b89e6d49a, 291 | 0xf870f34063ce4b16, 292 | 0xc2aa3c1fff1bbaa3, 293 | 0x60417787ee46e23f, 294 | ]); 295 | 296 | assert_eq!(fr_val, expect); 297 | } 298 | 299 | #[test] 300 | fn test_sig() { 301 | let msg = "this is the message"; 302 | let sk = "this is the key and it is very secret"; 303 | 304 | let sk = PrivateKey::new(sk); 305 | let sig = sk.sign(msg); 306 | let pk = sk.public_key(); 307 | 308 | assert!(pk.verify(sig, msg)); 309 | } 310 | 311 | #[test] 312 | fn test_from_bytes() { 313 | // Larger than the modulus 314 | assert!(PrivateKey::from_bytes(&[255u8; 32]).is_err()); 315 | 316 | // Scalar field modulus' bigint (i.e. non-Montgomery form) little-endian bytes. 317 | let modulus_repr: [u8; 32] = [ 318 | 0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0x02, 0xa4, 319 | 0xbd, 0x53, 0x05, 0xd8, 0xa1, 0x09, 0x08, 0xd8, 0x39, 0x33, 0x48, 0x7d, 0x9d, 0x29, 320 | 0x53, 0xa7, 0xed, 0x73, 321 | ]; 322 | 323 | // Largest field element. 324 | let neg1_repr: [u8; 32] = { 325 | let mut repr = modulus_repr; 326 | repr[0] -= 1; 327 | repr 328 | }; 329 | assert!(PrivateKey::from_bytes(&neg1_repr).is_ok()); 330 | 331 | // Smallest integer greater than the modulus. 332 | let modulus_plus_1_repr = { 333 | let mut repr = modulus_repr; 334 | repr[0] += 1; 335 | repr 336 | }; 337 | assert!(PrivateKey::from_bytes(&modulus_plus_1_repr).is_err()); 338 | 339 | // simple numbers below the modulus 340 | assert!(PrivateKey::from_bytes(&Scalar::from(1).to_repr()).is_ok()); 341 | assert!(PrivateKey::from_bytes(&Scalar::from(10).to_repr()).is_ok()); 342 | assert!(PrivateKey::from_bytes(&Scalar::from(100).to_repr()).is_ok()); 343 | 344 | // Larger than the modulus 345 | assert!(PublicKey::from_bytes(&[255u8; 48]).is_err()); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "pairing", feature = "blst"))] 2 | compile_error!("only pairing or blst can be enabled"); 3 | 4 | mod error; 5 | mod key; 6 | mod signature; 7 | 8 | pub use self::error::Error; 9 | pub use self::key::{PrivateKey, PublicKey, Serialize}; 10 | pub use self::signature::{aggregate, hash, verify, verify_messages, Signature}; 11 | 12 | #[cfg(test)] 13 | #[macro_use] 14 | extern crate base64_serde; 15 | -------------------------------------------------------------------------------- /src/signature.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, io}; 2 | 3 | #[cfg(feature = "multicore")] 4 | use rayon::prelude::*; 5 | 6 | #[cfg(feature = "pairing")] 7 | use bls12_381::{ 8 | hash_to_curve::{ExpandMsgXmd, HashToCurve}, 9 | Bls12, G1Affine, G2Affine, G2Projective, Gt, MillerLoopResult, 10 | }; 11 | use pairing_lib::MultiMillerLoop; 12 | 13 | #[cfg(feature = "blst")] 14 | use blstrs::{Bls12, G1Affine, G2Affine, G2Projective, Gt, MillerLoopResult}; 15 | #[cfg(feature = "blst")] 16 | use group::{prime::PrimeCurveAffine, Group}; 17 | #[cfg(feature = "blst")] 18 | use pairing_lib::MillerLoopResult as _; 19 | 20 | use crate::error::Error; 21 | use crate::key::*; 22 | 23 | const CSUITE: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; 24 | const G2_COMPRESSED_SIZE: usize = 96; 25 | 26 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 27 | pub struct Signature(G2Affine); 28 | 29 | impl From for Signature { 30 | fn from(val: G2Projective) -> Self { 31 | Signature(val.into()) 32 | } 33 | } 34 | impl From for G2Projective { 35 | fn from(val: Signature) -> Self { 36 | val.0.into() 37 | } 38 | } 39 | 40 | impl From for Signature { 41 | fn from(val: G2Affine) -> Self { 42 | Signature(val) 43 | } 44 | } 45 | 46 | impl From for G2Affine { 47 | fn from(val: Signature) -> Self { 48 | val.0 49 | } 50 | } 51 | 52 | impl Serialize for Signature { 53 | fn write_bytes(&self, dest: &mut impl io::Write) -> io::Result<()> { 54 | dest.write_all(&self.0.to_compressed())?; 55 | 56 | Ok(()) 57 | } 58 | 59 | fn from_bytes(raw: &[u8]) -> Result { 60 | let g2 = g2_from_slice(raw)?; 61 | Ok(g2.into()) 62 | } 63 | } 64 | 65 | fn g2_from_slice(raw: &[u8]) -> Result { 66 | if raw.len() != G2_COMPRESSED_SIZE { 67 | return Err(Error::SizeMismatch); 68 | } 69 | 70 | let mut res = [0u8; G2_COMPRESSED_SIZE]; 71 | res.copy_from_slice(raw); 72 | 73 | Option::from(G2Affine::from_compressed(&res)).ok_or(Error::GroupDecode) 74 | } 75 | 76 | /// Hash the given message, as used in the signature. 77 | #[cfg(feature = "pairing")] 78 | pub fn hash(msg: &[u8]) -> G2Projective { 79 | >>::hash_to_curve(msg, CSUITE) 80 | } 81 | 82 | #[cfg(feature = "blst")] 83 | pub fn hash(msg: &[u8]) -> G2Projective { 84 | G2Projective::hash_to_curve(msg, CSUITE, &[]) 85 | } 86 | 87 | /// Aggregate signatures by multiplying them together. 88 | /// Calculated by `signature = \sum_{i = 0}^n signature_i`. 89 | #[cfg(feature = "multicore")] 90 | pub fn aggregate(signatures: &[Signature]) -> Result { 91 | if signatures.is_empty() { 92 | return Err(Error::ZeroSizedInput); 93 | } 94 | 95 | let res = signatures 96 | .into_par_iter() 97 | .fold(G2Projective::identity, |mut acc, signature| { 98 | acc += &signature.0; 99 | acc 100 | }) 101 | .reduce(G2Projective::identity, |acc, val| acc + val); 102 | 103 | Ok(Signature(res.into())) 104 | } 105 | 106 | /// Aggregate signatures by multiplying them together. 107 | /// Calculated by `signature = \sum_{i = 0}^n signature_i`. 108 | #[cfg(not(feature = "multicore"))] 109 | pub fn aggregate(signatures: &[Signature]) -> Result { 110 | if signatures.is_empty() { 111 | return Err(Error::ZeroSizedInput); 112 | } 113 | 114 | let res = signatures 115 | .iter() 116 | .fold(G2Projective::identity(), |acc, signature| acc + signature.0); 117 | 118 | Ok(Signature(res.into())) 119 | } 120 | 121 | /// Verifies that the signature is the actual aggregated signature of hashes - pubkeys. 122 | /// Calculated by `e(g1, signature) == \prod_{i = 0}^n e(pk_i, hash_i)`. 123 | pub fn verify(signature: &Signature, hashes: &[G2Projective], public_keys: &[PublicKey]) -> bool { 124 | if hashes.is_empty() || public_keys.is_empty() { 125 | return false; 126 | } 127 | 128 | let n_hashes = hashes.len(); 129 | 130 | if n_hashes != public_keys.len() { 131 | return false; 132 | } 133 | 134 | // zero keys should always fail. 135 | if public_keys.iter().any(|pk| pk.0.is_identity().into()) { 136 | return false; 137 | } 138 | 139 | // Enforce that messages are distinct as a countermeasure against BLS's rogue-key attack. 140 | // See Section 3.1. of the IRTF's BLS signatures spec: 141 | // https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-3.1 142 | if hashes 143 | .iter() 144 | // This is the best way to get something we can actually hash. 145 | .map(|h| G2Affine::from(h).to_compressed()) 146 | .collect::>() 147 | .len() 148 | != hashes.len() 149 | { 150 | return false; 151 | } 152 | 153 | #[cfg(feature = "multicore")] 154 | let mut ml = public_keys 155 | .par_iter() 156 | .zip(hashes.par_iter()) 157 | .map(|(pk, h)| { 158 | let pk = pk.as_affine(); 159 | let h = G2Affine::from(h).into(); 160 | Bls12::multi_miller_loop(&[(&pk, &h)]) 161 | }) 162 | .reduce(MillerLoopResult::default, |acc, cur| acc + cur); 163 | 164 | #[cfg(not(feature = "multicore"))] 165 | let mut ml = public_keys 166 | .iter() 167 | .zip(hashes.iter()) 168 | .map(|(pk, h)| { 169 | let pk = pk.as_affine(); 170 | let h = G2Affine::from(h).into(); 171 | Bls12::multi_miller_loop(&[(&pk, &h)]) 172 | }) 173 | .fold(MillerLoopResult::default(), |acc, cur| acc + cur); 174 | 175 | let g1_neg = -G1Affine::generator(); 176 | 177 | ml += Bls12::multi_miller_loop(&[(&g1_neg, &signature.0.into())]); 178 | 179 | ml.final_exponentiation() == Gt::identity() 180 | } 181 | 182 | /// Verifies that the signature is the actual aggregated signature of messages - pubkeys. 183 | /// Calculated by `e(g1, signature) == \prod_{i = 0}^n e(pk_i, hash_i)`. 184 | #[cfg(feature = "pairing")] 185 | pub fn verify_messages( 186 | signature: &Signature, 187 | messages: &[&[u8]], 188 | public_keys: &[PublicKey], 189 | ) -> bool { 190 | #[cfg(feature = "multicore")] 191 | let hashes: Vec<_> = messages.par_iter().map(|msg| hash(msg)).collect(); 192 | 193 | #[cfg(not(feature = "multicore"))] 194 | let hashes: Vec<_> = messages.iter().map(|msg| hash(msg)).collect(); 195 | 196 | verify(signature, &hashes, public_keys) 197 | } 198 | 199 | /// Verifies that the signature is the actual aggregated signature of messages - pubkeys. 200 | /// Calculated by `e(g1, signature) == \prod_{i = 0}^n e(pk_i, hash_i)`. 201 | #[cfg(all(feature = "blst", feature = "multicore"))] 202 | pub fn verify_messages( 203 | signature: &Signature, 204 | messages: &[&[u8]], 205 | public_keys: &[PublicKey], 206 | ) -> bool { 207 | use blst_lib::BLST_ERROR; 208 | 209 | if messages.is_empty() || public_keys.is_empty() { 210 | return false; 211 | } 212 | 213 | let n_messages = messages.len(); 214 | 215 | if n_messages != public_keys.len() { 216 | return false; 217 | } 218 | 219 | // zero key & single message should fail 220 | if n_messages == 1 && public_keys[0].0.is_identity().into() { 221 | return false; 222 | } 223 | 224 | // Enforce that messages are distinct as a countermeasure against BLS's rogue-key attack. 225 | // See Section 3.1. of the IRTF's BLS signatures spec: 226 | // https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-3.1 227 | if !blstrs::unique_messages(messages) { 228 | return false; 229 | } 230 | 231 | let n_workers = std::cmp::min(rayon::current_num_threads(), n_messages); 232 | let Some(Ok(acc)) = messages 233 | .par_iter() 234 | .zip(public_keys.par_iter()) 235 | .chunks(n_messages / n_workers) 236 | .map(|chunk| -> Result<_, BLST_ERROR> { 237 | let mut pairing = blstrs::PairingG1G2::new(true, CSUITE); 238 | 239 | for (message, public_key) in chunk { 240 | pairing.aggregate(&public_key.0.into(), None, message, &[])?; 241 | } 242 | pairing.commit(); 243 | Ok(pairing) 244 | }) 245 | .try_reduce_with(|mut acc, pairing| -> Result<_, BLST_ERROR> { 246 | acc.merge(&pairing)?; 247 | Ok(acc) 248 | }) 249 | else { 250 | return false; 251 | }; 252 | 253 | let mut gtsig = Gt::default(); 254 | blstrs::PairingG1G2::aggregated(&mut gtsig, &signature.0); 255 | acc.finalverify(Some(>sig)) 256 | } 257 | 258 | /// Verifies that the signature is the actual aggregated signature of messages - pubkeys. 259 | /// Calculated by `e(g1, signature) == \prod_{i = 0}^n e(pk_i, hash_i)`. 260 | #[cfg(all(feature = "blst", not(feature = "multicore")))] 261 | pub fn verify_messages( 262 | signature: &Signature, 263 | messages: &[&[u8]], 264 | public_keys: &[PublicKey], 265 | ) -> bool { 266 | if messages.is_empty() || public_keys.is_empty() { 267 | return false; 268 | } 269 | 270 | let n_messages = messages.len(); 271 | 272 | if n_messages != public_keys.len() { 273 | return false; 274 | } 275 | 276 | // zero key & single message should fail 277 | if n_messages == 1 && public_keys[0].0.is_identity().into() { 278 | return false; 279 | } 280 | 281 | // Enforce that messages are distinct as a countermeasure against BLS's rogue-key attack. 282 | // See Section 3.1. of the IRTF's BLS signatures spec: 283 | // https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02#section-3.1 284 | if !blstrs::unique_messages(messages) { 285 | return false; 286 | } 287 | 288 | let mut pairing = blstrs::PairingG1G2::new(true, CSUITE); 289 | for (message, public_key) in messages.iter().zip(public_keys.iter()) { 290 | if pairing 291 | .aggregate(&public_key.0.into(), None, message, &[]) 292 | .is_err() 293 | { 294 | return false; 295 | } 296 | pairing.commit(); 297 | } 298 | 299 | let mut gtsig = Gt::default(); 300 | blstrs::PairingG1G2::aggregated(&mut gtsig, &signature.0); 301 | 302 | pairing.finalverify(Some(>sig)) 303 | } 304 | 305 | #[cfg(test)] 306 | mod tests { 307 | use super::*; 308 | 309 | use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; 310 | use base64::Engine as _; 311 | use ff::Field; 312 | use rand::{Rng, SeedableRng}; 313 | use rand_chacha::ChaCha8Rng; 314 | use serde::Deserialize; 315 | 316 | #[cfg(feature = "pairing")] 317 | use crate::key::G1_COMPRESSED_SIZE; 318 | #[cfg(feature = "pairing")] 319 | use bls12_381::{G1Projective, Scalar}; 320 | #[cfg(feature = "blst")] 321 | use blstrs::{G1Projective, Scalar}; 322 | 323 | #[test] 324 | fn basic_aggregation() { 325 | let mut rng = ChaCha8Rng::seed_from_u64(12); 326 | 327 | let num_messages = 10; 328 | 329 | // generate private keys 330 | let private_keys: Vec<_> = (0..num_messages) 331 | .map(|_| PrivateKey::generate(&mut rng)) 332 | .collect(); 333 | 334 | // generate messages 335 | let messages: Vec> = (0..num_messages) 336 | .map(|_| (0..64).map(|_| rng.random()).collect()) 337 | .collect(); 338 | 339 | // sign messages 340 | let sigs = messages 341 | .iter() 342 | .zip(&private_keys) 343 | .map(|(message, pk)| pk.sign(message)) 344 | .collect::>(); 345 | 346 | let aggregated_signature = aggregate(&sigs).expect("failed to aggregate"); 347 | 348 | let hashes = messages 349 | .iter() 350 | .map(|message| hash(message)) 351 | .collect::>(); 352 | let public_keys = private_keys 353 | .iter() 354 | .map(|pk| pk.public_key()) 355 | .collect::>(); 356 | 357 | assert!( 358 | verify(&aggregated_signature, &hashes, &public_keys), 359 | "failed to verify" 360 | ); 361 | 362 | let messages = messages.iter().map(|r| &r[..]).collect::>(); 363 | assert!(verify_messages( 364 | &aggregated_signature, 365 | &messages[..], 366 | &public_keys 367 | )); 368 | } 369 | 370 | #[test] 371 | fn aggregation_same_messages() { 372 | let mut rng = ChaCha8Rng::seed_from_u64(12); 373 | 374 | let num_messages = 10; 375 | 376 | // generate private keys 377 | let private_keys: Vec<_> = (0..num_messages) 378 | .map(|_| PrivateKey::generate(&mut rng)) 379 | .collect(); 380 | 381 | // generate messages 382 | let message: Vec = (0..64).map(|_| rng.random()).collect(); 383 | 384 | // sign messages 385 | let sigs = private_keys 386 | .iter() 387 | .map(|pk| pk.sign(&message)) 388 | .collect::>(); 389 | 390 | let aggregated_signature = aggregate(&sigs).expect("failed to aggregate"); 391 | 392 | // check that equal messages can not be aggreagated 393 | let hashes: Vec<_> = (0..num_messages).map(|_| hash(&message)).collect(); 394 | let public_keys = private_keys 395 | .iter() 396 | .map(|pk| pk.public_key()) 397 | .collect::>(); 398 | assert!( 399 | !verify(&aggregated_signature, &hashes, &public_keys), 400 | "must not verify aggregate with the same messages" 401 | ); 402 | let messages = vec![&message[..]; num_messages]; 403 | 404 | assert!(!verify_messages( 405 | &aggregated_signature, 406 | &messages[..], 407 | &public_keys 408 | )); 409 | } 410 | 411 | #[test] 412 | fn test_zero_key() { 413 | let mut rng = ChaCha8Rng::seed_from_u64(12); 414 | 415 | // In the current iteration we expect the zero key to be valid and work. 416 | let zero_key: PrivateKey = Scalar::ZERO.into(); 417 | assert!(bool::from(zero_key.public_key().0.is_identity())); 418 | 419 | println!( 420 | "{:?}\n{:?}", 421 | zero_key.public_key().as_bytes(), 422 | zero_key.as_bytes() 423 | ); 424 | let num_messages = 10; 425 | 426 | // generate private keys 427 | let mut private_keys: Vec<_> = (0..num_messages - 1) 428 | .map(|_| PrivateKey::generate(&mut rng)) 429 | .collect(); 430 | 431 | private_keys.push(zero_key); 432 | 433 | // generate messages 434 | let messages: Vec> = (0..num_messages) 435 | .map(|_| (0..64).map(|_| rng.random()).collect()) 436 | .collect(); 437 | 438 | // sign messages 439 | let sigs = messages 440 | .iter() 441 | .zip(&private_keys) 442 | .map(|(message, pk)| pk.sign(message)) 443 | .collect::>(); 444 | 445 | let aggregated_signature = aggregate(&sigs).expect("failed to aggregate"); 446 | 447 | let hashes = messages 448 | .iter() 449 | .map(|message| hash(message)) 450 | .collect::>(); 451 | let public_keys = private_keys 452 | .iter() 453 | .map(|pk| pk.public_key()) 454 | .collect::>(); 455 | 456 | assert!( 457 | !verify(&aggregated_signature, &hashes, &public_keys), 458 | "verified with zero key" 459 | ); 460 | 461 | let messages = messages.iter().map(|r| &r[..]).collect::>(); 462 | assert!(!verify_messages( 463 | &aggregated_signature, 464 | &messages[..], 465 | &public_keys 466 | )); 467 | 468 | // single message is rejected 469 | let signature = zero_key.sign(&messages[0]); 470 | 471 | assert!(!zero_key.public_key().verify(signature, &messages[0])); 472 | 473 | let aggregated_signature = aggregate(&[signature][..]).expect("failed to aggregate"); 474 | assert!(!verify_messages( 475 | &aggregated_signature, 476 | &messages[..1], 477 | &[zero_key.public_key()][..], 478 | )); 479 | } 480 | 481 | #[test] 482 | fn test_bytes_roundtrip() { 483 | let mut rng = ChaCha8Rng::seed_from_u64(12); 484 | let sk = PrivateKey::generate(&mut rng); 485 | 486 | let msg = (0..64).map(|_| rng.random()).collect::>(); 487 | let signature = sk.sign(&msg); 488 | 489 | let signature_bytes = signature.as_bytes(); 490 | assert_eq!(signature_bytes.len(), 96); 491 | assert_eq!(Signature::from_bytes(&signature_bytes).unwrap(), signature); 492 | } 493 | 494 | base64_serde_type!(Base64Standard, BASE64_STANDARD); 495 | 496 | #[derive(Debug, Clone, Deserialize)] 497 | struct Case { 498 | #[serde(rename = "Msg")] 499 | msg: String, 500 | #[serde(rename = "Ciphersuite")] 501 | ciphersuite: String, 502 | #[serde(rename = "G1Compressed", with = "Base64Standard")] 503 | g1_compressed: Vec, 504 | #[serde(rename = "G2Compressed", with = "Base64Standard")] 505 | g2_compressed: Vec, 506 | #[serde(rename = "BLSPrivKey")] 507 | priv_key: Option, 508 | #[serde(rename = "BLSPubKey")] 509 | pub_key: Option, 510 | #[serde(rename = "BLSSigG2")] 511 | signature: Option, 512 | } 513 | 514 | #[derive(Debug, Clone, Deserialize)] 515 | struct Cases { 516 | cases: Vec, 517 | } 518 | 519 | fn g1_from_slice(raw: &[u8]) -> Result { 520 | if raw.len() != G1_COMPRESSED_SIZE { 521 | return Err(Error::SizeMismatch); 522 | } 523 | 524 | let mut res = [0u8; G1_COMPRESSED_SIZE]; 525 | res.as_mut().copy_from_slice(raw); 526 | 527 | Option::from(G1Affine::from_compressed(&res)).ok_or(Error::GroupDecode) 528 | } 529 | 530 | #[cfg(feature = "pairing")] 531 | fn hash_to_g1(msg: &[u8], suite: &[u8]) -> G1Projective { 532 | >>::hash_to_curve(msg, suite) 533 | } 534 | #[cfg(feature = "blst")] 535 | fn hash_to_g1(msg: &[u8], suite: &[u8]) -> G1Projective { 536 | G1Projective::hash_to_curve(msg, suite, &[]) 537 | } 538 | 539 | #[cfg(feature = "pairing")] 540 | fn hash_to_g2(msg: &[u8], suite: &[u8]) -> G2Projective { 541 | >>::hash_to_curve(msg, suite) 542 | } 543 | #[cfg(feature = "blst")] 544 | fn hash_to_g2(msg: &[u8], suite: &[u8]) -> G2Projective { 545 | G2Projective::hash_to_curve(msg, suite, &[]) 546 | } 547 | 548 | #[test] 549 | fn test_vectors() { 550 | let cases: Cases = 551 | serde_json::from_slice(&std::fs::read("./tests/data.json").unwrap()).unwrap(); 552 | 553 | for case in cases.cases { 554 | let g1: G1Projective = g1_from_slice(&case.g1_compressed).unwrap().into(); 555 | 556 | assert_eq!( 557 | g1, 558 | hash_to_g1(case.msg.as_bytes(), case.ciphersuite.as_bytes()) 559 | ); 560 | 561 | let g2: G2Projective = g2_from_slice(&case.g2_compressed).unwrap().into(); 562 | assert_eq!( 563 | g2, 564 | hash_to_g2(case.msg.as_bytes(), case.ciphersuite.as_bytes()) 565 | ); 566 | 567 | if case.ciphersuite.as_bytes() == CSUITE { 568 | let pub_key = PublicKey::from_bytes( 569 | &BASE64_STANDARD 570 | .decode(case.pub_key.as_ref().unwrap()) 571 | .unwrap(), 572 | ) 573 | .unwrap(); 574 | let priv_key = PrivateKey::from_string(case.priv_key.as_ref().unwrap()).unwrap(); 575 | let signature = Signature::from_bytes( 576 | &BASE64_STANDARD 577 | .decode(case.signature.as_ref().unwrap()) 578 | .unwrap(), 579 | ) 580 | .unwrap(); 581 | 582 | let sig2 = priv_key.sign(&case.msg); 583 | assert_eq!(signature, sig2, "signatures do not match"); 584 | 585 | assert!(pub_key.verify(signature, &case.msg), "failed to verify"); 586 | } 587 | } 588 | } 589 | } 590 | --------------------------------------------------------------------------------