Skip to content

Commit 63e47ae

Browse files
author
zz-sol
committed
impl verification algorithm for heea
1 parent a609603 commit 63e47ae

File tree

5 files changed

+230
-2
lines changed

5 files changed

+230
-2
lines changed

curve25519-dalek/benches/dalek_benchmarks.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,44 @@ mod scalar_benches {
442442
}
443443
}
444444

445+
mod heea_benches {
446+
use curve25519_dalek::traits::HEEADecomposition;
447+
448+
use super::*;
449+
450+
fn heea_generate_half_size_scalars<M: Measurement>(c: &mut BenchmarkGroup<M>) {
451+
let mut rng = rng();
452+
let random_scalars: Vec<Scalar> = (0..100)
453+
.map(|_| {
454+
let mut random_bytes = [0u8; 32];
455+
rng.fill_bytes(&mut random_bytes);
456+
Scalar::from_bytes_mod_order(random_bytes)
457+
})
458+
.collect();
459+
460+
c.bench_function("Generate half-size scalars (hEEA)", |bench| {
461+
let mut i = 0;
462+
bench.iter(|| {
463+
let h = &random_scalars[i % random_scalars.len()];
464+
i += 1;
465+
h.heea_decompose()
466+
})
467+
});
468+
}
469+
470+
pub(crate) fn heea_benches() {
471+
let mut c = Criterion::default();
472+
let mut g = c.benchmark_group("heea benches");
473+
474+
heea_generate_half_size_scalars(&mut g);
475+
}
476+
}
477+
445478
criterion_main!(
446479
scalar_benches::scalar_benches,
447480
montgomery_benches::montgomery_benches,
448481
ristretto_benches::ristretto_benches,
449482
edwards_benches::edwards_benches,
450483
multiscalar_benches::multiscalar_benches,
484+
heea_benches::heea_benches,
451485
);

curve25519-dalek/src/scalar.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2210,4 +2210,4 @@ pub(crate) mod test {
22102210
assert_eq!(a * c.reduce(), reduced_mul_ac);
22112211
}
22122212
}
2213-
}
2213+
}

curve25519-dalek/src/scalar/heea.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,4 @@ mod tests {
182182
);
183183
assert_eq!(tau, -I256::ONE, "tau should be -1 for 2^252");
184184
}
185-
}
185+
}

ed25519-dalek/src/verifying.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use curve25519_dalek::{
1717
edwards::{CompressedEdwardsY, EdwardsPoint},
1818
montgomery::MontgomeryPoint,
1919
scalar::Scalar,
20+
traits::IsIdentity,
2021
};
2122

2223
use ed25519::signature::{MultipartVerifier, Verifier};
@@ -381,6 +382,82 @@ impl VerifyingKey {
381382
}
382383
}
383384

