diff --git a/Cargo.lock b/Cargo.lock index 2c27e0e..78e4604 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,9 +132,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.57" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -645,9 +645,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ "once_cell", "wasm-bindgen", @@ -807,9 +807,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", @@ -1132,12 +1132,12 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0-rc.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", "digest 0.11.2", ] @@ -1191,7 +1191,7 @@ dependencies = [ "rustcrypto-ff", "rustcrypto-group", "serde", - "sha2 0.11.0-rc.5", + "sha2 0.11.0", "subtle", "zeroize", ] @@ -1338,9 +1338,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ "cfg-if", "once_cell", @@ -1351,9 +1351,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1361,9 +1361,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ "bumpalo", "proc-macro2", @@ -1374,9 +1374,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] @@ -1417,9 +1417,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" dependencies = [ "js-sys", "wasm-bindgen", @@ -1633,18 +1633,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 57e11d9..fd53b7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,17 +5,11 @@ members = [ ] resolver = "2" -[profile.dev] -opt-level = 2 - - [workspace.dependencies] bincode = "1" cc = "1.0" cfg-if = "1" criterion = { version = "0.5", features = ["html_reports"] } -# Keep the dependency key as "curve25519" so that all existing `use curve25519::…` -# references in source files remain valid; the actual package is solanacurve25519. curve25519 = { path = "curve25519/solana-ed25519", package = "solana-ed25519" } curve25519-dalek-derive = "0.1.1" cpufeatures = "0.2.17" @@ -38,4 +32,7 @@ serde = { version = "1.0", default-features = false, features = ["derive"] } sha2 = { version = "0.11.0-rc.3", default-features = false } subtle = { version = "2.6.0", default-features = false, features = ["const-generics"] } which = "4.4" -zeroize = { version = "1", default-features = false, features = ["derive"] } \ No newline at end of file +zeroize = { version = "1", default-features = false, features = ["derive"] } + +[profile.dev] +opt-level = 2 diff --git a/curve25519/solana-ed25519/README.md b/curve25519/solana-ed25519/README.md index ec14d13..7557d52 100644 --- a/curve25519/solana-ed25519/README.md +++ b/curve25519/solana-ed25519/README.md @@ -1,11 +1,11 @@ -# curve25519 (curve25519-sol) +# curve25519 (`solana-ed25519`) **A pure-Rust implementation of group operations on Ristretto and Curve25519, forked from [curve25519-dalek] with HEEA scalar decomposition and a reduced backend set.** > For the original curve25519-dalek documentation see [README_dalek.md](README_dalek.md). -This crate is part of the [curve25519-sol](../README.md) workspace. +This crate is part of the [cryptography](https://github.com/anza-xyz/cryptography/) workspace. --- @@ -56,10 +56,10 @@ verification. > For the original ed25519-zebra documentation see [README_zebra.md](README_zebra.md). -### `verify_heea`: fast-path signature verification +### `verify_zebra`: fast-path signature verification -A new method `VerificationKey::verify_heea` sits alongside the existing `verify`. -Both accept the same arguments and produce identical results — `verify_heea` is a +A new method `VerificationKey::verify_zebra` sits alongside the existing `verify`. +Both accept the same arguments and produce identical results — `verify_zebra` is a **drop-in accelerated replacement** for `verify`. The HEEA method (TCHES 2025) transforms the standard 2-point MSM: @@ -93,7 +93,39 @@ See [ZIP 215] for full details. ## Use ```toml -curve25519-sol = { git = "https://github.com/zz-sol/ed25519-sol" } +curve25519 = { package = "solana-ed25519", git = "https://github.com/anza-xyz/cryptography" } +``` + +### Ed25519 signing and verification + +```rust,no_run +use core::convert::TryFrom; +use curve25519::ed_sigs::{SigningKey, VerificationKey}; + +let msg = b"solana-ed25519"; + +// Generate key and sign +let sk = SigningKey::new(rand::rng()); +let sig = sk.sign(msg); +let vk = VerificationKey::from(&sk); + +// Standard ZIP-215 verification (from ed25519-zebra) +vk.verify(&sig, msg).expect("valid signature"); + +// HEEA-accelerated verification (same result, ~15% faster) +vk.verify_zebra(&sig, msg).expect("valid signature"); +``` + +### Batch verification + +```rust,ignore +use curve25519::ed_sigs::batch; + +let mut verifier = batch::Verifier::new(); +for (vk_bytes, sig, msg) in items { + verifier.queue((vk_bytes, sig, msg)); +} +verifier.verify(rand::rng()).expect("all valid"); ``` ### Ed25519 signing and verification @@ -109,11 +141,8 @@ let sk = SigningKey::new(rand::rng()); let sig = sk.sign(msg); let vk = VerificationKey::from(&sk); -// Standard ZIP-215 verification (from ed25519-zebra) +// Standard ZIP-215 verification with heea acceleration vk.verify(&sig, msg).expect("valid signature"); - -// HEEA-accelerated verification (same result, ~15% faster) -vk.verify_heea(&sig, msg).expect("valid signature"); ``` ### Batch verification diff --git a/curve25519/solana-ed25519/benches/bench.rs b/curve25519/solana-ed25519/benches/bench.rs index 640e476..08df1bf 100644 --- a/curve25519/solana-ed25519/benches/bench.rs +++ b/curve25519/solana-ed25519/benches/bench.rs @@ -1,6 +1,10 @@ -use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use core::convert::TryFrom; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use curve25519::ed_sigs::*; +use ed25519::signature::Verifier as _; +use ed25519_dalek::VerifyingKey as DalekVerifyingKey; +use ed25519_zebra::VerificationKey as ZebraVerificationKey; fn sigs_with_distinct_pubkeys() -> impl Iterator { std::iter::repeat_with(|| { @@ -20,6 +24,23 @@ fn sigs_with_same_pubkey() -> impl Iterator ( + VerificationKey, + Signature, + ZebraVerificationKey, + DalekVerifyingKey, +) { + let sk = SigningKey::new(rand::rng()); + let vk = VerificationKey::from(&sk); + let sig = sk.sign(b""); + let vk_bytes: [u8; 32] = vk.into(); + + let zebra_vk = ZebraVerificationKey::try_from(vk_bytes).expect("zebra verification key"); + let dalek_vk = DalekVerifyingKey::from_bytes(&vk_bytes).expect("dalek verification key"); + + (vk, sig, zebra_vk, dalek_vk) +} + fn bench_batch_verify(c: &mut Criterion) { let mut group = c.benchmark_group("Batch Verification"); for n in [8usize, 16, 24, 32, 40, 48, 56, 64].iter() { @@ -74,21 +95,31 @@ fn bench_batch_verify(c: &mut Criterion) { fn bench_single_verify(c: &mut Criterion) { let mut group = c.benchmark_group("Single Verification"); - group.bench_function("ed25519", |b| { - let sk = SigningKey::new(rand::rng()); - let vk = VerificationKey::from(&sk); - let sig = sk.sign(b""); + group.bench_function("local_verify_zebra", |b| { + let (vk, sig, _, _) = single_verify_inputs(); b.iter(|| { - let _ = vk.verify(&sig, b""); + let _ = vk.verify_zebra(&sig, b""); }) }); - group.bench_function("ed25519_hEEA", |b| { - let sk = SigningKey::new(rand::rng()); - let vk = VerificationKey::from(&sk); - let sig = sk.sign(b""); + group.bench_function("local_verify_dalek", |b| { + let (vk, sig, _, _) = single_verify_inputs(); b.iter(|| { - let _ = vk.verify_heea(&sig, b""); + let _ = vk.verify_dalek(&sig, b""); + }) + }); + + group.bench_function("crates_io_ed25519_zebra", |b| { + let (_, sig, zebra_vk, _) = single_verify_inputs(); + b.iter(|| { + let _ = zebra_vk.verify(&sig, b""); + }) + }); + + group.bench_function("crates_io_ed25519_dalek", |b| { + let (_, sig, _, dalek_vk) = single_verify_inputs(); + b.iter(|| { + let _ = dalek_vk.verify(b"", &sig); }) }); diff --git a/curve25519/solana-ed25519/src/ed_sigs/batch.rs b/curve25519/solana-ed25519/src/ed_sigs/batch.rs index d808712..6d8d0ec 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/batch.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/batch.rs @@ -106,7 +106,7 @@ impl Item { /// the message. pub fn verify_single(self) -> Result<(), Error> { VerificationKey::try_from(self.vk_bytes) - .and_then(|vk| vk.verify_prehashed(&self.sig, self.k)) + .and_then(|vk| vk.verify_zebra_prehashed(&self.sig, self.k)) } } diff --git a/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs b/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs index 36fedac..44d5806 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs @@ -1,5 +1,11 @@ use crate::ed_sigs::SigningKey; use crate::ed_sigs::VerificationKey; +#[cfg(feature = "std")] +use crate::ed_sigs::tests::small_order::SMALL_ORDER_SIGS; +#[cfg(feature = "std")] +use core::convert::TryFrom; +#[cfg(feature = "std")] +use ed25519::Signature; #[test] fn test_verify_heea_invalid_signature() { @@ -13,17 +19,21 @@ fn test_verify_heea_invalid_signature() { // Try to verify with different message let wrong_msg = b"Different message"; - let result_standard = verification_key.verify(&signature, wrong_msg); - let result_heea = verification_key.verify_heea(&signature, wrong_msg); + let result_default = verification_key.verify(&signature, wrong_msg); + let result_zebra = verification_key.verify_zebra(&signature, wrong_msg); + let result_dalek = verification_key.verify_dalek(&signature, wrong_msg); - // Both should fail assert!( - result_standard.is_err(), - "Standard verification should fail for wrong message" + result_default.is_err(), + "Default verification should fail for wrong message" ); assert!( - result_heea.is_err(), - "heea verification should fail for wrong message" + result_zebra.is_err(), + "Zebra verification should fail for wrong message" + ); + assert!( + result_dalek.is_err(), + "Dalek verification should fail for wrong message" ); } @@ -38,18 +48,56 @@ fn test_verify_heea_multiple_signatures() { let msg = format!("Message number {}", i); let signature = signing_key.sign(msg.as_bytes()); - let result_standard = verification_key.verify(&signature, msg.as_bytes()); - let result_heea = verification_key.verify_heea(&signature, msg.as_bytes()); + let result_default = verification_key.verify(&signature, msg.as_bytes()); + let result_zebra = verification_key.verify_zebra(&signature, msg.as_bytes()); + let result_dalek = verification_key.verify_dalek(&signature, msg.as_bytes()); assert!( - result_standard.is_ok(), - "Standard verification should succeed for signature {}", + result_default.is_ok(), + "Default verification should succeed for signature {}", i ); assert!( - result_heea.is_ok(), - "heea verification should succeed for signature {}", + result_zebra.is_ok(), + "Zebra verification should succeed for signature {}", + i + ); + assert!( + result_dalek.is_ok(), + "Dalek verification should succeed for signature {}", i ); } } + +#[test] +fn test_default_verification_matches_zebra() { + let mut rng = rand::rng(); + let signing_key = SigningKey::new(&mut rng); + let verification_key = VerificationKey::from(&signing_key); + let msg = b"default verification mode"; + let signature = signing_key.sign(msg); + + assert_eq!( + verification_key.verify(&signature, msg), + verification_key.verify_zebra(&signature, msg) + ); +} + +#[cfg(feature = "std")] +#[test] +fn test_verify_dalek_matches_legacy_edge_cases() { + for case in SMALL_ORDER_SIGS.iter() { + let sig = Signature::from(case.sig_bytes); + let vk = VerificationKey::try_from(case.vk_bytes).unwrap(); + let result = vk.verify_dalek(&sig, b"Zcash"); + + assert_eq!( + result.is_ok(), + case.valid_legacy, + "dalek-compatible verification mismatch for vk={} sig={}", + hex::encode(case.vk_bytes), + hex::encode(case.sig_bytes) + ); + } +} diff --git a/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs b/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs index 421c46c..8c29abe 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs @@ -237,8 +237,20 @@ impl Verifier for VerificationKey { } impl VerificationKey { + fn challenge_scalar(&self, signature: &Signature, msg: &[u8]) -> Scalar { + Scalar::from_hash( + Sha512::default() + .chain(&signature.r_bytes()[..]) + .chain(&self.A_bytes.0[..]) + .chain(msg), + ) + } + /// Verify a purported `signature` on the given `msg`. /// + /// This is the default verification mode and uses the HEEA-accelerated + /// verification path with Zebra / ZIP-215 semantics. + /// /// ## Zcash-specific consensus properties /// /// Ed25519 checks are described in [§5.4.5][ps] of the Zcash protocol specification and in @@ -259,16 +271,10 @@ impl VerificationKey { /// [ps]: https://zips.z.cash/protocol/protocol.pdf#concreteed25519 /// [ZIP215]: https://zips.z.cash/zip-0215 pub fn verify(&self, signature: &Signature, msg: &[u8]) -> Result<(), Error> { - let k = Scalar::from_hash( - Sha512::default() - .chain(&signature.r_bytes()[..]) - .chain(&self.A_bytes.0[..]) - .chain(msg), - ); - self.verify_prehashed(signature, k) + self.verify_zebra(signature, msg) } - /// Verify a signature using the heea half-size scalar optimization. + /// Verify a signature using HEEA with Zebra / ZIP-215 semantics. /// /// This implements the algorithm from "Accelerating EdDSA Signature Verification /// with Faster Scalar Size Halving" (TCHES 2025). @@ -285,15 +291,16 @@ impl VerificationKey { /// τs_lo B + τs_hi (2^128 B) = τR + ρA /// which can be done via 4-variable MSM with half-size scalars. #[allow(non_snake_case)] - pub fn verify_heea(&self, signature: &Signature, msg: &[u8]) -> Result<(), Error> { - // Compute the hash scalar h (called k in the standard implementation) - let h = Scalar::from_hash( - Sha512::default() - .chain(&signature.r_bytes()[..]) - .chain(&self.A_bytes.0[..]) - .chain(msg), - ); + pub fn verify_zebra(&self, signature: &Signature, msg: &[u8]) -> Result<(), Error> { + self.verify_zebra_prehashed(signature, self.challenge_scalar(signature, msg)) + } + #[allow(non_snake_case)] + pub(crate) fn verify_zebra_prehashed( + &self, + signature: &Signature, + h: Scalar, + ) -> Result<(), Error> { // Generate half-size scalars ρ and τ such that ρ ≡ τh (mod ℓ) // in order to have rho and tau approximately half the size of h // it is possible that we compute ρ ≡ -τh (mod ℓ) @@ -323,7 +330,6 @@ impl VerificationKey { // Compute the multi-scalar multiplication let result = EdwardsPoint::vartime_triple_scalar_mul_basepoint(&tau, &neg_R, &rho, &A, &ts); - // Check if [8] τs B + [8] τ (-R) + [8] ρ (-A) == 0 if result.mul_by_cofactor().is_identity() { Ok(()) } else { @@ -331,26 +337,28 @@ impl VerificationKey { } } - /// Verify a signature with a prehashed `k` value. Note that this is not the - /// same as "prehashing" in RFC8032. + /// Verify a signature with exact `ed25519-dalek`-style byte-level behavior. + /// + /// This recomputes the expected canonical `R` encoding and compares it to the + /// signature's `R` bytes, matching dalek's ordinary verification behavior. + /// + /// Note that exact dalek-compatible behavior is incompatible with the HEEA + /// transformed equation because the transformed check does not preserve the + /// original `R` encoding needed for the byte comparison. #[allow(non_snake_case)] - pub(crate) fn verify_prehashed(&self, signature: &Signature, k: Scalar) -> Result<(), Error> { - // `s_bytes` MUST represent an integer less than the prime `l`. + pub fn verify_dalek(&self, signature: &Signature, msg: &[u8]) -> Result<(), Error> { + self.verify_dalek_prehashed(signature, self.challenge_scalar(signature, msg)) + } + + #[allow(non_snake_case)] + fn verify_dalek_prehashed(&self, signature: &Signature, h: Scalar) -> Result<(), Error> { let s = Option::::from(Scalar::from_canonical_bytes(*signature.s_bytes())) .ok_or(Error::InvalidSignature)?; - // `R_bytes` MUST be an encoding of a point on the twisted Edwards form of Curve25519. - let R = CompressedEdwardsY(*signature.r_bytes()) - .decompress() - .ok_or(Error::InvalidSignature)?; - // We checked the encoding of A_bytes when constructing `self`. - // [8][s]B = [8]R + [8][k]A - // <=> [8]R = [8][s]B - [8][k]A - // <=> 0 = [8](R - ([s]B - [k]A)) - // <=> 0 = [8](R - R') where R' = [s]B - [k]A - let R_prime = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &self.minus_A, &s); + let expected_R = + EdwardsPoint::vartime_double_scalar_mul_basepoint(&h, &self.minus_A, &s).compress(); - if (R - R_prime).mul_by_cofactor().is_identity() { + if expected_R.as_bytes() == signature.r_bytes() { Ok(()) } else { Err(Error::InvalidSignature)