From c76c07874f21aa91d4676d10eb091557444683ab Mon Sep 17 00:00:00 2001 From: Cassandra Heart Date: Wed, 8 Jan 2025 18:31:02 -0600 Subject: [PATCH] additional --- Cargo.lock | 16 +- Cargo.toml | 1 + crates/channel-wasm/Cargo.toml | 2 +- crates/channel-wasm/src/lib.rs | 62 ++- crates/channel/src/lib.rs | 40 +- crates/channel/src/protocols/feldman.rs | 92 +++- crates/channel/src/protocols/tripleratchet.rs | 64 ++- crates/ed448-rust/.editorconfig | 10 + crates/ed448-rust/.github/workflows/audit.yml | 28 ++ crates/ed448-rust/.github/workflows/ci.yml | 171 +++++++ crates/ed448-rust/.gitignore | 4 + crates/ed448-rust/CHANGELOG.md | 14 + crates/ed448-rust/Cargo.toml | 55 +++ crates/ed448-rust/LICENSE_APACHE2.txt | 175 +++++++ crates/ed448-rust/LICENSE_MIT.txt | 21 + crates/ed448-rust/README.md | 68 +++ crates/ed448-rust/src/error.rs | 46 ++ crates/ed448-rust/src/lib.rs | 291 ++++++++++++ crates/ed448-rust/src/point.rs | 433 ++++++++++++++++++ crates/ed448-rust/src/private_key.rs | 255 +++++++++++ crates/ed448-rust/src/public_key.rs | 293 ++++++++++++ crates/ed448-rust/tests/rfc8032.rs | 215 +++++++++ 22 files changed, 2328 insertions(+), 28 deletions(-) create mode 100644 crates/ed448-rust/.editorconfig create mode 100644 crates/ed448-rust/.github/workflows/audit.yml create mode 100644 crates/ed448-rust/.github/workflows/ci.yml create mode 100644 crates/ed448-rust/.gitignore create mode 100644 crates/ed448-rust/CHANGELOG.md create mode 100644 crates/ed448-rust/Cargo.toml create mode 100644 crates/ed448-rust/LICENSE_APACHE2.txt create mode 100644 crates/ed448-rust/LICENSE_MIT.txt create mode 100644 crates/ed448-rust/README.md create mode 100644 crates/ed448-rust/src/error.rs create mode 100644 crates/ed448-rust/src/lib.rs create mode 100644 crates/ed448-rust/src/point.rs create mode 100644 crates/ed448-rust/src/private_key.rs create mode 100644 crates/ed448-rust/src/public_key.rs create mode 100644 crates/ed448-rust/tests/rfc8032.rs diff --git a/Cargo.lock b/Cargo.lock index faaf19b..99c4493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -355,7 +361,7 @@ name = "channel" version = "0.1.0" dependencies = [ "aes-gcm", - "base64", + "base64 0.22.1", "criterion 0.4.0", "ed448-goldilocks-plus", "hex 0.4.3", @@ -375,7 +381,7 @@ name = "channelwasm" version = "0.1.0" dependencies = [ "aes-gcm", - "base64", + "base64 0.22.1", "channel", "ed448-goldilocks-plus", "ed448-rust", @@ -724,10 +730,10 @@ dependencies = [ [[package]] name = "ed448-rust" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428b9b8138c483798ceb6a21004de0f24e3a2311195f13829ce1d059037d703" +version = "0.1.2" dependencies = [ + "base64 0.13.1", + "hex 0.4.3", "lazy_static", "num-bigint", "num-integer", diff --git a/Cargo.toml b/Cargo.toml index a4c8196..13ef076 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/channel-wasm", "crates/classgroup", "crates/bls48581", + "crates/ed448-rust", "crates/rpm", ] diff --git a/crates/channel-wasm/Cargo.toml b/crates/channel-wasm/Cargo.toml index 5e65432..957e94a 100644 --- a/crates/channel-wasm/Cargo.toml +++ b/crates/channel-wasm/Cargo.toml @@ -15,7 +15,7 @@ serde = "1.0.208" serde_json = "1.0.117" base64 = "0.22.1" ed448-goldilocks-plus = "0.11.2" -ed448-rust = "0.1.1" +ed448-rust = { path = "../ed448-rust", version = "0.1.2" } rand = "0.8.5" sha2 = "0.10.8" hkdf = "0.12.4" diff --git a/crates/channel-wasm/src/lib.rs b/crates/channel-wasm/src/lib.rs index 7ede250..07302a1 100644 --- a/crates/channel-wasm/src/lib.rs +++ b/crates/channel-wasm/src/lib.rs @@ -81,6 +81,22 @@ pub struct SealedInboxMessageEncryptRequest { pub plaintext: Vec, } +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct ResizeRequest { + pub ratchet_state: String, + pub other: String, + pub id: usize, + pub total: usize, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct TripleRatchetStateAndPoint { + pub ratchet_state: String, + pub point: String, + pub index: usize, +} + + fn encrypt(plaintext: &[u8], key: &[u8]) -> Result> { use aes_gcm::KeyInit; @@ -413,7 +429,6 @@ pub fn js_verify_ed448(public_key: &str, message: &str, signature: &str) -> Stri } let pub_bytes: [u8; 57] = key.try_into().unwrap(); - let pub_key = ed448_rust::PublicKey::from(pub_bytes); let signature = pub_key.verify(&maybe_message.unwrap(), &maybe_signature.unwrap(), None); @@ -572,3 +587,48 @@ pub fn js_triple_ratchet_decrypt(params: &str) -> String { } } +#[wasm_bindgen] +pub fn js_triple_ratchet_resize(params: &str) -> String { + let json: Result = serde_json::from_str(params); + match json { + Ok(request) => { + return serde_json::to_string(&triple_ratchet_resize(request.ratchet_state, request.other, request.id, request.total)).unwrap_or_else(|e| e.to_string()); + } + Err(e) => { + return e.to_string(); + } + } +} + +#[wasm_bindgen] +pub fn js_verify_point(params: &str) -> String { + let json: Result = serde_json::from_str(params); + match json { + Ok(request) => { + let verify = triple_ratchet_verify_point(request.ratchet_state, request.point, request.index); + return match verify { + Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| e.to_string()), + Err(e) => serde_json::to_string(&e.to_string()).unwrap_or_else(|e| e.to_string()) + } + } + Err(e) => { + return e.to_string(); + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use ed448_goldilocks_plus::{Scalar, elliptic_curve::Group, EdwardsPoint}; + + #[test] + fn test_verify() { + let priv_key = ed448_rust::PrivateKey::new(&mut rand::thread_rng()); + let pub_key = ed448_rust::PublicKey::from(&priv_key); + let sig = js_sign_ed448(&BASE64_STANDARD.encode(priv_key.as_bytes()).to_string(), "AQAB"); + assert_eq!(js_verify_ed448(&BASE64_STANDARD.encode(pub_key.as_byte()).to_string(), "AQAB", &serde_json::from_str::(&sig.to_string()).unwrap()), "true") + } +} \ No newline at end of file diff --git a/crates/channel/src/lib.rs b/crates/channel/src/lib.rs index 59c7514..22d20ca 100644 --- a/crates/channel/src/lib.rs +++ b/crates/channel/src/lib.rs @@ -1,6 +1,6 @@ use base64::prelude::*; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, io::Read}; +use std::{collections::HashMap, error::Error, io::Read}; use ed448_goldilocks_plus::{elliptic_curve::group::GroupEncoding, CompressedEdwardsY, EdwardsPoint, Scalar}; use protocols::{doubleratchet::{DoubleRatchetParticipant, P2PChannelEnvelope}, tripleratchet::{PeerInfo, TripleRatchetParticipant}, x3dh}; @@ -662,6 +662,44 @@ pub fn triple_ratchet_decrypt(ratchet_state_and_envelope: TripleRatchetStateAndE }; } +pub fn triple_ratchet_resize(ratchet_state: String, other: String, id: usize, total: usize) -> Vec> { + let tr = TripleRatchetParticipant::from_json(&ratchet_state); + if tr.is_err() { + return vec![vec![1]]; + } + + let other_bytes = hex::decode(other); + if other_bytes.is_err() { + return vec![other_bytes.unwrap_err().to_string().as_bytes().to_vec()]; + } + + let result = tr.unwrap().ratchet_resize(other_bytes.unwrap(), id, total); + if result.is_err() { + return vec![result.unwrap_err().to_string().as_bytes().to_vec()]; + } + + return result.unwrap(); +} + +pub fn triple_ratchet_verify_point(ratchet_state: String, point: String, id: usize) -> Result> { + let tr = TripleRatchetParticipant::from_json(&ratchet_state); + if tr.is_err() { + return Err(tr.unwrap_err()); + } + + let point_bytes = hex::decode(point); + if point_bytes.is_err() { + return Err(Box::new(point_bytes.unwrap_err())); + } + + let result = tr.unwrap().point_verify(point_bytes.unwrap(), id); + if result.is_err() { + return Err(result.unwrap_err()); + } + + return Ok(result.unwrap()); +} + #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/crates/channel/src/protocols/feldman.rs b/crates/channel/src/protocols/feldman.rs index c756523..4ca86b2 100644 --- a/crates/channel/src/protocols/feldman.rs +++ b/crates/channel/src/protocols/feldman.rs @@ -16,7 +16,7 @@ pub enum FeldmanError { CryptoError(String), } -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Debug)] enum FeldmanRound { Uninitialized, Initialized, @@ -25,6 +25,7 @@ enum FeldmanRound { Reconstructed, } +#[derive(Debug)] pub struct Feldman { threshold: usize, total: usize, @@ -226,25 +227,13 @@ impl Feldman { return Err(FeldmanError::WrongRound); } - let mut coeffs = vec![self.secret]; - - for _ in 1..self.threshold { - coeffs.push(Scalar::random(rng)); - } + let samples = Feldman::construct_polynomial_samples(rng, self.secret, self.threshold, self.total); for i in 1..=self.total { - let mut result = coeffs[0]; - let x = Scalar::from(i as u32); - - for j in 1..self.threshold { - let term = coeffs[j] * Scalar::from(i.pow(j as u32) as u32); - result += term; - } - if i == self.id { - self.scalar = Some(result); + self.scalar = Some(samples[i-1]); } else { - self.frags_for_counterparties.insert(i, result.to_bytes().to_vec()); + self.frags_for_counterparties.insert(i, samples[i-1].to_bytes().to_vec()); } } @@ -252,6 +241,29 @@ impl Feldman { Ok(()) } + fn construct_polynomial_samples(rng: &mut R, secret: Scalar, threshold: usize, total: usize) -> Vec { + let mut coeffs = vec![secret]; + + for _ in 1..threshold { + coeffs.push(Scalar::random(rng)); + } + + let mut samples = Vec::::new(); + for i in 1..=total { + let mut result = coeffs[0]; + let x = Scalar::from(i as u32); + + for j in 1..threshold { + let term = coeffs[j] * Scalar::from(i.pow(j as u32) as u32); + result += term; + } + + samples.push(result); + } + + return samples; + } + pub fn scalar(&self) -> Option<&Scalar> { self.scalar.as_ref() } @@ -481,5 +493,51 @@ impl Feldman { pub fn public_key_bytes(&self) -> Vec { self.public_key.to_bytes().to_vec() } -} + pub fn redistribute(rng: &mut R, shares: Vec>, ids: &[usize], threshold: usize, total: usize) -> Result>, FeldmanError> { + if shares.len() != ids.len() { + return Err(FeldmanError::InvalidData("mismatch of shares and ids len".to_string())); + } + + let mut points = HashMap::::new(); + for (i, share) in shares.iter().enumerate() { + let point = Scalar::from_bytes(&(*share).clone().try_into().unwrap()); + if point.is_zero().into() { + return Err(FeldmanError::InvalidData(format!("invalid pubkey for {}", ids[i]).to_string())); + } + + points.insert(ids[i], point); + } + + let mut reconstructed_sum = Scalar::ZERO; + + for j in ids { + let mut num = Scalar::ONE; + let mut den = Scalar::ONE; + + for k in ids { + if j != k { + let j_scalar = Scalar::from(*j as u32); + let k_scalar = Scalar::from(*k as u32); + + num *= k_scalar; + den *= k_scalar - j_scalar; + } + } + + let den_inv = den.invert(); + let reconstructed_fragment = points[&j] * (num * den_inv); + reconstructed_sum += reconstructed_fragment; + } + + return Ok(Feldman::construct_polynomial_samples(rng, reconstructed_sum, threshold, total).iter().map(|s| s.to_bytes().to_vec()).collect()) + } + + pub fn get_scalar(&self) -> Scalar { + return self.scalar.unwrap(); + } + + pub fn get_id(&self) -> usize { + return self.id; + } +} diff --git a/crates/channel/src/protocols/tripleratchet.rs b/crates/channel/src/protocols/tripleratchet.rs index 85eb169..aa9e00c 100644 --- a/crates/channel/src/protocols/tripleratchet.rs +++ b/crates/channel/src/protocols/tripleratchet.rs @@ -48,6 +48,7 @@ pub struct PeerInfo { pub(crate) signed_pre_public_key: Vec, } +#[derive(Debug)] pub struct TripleRatchetParticipant { peer_key: Scalar, sending_ephemeral_private_key: Scalar, @@ -694,6 +695,63 @@ impl TripleRatchetParticipant { Ok((plaintext, should_dkg_ratchet)) } + pub fn ratchet_resize(&mut self, other: Vec, id: usize, total: usize) -> Result>, Box> { + if !self.async_dkg_ratchet { + return Err(Box::new(TripleRatchetError::CryptoError("cannot use non-async triple ratchet for resize".to_owned()))); + } + + if self.threshold != 2 { + return Err(Box::new(TripleRatchetError::CryptoError("cannot use larger threshold size than two for resize".to_owned()))); + } + + let rescaled = Feldman::redistribute(&mut OsRng, vec![self.dkg_ratchet.get_scalar().to_bytes().to_vec(), other], &vec![self.dkg_ratchet.get_id(), id], 2, total); + + if rescaled.is_err() { + return Err(Box::new(rescaled.unwrap_err())); + } + + return Ok(rescaled.unwrap()); + } + + + pub fn point_verify(&mut self, point: Vec, id: usize) -> Result> { + if !self.async_dkg_ratchet { + return Err(Box::new(TripleRatchetError::CryptoError("cannot use non-async triple ratchet for point verify".to_owned()))); + } + + if self.threshold != 2 { + return Err(Box::new(TripleRatchetError::CryptoError("cannot use larger threshold size than two for point verify".to_owned()))); + } + + if id == 0 { + return Err(Box::new(TripleRatchetError::CryptoError("invalid id".to_owned()))) + } + + let ours = (self.dkg_ratchet.get_scalar() * EdwardsPoint::generator()).compress(); + let ours_bytes = ours.as_bytes(); + + let mut shares = Vec::<&[u8]>::new(); + let mut ids = Vec::::new(); + + if self.dkg_ratchet.get_id() > id { + ids.push(id); + shares.push(&point); + ids.push(self.dkg_ratchet.get_id()); + shares.push(ours_bytes); + } else { + ids.push(self.dkg_ratchet.get_id()); + shares.push(ours_bytes); + ids.push(id); + shares.push(&point); + } + + let result = self.dkg_ratchet.combine_mul_share(shares, ids.as_slice()); + match result { + Ok(pubkey) => Ok(pubkey == self.dkg_ratchet.public_key_bytes()), + Err(e) => Err(Box::new(e)) + } + } + fn ratchet_sender_ephemeral_keys(&mut self) -> Result<(), Box> { let receiving_group_key = self.receiving_group_key.as_ref().ok_or_else(|| TripleRatchetError::CryptoError("Receiving group key not set".into()))?; self.sending_ephemeral_private_key = Scalar::random(&mut OsRng); @@ -823,16 +881,16 @@ impl TripleRatchetParticipant { let current_header_key = rkck[32..64].to_vec(); match self.decrypt(ciphertext, ¤t_header_key, None) { Ok(header) => Ok((header, true, true)), - Err(e) => Err(Box::new(e)), + Err(e) => Err(Box::new(TripleRatchetError::CryptoError(format!("header: {}", e.to_string())))), } } else { match self.decrypt(ciphertext, &self.next_header_key, None) { Ok(header) => Ok((header, true, false)), - Err(e) => Err(Box::new(e)), + Err(e) => Err(Box::new(TripleRatchetError::CryptoError(format!("header: {}", e.to_string())))), } } }, - Err(e) => Err(Box::new(e)), + Err(e) => Err(Box::new(TripleRatchetError::CryptoError(format!("header: {}", e.to_string())))), } } diff --git a/crates/ed448-rust/.editorconfig b/crates/ed448-rust/.editorconfig new file mode 100644 index 0000000..b175ed5 --- /dev/null +++ b/crates/ed448-rust/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 diff --git a/crates/ed448-rust/.github/workflows/audit.yml b/crates/ed448-rust/.github/workflows/audit.yml new file mode 100644 index 0000000..74f90da --- /dev/null +++ b/crates/ed448-rust/.github/workflows/audit.yml @@ -0,0 +1,28 @@ +name: Security audit + +on: + schedule: + # Launch everyday, at midnight + - cron: '0 0 * * *' + push: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + pull_request: + # Allow to manually trigger the check + workflow_dispatch: + +jobs: + security_audit: + runs-on: ubuntu-latest + + steps: + # Checkout Git repository + - name: Checkout sources + uses: actions/checkout@v2 + + # Security scan all dependencies, create a Pull Request if anyone found + - name: Security audit used dependencies + uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/crates/ed448-rust/.github/workflows/ci.yml b/crates/ed448-rust/.github/workflows/ci.yml new file mode 100644 index 0000000..f50a993 --- /dev/null +++ b/crates/ed448-rust/.github/workflows/ci.yml @@ -0,0 +1,171 @@ +name: Ci + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + branches: + - main + - dev + pull_request: + branches: + - main + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + check: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout sources + uses: actions/checkout@v2 + + # Install stable toolchain + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + # Runs the compilation checks + - name: Run cargo check with defaults feature + uses: actions-rs/cargo@v1 + with: + command: check + + # - name: Run cargo check with all features + # uses: actions-rs/cargo@v1 + # with: + # command: check + # args: --all-features + + clippy_and_fmt: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout sources + uses: actions/checkout@v2 + + # Install nightly toolchain + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + profile: default + toolchain: nightly + override: true + components: clippy, rustfmt + + # Checks Rust code formatting + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + # Checks Rust code with Clippy + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + continue-on-error: true # note: the compiler unexpectedly panicked. this is a bug. + with: + command: clippy + args: --all-features -- -D warnings + + test: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout sources + uses: actions/checkout@v2 + + # Install nightly toolchain + - name: Install nightly toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: llvm-tools-preview + + # Build the binary + - name: Build binary + uses: actions-rs/cargo@v1 + with: + command: build + args: --all-features + env: + RUSTFLAGS: "-Zinstrument-coverage" + + # Runs the units tests in Debug mode + - name: Execute tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features + env: + RUSTFLAGS: "-Zinstrument-coverage" + LLVM_PROFILE_FILE: "your_name-%p-%m.profraw" + RUST_LOG: "ed448_rust" + + - name: Install grcov + run: curl -L https://github.com/mozilla/grcov/releases/latest/download/grcov-linux-x86_64.tar.bz2 | tar jxf - + + - name: Gather coverage data + run: ./grcov $GITHUB_WORKSPACE --source-dir $GITHUB_WORKSPACE --llvm --commit-sha $GITHUB_SHA --binary-path $GITHUB_WORKSPACE/target/debug/ --output-type lcov --branch --ignore-not-existing --ignore "/*" --output-path $LCOV_DESTINATION --service-name $GITHUB_WORKFLOW + env: + LCOV_DESTINATION: ${{ runner.temp }}/lcov.info + + - name: Codecov upload + uses: codecov/codecov-action@v1 + with: + #token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + files: ${{ runner.temp }}/lcov.info + name: codecov-ed448-rust # optional + fail_ci_if_error: true # optional (default = false) + + build: + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + + # The type of runner that the job will run on + runs-on: ${{ matrix.os }} + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout sources + uses: actions/checkout@v2 + + # Install stable toolchain + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + # Build the project + - name: Build the project + uses: actions-rs/cargo@v1 + with: + command: build + args: --release diff --git a/crates/ed448-rust/.gitignore b/crates/ed448-rust/.gitignore new file mode 100644 index 0000000..a1a6602 --- /dev/null +++ b/crates/ed448-rust/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock +default.profraw +.DS_Store diff --git a/crates/ed448-rust/CHANGELOG.md b/crates/ed448-rust/CHANGELOG.md new file mode 100644 index 0000000..f5bb54b --- /dev/null +++ b/crates/ed448-rust/CHANGELOG.md @@ -0,0 +1,14 @@ +## v0.1.2 - upcoming + +* A feature was added to include [README.md](./README.md) during the tests to check examples in the usage. + + +## v0.1.1 + +* Add clippy directives +* Fix some documentation +* Add better documentation for the `PublicKey` struct + +## v0.1.0 + +Initial release \ No newline at end of file diff --git a/crates/ed448-rust/Cargo.toml b/crates/ed448-rust/Cargo.toml new file mode 100644 index 0000000..52cc4a2 --- /dev/null +++ b/crates/ed448-rust/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "ed448-rust" +version = "0.1.2" +authors = ["Lolo_32 "] +edition = "2018" +description = "Implementation of Edwards-Curve Digital Signature Algorithm (EdDSA) for ed448 only." +license = "MIT/Apache-2.0" +repository = "https://github.com/lolo32/ed448-rust" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +docinclude = [] # Used only for activating `doc(include="...")` on stable. + +[dependencies.lazy_static] +version = "1.4.0" + +[dependencies.num-bigint] +version = "0.4.0" + +[dependencies.num-integer] +version = "0.1.44" + +[dependencies.num-traits] +version = "0.2.14" + +[dependencies.opaque-debug] +version = "0.3.0" + +[dependencies.rand_core] +version = "0.6.2" +default-features = false +features = ["alloc"] + +[dependencies.sha3] +version = "0.9.1" + +[dependencies.subtle] +version = "2.4.0" +default-features = false +features = ["std"] + +[dev-dependencies.base64] +version = "0.13.0" + +[dev-dependencies.hex] +version = "0.4.3" + +[dev-dependencies.rand_core] +version = "0.6.2" +features = ["getrandom"] + +[package.metadata.docs.rs] +rustc-args = ["--cfg", "docsrs"] +features = ["docinclude"] # Activate `docinclude` during docs.rs build. diff --git a/crates/ed448-rust/LICENSE_APACHE2.txt b/crates/ed448-rust/LICENSE_APACHE2.txt new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/crates/ed448-rust/LICENSE_APACHE2.txt @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/crates/ed448-rust/LICENSE_MIT.txt b/crates/ed448-rust/LICENSE_MIT.txt new file mode 100644 index 0000000..6f57f3e --- /dev/null +++ b/crates/ed448-rust/LICENSE_MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Lolo_32 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/ed448-rust/README.md b/crates/ed448-rust/README.md new file mode 100644 index 0000000..88e4c48 --- /dev/null +++ b/crates/ed448-rust/README.md @@ -0,0 +1,68 @@ +# Ed448-Rust + +[![Ci](https://github.com/lolo32/ed448-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/lolo32/ed448-rust/actions/workflows/ci.yml) +[![Security audit](https://github.com/lolo32/ed448-rust/actions/workflows/audit.yml/badge.svg)](https://github.com/lolo32/ed448-rust/actions/workflows/audit.yml) +[![codecov](https://codecov.io/gh/lolo32/ed448-rust/branch/main/graph/badge.svg?token=V206OZ48AA)](https://codecov.io/gh/lolo32/ed448-rust) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Docs.rs](https://docs.rs/ed448-rust/badge.svg)](https://docs.rs/ed448-rust/) +[![Crates.io](https://img.shields.io/crates/v/ed448-rust)](https://crates.io/crates/ed448-rust) + +This is an implementation of Edwards-Curve Digital Signature Algorithm (EdDSA) +from the [RFC8032](https://tools.ietf.org/html/rfc8032) in pure Rust, +but only the ed448 support is implemented. + +It's a EdDSA for ed448 signing/verifying. + +_This is direct port of the Python code in the RFC, so it's the same warning +as it:_ + +> _**Note: This code is not intended for production. Although it should**_ +> _**produce correct results for every input, it is slow and makes no**_ +> _**attempt to avoid side-channel attacks.**_ + +## Usage + +```rust +use core::convert::TryFrom; +use rand_core::OsRng; +use ed448_rust::{PrivateKey, PublicKey}; + +fn main () { + // Generate a new random private key + let private_key = PrivateKey::new(&mut OsRng); + + // Store the key + let pkey_stored = private_key.as_bytes(); + + // Load a stored key before using it, or generating the public key + let private_key = PrivateKey::try_from(pkey_stored).unwrap(); + + // Extract associated public key + let public_key = PublicKey::from(&private_key); + + // Store the public key + let pubkey_stored = public_key.as_byte(); + + // Sign a message without context + let signature = private_key.sign(b"Message to sign", None).unwrap(); + // Sign a message with a context + let signature_ctx = private_key.sign(b"Message to sign", Some(&[0x01, 0xA6])).unwrap(); + // Sign a pre-hashed message without context + let signature_ph = private_key.sign_ph(b"Message to sign", None).unwrap(); + + // Verify the signature without context + assert!(public_key.verify(b"Message to sign", &signature, None).is_ok()); + // Verify the signature with context + assert!(public_key.verify(b"Message to sign", &signature_ctx, Some(&[0x01, 0xA6])).is_ok()); + // Verify the signature with the pre-hash and without context + assert!(public_key.verify_ph(b"Message to sign", &signature_ph, None).is_ok()); +} +``` + +## License + +This code is licensed under [MIT] / [Apache2.0] + +[MIT]: LICENSE_MIT.txt +[Apache2.0]: LICENSE_APACHE2.txt diff --git a/crates/ed448-rust/src/error.rs b/crates/ed448-rust/src/error.rs new file mode 100644 index 0000000..2b82fe5 --- /dev/null +++ b/crates/ed448-rust/src/error.rs @@ -0,0 +1,46 @@ +// Copyright 2021 Lolo_32 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Errors of this crate +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Ed448Error { + /// The provided array is not in the correct length for the private key. + /// + /// It must be [`crate::KEY_LENGTH`]. + /// + /// See [PrivateKey::from](crate::PrivateKey::from). + WrongKeyLength, + /// The provided array is not in the correct length for the public key. + /// + /// It must be [`crate::KEY_LENGTH`]. + /// + /// See [PublicKey::from](crate::PublicKey::from). + WrongPublicKeyLength, + /// The provided array is not in the correct length for the signature. + /// + /// It must be [`SIG_LENGTH`](crate::SIG_LENGTH). + /// + /// See [PublicKey::verify](crate::PublicKey::verify). + WrongSignatureLength, + /// The computed point is not valid (maybe forged/altered public key or signature). + InvalidPoint, + /// Signature verification failed. + /// + /// See [PublicKey::verify](crate::PublicKey::verify). + InvalidSignature, + /// The provided context byte array is too long. + /// + /// It must not be more than 256 byte. + ContextTooLong, +} diff --git a/crates/ed448-rust/src/lib.rs b/crates/ed448-rust/src/lib.rs new file mode 100644 index 0000000..93cc579 --- /dev/null +++ b/crates/ed448-rust/src/lib.rs @@ -0,0 +1,291 @@ +// Copyright 2021 Lolo_32 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(feature = "docinclude", feature(external_doc))] +#![deny( + missing_docs, + missing_copy_implementations, + missing_debug_implementations, + trivial_numeric_casts, + unreachable_pub, + unsafe_code, + unused_extern_crates, + unused_qualifications, + single_use_lifetimes, + unused_import_braces, + unused_lifetimes, + unused_results, + clippy::all, + clippy::pedantic, + clippy::nursery +)] +#![doc( + test(no_crate_inject, attr(deny(warnings))), + test(attr(allow(unused_variables))), + html_no_source +)] +#![deny( + clippy::absurd_extreme_comparisons, + clippy::almost_swapped, + clippy::approx_constant, + clippy::async_yields_async, + clippy::bad_bit_mask, + clippy::cast_ref_to_mut, + clippy::clone_double_ref, + clippy::cmp_nan, + clippy::deprecated_semver, + clippy::derive_hash_xor_eq, + clippy::derive_ord_xor_partial_ord, + clippy::drop_copy, + clippy::drop_ref, + clippy::enum_clike_unportable_variant, + clippy::eq_op, + clippy::erasing_op, + clippy::float_cmp, + clippy::float_equality_without_abs, + clippy::fn_address_comparisons, + clippy::for_loops_over_fallibles, + clippy::forget_copy, + clippy::forget_ref, + clippy::if_let_mutex, + clippy::if_same_then_else, + clippy::ifs_same_cond, + clippy::ineffective_bit_mask, + clippy::infinite_iter, + clippy::inherent_to_string_shadow_display, + clippy::inline_fn_without_body, + clippy::invalid_atomic_ordering, + clippy::invalid_regex, + clippy::invisible_characters, + clippy::iter_next_loop, + clippy::iterator_step_by_zero, + clippy::let_underscore_lock, + clippy::logic_bug, + clippy::mem_discriminant_non_enum, + clippy::mem_replace_with_uninit, + clippy::min_max, + clippy::mismatched_target_os, + clippy::mistyped_literal_suffixes, + clippy::modulo_one, + clippy::mut_from_ref, + clippy::mutable_key_type, + clippy::never_loop, + clippy::nonsensical_open_options, + clippy::not_unsafe_ptr_arg_deref, + clippy::option_env_unwrap, + clippy::out_of_bounds_indexing, + clippy::panicking_unwrap, + clippy::possible_missing_comma, + clippy::reversed_empty_ranges, + clippy::self_assignment, + clippy::serde_api_misuse, + clippy::size_of_in_element_count, + clippy::suspicious_arithmetic_impl, + clippy::suspicious_op_assign_impl, + clippy::to_string_in_display, + clippy::transmuting_null, + clippy::undropped_manually_drops, + clippy::uninit_assumed_init, + clippy::unit_cmp, + clippy::unit_return_expecting_ord, + clippy::unsound_collection_transmute, + clippy::unused_io_amount, + clippy::useless_attribute, + clippy::vec_resize_to_zero, + clippy::vtable_address_comparisons, + clippy::while_immutable_condition, + clippy::wrong_transmute, + clippy::zst_offset +)] +#![allow( + non_snake_case, + non_upper_case_globals, + clippy::similar_names, + clippy::module_name_repetitions +)] + +//! # EdDSA implementation for ed448 +//! +//! This is a Edwards-Curve Digital Signature Algorithm (EdDSA) for ed448 only +//! in pure rust. +//! +//! # Usage +//! +//! There is two variants that can be combined to sign/verify: +//! +//! 1. [`PrivateKey::sign`](crate::PrivateKey::sign) to sign all the content +//! as-it and [`PrivateKey::sign_ph`](crate::PrivateKey::sign_ph) to +//! pre-hash the message internaly before signing it. It will be hashed +//! using Shake256 and the result of 64 byte will be signed/verified. +//! +//! Note: use the same variant for verifying the signature. +//! +//! 2. The second parameter of [`sign`](crate::PrivateKey::sign)/ +//! [`sign_ph`](crate::PrivateKey::sign_ph) and the third of +//! [`verify`](crate::PublicKey::verify)/ +//! [`verify_ph`](crate::PublicKey::verify_ph) if an optional context +//! of 255 byte length max. +//! +//! The context can be used to facilitate different signature over +//! different protocol, but it must be immuable over the protocol. +//! More information about this can be found at +//! [RFC 8032 Use of Contexts](https://tools.ietf.org/html/rfc8032#section-8.3). +//! +//! # Examples +//! +//! ## Generating a new key pair +//! +//! ``` +//! use rand_core::OsRng; +//! use ed448_rust::{PrivateKey, PublicKey}; +//! let private_key = PrivateKey::new(&mut OsRng); +//! let public_key = PublicKey::from(&private_key); +//! ``` +//! +//! ## Sign a message +//! +//! ``` +//! # use rand_core::OsRng; +//! use ed448_rust::{PrivateKey, Ed448Error}; +//! # let retrieve_pkey = || PrivateKey::new(&mut OsRng); +//! let message = b"Message to sign"; +//! let private_key = retrieve_pkey(); +//! match private_key.sign(message, None) { +//! Ok(signature) => { +//! // Signature OK, use it +//! // This is a slice of 144 byte length +//! } +//! Err(Ed448Error::ContextTooLong) => { +//! // The used context is more than 255 bytes length +//! } +//! Err(_) => unreachable!() +//! } +//! ``` +//! +//! ## Verify a signature +//! +//! ``` +//! # use rand_core::OsRng; +//! use ed448_rust::{PublicKey, Ed448Error}; +//! # let private_key = ed448_rust::PrivateKey::new(&mut OsRng); +//! let message = b"Signed message to verify"; +//! # let retrieve_signature = || private_key.sign(message, None).unwrap(); +//! # let retrieve_pubkey = || PublicKey::from(&private_key); +//! let public_key = retrieve_pubkey(); // A slice or array of KEY_LENGTH byte length +//! let signature = retrieve_signature(); // A slice or array of SIG_LENGTH byte length +//! match public_key.verify(message, &signature, None) { +//! Ok(()) => { +//! // Signature OK, use the message +//! } +//! Err(Ed448Error::InvalidSignature) => { +//! // The verification of the signature is invalid +//! } +//! Err(Ed448Error::ContextTooLong) => { +//! // The used context is more than 255 bytes length +//! } +//! Err(Ed448Error::WrongSignatureLength) => { +//! // The signature is not 144 bytes length +//! } +//! Err(_) => unreachable!() +//! } +//! ``` + +use sha3::{ + digest::{ExtendableOutput, Update}, + Shake256, +}; + +pub use crate::error::Ed448Error; + +pub use private_key::PrivateKey; +pub use public_key::PublicKey; +use std::borrow::Cow; + +mod error; +mod point; +mod private_key; +mod public_key; + +#[allow( + missing_docs, + missing_copy_implementations, + missing_debug_implementations +)] +#[doc(hidden)] +#[cfg_attr(feature = "docinclude", doc(include = "../README.md"))] +pub struct ReadmeDoctests; + +/// Specialized [`Result`](core::result::Result) for this crate. +pub type Result = core::result::Result; + +/// Length of either a public or a private key length in byte. +pub const KEY_LENGTH: usize = 57; +/// Length of the signature length in byte. +pub const SIG_LENGTH: usize = 114; + +/// Indicate if the message need to be pre-hashed before being signed/verified +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum PreHash { + /// Pre-hash the message + True, + /// Leave the message unchanged + False, +} + +impl From for u8 { + #[inline] + fn from(hash: PreHash) -> Self { + match hash { + PreHash::False => 0, + PreHash::True => 1, + } + } +} + +/// Produce a Shake256 for signing/verifying signatures +fn shake256(items: Vec<&[u8]>, ctx: &[u8], pre_hash: PreHash) -> Box<[u8]> { + #[allow(clippy::cast_possible_truncation)] + let mut shake = Shake256::default() + .chain(b"SigEd448") + .chain(&[pre_hash.into(), ctx.len() as u8]) + .chain(ctx); + for item in items { + shake.update(item); + } + shake.finalize_boxed(114) +} + +/// Common tasks for signing/verifying +#[allow(clippy::type_complexity)] +fn init_sig<'a, 'b>( + ctx: Option<&'b [u8]>, + pre_hash: PreHash, + msg: &'a [u8], +) -> Result<(Cow<'b, [u8]>, Cow<'a, [u8]>)> { + let ctx = ctx.unwrap_or(b""); + if ctx.len() > 255 { + return Err(Ed448Error::ContextTooLong); + } + let ctx = Cow::Borrowed(ctx); + + let msg = match pre_hash { + PreHash::False => Cow::Borrowed(msg), + PreHash::True => { + let hash = Shake256::default().chain(msg).finalize_boxed(64).to_vec(); + Cow::Owned(hash) + } + }; + + Ok((ctx, msg)) +} diff --git a/crates/ed448-rust/src/point.rs b/crates/ed448-rust/src/point.rs new file mode 100644 index 0000000..e952fe7 --- /dev/null +++ b/crates/ed448-rust/src/point.rs @@ -0,0 +1,433 @@ +// Copyright 2021 Lolo_32 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::{ + convert::TryInto, + ops::{Add, Div, Mul, Neg, Sub}, +}; + +use lazy_static::lazy_static; +use num_bigint::{BigInt, Sign}; +use num_traits::{One, Zero}; + +use crate::{Ed448Error, KEY_LENGTH}; +use subtle::{Choice, ConstantTimeEq}; + +lazy_static! { + // 2 ^ 448 - 2 ^224 - 1 + static ref p: BigInt = BigInt::from(2).pow(448).sub(BigInt::from(2).pow(224)) - 1; + static ref d: Field = Field::new(BigInt::from(-39081)); + static ref f0: Field = Field::new(BigInt::zero()); + static ref f1: Field = Field::new(BigInt::one()); + static ref xb: Field = Field::new(BigInt::from_bytes_be( + Sign::Plus, + &[ + 0x4F, 0x19, 0x70, 0xC6, 0x6B, 0xED, 0x0D, 0xED, 0x22, 0x1D, 0x15, 0xA6, 0x22, 0xBF, + 0x36, 0xDA, 0x9E, 0x14, 0x65, 0x70, 0x47, 0x0F, 0x17, 0x67, 0xEA, 0x6D, 0xE3, 0x24, + 0xA3, 0xD3, 0xA4, 0x64, 0x12, 0xAE, 0x1A, 0xF7, 0x2A, 0xB6, 0x65, 0x11, 0x43, 0x3B, + 0x80, 0xE1, 0x8B, 0x00, 0x93, 0x8E, 0x26, 0x26, 0xA8, 0x2B, 0xC7, 0x0C, 0xC0, 0x5E, + ] + )); + static ref yb: Field = Field::new(BigInt::from_bytes_be( + Sign::Plus, + &[ + 0x69, 0x3F, 0x46, 0x71, 0x6E, 0xB6, 0xBC, 0x24, 0x88, 0x76, 0x20, 0x37, 0x56, 0xC9, + 0xC7, 0x62, 0x4B, 0xEA, 0x73, 0x73, 0x6C, 0xA3, 0x98, 0x40, 0x87, 0x78, 0x9C, 0x1E, + 0x05, 0xA0, 0xC2, 0xD7, 0x3A, 0xD3, 0xFF, 0x1C, 0xE6, 0x7C, 0x39, 0xC4, 0xFD, 0xBD, + 0x13, 0x2C, 0x4E, 0xD7, 0xC8, 0xAD, 0x98, 0x08, 0x79, 0x5B, 0xF2, 0x30, 0xFA, 0x14, + ] + )); + + static ref l: BigInt = BigInt::from_bytes_be( + Sign::Plus, + &[ + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x7c, 0xca, 0x23, 0xe9, 0xc4, 0x4e, 0xdb, 0x49, 0xae, 0xd6, 0x36, 0x90, 0x21, 0x6c, + 0xc2, 0x72, 0x8d, 0xc5, 0x8f, 0x55, 0x23, 0x78, 0xc2, 0x92, 0xab, 0x58, 0x44, 0xf3, + ] + ); +} + +#[derive(Debug, Clone)] +pub struct Field(BigInt); + +impl Field { + pub fn new(value: BigInt) -> Self { + if value < BigInt::zero() { + Self((&p as &BigInt) + value) + } else { + Self(value % &p as &BigInt) + } + } + + /// Field inverse (inverse of 0 is 0). + #[inline] + pub fn inv(self) -> Self { + Self::new(self.0.modpow(&(&p as &BigInt - 2), &p)) + } + + /// Compute sign of number, 0 or 1. The sign function + /// has the following property: + /// sign(x) = 1 - sign(-x) if x != 0. + #[inline] + pub fn sign(&self) -> BigInt { + &self.0 % 2 + } + + /// Field square root. Returns none if square root does not exist. + /// Note: not presently implemented for p mod 8 = 1 case. + pub fn sqrt(self) -> crate::Result { + // Compute candidate square root. + let y = self + .0 + .modpow(&((&p as &BigInt).add(1_u32).div(&4)), &p as &BigInt); + let y = Self::new(y); + // Check square root candidate valid. + if &y * &y == self { + Ok(y) + } else { + Err(Ed448Error::InvalidPoint) + } + } + + /// Is the field element the additive identity? + #[inline] + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +impl PartialEq for Field { + fn eq(&self, other: &Self) -> bool { + fn sign_to_choice(sign: Sign) -> Choice { + match sign { + Sign::Plus => 1, + Sign::Minus => 0, + Sign::NoSign => unreachable!(), + } + .into() + } + + let me = self.0.to_u64_digits(); + let other = other.0.to_u64_digits(); + let val = me.1.ct_eq(&other.1); + let sign_me = sign_to_choice(me.0); + let sign_other = sign_to_choice(other.0); + let sign = sign_me ^ sign_other; + (val & !sign).into() + } +} + +impl Add for Field { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + self + &other + } +} + +impl Add<&'_ Self> for Field { + type Output = Self; + + #[inline] + fn add(self, rhs: &Self) -> Self { + Self::new(self.0 + &rhs.0) + } +} + +impl Add<&'_ Field> for &'_ Field { + type Output = Field; + + #[inline] + fn add(self, other: &Field) -> Self::Output { + self.clone() + other + } +} + +impl Sub for Field { + type Output = Self; + + #[inline] + fn sub(self, other: Self) -> Self { + self - &other + } +} + +impl Sub<&'_ Self> for Field { + type Output = Self; + + #[inline] + fn sub(self, other: &Self) -> Self { + Self::new(self.0 + &p as &BigInt - &other.0) + } +} + +impl Sub<&'_ Field> for &'_ Field { + type Output = Field; + + #[inline] + fn sub(self, other: &Field) -> Field { + self.clone() - other + } +} + +impl Mul for Field { + type Output = Self; + + #[inline] + fn mul(self, other: Self) -> Self { + self * &other + } +} + +impl Mul<&'_ Self> for Field { + type Output = Self; + + #[inline] + fn mul(self, other: &Self) -> Self { + Self::new(self.0 * &other.0) + } +} + +impl Mul<&'_ Field> for &'_ Field { + type Output = Field; + + #[inline] + fn mul(self, other: &Field) -> Field { + self.clone() * other + } +} + +impl Neg for Field { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + Self::new(&p as &BigInt - self.0) + } +} + +impl Div for Field { + type Output = Self; + + #[inline] + fn div(self, other: Self) -> Self { + self / &other + } +} + +#[allow(clippy::suspicious_arithmetic_impl)] +impl Div<&'_ Self> for Field { + type Output = Self; + + #[inline] + fn div(self, other: &Self) -> Self { + self * other.clone().inv() + } +} + +impl Div<&'_ Field> for &'_ Field { + type Output = Field; + + #[inline] + fn div(self, other: &'_ Field) -> Field { + self.clone() / other + } +} + +#[derive(Debug, Clone)] +pub struct Point { + x: Field, + y: Field, + z: Field, +} + +impl Point { + pub fn new(x: &Field, y: &Field) -> crate::Result { + // Check that the point is actually on the curve. + if y * y + x * x == (&f1 as &Field) + &((&d as &Field) * x * x * y * y) { + Ok(Self { + x: x.clone(), + y: y.clone(), + ..Self::default() + }) + } else { + Err(Ed448Error::InvalidPoint) + } + } + + /// Order of basepoint. + #[inline] + pub fn l() -> &'static BigInt { + &l as &BigInt + } + + #[inline] + pub fn new_stdbase() -> Self { + Self::new(&f0, &f1).unwrap() + } + + /// Point doubling. + pub fn double(self) -> Self { + // The formulas are from EFD. + let (x1s, y1s, z1s) = (&self.x * &self.x, &self.y * &self.y, &self.z * &self.z); + let xys = &self.x + &self.y; + let F = &x1s + &y1s; + let J = &F - &(&z1s + &z1s); + let (x, y, z) = ( + (&xys * &xys - &x1s - &y1s) * &J, + &F * &(&x1s - &y1s), + &F * &J, + ); + + Self { x, y, z } + } + + /// Encode a point representation. + pub fn encode(&self) -> [u8; KEY_LENGTH] { + let (xp, yp) = (&self.x / &self.z, &self.y / &self.z); + + // Encode y. + let mut tmp = yp.0.magnitude().to_bytes_le(); + tmp.resize_with(KEY_LENGTH, Default::default); + let mut s: [u8; KEY_LENGTH] = tmp.try_into().unwrap(); + + // Add sign bit of x to encoding. + if !xp.sign().is_zero() { + s[56] |= 0b1000_0000; + } + s + } + + /// Decode a point representation. + pub fn decode(s: &[u8]) -> crate::Result { + // Extract signbit. + let xs = BigInt::from(s[56] >> 7); + // Decode y. If this fails, fail. + let y = Self::frombytes(s)?; + // Try to recover x. If it does not exist, or if zero and xs + // are wrong, fail. + let mut x = Self::solve_x2(&y).sqrt()?; + if x.is_zero() && xs != x.sign() { + return Err(Ed448Error::InvalidPoint); + } + // If sign of x isn't correct, flip it. + if x.sign() != xs { + x = -x; + } + // Return the constructed point. + Self::new(&x, &y) + } + + /// Unserialize number from bits. + fn frombytes(x: &[u8]) -> crate::Result { + let rv = BigInt::from_bytes_le(Sign::Plus, x) % BigInt::from(2).pow(455); + if &rv < &p as &BigInt { + Ok(Field::new(rv)) + } else { + Err(Ed448Error::InvalidPoint) + } + } + + /// Solve for x^2. + #[inline] + fn solve_x2(y: &Field) -> Field { + (y * y - &f1 as &Field) / (&d as &Field * y * y - &f1 as &Field) + } +} + +impl Mul<&'_ BigInt> for Point { + type Output = Self; + + #[inline] + fn mul(self, x: &BigInt) -> Self { + self * x.clone() + } +} + +impl Mul for Point { + type Output = Self; + + fn mul(mut self, mut x: BigInt) -> Self { + let mut r = Self::new_stdbase(); + while !x.is_zero() { + if !((&x % 2) as BigInt).is_zero() { + r = r + &self; + } + self = self.double(); + x /= 2; + } + r + } +} + +impl Add for Point { + type Output = Self; + + fn add(self, y: Self) -> Self { + // The formulas are from EFD. + let (xcp, ycp, zcp) = (&self.x * &y.x, &self.y * &y.y, &self.z * &y.z); + let B = &zcp * &zcp; + let E = &d as &Field * &xcp * &ycp; + let (F, G) = (&B - &E, B + E); + + let x = &zcp * &F * ((self.x + self.y) * (y.x + y.y) - &xcp - &ycp); + let (y, z) = (zcp * &G * (ycp - xcp), F * G); + + Self { x, y, z } + } +} + +impl Add<&'_ Self> for Point { + type Output = Self; + + #[inline] + fn add(self, other: &Self) -> Self { + self + other.clone() + } +} + +impl Add<&'_ Point> for &'_ Point { + type Output = Point; + + #[inline] + fn add(self, other: &Point) -> Point { + self.clone() + other.clone() + } +} + +impl PartialEq for Point { + fn eq(&self, other: &Self) -> bool { + // Need to check x1/z1 == x2/z2 and similarly for y, so cross + // multiply to eliminate divisions. + let xn1 = &self.x * &other.z; + let xn2 = &other.x * &self.z; + let yn1 = &self.y * &other.z; + let yn2 = &other.y * &self.z; + xn1 == xn2 && yn1 == yn2 + } +} + +impl Default for Point { + #[inline] + fn default() -> Self { + Self { + x: xb.clone(), + y: yb.clone(), + z: Field::new(BigInt::one()), + } + } +} diff --git a/crates/ed448-rust/src/private_key.rs b/crates/ed448-rust/src/private_key.rs new file mode 100644 index 0000000..5e2b238 --- /dev/null +++ b/crates/ed448-rust/src/private_key.rs @@ -0,0 +1,255 @@ +// Copyright 2021 Lolo_32 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::convert::{TryFrom, TryInto}; + +use num_bigint::{BigInt, Sign}; +use rand_core::{CryptoRng, RngCore}; +use sha3::{ + digest::{ExtendableOutput, Update}, + Shake256, +}; + +use crate::{ + init_sig, point::Point, shake256, Ed448Error, PreHash, PublicKey, KEY_LENGTH, SIG_LENGTH, +}; + +#[allow(clippy::redundant_pub_crate)] +pub(crate) type PrivateKeyRaw = [u8; KEY_LENGTH]; +#[allow(clippy::redundant_pub_crate)] +pub(crate) type SeedRaw = [u8; KEY_LENGTH]; + +/// This represent a private key. **Must be kept secret.** +/// +/// Could be used to generate a new one or restore an older already saved. +#[derive(Copy, Clone)] +pub struct PrivateKey(PrivateKeyRaw); + +opaque_debug::implement!(PrivateKey); + +impl PrivateKey { + /// Generate a random key. + /// + /// # Example + /// + /// ``` + /// use rand_core::OsRng; + /// use ed448_rust::PrivateKey; + /// let private_key = PrivateKey::new(&mut OsRng); + /// ``` + pub fn new(rnd: &mut T) -> Self + where + T: CryptoRng + RngCore, + { + let mut key = [0; KEY_LENGTH]; + rnd.fill_bytes(&mut key); + Self::from(key) + } + + /// Convert the private key to a format exportable. + /// + /// # Example + /// + /// ``` + /// # use rand_core::OsRng; + /// # use ed448_rust::PrivateKey; + /// # let private_key = PrivateKey::new(&mut OsRng); + /// let exportable_pkey = private_key.as_bytes(); + /// ``` + #[inline] + #[must_use] + pub const fn as_bytes(&self) -> &[u8; KEY_LENGTH] { + &self.0 + } + + pub(crate) fn expand(&self) -> (PrivateKeyRaw, SeedRaw) { + // 1. Hash the 57-byte private key using SHAKE256(x, 114), storing the + // digest in a 114-octet large buffer, denoted h. + let h = Shake256::default() + .chain(self.as_bytes()) + .finalize_boxed(114); + // Only the lower 57 bytes are used for generating the public key. + let mut s: [u8; KEY_LENGTH] = h[..KEY_LENGTH].try_into().unwrap(); + + // 2. Prune the buffer: The two least significant bits of the first + // octet are cleared, all eight bits the last octet are cleared, and + // the highest bit of the second to last octet is set. + s[0] &= 0b1111_1100; + s[56] = 0; + s[55] |= 0b1000_0000; + + let seed: [u8; KEY_LENGTH] = h[KEY_LENGTH..].try_into().unwrap(); + + (s, seed) + } + + /// Sign with key pair. + /// + /// It's possible to indicate a context. More information in + /// [RFC8032 8.3 Use of contexts](https://tools.ietf.org/html/rfc8032#section-8.3). + /// + /// # Examples + /// + /// Without any context. + /// + /// ``` + /// use ed448_rust::PrivateKey; + /// let pkey = PrivateKey::from([0xcd, 0x23, 0xd2, 0x4f, 0x71, 0x42, 0x74, 0xe7, 0x44, 0x34, 0x32, 0x37, 0xb9, + /// 0x32, 0x90, 0xf5, 0x11, 0xf6, 0x42, 0x5f, 0x98, 0xe6, 0x44, 0x59, 0xff, 0x20, 0x3e, 0x89, 0x85, + /// 0x08, 0x3f, 0xfd, 0xf6, 0x05, 0x00, 0x55, 0x3a, 0xbc, 0x0e, 0x05, 0xcd, 0x02, 0x18, 0x4b, 0xdb, + /// 0x89, 0xc4, 0xcc, 0xd6, 0x7e, 0x18, 0x79, 0x51, 0x26, 0x7e, 0xb3, 0x28]); + /// let msg = &[0x0c, 0x3e, 0x54, 0x40, 0x74, 0xec, 0x63, 0xb0, 0x26, 0x5e, 0x0c]; + /// let sig = pkey.sign(msg, None).unwrap(); + /// + /// assert_eq!( + /// sig.iter().map(|b| format!("{:02x}", b)).collect::>().concat(), + /// "1f0a8888ce25e8d458a21130879b840a9089d999aaba039eaf3e3afa090a09d389dba82c4ff2ae8a\ + /// c5cdfb7c55e94d5d961a29fe0109941e00b8dbdeea6d3b051068df7254c0cdc129cbe62db2dc9\ + /// 57dbb47b51fd3f213fb8698f064774250a5028961c9bf8ffd973fe5d5c206492b140e00" + /// ); + /// ``` + /// + /// With a context. + /// + /// ``` + /// use ed448_rust::PrivateKey; + /// let pkey = PrivateKey::from([0xc4, 0xea, 0xb0, 0x5d, 0x35, 0x70, 0x07, 0xc6, 0x32, 0xf3, 0xdb, 0xb4, 0x84, + /// 0x89, 0x92, 0x4d, 0x55, 0x2b, 0x08, 0xfe, 0x0c, 0x35, 0x3a, 0x0d, 0x4a, 0x1f, 0x00, 0xac, 0xda, + /// 0x2c, 0x46, 0x3a, 0xfb, 0xea, 0x67, 0xc5, 0xe8, 0xd2, 0x87, 0x7c, 0x5e, 0x3b, 0xc3, 0x97, 0xa6, + /// 0x59, 0x94, 0x9e, 0xf8, 0x02, 0x1e, 0x95, 0x4e, 0x0a, 0x12, 0x27, 0x4e]); + /// let msg = &[03]; + /// let sig = pkey.sign(msg, Some(&[0x66, 0x6f, 0x6f])).unwrap(); + /// + /// assert_eq!( + /// sig.iter().map(|b| format!("{:02x}", b)).collect::>().concat(), + /// "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b32a89f7d2151f7647f11d8ca2\ + /// ae279fb842d607217fce6e042f6815ea000c85741de5c8da1144a6a1aba7f96de42505d7a7298\ + /// 524fda538fccbbb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c00" + /// ); + /// ``` + /// + /// # Errors + /// + /// * [`Ed448Error::ContextTooLong`] if the context is more than 255 byte length. + #[inline] + pub fn sign(&self, msg: &[u8], ctx: Option<&[u8]>) -> crate::Result<[u8; SIG_LENGTH]> { + self.sign_real(msg, ctx, PreHash::False) + } + + /// Sign with key pair. Message is pre-hashed before signed. + /// + /// The message is hashed before being signed. The size of the signed message in this + /// case is always 64 bytes length. + /// + /// See [`PrivateKey::sign`]. + /// + /// # Errors + /// + /// * [`Ed448Error::ContextTooLong`] if the context is more than 255 byte length. + #[inline] + pub fn sign_ph(&self, msg: &[u8], ctx: Option<&[u8]>) -> crate::Result<[u8; SIG_LENGTH]> { + self.sign_real(msg, ctx, PreHash::True) + } + + fn sign_real( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + pre_hash: PreHash, + ) -> crate::Result<[u8; SIG_LENGTH]> { + let (ctx, msg) = init_sig(ctx, pre_hash, msg)?; + // Expand key. + let (a, seed) = &self.expand(); + let a = BigInt::from_bytes_le(Sign::Plus, a); + // Calculate r and R (R only used in encoded form). + let r = shake256(vec![seed, &msg], ctx.as_ref(), pre_hash); + let r = BigInt::from_bytes_le(Sign::Plus, r.as_ref()) % Point::l(); + let R = (Point::default() * &r).encode(); + // Calculate h. + let h = shake256( + vec![&R, &PublicKey::from(a.clone()).as_byte(), &msg], + ctx.as_ref(), + pre_hash, + ); + let h = BigInt::from_bytes_le(Sign::Plus, h.as_ref()) % Point::l(); + // Calculate s. + let S = (r + h * a) % Point::l(); + // The final signature is a concatenation of R and S. + let mut S = S.magnitude().to_bytes_le(); + S.resize_with(KEY_LENGTH, Default::default); + let S: [u8; KEY_LENGTH] = S.try_into().unwrap(); + + Ok([R, S].concat().try_into().unwrap()) + } +} + +/// Restore the private key from the slice. +impl From for PrivateKey { + #[inline] + fn from(array: PrivateKeyRaw) -> Self { + Self(array) + } +} + +/// Restore the private key from an array. +/// +/// # Error +/// +/// Could return [`Ed448Error::WrongKeyLength`] if the array's length +/// is not [`KEY_LENGTH`]. +impl TryFrom<&'_ [u8]> for PrivateKey { + type Error = Ed448Error; + + fn try_from(bytes: &[u8]) -> crate::Result { + if bytes.len() != KEY_LENGTH { + return Err(Ed448Error::WrongKeyLength); + } + let bytes: &[u8; KEY_LENGTH] = bytes.try_into().unwrap(); + Ok(Self::from(bytes)) + } +} + +impl From<&'_ PrivateKeyRaw> for PrivateKey { + #[inline] + fn from(bytes: &PrivateKeyRaw) -> Self { + Self::from(*bytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::OsRng; + + #[test] + fn create_new_pkey() { + let pkey = PrivateKey::new(&mut OsRng); + let a = pkey.as_bytes(); + assert_eq!(a.len(), KEY_LENGTH); + } + + #[test] + fn invalid_key_len() { + let invalid_pk = PrivateKey::try_from(&[0x01_u8][..]); + assert_eq!(invalid_pk.unwrap_err(), Ed448Error::WrongKeyLength); + } + + #[test] + fn invalid_context_length() { + let pkey = PrivateKey::new(&mut OsRng); + let ctx = [0; 256]; + let invalid_sig = pkey.sign(b"message", Some(&ctx)); + assert_eq!(invalid_sig.unwrap_err(), Ed448Error::ContextTooLong); + } +} diff --git a/crates/ed448-rust/src/public_key.rs b/crates/ed448-rust/src/public_key.rs new file mode 100644 index 0000000..3574273 --- /dev/null +++ b/crates/ed448-rust/src/public_key.rs @@ -0,0 +1,293 @@ +// Copyright 2021 Lolo_32 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::convert::TryFrom; + +use num_bigint::{BigInt, Sign}; + +use crate::{ + init_sig, + point::Point, + private_key::PrivateKey, + {shake256, Ed448Error, PreHash, KEY_LENGTH, SIG_LENGTH}, +}; + +/// This is a public key. _Should be distributed._ +/// +/// You can extract a `PublicKey` by calling [`Self::from()`]. +#[derive(Clone)] +pub struct PublicKey(Point); + +opaque_debug::implement!(PublicKey); + +impl PublicKey { + /// Convert the public key to an easily exportable format. + #[inline] + #[must_use] + pub fn as_byte(&self) -> [u8; 57] { + // 4. The public key A is the encoding of the point [s]B. + self.0.encode() + } + + /// Verify signature with public key. + /// + /// # Example + /// + /// ``` + /// # use rand_core::OsRng; + /// use ed448_rust::{PublicKey, Ed448Error}; + /// # let private_key = ed448_rust::PrivateKey::new(&mut OsRng); + /// let message = b"Signed message to verify"; + /// # let retrieve_signature = || private_key.sign(message, None).unwrap(); + /// # let retrieve_pubkey = || PublicKey::from(&private_key); + /// let public_key = retrieve_pubkey(); + /// let signature = retrieve_signature(); + /// match public_key.verify(message, &signature, None) { + /// Ok(()) => { + /// // Signature OK, use the message + /// } + /// Err(Ed448Error::InvalidSignature) => { + /// // The verification of the signature is invalid + /// } + /// Err(Ed448Error::ContextTooLong) => { + /// // The used context is more than 255 bytes length + /// } + /// Err(Ed448Error::WrongSignatureLength) => { + /// // The signature is not 144 bytes length + /// } + /// Err(_) => unreachable!() + /// } + /// ``` + /// + /// # Errors + /// + /// * [`Ed448Error::InvalidSignature`] if the signature is not valid, either the public key + /// or the signature used are not the right, or the message has been altered. + /// * [`Ed448Error::ContextTooLong`] if the optional context is more than 255 byte length. + /// * [`Ed448Error::WrongSignatureLength`] if the signature is not `SIG_LENGTH` byte. + #[inline] + pub fn verify(&self, msg: &[u8], sign: &[u8], ctx: Option<&[u8]>) -> crate::Result<()> { + self.verify_real(msg, sign, ctx, PreHash::False) + } + + /// Verify signature with public key. Message is pre-hashed before checked. + /// + /// See [`PublicKey::verify`] for more information. + /// + /// # Errors + /// + /// * [`Ed448Error::InvalidSignature`] if the signature is not valid, either the public key + /// or the signature used are not the right, or the message has been altered. + /// * [`Ed448Error::ContextTooLong`] if the optional context is more than 255 byte length. + /// * [`Ed448Error::WrongSignatureLength`] if the signature is not `SIG_LENGTH` byte. + #[inline] + pub fn verify_ph(&self, msg: &[u8], sign: &[u8], ctx: Option<&[u8]>) -> crate::Result<()> { + self.verify_real(msg, sign, ctx, PreHash::True) + } + + fn verify_real( + &self, + msg: &[u8], + sign: &[u8], + ctx: Option<&[u8]>, + pre_hash: PreHash, + ) -> crate::Result<()> { + // Sanity-check sizes. + if sign.len() < SIG_LENGTH { + return Err(Ed448Error::WrongSignatureLength); + } + + // Split signature into R and S, and parse. + let (Rraw, Sraw) = sign.split_at(KEY_LENGTH); + let (R, S) = ( + Point::decode(Rraw).map_err(|_| Ed448Error::InvalidSignature)?, + BigInt::from_bytes_le(Sign::Plus, Sraw), + ); + // Parse public key. + let A = Point::decode(&self.as_byte()).map_err(|_| Ed448Error::InvalidSignature)?; + if &S >= Point::l() { + return Err(Ed448Error::InvalidSignature); + } + // Calculate h. + let h = { + let (ctx, msg) = init_sig(ctx, pre_hash, msg)?; + shake256(vec![Rraw, &self.as_byte(), &msg], ctx.as_ref(), pre_hash) + }; + let h = BigInt::from_bytes_le(Sign::Plus, &h) % Point::l(); + // Calculate left and right sides of check eq. + let rhs = R + (A * h); + let lhs = Point::default() * S; + // Check eq. holds? + if lhs.double().double() == rhs.double().double() { + Ok(()) + } else { + Err(Ed448Error::InvalidSignature) + } + } +} + +/// Instantiate a `PublicKey` from the `PrivateKey`. +impl From<&PrivateKey> for PublicKey { + #[inline] + fn from(private_key: &PrivateKey) -> Self { + let (s, _) = &private_key.expand(); + // 3. Interpret the buffer as the little-endian integer, forming a + // secret scalar s. + Self::from(BigInt::from_bytes_le(Sign::Plus, s)) + } +} + +/// Do not use, it's for internal use only to generate the PublicKey +#[doc(hidden)] +impl From for PublicKey { + #[inline] + fn from(s: BigInt) -> Self { + // Perform a known-base-point scalar multiplication [s]B. + let A = Point::default() * s; + + // 4. The public key A is the encoding of the point [s]B. + Self(A) + } +} + +impl From<[u8; KEY_LENGTH]> for PublicKey { + #[inline] + fn from(array: [u8; KEY_LENGTH]) -> Self { + Self(Point::decode(&array).expect("bad ed448 public key")) + } +} + +impl From<&'_ [u8; KEY_LENGTH]> for PublicKey { + #[inline] + fn from(array: &'_ [u8; KEY_LENGTH]) -> Self { + Self(Point::decode(array).expect("bad ed448 public key")) + } +} + +impl TryFrom<&[u8]> for PublicKey { + type Error = Ed448Error; + + #[inline] + fn try_from(array: &[u8]) -> Result { + if array.len() != KEY_LENGTH { + return Err(Ed448Error::WrongPublicKeyLength); + } + Ok(Self(Point::decode(array)?)) + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use super::*; + use rand_core::OsRng; + + #[test] + fn test_vectors_rfc8032_public() { + let secret_vec = hex::decode( + "6c82a562cb808d10d632be89c8513ebf6c929f34ddfa8c9f63c9960ef6e348a3\ + 528c8a3fcc2f044e39a3fc5b94492f8f032e7549a20098f95b", + ) + .unwrap(); + let ref_public = hex::decode( + "5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778\ + edf124769b46c7061bd6783df1e50f6cd1fa1abeafe8256180", + ) + .unwrap(); + + let secret = PrivateKey::try_from(&secret_vec[..]).unwrap(); + let public = PublicKey::from(&secret); + + assert_eq!(&public.as_byte()[..], &ref_public[..]); + } + + #[test] + fn wrong_verification_with_another_pub_key() { + let secret_1 = PrivateKey::new(&mut OsRng); + let msg = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec."; + let sig_1 = secret_1.sign(msg, None).unwrap(); + let public_2 = PublicKey::from(&PrivateKey::new(&mut OsRng)); + assert_eq!( + public_2.verify(msg, &sig_1, None).unwrap_err(), + Ed448Error::InvalidSignature + ); + } + + #[test] + fn wrong_pubkey_length() { + let pub_key = PublicKey::try_from(&[0x01_u8][..]); + assert_eq!(pub_key.unwrap_err(), Ed448Error::WrongPublicKeyLength); + } + + #[test] + fn wrong_sign_length() { + let pubkey = PublicKey::from(&PrivateKey::new(&mut OsRng)); + let sig = [0x01; SIG_LENGTH - 1]; + assert_eq!( + pubkey.verify(b"message", &sig, None).unwrap_err(), + Ed448Error::WrongSignatureLength + ); + } + + #[test] + fn instantiate_pubkey() { + let pkey = PrivateKey::new(&mut OsRng); + let pkey_slice = *pkey.as_bytes(); + let pub_key1 = PublicKey::from(&pkey_slice); + let pub_key2 = PublicKey::from(pkey_slice); + + assert_eq!(pub_key1.as_byte(), pub_key2.as_byte()); + } + + #[test] + fn wrong_with_altered_message() { + let secret = PrivateKey::new(&mut OsRng); + let public = PublicKey::from(&PrivateKey::new(&mut OsRng)); + let msg_1 = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec."; + // One dot missing at the end + let msg_2 = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec"; + let sig = secret.sign(msg_1, None).unwrap(); + assert_eq!( + public.verify(msg_2, &sig, None).unwrap_err(), + Ed448Error::InvalidSignature + ); + } + + #[test] + fn wrong_with_forged_pub_key() { + let secret = PrivateKey::new(&mut OsRng); + let public = PublicKey::from(&[255; KEY_LENGTH]); + let msg = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec."; + // One dot missing at the end + let sig = secret.sign(msg, None).unwrap(); + assert_eq!( + public.verify(msg, &sig, None).unwrap_err(), + Ed448Error::InvalidSignature + ); + } + + #[test] + fn wrong_with_forged_signature() { + let secret = PrivateKey::new(&mut OsRng); + let public = PublicKey::from(&secret); + let msg = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec."; + // One dot missing at the end + let sig = [1; SIG_LENGTH]; + assert_eq!( + public.verify(msg, &sig, None).unwrap_err(), + Ed448Error::InvalidSignature + ); + } +} diff --git a/crates/ed448-rust/tests/rfc8032.rs b/crates/ed448-rust/tests/rfc8032.rs new file mode 100644 index 0000000..2450c83 --- /dev/null +++ b/crates/ed448-rust/tests/rfc8032.rs @@ -0,0 +1,215 @@ +use std::convert::TryFrom; + +use ed448_rust::{PrivateKey, PublicKey}; + +#[test] +fn ed448() { + vec![ + ( + "-----Blank", + "6c82a562cb808d10d632be89c8513ebf6c929f34ddfa8c9f63c9960ef6e348a3528c8a3fcc2f044e39a3\ + fc5b94492f8f032e7549a20098f95b", + "5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778edf124769b46c7061bd6\ + 783df1e50f6cd1fa1abeafe8256180", + "", + None, + "533a37f6bbe457251f023c0d88f976ae2dfb504a843e34d2074fd823d41a591f2b233f034f628281f2fd\ + 7a22ddd47d7828c59bd0a21bfd3980ff0d2028d4b18a9df63e006c5d1c2d345b925d8dc00b4104852\ + db99ac5c7cdda8530a113a0f4dbb61149f05a7363268c71d95808ff2e652600", + ), + ( + "-----1 octet", + "c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463afbea67c5e8d2877c5e3b\ + c397a659949ef8021e954e0a12274e", + "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b843\ + 8ea4cb82169c235160627b4c3a9480", + "03", + None, + "26b8f91727bd62897af15e41eb43c377efb9c610d48f2335cb0bd0087810f4352541b143c4b981b7e18f\ + 62de8ccdf633fc1bf037ab7cd779805e0dbcc0aae1cbcee1afb2e027df36bc04dcecbf154336c19f0\ + af7e0a6472905e799f1953d2a0ff3348ab21aa4adafd1d234441cf807c03a00", + ), + ( + "-----1 octet (with context)", + "c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463afbea67c5e8d2877c5e3b\ + c397a659949ef8021e954e0a12274e", + "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b843\ + 8ea4cb82169c235160627b4c3a9480", + "03", + Some("666f6f"), + "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b32a89f7d2151f7647f11d8ca2ae27\ + 9fb842d607217fce6e042f6815ea000c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda53\ + 8fccbbb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c00", + ), + ( + "-----11 octets", + "cd23d24f714274e744343237b93290f511f6425f98e64459ff203e8985083ffdf60500553abc0e05cd02\ + 184bdb89c4ccd67e187951267eb328", + "dcea9e78f35a1bf3499a831b10b86c90aac01cd84b67a0109b55a36e9328b1e365fce161d71ce7131a54\ + 3ea4cb5f7e9f1d8b00696447001400", + "0c3e544074ec63b0265e0c", + None, + "1f0a8888ce25e8d458a21130879b840a9089d999aaba039eaf3e3afa090a09d389dba82c4ff2ae8ac5cd\ + fb7c55e94d5d961a29fe0109941e00b8dbdeea6d3b051068df7254c0cdc129cbe62db2dc957dbb47b\ + 51fd3f213fb8698f064774250a5028961c9bf8ffd973fe5d5c206492b140e00", + ), + ( + "-----12 octets", + "258cdd4ada32ed9c9ff54e63756ae582fb8fab2ac721f2c8e676a72768513d939f63dddb55609133f29a\ + df86ec9929dccb52c1c5fd2ff7e21b", + "3ba16da0c6f2cc1f30187740756f5e798d6bc5fc015d7c63cc9510ee3fd44adc24d8e968b6e46e6f94d1\ + 9b945361726bd75e149ef09817f580", + "64a65f3cdedcdd66811e2915", + None, + "7eeeab7c4e50fb799b418ee5e3197ff6bf15d43a14c34389b59dd1a7b1b85b4ae90438aca634bea45e3a\ + 2695f1270f07fdcdf7c62b8efeaf00b45c2c96ba457eb1a8bf075a3db28e5c24f6b923ed4ad747c3c\ + 9e03c7079efb87cb110d3a99861e72003cbae6d6b8b827e4e6c143064ff3c00", + ), + ( + "-----13 octets", + "7ef4e84544236752fbb56b8f31a23a10e42814f5f55ca037cdcc11c64c9a3b2949c1bb60700314611732\ + a6c2fea98eebc0266a11a93970100e", + "b3da079b0aa493a5772029f0467baebee5a8112d9d3a22532361da294f7bb3815c5dc59e176b4d9f381c\ + a0938e13c6c07b174be65dfa578e80", + "64a65f3cdedcdd66811e2915e7", + None, + "6a12066f55331b6c22acd5d5bfc5d71228fbda80ae8dec26bdd306743c5027cb4890810c162c02746867\ + 5ecf645a83176c0d7323a2ccde2d80efe5a1268e8aca1d6fbc194d3f77c44986eb4ab4177919ad8be\ + c33eb47bbb5fc6e28196fd1caf56b4e7e0ba5519234d047155ac727a1053100", + ), + ( + "-----64 octets", + "d65df341ad13e008567688baedda8e9dcdc17dc024974ea5b4227b6530e339bff21f99e68ca6968f3cca\ + 6dfe0fb9f4fab4fa135d5542ea3f01", + "df9705f58edbab802c7f8363cfe5560ab1c6132c20a9f1dd163483a26f8ac53a39d6808bf4a1dfbd261b\ + 099bb03b3fb50906cb28bd8a081f00", + "bd0f6a3747cd561bdddf4640a332461a4a30a12a434cd0bf40d766d9c6d458e5512204a30c17d1f50b50\ + 79631f64eb3112182da3005835461113718d1a5ef944", + None, + "554bc2480860b49eab8532d2a533b7d578ef473eeb58c98bb2d0e1ce488a98b18dfde9b9b90775e67f47\ + d4a1c3482058efc9f40d2ca033a0801b63d45b3b722ef552bad3b4ccb667da350192b61c508cf7b6b\ + 5adadc2c8d9a446ef003fb05cba5f30e88e36ec2703b349ca229c2670833900", + ), + ( + "-----256 octets", + "2ec5fe3c17045abdb136a5e6a913e32ab75ae68b53d2fc149b77e504132d37569b7e766ba74a19bd6162\ + 343a21c8590aa9cebca9014c636df5", + "79756f014dcfe2079f5dd9e718be4171e2ef2486a08f25186f6bff43a9936b9bfe12402b08ae65798a3d\ + 81e22e9ec80e7690862ef3d4ed3a00", + "15777532b0bdd0d1389f636c5f6b9ba734c90af572877e2d272dd078aa1e567cfa80e12928bb542330e8\ + 409f3174504107ecd5efac61ae7504dabe2a602ede89e5cca6257a7c77e27a702b3ae39fc769fc54f\ + 2395ae6a1178cab4738e543072fc1c177fe71e92e25bf03e4ecb72f47b64d0465aaea4c7fad372536\ + c8ba516a6039c3c2a39f0e4d832be432dfa9a706a6e5c7e19f397964ca4258002f7c0541b590316db\ + c5622b6b2a6fe7a4abffd96105eca76ea7b98816af0748c10df048ce012d901015a51f189f3888145\ + c03650aa23ce894c3bd889e030d565071c59f409a9981b51878fd6fc110624dcbcde0bf7a69ccce38\ + fabdf86f3bef6044819de11", + None, + "c650ddbb0601c19ca11439e1640dd931f43c518ea5bea70d3dcde5f4191fe53f00cf966546b72bcc7d58\ + be2b9badef28743954e3a44a23f880e8d4f1cfce2d7a61452d26da05896f0a50da66a239a8a188b6d\ + 825b3305ad77b73fbac0836ecc60987fd08527c1a8e80d5823e65cafe2a3d00", + ), + ( + "-----1023 octets", + "872d093780f5d3730df7c212664b37b8a0f24f56810daa8382cd4fa3f77634ec44dc54f1c2ed9bea86fa\ + fb7632d8be199ea165f5ad55dd9ce8", + "a81b2e8a70a5ac94ffdbcc9badfc3feb0801f258578bb114ad44ece1ec0e799da08effb81c5d685c0c56\ + f64eecaef8cdf11cc38737838cf400", + "6ddf802e1aae4986935f7f981ba3f0351d6273c0a0c22c9c0e8339168e675412a3debfaf435ed6515580\ + 07db4384b650fcc07e3b586a27a4f7a00ac8a6fec2cd86ae4bf1570c41e6a40c931db27b2faa15a8c\ + edd52cff7362c4e6e23daec0fbc3a79b6806e316efcc7b68119bf46bc76a26067a53f296dafdbdc11\ + c77f7777e972660cf4b6a9b369a6665f02e0cc9b6edfad136b4fabe723d2813db3136cfde9b6d0443\ + 22fee2947952e031b73ab5c603349b307bdc27bc6cb8b8bbd7bd323219b8033a581b59eadebb09b3c\ + 4f3d2277d4f0343624acc817804728b25ab797172b4c5c21a22f9c7839d64300232eb66e53f31c723\ + fa37fe387c7d3e50bdf9813a30e5bb12cf4cd930c40cfb4e1fc622592a49588794494d56d24ea4b40\ + c89fc0596cc9ebb961c8cb10adde976a5d602b1c3f85b9b9a001ed3c6a4d3b1437f52096cd1956d04\ + 2a597d561a596ecd3d1735a8d570ea0ec27225a2c4aaff26306d1526c1af3ca6d9cf5a2c98f47e1c4\ + 6db9a33234cfd4d81f2c98538a09ebe76998d0d8fd25997c7d255c6d66ece6fa56f11144950f02779\ + 5e653008f4bd7ca2dee85d8e90f3dc315130ce2a00375a318c7c3d97be2c8ce5b6db41a6254ff264f\ + a6155baee3b0773c0f497c573f19bb4f4240281f0b1f4f7be857a4e59d416c06b4c50fa09e1810ddc\ + 6b1467baeac5a3668d11b6ecaa901440016f389f80acc4db977025e7f5924388c7e340a732e554440\ + e76570f8dd71b7d640b3450d1fd5f0410a18f9a3494f707c717b79b4bf75c98400b096b21653b5d21\ + 7cf3565c9597456f70703497a078763829bc01bb1cbc8fa04eadc9a6e3f6699587a9e75c94e5bab00\ + 36e0b2e711392cff0047d0d6b05bd2a588bc109718954259f1d86678a579a3120f19cfb2963f177ae\ + b70f2d4844826262e51b80271272068ef5b3856fa8535aa2a88b2d41f2a0e2fda7624c2850272ac4a\ + 2f561f8f2f7a318bfd5caf9696149e4ac824ad3460538fdc25421beec2cc6818162d06bbed0c40a38\ + 7192349db67a118bada6cd5ab0140ee273204f628aad1c135f770279a651e24d8c14d75a6059d76b9\ + 6a6fd857def5e0b354b27ab937a5815d16b5fae407ff18222c6d1ed263be68c95f32d908bd895cd76\ + 207ae726487567f9a67dad79abec316f683b17f2d02bf07e0ac8b5bc6162cf94697b3c27cd1fea49b\ + 27f23ba2901871962506520c392da8b6ad0d99f7013fbc06c2c17a569500c8a7696481c1cd33e9b14\ + e40b82e79a5f5db82571ba97bae3ad3e0479515bb0e2b0f3bfcd1fd33034efc6245eddd7ee2086dda\ + e2600d8ca73e214e8c2b0bdb2b047c6a464a562ed77b73d2d841c4b34973551257713b753632efba3\ + 48169abc90a68f42611a40126d7cb21b58695568186f7e569d2ff0f9e745d0487dd2eb997cafc5abf\ + 9dd102e62ff66cba87", + None, + "e301345a41a39a4d72fff8df69c98075a0cc082b802fc9b2b6bc503f926b65bddf7f4c8f1cb49f6396af\ + c8a70abe6d8aef0db478d4c6b2970076c6a0484fe76d76b3a97625d79f1ce240e7c576750d2955282\ + 86f719b413de9ada3e8eb78ed573603ce30d8bb761785dc30dbc320869e1a00", + ), + ] + .iter() + .for_each(|(name, pkey, pub_key, msg, context, sig)| { + println!("Test: {}", name); + let pkey = hex::decode(pkey).unwrap(); + let pub_key = hex::decode(pub_key).unwrap(); + let msg = hex::decode(msg).unwrap(); + let context = context.map(|ctx| hex::decode(ctx).unwrap()); + let sig = hex::decode(sig).unwrap(); + + let pkey = PrivateKey::try_from(&pkey[..]).unwrap(); + let pub_k = PublicKey::from(&pkey); + assert_eq!(&pub_k.as_byte()[..], &pub_key[..]); + + let sig_ = pkey.sign(&msg, context.as_deref()).unwrap(); + assert_eq!(&sig_[..], &sig[..]); + + pub_k.verify(&msg, &sig, context.as_deref()).unwrap(); + }) +} + +#[test] +fn ed448ph() { + vec![ + ( + "-----TEST abc", + "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05\ + d6dbefde69e3ab2cec7c867c6e2c49", + "259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303\ + d90d8132c276d5ed3d5d01c0f53880", + "616263", + None, + "822f6901f7480f3d5f562c592994d9693602875614483256505600bbc281ae381f54d6bce2ea91157493\ + 2f52a4e6cadd78769375ec3ffd1b801a0d9b3f4030cd433964b6457ea39476511214f97469b57dd32\ + dbc560a9a94d00bff07620464a3ad203df7dc7ce360c3cd3696d9d9fab90f00", + ), + ( + "-----TEST abc (with context)", + "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05\ + d6dbefde69e3ab2cec7c867c6e2c49", + "259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303\ + d90d8132c276d5ed3d5d01c0f53880", + "616263", + Some("666f6f"), + "c32299d46ec8ff02b54540982814dce9a05812f81962b649d528095916a2aa481065b1580423ef927ecf\ + 0af5888f90da0f6a9a85ad5dc3f280d91224ba9911a3653d00e484e2ce232521481c8658df304bb77\ + 45a73514cdb9bf3e15784ab71284f8d0704a608c54a6b62d97beb511d132100", + ), + ] + .iter() + .for_each(|(name, pkey, pub_key, msg, context, sig)| { + println!("Test pre-hashed: {}", name); + let pkey = hex::decode(pkey).unwrap(); + let pub_key = hex::decode(pub_key).unwrap(); + let msg = hex::decode(msg).unwrap(); + let context = context.map(|ctx| hex::decode(ctx).unwrap()); + let sig = hex::decode(sig).unwrap(); + + let pkey = PrivateKey::try_from(&pkey[..]).unwrap(); + let pub_k = PublicKey::from(&pkey); + assert_eq!(&pub_k.as_byte()[..], &pub_key[..]); + + let sig_ = pkey.sign_ph(&msg, context.as_deref()).unwrap(); + assert_eq!(&sig_[..], &sig[..]); + + pub_k.verify_ph(&msg, &sig, context.as_deref()).unwrap(); + }) +}