385+
/// Verify a signature using the heea half-size scalar optimization.
386+
///
387+
/// This implements the algorithm from "Accelerating EdDSA Signature Verification
388+
/// with Faster Scalar Size Halving" (TCHES 2025).
389+
///
390+
/// The standard verification equation sB = R + hA is transformed to:
391+
/// τsB = τR + ρA where ρ ≡ τh (mod ℓ)
392+
///
393+
/// Both ρ and τ are approximately half the size of h.
394+
///
395+
/// We then decompose τs into two 128-bit scalars:
396+
/// τs = τs_hi * 2^128 + τs_lo
397+
///
398+
/// The verification equation becomes:
399+
/// τs_lo B + τs_hi (2^128 B) = τR + ρA
400+
/// which can be done via 4-variable MSM with half-size scalars.
401+
#[allow(non_snake_case)]
402+
pub fn verify_heea(
403+
&self,
404+
message: &[u8],
405+
signature: &ed25519::Signature,
406+
) -> Result<(), SignatureError> {
407+
use curve25519_dalek::traits::HEEADecomposition;
408+
409+
let signature = InternalSignature::try_from(signature)?;
410+
411+
let signature_R = signature
412+
.R
413+
.decompress()
414+
.ok_or_else(|| SignatureError::from(InternalError::Verify))?;
415+
416+
// Logical OR is fine here as we're not trying to be constant time.
417+
if signature_R.is_small_order() || self.point.is_small_order() {
418+
return Err(InternalError::Verify.into());
419+
}
420+
421+
// Compute h = H(R || A || M)
422+
let mut h = Sha512::new();
423+
Digest::update(&mut h, signature.R.as_bytes());
424+
Digest::update(&mut h, self.compressed.as_bytes());
425+
Digest::update(&mut h, message);
426+
let h = Scalar::from_hash(h);
427+
428+
// Generate half-size scalars ρ and τ such that ρ ≡ τh (mod ℓ)
429+
// in order to have rho and tau approximately half the size of h
430+
// it is possible that we compute ρ ≡ -τh (mod ℓ)
431+
// this is indicated by `flip_h` flag being true,
432+
// in which case we will need to negate A later
433+
let (rho, tau, flip_h) = h.heea_decompose();
434+
435+
// Standard verification checks: sB = R + hA
436+
// Transformed verification: -τsB + τR + ρA == 0
437+
let s = signature.s;
438+
439+
// Compute τs
440+
let ts = tau * s;
441+
let A = if flip_h { -self.point } else { self.point };
442+
let neg_ts = -ts;
443+
444+
// Compute the multi-scalar multiplication: -τs·B + τ·R + ρ·A
445+
let result = EdwardsPoint::vartime_triple_scalar_mul_basepoint(
446+
&tau,
447+
&signature_R,
448+
&rho,
449+
&A,
450+
&neg_ts,
451+
);
452+
453+
// Check if result is identity (zero point)
454+
if result.is_identity() {
455+
Ok(())
456+
} else {
457+
Err(InternalError::Verify.into())
458+
}
459+
}
460+
384461
/// Constructs stream verifier with candidate `signature`.
385462
///
386463
/// Useful for cases where the whole message is not available all at once, allowing the

ed25519-dalek/tests/ed25519.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ mod vectors {
9595
"Signature strict verification failed on line {}",
9696
lineno
9797
);
98+
assert!(
99+
expected_verifying_key
100+
.verify_heea(&msg_bytes, &sig2)
101+
.is_ok(),
102+
"Signature heea verification failed on line {}",
103+
lineno
104+
);
98105
}
99106
}
100107

@@ -239,6 +246,8 @@ mod vectors {
239246
// small order pubkeys.
240247
assert!(vk.verify_strict(message1, &sig).is_err());
241248
assert!(vk.verify_strict(message2, &sig).is_err());
249+
assert!(vk.verify_heea(message1, &sig).is_err());
250+
assert!(vk.verify_heea(message2, &sig).is_err());
242251
}
243252

244253
// Identical to repudiation() above, but testing verify_prehashed against
@@ -323,6 +332,10 @@ mod integrations {
323332
verifying_key.verify_strict(good, &good_sig).is_ok(),
324333
"Strict verification of a valid signature failed!"
325334
);
335+
assert!(
336+
verifying_key.verify_heea(good, &good_sig).is_ok(),
337+
"HEEA verification of a valid signature failed!"
338+
);
326339
assert!(
327340
signing_key.verify(good, &bad_sig).is_err(),
328341
"Verification of a signature on a different message passed!"
@@ -331,6 +344,10 @@ mod integrations {
331344
verifying_key.verify_strict(good, &bad_sig).is_err(),
332345
"Strict verification of a signature on a different message passed!"
333346
);
347+
assert!(
348+
verifying_key.verify_heea(good, &bad_sig).is_err(),
349+
"HEEA verification of a signature on a different message passed!"
350+
);
334351
assert!(
335352
signing_key.verify(bad, &good_sig).is_err(),
336353
"Verification of a signature on a different message passed!"
@@ -339,6 +356,10 @@ mod integrations {
339356
verifying_key.verify_strict(bad, &good_sig).is_err(),
340357
"Strict verification of a signature on a different message passed!"
341358
);
359+
assert!(
360+
verifying_key.verify_heea(bad, &good_sig).is_err(),
361+
"HEEA verification of a signature on a different message passed!"
362+
);
342363
}
343364

344365
#[cfg(feature = "digest")]
@@ -727,4 +748,100 @@ mod serialisation {
727748
BINCODE_INT_LENGTH + SECRET_KEY_LENGTH
728749
);
729750
}
751+
752+
// Test verify_heea against standard verification
753+
// verify_heea should accept the same signatures as verify_strict
754+
#[test]
755+
fn verify_heea_basic() {
756+
let sec_bytes =
757+
hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
758+
.unwrap();
759+
let pub_bytes =
760+
hex::decode("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")
761+
.unwrap();
762+
let msg_bytes = b"";
763+
let sig_bytes = hex::decode("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").unwrap();
764+
765+
let sec_bytes: &[u8; 32] = sec_bytes.as_slice().try_into().unwrap();
766+
let pub_bytes: &[u8; 32] = pub_bytes.as_slice().try_into().unwrap();
767+
768+
let signing_key = SigningKey::from_bytes(sec_bytes);
769+
let verifying_key = VerifyingKey::from_bytes(pub_bytes).unwrap();
770+
assert_eq!(verifying_key, signing_key.verifying_key());
771+
772+
let sig = Signature::try_from(&sig_bytes[..]).unwrap();
773+
774+
// Test that verify_heea accepts the same signatures as verify_strict
775+
assert!(
776+
verifying_key.verify_heea(msg_bytes, &sig).is_ok(),
777+
"verify_heea failed for valid signature"
778+
);
779+
assert!(
780+
verifying_key.verify_strict(msg_bytes, &sig).is_ok(),
781+
"verify_strict failed for valid signature"
782+
);
783+
}
784+
785+
// Test verify_heea with multiple test vectors
786+
#[test]
787+
fn verify_heea_test_vectors() {
788+
let test_cases = vec![
789+
(
790+
"c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
791+
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
792+
"af82",
793+
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
794+
),
795+
(
796+
"f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5",
797+
"278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e",
798+
"08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0",
799+
"0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03",
800+
),
801+
];
802+
803+
for (_secret, public, message, signature) in test_cases {
804+
let pub_bytes = hex::decode(public).unwrap();
805+
let msg_bytes = hex::decode(message).unwrap();
806+
let sig_bytes = hex::decode(signature).unwrap();
807+
808+
let pub_bytes: &[u8; 32] = pub_bytes.as_slice().try_into().unwrap();
809+
let verifying_key = VerifyingKey::from_bytes(pub_bytes).unwrap();
810+
let sig = Signature::try_from(&sig_bytes[..]).unwrap();
811+
812+
// Test that verify_heea accepts valid signatures
813+
assert!(
814+
verifying_key.verify_heea(&msg_bytes, &sig).is_ok(),
815+
"verify_heea failed for test vector with public key: {}",
816+
public
817+
);
818+
819+
// Also verify with standard verification
820+
assert!(
821+
verifying_key.verify_strict(&msg_bytes, &sig).is_ok(),
822+
"verify_strict failed for test vector with public key: {}",
823+
public
824+
);
825+
}
826+
}
827+
828+
// Test that verify_heea rejects invalid signatures
829+
#[test]
830+
fn verify_heea_rejects_invalid() {
831+
let pub_bytes =
832+
hex::decode("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")
833+
.unwrap();
834+
let msg_bytes = b"test message";
835+
let sig_bytes = hex::decode("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b").unwrap();
836+
837+
let pub_bytes: &[u8; 32] = pub_bytes.as_slice().try_into().unwrap();
838+
let verifying_key = VerifyingKey::from_bytes(pub_bytes).unwrap();
839+
let sig = Signature::try_from(&sig_bytes[..]).unwrap();
840+
841+
// This signature is valid for an empty message, but we're verifying with a different message
842+
assert!(
843+
verifying_key.verify_heea(msg_bytes, &sig).is_err(),
844+
"verify_heea should reject invalid signature"
845+
);
846+
}
730847
}

0 commit comments

Comments
 (0)