mirror of
https://github.com/saymrwulf/anza-cryptography-source.git
synced 2026-09-04 20:24:04 +00:00
parent
c41adab68f
commit
eed50b4d8d
13 changed files with 119 additions and 85 deletions
|
|
@ -5,8 +5,8 @@ accelerated Ed25519 signature verification via the **HEEA** (Half-Extended Eucli
|
||||||
method and a reduced set of well-tested backends.
|
method and a reduced set of well-tested backends.
|
||||||
|
|
||||||
> Original library READMEs: [README_dalek.md](README_dalek.md) (workspace) ·
|
> Original library READMEs: [README_dalek.md](README_dalek.md) (workspace) ·
|
||||||
> [curve25519/README_dalek.md](curve25519/README_dalek.md) ·
|
> [solana-ed25519/README_dalek.md](solana-ed25519/README_dalek.md) ·
|
||||||
> [ed25519-heea/README_zebra.md](ed25519-heea/README_zebra.md)
|
> [solana-ed25519/README_zebra.md](solana-ed25519/README_zebra.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -14,10 +14,9 @@ method and a reduced set of well-tested backends.
|
||||||
|
|
||||||
| Crate | Description |
|
| Crate | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`curve25519`](./curve25519) | Fork of `curve25519-dalek`. Core elliptic-curve arithmetic over Curve25519, Edwards, Ristretto, and Short-Weierstrass forms, with HEEA scalar decomposition and a narrowed backend set (removed `u32` and constraint device supports). |
|
| [`solana-ed25519`](./solana-ed25519) | Fork of `curve25519-dalek` with ZIP-215-compliant Ed25519 from `ed25519-zebra`, HEEA-accelerated `verify` / `verify_zebra`, and a narrowed backend set (removed `u32` and constraint device supports). |
|
||||||
| [`ed25519-heea`](./ed25519-heea) | Fork of `ed25519-zebra`. ZIP-215-compliant Ed25519 with an added `verify_heea` fast-path that uses HEEA half-size scalars. |
|
|
||||||
| [`curve25519-cuda`](./curve25519-cuda) | GPU-accelerated multi-scalar multiplication (MSM) via CUDA/SPPARK. Falls back to CPU when CUDA is unavailable. |
|
| [`curve25519-cuda`](./curve25519-cuda) | GPU-accelerated multi-scalar multiplication (MSM) via CUDA/SPPARK. Falls back to CPU when CUDA is unavailable. |
|
||||||
| [`curve25519-derive`](./curve25519-derive) | Helper proc-macro crate (`#[unsafe_target_feature]`) inherited from upstream; required to write clean SIMD code. Identical to the one in dalek 0.5.0 |
|
| [`curve25519-derive`](../curve25519-derive) | Helper proc-macro crate (`#[unsafe_target_feature]`) inherited from upstream; required to write clean SIMD code. Identical to the one in dalek 0.5.0 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -30,7 +29,8 @@ HEEA (from the TCHES 2025 paper _"Accelerating EdDSA Signature Verification with
|
||||||
Size Halving"_) transforms this into a 4-point MSM with ~128-bit scalars:
|
Size Halving"_) transforms this into a 4-point MSM with ~128-bit scalars:
|
||||||
|
|
||||||
```
|
```
|
||||||
τs_lo · B + τs_hi · (2¹²⁸·B) = τ·R + ρ·A
|
flip_h = false: τs_lo · B + τs_hi · (2¹²⁸·B) = τ·R + ρ·A
|
||||||
|
flip_h = true: τs_lo · B + τs_hi · (2¹²⁸·B) = τ·R - ρ·A
|
||||||
```
|
```
|
||||||
|
|
||||||
where `ρ` and `τ` are half-size (~127-bit) values derived from `h` via a half-extended
|
where `ρ` and `τ` are half-size (~127-bit) values derived from `h` via a half-extended
|
||||||
|
|
@ -39,10 +39,10 @@ two of the bases (`B` and `2¹²⁸B`) use precomputed tables. In practice this
|
||||||
**~15% faster** verification compared to the standard double-scalar-multiplication path.
|
**~15% faster** verification compared to the standard double-scalar-multiplication path.
|
||||||
|
|
||||||
The algorithm is implemented in:
|
The algorithm is implemented in:
|
||||||
- [`curve25519/src/scalar/heea.rs`](curve25519/src/scalar/heea.rs) – `curve25519_heea_vartime`
|
- [`solana-ed25519/src/scalar/heea.rs`](solana-ed25519/src/scalar/heea.rs) – `curve25519_heea_vartime`
|
||||||
- [`curve25519/src/traits.rs`](curve25519/src/traits.rs) – `HEEADecomposition` trait
|
- [`solana-ed25519/src/traits.rs`](solana-ed25519/src/traits.rs) – `HEEADecomposition` trait
|
||||||
- [`curve25519/src/backend/serial/scalar_mul/vartime_triple_base.rs`](curve25519/src/backend/serial/scalar_mul/vartime_triple_base.rs) – optimised 128+128+256 MSM
|
- [`solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs`](solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs) – optimised 128+128+256 MSM
|
||||||
- [`ed25519-heea/src/verification_key.rs`](ed25519-heea/src/verification_key.rs) – `VerificationKey::verify_heea`
|
- [`solana-ed25519/src/ed_sigs/verification_key.rs`](solana-ed25519/src/ed_sigs/verification_key.rs) – `VerificationKey::verify` / `VerificationKey::verify_zebra`
|
||||||
|
|
||||||
### Reduced Backend Set
|
### Reduced Backend Set
|
||||||
|
|
||||||
|
|
@ -65,17 +65,13 @@ maintenance surface. If you need them, use upstream `curve25519-dalek` directly.
|
||||||
Add the relevant crate to `Cargo.toml`:
|
Add the relevant crate to `Cargo.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Core curve arithmetic with HEEA
|
curve25519 = { package = "solana-ed25519", git = "https://github.com/anza-xyz/cryptography" }
|
||||||
curve25519-sol = { git = "https://github.com/zz-sol/ed25519-sol" }
|
|
||||||
|
|
||||||
# Ed25519 signatures with fast HEEA verification
|
|
||||||
ed25519-heea = { git = "https://github.com/zz-sol/ed25519-sol" }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standard Ed25519 verification
|
### Standard Ed25519 verification
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use ed25519_heea::{SigningKey, VerificationKey};
|
use curve25519::ed_sigs::{SigningKey, VerificationKey};
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
|
|
||||||
let msg = b"hello world";
|
let msg = b"hello world";
|
||||||
|
|
@ -87,11 +83,11 @@ let vk = VerificationKey::from(&sk);
|
||||||
vk.verify(&sig, msg).expect("valid signature");
|
vk.verify(&sig, msg).expect("valid signature");
|
||||||
```
|
```
|
||||||
|
|
||||||
### HEEA-accelerated verification
|
### Explicit HEEA-accelerated verification
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Fast path: ~15% faster via half-size scalars (same result)
|
// Same ZIP-215 result as verify(), using the HEEA path explicitly.
|
||||||
vk.verify_heea(&sig, msg).expect("valid signature");
|
vk.verify_zebra(&sig, msg).expect("valid signature");
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -106,8 +102,8 @@ cargo build --release
|
||||||
RUSTFLAGS='-C target-feature=+avx2' cargo build --release
|
RUSTFLAGS='-C target-feature=+avx2' cargo build --release
|
||||||
|
|
||||||
# Run benchmarks
|
# Run benchmarks
|
||||||
cargo bench --features "rand_core" -p curve25519
|
cargo bench --features "rand_core" -p solana-ed25519
|
||||||
cargo bench -p ed25519-heea
|
cargo bench -p curve25519-cuda
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ A new `HEEADecomposition` trait and implementation have been added in:
|
||||||
Given a 256-bit hash scalar `h`, `heea_decompose` returns `(ρ, τ, flip_h)` such that:
|
Given a 256-bit hash scalar `h`, `heea_decompose` returns `(ρ, τ, flip_h)` such that:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ρ ≡ ±τ·h (mod ℓ) // ρ and τ are both ≤ 128 bits
|
flip_h = false: ρ ≡ τ·h (mod ℓ)
|
||||||
|
flip_h = true: ρ ≡ -τ·h (mod ℓ)
|
||||||
```
|
```
|
||||||
|
|
||||||
This allows verification of `sB = R + hA` to be rewritten as a 4-point MSM over ~128-bit
|
This allows verification of `sB = R + hA` to be rewritten as a 4-point MSM over ~128-bit
|
||||||
|
|
@ -58,9 +59,9 @@ verification.
|
||||||
|
|
||||||
### `verify_zebra`: fast-path signature verification
|
### `verify_zebra`: fast-path signature verification
|
||||||
|
|
||||||
A new method `VerificationKey::verify_zebra` sits alongside the existing `verify`.
|
`VerificationKey::verify_zebra` is the HEEA implementation used by the default
|
||||||
Both accept the same arguments and produce identical results — `verify_zebra` is a
|
`VerificationKey::verify` method. Both accept the same arguments and produce identical
|
||||||
**drop-in accelerated replacement** for `verify`.
|
ZIP-215 results.
|
||||||
|
|
||||||
The HEEA method (TCHES 2025) transforms the standard 2-point MSM:
|
The HEEA method (TCHES 2025) transforms the standard 2-point MSM:
|
||||||
|
|
||||||
|
|
@ -71,10 +72,12 @@ The HEEA method (TCHES 2025) transforms the standard 2-point MSM:
|
||||||
into a 4-point MSM over half-size (~128-bit) scalars:
|
into a 4-point MSM over half-size (~128-bit) scalars:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
τs_lo·B + τs_hi·(2¹²⁸·B) = τ·R + ρ·A (HEEA)
|
flip_h = false: τs_lo·B + τs_hi·(2¹²⁸·B) = τ·R + ρ·A
|
||||||
|
flip_h = true: τs_lo·B + τs_hi·(2¹²⁸·B) = τ·R - ρ·A
|
||||||
```
|
```
|
||||||
|
|
||||||
where `ρ ≡ ±τ·h (mod ℓ)` and `τs = τs_hi·2¹²⁸ + τs_lo`. All four scalars are ≤128 bits
|
where `ρ ≡ τ·h (mod ℓ)` when `flip_h` is false, `ρ ≡ -τ·h (mod ℓ)` when
|
||||||
|
`flip_h` is true, and `τs = τs_hi·2¹²⁸ + τs_lo`. All four scalars are ≤128 bits
|
||||||
and the two basepoints (`B` and `2¹²⁸B`) use precomputed lookup tables, giving approximately
|
and the two basepoints (`B` and `2¹²⁸B`) use precomputed lookup tables, giving approximately
|
||||||
**~15% faster** verification compared to the standard path.
|
**~15% faster** verification compared to the standard path.
|
||||||
|
|
||||||
|
|
@ -137,7 +140,8 @@ let h = Scalar::from_hash(Sha512::new().chain_update(b"some message"));
|
||||||
|
|
||||||
// Decompose into two ~128-bit scalars
|
// Decompose into two ~128-bit scalars
|
||||||
let (rho, tau, flip_h) = h.heea_decompose();
|
let (rho, tau, flip_h) = h.heea_decompose();
|
||||||
// rho ≡ ±tau·h (mod ℓ)
|
// flip_h == false: rho ≡ tau·h (mod ℓ)
|
||||||
|
// flip_h == true: rho ≡ -tau·h (mod ℓ)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -146,10 +150,10 @@ let (rho, tau, flip_h) = h.heea_decompose();
|
||||||
|
|
||||||
| Feature | Default? | Description |
|
| Feature | Default? | Description |
|
||||||
|---|:---:|---|
|
|---|:---:|---|
|
||||||
| `alloc` | ✓ | Multiscalar multiplication, batch inversion, batch compress, batch Ed25519 verification. |
|
| `alloc` | ✓ | Multiscalar multiplication, batch inversion, batch compress, and the Ed25519 batch module. |
|
||||||
| `zeroize` | ✓ | `Zeroize` for all scalar and point types. |
|
| `zeroize` | ✓ | `Zeroize` for all scalar and point types. |
|
||||||
| `precomputed-tables` | ✓ | Precomputed basepoint tables (~400 KB, ~4× faster basepoint mul). |
|
| `precomputed-tables` | ✓ | Precomputed basepoint tables (~400 KB, ~4× faster basepoint mul). |
|
||||||
| `rand_core` | ✓ | `Scalar::random`, `RistrettoPoint::random`, `SigningKey::new`. |
|
| `rand_core` | ✓ | `Scalar::random`, `RistrettoPoint::random`, `SigningKey::new`, and randomized batch verification. |
|
||||||
| `digest` | ✓ | Hash-to-curve, `Scalar::from_hash`, and Ed25519 hashing. |
|
| `digest` | ✓ | Hash-to-curve, `Scalar::from_hash`, and Ed25519 hashing. |
|
||||||
| `std` | | Enables `std::error::Error` impl on `ed_sigs::Error`. |
|
| `std` | | Enables `std::error::Error` impl on `ed_sigs::Error`. |
|
||||||
| `serde` | | Serialization for all point, scalar, and key types. |
|
| `serde` | | Serialization for all point, scalar, and key types. |
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
> This is an upstream `ed25519-zebra` README snapshot retained for provenance.
|
||||||
|
> It describes the upstream crate, not this fork's current `curve25519::ed_sigs`
|
||||||
|
> API. See [README.md](README.md) for current `solana-ed25519` usage.
|
||||||
|
|
||||||
[](https://github.com/ZcashFoundation/ed25519-zebra/actions/workflows/main.yml?query=branch%3Amain)
|
[](https://github.com/ZcashFoundation/ed25519-zebra/actions/workflows/main.yml?query=branch%3Amain)
|
||||||
[](https://deps.rs/repo/github/ZcashFoundation/ed25519-zebra)
|
[](https://deps.rs/repo/github/ZcashFoundation/ed25519-zebra)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,13 @@ use crate::window::NafLookupTable5;
|
||||||
/// # Implementation
|
/// # Implementation
|
||||||
///
|
///
|
||||||
/// - For \\(A_1\\) and \\(A_2\\): NAF with window width 5 (8 precomputed points each)
|
/// - For \\(A_1\\) and \\(A_2\\): NAF with window width 5 (8 precomputed points each)
|
||||||
/// - For \\(B\\): NAF with window width 8 when precomputed tables available (64 points)
|
/// - For \\(B\\): NAF with window width 5. When precomputed tables are enabled,
|
||||||
/// - For \\(B'\\): NAF with window width 5 (could be optimized with precomputed table)
|
/// these width-5 digits are selected from the larger width-8 basepoint table.
|
||||||
|
/// - For \\(B'\\): NAF with window width 5.
|
||||||
|
///
|
||||||
|
/// The vector backend uses width 8 for \\(b_{lo}\\) when precomputed tables are
|
||||||
|
/// enabled. This serial backend keeps width 5 for \\(b_{lo}\\) as a
|
||||||
|
/// backend-specific performance tradeoff.
|
||||||
///
|
///
|
||||||
/// The algorithm shares doublings across all four scalar multiplications, processing
|
/// The algorithm shares doublings across all four scalar multiplications, processing
|
||||||
/// only 128 bits instead of 256, providing approximately 2x speedup over the naive approach.
|
/// only 128 bits instead of 256, providing approximately 2x speedup over the naive approach.
|
||||||
|
|
@ -70,7 +75,11 @@ pub fn mul_128_128_256(
|
||||||
let b_lo = Scalar::from_canonical_bytes(b_lo_bytes).unwrap();
|
let b_lo = Scalar::from_canonical_bytes(b_lo_bytes).unwrap();
|
||||||
let b_hi = Scalar::from_canonical_bytes(b_hi_bytes).unwrap();
|
let b_hi = Scalar::from_canonical_bytes(b_hi_bytes).unwrap();
|
||||||
|
|
||||||
// Compute NAF representations (all scalars are now ~128 bits)
|
// Compute NAF representations (all scalars are now ~128 bits).
|
||||||
|
//
|
||||||
|
// The serial backend keeps b_lo at width 5 even when table_B is the larger
|
||||||
|
// precomputed width-8 basepoint table. The vector backend uses width 8 in
|
||||||
|
// that configuration.
|
||||||
let a1_naf = a1.non_adjacent_form(5);
|
let a1_naf = a1.non_adjacent_form(5);
|
||||||
let a2_naf = a2.non_adjacent_form(5);
|
let a2_naf = a2.non_adjacent_form(5);
|
||||||
let b_lo_naf = b_lo.non_adjacent_form(5);
|
let b_lo_naf = b_lo.non_adjacent_form(5);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ pub mod spec {
|
||||||
/// - For \\(B\\): NAF with window width 8 when precomputed tables available (64 points), otherwise width 5
|
/// - For \\(B\\): NAF with window width 8 when precomputed tables available (64 points), otherwise width 5
|
||||||
/// - For \\(B'\\): NAF with window width 5
|
/// - For \\(B'\\): NAF with window width 5
|
||||||
///
|
///
|
||||||
|
/// The serial backend keeps \\(b_{lo}\\) at width 5 even when precomputed
|
||||||
|
/// tables are enabled. This vector backend uses width 8 in that
|
||||||
|
/// configuration as a backend-specific performance tradeoff.
|
||||||
|
///
|
||||||
/// The algorithm shares doublings across all four scalar multiplications, processing
|
/// The algorithm shares doublings across all four scalar multiplications, processing
|
||||||
/// only 128 bits instead of 256, providing approximately 2x speedup over the naive approach.
|
/// only 128 bits instead of 256, providing approximately 2x speedup over the naive approach.
|
||||||
///
|
///
|
||||||
|
|
@ -88,6 +92,8 @@ pub mod spec {
|
||||||
let a1_naf = a1.non_adjacent_form(5);
|
let a1_naf = a1.non_adjacent_form(5);
|
||||||
let a2_naf = a2.non_adjacent_form(5);
|
let a2_naf = a2.non_adjacent_form(5);
|
||||||
|
|
||||||
|
// With precomputed tables, the vector backend uses the larger width-8
|
||||||
|
// basepoint table for b_lo. The serial backend keeps b_lo at width 5.
|
||||||
#[cfg(feature = "precomputed-tables")]
|
#[cfg(feature = "precomputed-tables")]
|
||||||
let b_lo_naf = b_lo.non_adjacent_form(8);
|
let b_lo_naf = b_lo.non_adjacent_form(8);
|
||||||
#[cfg(not(feature = "precomputed-tables"))]
|
#[cfg(not(feature = "precomputed-tables"))]
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,9 @@ impl Item {
|
||||||
///
|
///
|
||||||
/// This is useful (in combination with `Item::clone`) for implementing fallback
|
/// This is useful (in combination with `Item::clone`) for implementing fallback
|
||||||
/// logic when batch verification fails. In contrast to
|
/// logic when batch verification fails. In contrast to
|
||||||
/// [`VerificationKey::verify`](crate::VerificationKey::verify), which requires
|
/// [`VerificationKey::verify`](crate::ed_sigs::VerificationKey::verify),
|
||||||
/// borrowing the message data, the `Item` type is unlinked from the lifetime of
|
/// which requires borrowing the message data, the `Item` type is unlinked
|
||||||
/// the message.
|
/// from the lifetime of the message.
|
||||||
pub fn verify_single(self) -> Result<(), Error> {
|
pub fn verify_single(self) -> Result<(), Error> {
|
||||||
VerificationKey::try_from(self.vk_bytes)
|
VerificationKey::try_from(self.vk_bytes)
|
||||||
.and_then(|vk| vk.verify_zebra_prehashed(&self.sig, self.k))
|
.and_then(|vk| vk.verify_zebra_prehashed(&self.sig, self.k))
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const PKCS8_V1_PEM: &str = include_str!("../../../examples/pkcs8-v1.pem");
|
||||||
#[cfg(feature = "pkcs8")]
|
#[cfg(feature = "pkcs8")]
|
||||||
const PKCS8_V2_DER: &[u8] = include_bytes!("../../../examples/pkcs8-v2.der");
|
const PKCS8_V2_DER: &[u8] = include_bytes!("../../../examples/pkcs8-v2.der");
|
||||||
|
|
||||||
/// Ed25519 PKCS#8 v1 private key encoded as PEM.
|
/// Ed25519 PKCS#8 v2 private key + public key encoded as PEM.
|
||||||
#[cfg(feature = "pem")]
|
#[cfg(feature = "pem")]
|
||||||
const PKCS8_V2_PEM: &str = include_str!("../../../examples/pkcs8-v2.pem");
|
const PKCS8_V2_PEM: &str = include_str!("../../../examples/pkcs8-v2.pem");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const PKCS8_V1_PEM: &str = include_str!("../../../examples/pkcs8-v1.pem");
|
||||||
#[cfg(feature = "pkcs8")]
|
#[cfg(feature = "pkcs8")]
|
||||||
const PKCS8_V2_DER: &[u8] = include_bytes!("../../../examples/pkcs8-v2.der");
|
const PKCS8_V2_DER: &[u8] = include_bytes!("../../../examples/pkcs8-v2.der");
|
||||||
|
|
||||||
/// Ed25519 PKCS#8 v1 private key encoded as PEM.
|
/// Ed25519 PKCS#8 v2 private key + public key encoded as PEM.
|
||||||
#[cfg(feature = "pem")]
|
#[cfg(feature = "pem")]
|
||||||
const PKCS8_V2_PEM: &str = include_str!("../../../examples/pkcs8-v2.pem");
|
const PKCS8_V2_PEM: &str = include_str!("../../../examples/pkcs8-v2.pem");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use core::convert::TryFrom;
|
||||||
use ed25519::Signature;
|
use ed25519::Signature;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_verify_heea_invalid_signature() {
|
fn test_verify_zebra_invalid_signature() {
|
||||||
let signing_key = SigningKey::from([1u8; 32]);
|
let signing_key = SigningKey::from([1u8; 32]);
|
||||||
let verification_key = VerificationKey::from(&signing_key);
|
let verification_key = VerificationKey::from(&signing_key);
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ fn test_verify_heea_invalid_signature() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_verify_heea_multiple_signatures() {
|
fn test_verify_zebra_multiple_signatures() {
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
let mut seed = [0u8; 32];
|
let mut seed = [0u8; 32];
|
||||||
seed[0] = i;
|
seed[0] = i;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
// -*- mode: rust; -*-
|
// -*- mode: rust; -*-
|
||||||
//
|
//
|
||||||
// This file is part of ed25519-heea, a fork of ed25519-zebra.
|
// This file is part of solana-ed25519's ed_sigs module, forked from ed25519-zebra.
|
||||||
// Original ed25519-zebra code: Copyright (c) Zcash Foundation contributors
|
// Original ed25519-zebra code: Copyright (c) Zcash Foundation contributors
|
||||||
// Modifications for HEEA: Copyright (c) 2025 curve25519-sol contributors
|
// Modifications for HEEA: Copyright (c) 2025 curve25519-sol contributors
|
||||||
// See LICENSE-APACHE and LICENSE-MIT for licensing information.
|
// See LICENSE-APACHE and LICENSE-MIT for licensing information.
|
||||||
//
|
//
|
||||||
// Modifications from ed25519-zebra:
|
// Modifications from ed25519-zebra:
|
||||||
// - Added `verify_heea`, an accelerated verification path using the HEEA
|
// - Added `verify_zebra`, an accelerated verification path using the HEEA
|
||||||
// scalar decomposition from curve25519-sol's `HEEADecomposition` trait.
|
// scalar decomposition from curve25519-sol's `HEEADecomposition` trait.
|
||||||
// See "Accelerating EdDSA Signature Verification with Faster Scalar Size
|
// See "Accelerating EdDSA Signature Verification with Faster Scalar Size
|
||||||
// Halving" (TCHES 2025) for the algorithm.
|
// Halving" (TCHES 2025) for the algorithm.
|
||||||
// - `verify` and all ZIP-215 consensus logic are unchanged from ed25519-zebra.
|
// - `verify` dispatches to `verify_zebra`, preserving ZIP-215 semantics.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
edwards::{CompressedEdwardsY, EdwardsPoint},
|
edwards::{CompressedEdwardsY, EdwardsPoint},
|
||||||
|
|
@ -105,12 +105,12 @@ const LEGACY_EXCLUDED_R_ENCODINGS: [[u8; 32]; 11] = [
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
/// A refinement type for `[u8; 32]` indicating that the bytes represent an
|
/// A container for the 32-byte encoded form of an Ed25519 verification key.
|
||||||
/// encoding of an Ed25519 verification key.
|
|
||||||
///
|
///
|
||||||
/// This is useful for representing an encoded verification key, while the
|
/// This type only checks or carries the byte length. It does not prove that the
|
||||||
/// [`VerificationKey`] type in this library caches other decoded state used in
|
/// bytes decompress to a valid Ed25519 verification key. Convert it to
|
||||||
/// signature verification.
|
/// [`VerificationKey`] to validate the encoded point and cache decoded state
|
||||||
|
/// used in signature verification.
|
||||||
///
|
///
|
||||||
/// A `VerificationKeyBytes` can be used to verify a single signature using the
|
/// A `VerificationKeyBytes` can be used to verify a single signature using the
|
||||||
/// following idiom:
|
/// following idiom:
|
||||||
|
|
@ -204,7 +204,7 @@ fn verification_key_bytes_from_spki(
|
||||||
///
|
///
|
||||||
/// This type holds decompressed state used in signature verification; if the
|
/// This type holds decompressed state used in signature verification; if the
|
||||||
/// verification key may not be used immediately, it is probably better to use
|
/// verification key may not be used immediately, it is probably better to use
|
||||||
/// [`VerificationKeyBytes`], which is a refinement type for `[u8; 32]`.
|
/// [`VerificationKeyBytes`], which stores only the length-checked encoded bytes.
|
||||||
///
|
///
|
||||||
/// ## Zcash-specific consensus properties
|
/// ## Zcash-specific consensus properties
|
||||||
///
|
///
|
||||||
|
|
@ -366,17 +366,17 @@ impl VerificationKey {
|
||||||
/// This implements the algorithm from "Accelerating EdDSA Signature Verification
|
/// This implements the algorithm from "Accelerating EdDSA Signature Verification
|
||||||
/// with Faster Scalar Size Halving" (TCHES 2025).
|
/// with Faster Scalar Size Halving" (TCHES 2025).
|
||||||
///
|
///
|
||||||
/// The standard verification equation sB = R + hA is transformed to:
|
/// The decomposition returns ρ and τ such that either ρ ≡ τh (mod ℓ) or
|
||||||
/// τsB = τR + ρA where ρ ≡ τh (mod ℓ)
|
/// ρ ≡ -τh (mod ℓ). The standard verification equation sB = R + hA is
|
||||||
|
/// multiplied by τ and the sign of A is selected according to `flip_h`.
|
||||||
///
|
///
|
||||||
/// Both ρ and τ are approximately half the size of h.
|
/// Both ρ and τ are approximately half the size of h.
|
||||||
///
|
///
|
||||||
/// We then decompose τs into two 128-bit scalars:
|
/// We then decompose τs into two 128-bit scalars:
|
||||||
/// τs = τs_hi * 2^128 + τs_lo
|
/// τs = τs_hi * 2^128 + τs_lo
|
||||||
///
|
///
|
||||||
/// The verification equation becomes:
|
/// The resulting equation can be checked with a 4-variable MSM with
|
||||||
/// τs_lo B + τs_hi (2^128 B) = τR + ρA
|
/// half-size scalars.
|
||||||
/// which can be done via 4-variable MSM with half-size scalars.
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn verify_zebra(&self, signature: &Signature, msg: &[u8]) -> Result<(), Error> {
|
pub fn verify_zebra(&self, signature: &Signature, msg: &[u8]) -> Result<(), Error> {
|
||||||
self.verify_zebra_prehashed(signature, self.challenge_scalar(signature, msg))
|
self.verify_zebra_prehashed(signature, self.challenge_scalar(signature, msg))
|
||||||
|
|
@ -388,12 +388,9 @@ impl VerificationKey {
|
||||||
signature: &Signature,
|
signature: &Signature,
|
||||||
h: Scalar,
|
h: Scalar,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
// Generate half-size scalars ρ and τ such that ρ ≡ τh (mod ℓ)
|
// Generate half-size scalars ρ and τ. If flip_h is false, then
|
||||||
// in order to have rho and tau approximately half the size of h
|
// ρ ≡ τh (mod ℓ). If flip_h is true, then ρ ≡ -τh (mod ℓ), so the
|
||||||
// it is possible that we compute ρ ≡ -τh (mod ℓ)
|
// sign of A is flipped below.
|
||||||
// this is indicated by `flip_h` flag being true,
|
|
||||||
// in which case we will need to negate A later
|
|
||||||
// let (rho, tau, flip_h) = crate::heea::generate_half_size_scalars(&h);
|
|
||||||
let (rho, tau, flip_h) = h.heea_decompose();
|
let (rho, tau, flip_h) = h.heea_decompose();
|
||||||
|
|
||||||
// Extract s from the signature
|
// Extract s from the signature
|
||||||
|
|
@ -405,11 +402,11 @@ impl VerificationKey {
|
||||||
.decompress()
|
.decompress()
|
||||||
.ok_or(Error::InvalidSignature)?;
|
.ok_or(Error::InvalidSignature)?;
|
||||||
|
|
||||||
// Standard verification checks: sB = R + hA
|
// Standard verification checks: sB = R + hA.
|
||||||
// Transformed verification: -τsB + τR + ρA == 0
|
|
||||||
//
|
//
|
||||||
// We verify:
|
// We verify:
|
||||||
// [8] τs B + [8] τ (-R) + [8] ρ (-A) == 0
|
// [8] τs B + [8] τ (-R) + [8] ρ A_term == 0
|
||||||
|
// where A_term is -A when ρ ≡ τh and A when ρ ≡ -τh.
|
||||||
|
|
||||||
// Compute τs
|
// Compute τs
|
||||||
let ts = tau * s;
|
let ts = tau * s;
|
||||||
|
|
@ -424,12 +421,18 @@ impl VerificationKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify a signature with exact `ed25519-dalek`-style byte-level behavior.
|
/// Verify a signature with dalek-style canonical-`R` byte comparison.
|
||||||
///
|
///
|
||||||
/// This recomputes the expected canonical `R` encoding and compares it to the
|
/// This recomputes the expected canonical `R` encoding and compares it to the
|
||||||
/// signature's `R` bytes, matching dalek's ordinary verification behavior.
|
/// signature's `R` bytes.
|
||||||
///
|
///
|
||||||
/// Note that exact dalek-compatible behavior is incompatible with the HEEA
|
/// This helper also preserves this crate's legacy compatibility filters: it
|
||||||
|
/// rejects an all-zero encoded public key and the known legacy-excluded `R`
|
||||||
|
/// encodings before running the canonical-`R` comparison. Because of those
|
||||||
|
/// extra checks, this is not a byte-for-byte clone of every `ed25519-dalek`
|
||||||
|
/// release.
|
||||||
|
///
|
||||||
|
/// Note that dalek-style canonical-`R` comparison is incompatible with the HEEA
|
||||||
/// transformed equation because the transformed check does not preserve the
|
/// transformed equation because the transformed check does not preserve the
|
||||||
/// original `R` encoding needed for the byte comparison.
|
/// original `R` encoding needed for the byte comparison.
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|
|
||||||
|
|
@ -537,12 +537,15 @@ impl Zeroize for Scalar {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HEEADecomposition for Scalar {
|
impl HEEADecomposition for Scalar {
|
||||||
/// Generate half-size scalars (rho, tau) for a given hash value h
|
/// Generate approximately half-size non-negative scalars `(rho, tau)` for a
|
||||||
|
/// given challenge scalar `h`.
|
||||||
///
|
///
|
||||||
/// This function takes the hash value h from the signature verification equation
|
/// The signed HEEA output satisfies `rho_i = tau_i * h (mod ell)`. This
|
||||||
/// and produces two half-size scalars rho and tau such that rho = tau*h (mod ell).
|
/// method returns the absolute values of `rho_i` and `tau_i`, plus `flip_h`
|
||||||
|
/// to record whether exactly one of the signed values was negative.
|
||||||
///
|
///
|
||||||
/// And a flag indicating if rho is negative in its signed representation.
|
/// If `flip_h` is false, then `rho = tau * h (mod ell)`. If `flip_h` is
|
||||||
|
/// true, then `rho = -tau * h (mod ell)`.
|
||||||
fn heea_decompose(&self) -> (Scalar, Scalar, bool) {
|
fn heea_decompose(&self) -> (Scalar, Scalar, bool) {
|
||||||
// Convert h to I256
|
// Convert h to I256
|
||||||
let v = self.into();
|
let v = self.into();
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@
|
||||||
//! This module implements Algorithm 4 (hEEA_approx_q) from the paper, which generates
|
//! This module implements Algorithm 4 (hEEA_approx_q) from the paper, which generates
|
||||||
//! half-size scalars for faster EdDSA verification.
|
//! half-size scalars for faster EdDSA verification.
|
||||||
//!
|
//!
|
||||||
//! For verification sB = R + hA, we find rho, tau such that rho = tau*h (mod ell)
|
//! For verification `sB = R + hA`, this module finds signed values `rho` and
|
||||||
|
//! `tau` such that `rho == tau * h (mod ell)`. The public
|
||||||
|
//! `HEEADecomposition` implementation converts them to non-negative scalars and
|
||||||
|
//! reports whether the sign of `h` must be flipped in the transformed equation.
|
||||||
use core::ops::Neg;
|
use core::ops::Neg;
|
||||||
|
|
||||||
use crate::constants;
|
use crate::constants;
|
||||||
|
|
@ -34,9 +37,11 @@ pub(crate) struct I256 {
|
||||||
|
|
||||||
pub(crate) const HEEA_MAX_INDEX: usize = 129;
|
pub(crate) const HEEA_MAX_INDEX: usize = 129;
|
||||||
|
|
||||||
/// Implement curve25519_hEEA_vartime algorithm
|
/// Implement the curve25519 hEEA variable-time algorithm.
|
||||||
/// Returns (rho, tau) such that rho ≡ tau * v (mod L)
|
///
|
||||||
/// where L is the Ed25519 group order (2^252 + 27742317777372353535851937790883648493)
|
/// Returns signed `(rho, tau)` such that `rho == tau * v (mod L)`, where `L` is
|
||||||
|
/// the Ed25519 group order
|
||||||
|
/// `2^252 + 27742317777372353535851937790883648493`.
|
||||||
pub(crate) fn curve25519_heea_vartime(v: I256) -> (I256, I256) {
|
pub(crate) fn curve25519_heea_vartime(v: I256) -> (I256, I256) {
|
||||||
// Get L from the existing BASEPOINT_ORDER constant
|
// Get L from the existing BASEPOINT_ORDER constant
|
||||||
let mut r0: I256 = (&constants::BASEPOINT_ORDER).into();
|
let mut r0: I256 = (&constants::BASEPOINT_ORDER).into();
|
||||||
|
|
@ -427,7 +432,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(all(feature = "rand_core", feature = "digest"))]
|
#[cfg(all(feature = "rand_core", feature = "digest"))]
|
||||||
fn test_generate_half_size_scalars() {
|
fn test_heea_decompose_half_size_scalars() {
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use sha2::{Digest, Sha512};
|
use sha2::{Digest, Sha512};
|
||||||
|
|
||||||
|
|
@ -453,10 +458,13 @@ mod tests {
|
||||||
// Now convert to Scalars and verify the equation
|
// Now convert to Scalars and verify the equation
|
||||||
let (rho, tau, flip) = h.heea_decompose();
|
let (rho, tau, flip) = h.heea_decompose();
|
||||||
|
|
||||||
// Verify that rho = tau * h (mod ell)
|
// Verify the non-negative scalar relation reported by flip.
|
||||||
let computed_rho = tau * h;
|
let computed_rho = tau * h;
|
||||||
let computed_rho = if flip { -computed_rho } else { computed_rho };
|
let computed_rho = if flip { -computed_rho } else { computed_rho };
|
||||||
assert_eq!(rho, computed_rho, "rho should equal tau * h");
|
assert_eq!(
|
||||||
|
rho, computed_rho,
|
||||||
|
"rho should equal tau * h with flip applied"
|
||||||
|
);
|
||||||
|
|
||||||
// Check that they are non-zero
|
// Check that they are non-zero
|
||||||
assert_ne!(rho, Scalar::ZERO, "rho should be non-zero");
|
assert_ne!(rho, Scalar::ZERO, "rho should be non-zero");
|
||||||
|
|
|
||||||
|
|
@ -422,18 +422,19 @@ pub trait VartimePrecomputedMultiscalarMul: Sized {
|
||||||
|
|
||||||
/// A trait for decomposing scalars for the HEEA method.
|
/// A trait for decomposing scalars for the HEEA method.
|
||||||
///
|
///
|
||||||
/// The HEEA method is a technique for accelerating scalar multiplication
|
/// The HEEA method is used by EdDSA verification to replace multiplication by
|
||||||
/// on elliptic curves by decomposing a scalar into two smaller scalars.
|
/// a full-width challenge scalar with multiplications by two shorter scalars.
|
||||||
/// The smaller scalars then can be multiplied with multi-scalar multiplication
|
/// For a challenge scalar `h`, the decomposition returns non-negative scalars
|
||||||
/// which is in general faster than single point full multiplication.
|
/// `rho` and `tau` that satisfy either `rho == tau * h (mod ell)` or
|
||||||
|
/// `rho == -tau * h (mod ell)`.
|
||||||
///
|
///
|
||||||
/// This trait provides a method for performing this decomposition.
|
/// This trait provides a method for performing this decomposition.
|
||||||
pub trait HEEADecomposition {
|
pub trait HEEADecomposition {
|
||||||
/// Decomposes a scalar `k` into two half-size scalars `k1` and `k2`
|
/// Decomposes a scalar `h` into two approximately half-size scalars.
|
||||||
/// such that `k = k1 + k2 * h (mod ell)`, where `h` is a fixed constant.
|
|
||||||
///
|
///
|
||||||
/// Returns the tuple `(k1, k2, flip_h)`, where `flip_h` is a boolean
|
/// Returns `(rho, tau, flip_h)`. If `flip_h` is false, then
|
||||||
/// indicating whether the sign of `h` was flipped during the decomposition.
|
/// `rho == tau * h (mod ell)`. If `flip_h` is true, then
|
||||||
|
/// `rho == -tau * h (mod ell)`.
|
||||||
fn heea_decompose(&self) -> (Scalar, Scalar, bool);
|
fn heea_decompose(&self) -> (Scalar, Scalar, bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue