mirror of
https://github.com/saymrwulf/anza-cryptography-source.git
synced 2026-09-04 20:24:04 +00:00
540 lines
16 KiB
Rust
540 lines
16 KiB
Rust
// -*- mode: rust; -*-
|
|
//
|
|
// This file is part of curve25519-sol.
|
|
// Copyright (c) 2025 curve25519-sol contributors
|
|
// See LICENSE for licensing information.
|
|
//
|
|
// This file is new in curve25519-sol and has no upstream equivalent in
|
|
// curve25519-dalek. It implements the half-extended Euclidean algorithm
|
|
// (HEEA) for scalar decomposition as described in:
|
|
//
|
|
// "Accelerating EdDSA Signature Verification with Faster Scalar Size
|
|
// Halving", TCHES 2025. Algorithm 4 (hEEA_approx_q).
|
|
|
|
//! Implementation of the paper "Accelerating EdDSA Signature Verification
|
|
//! with Faster Scalar Size Halving" (TCHES 2025).
|
|
//!
|
|
//! This module implements Algorithm 4 (hEEA_approx_q) from the paper, which generates
|
|
//! half-size scalars for faster EdDSA verification.
|
|
//!
|
|
//! 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 crate::constants;
|
|
|
|
/// A signed 256-bit integer represented as 4 u64 limbs (little-endian)
|
|
/// Used for the half-extended Euclidean algorithm
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct I256 {
|
|
/// Limbs in little-endian order: [low, ..., high]
|
|
limbs: [u64; 4],
|
|
/// Sign: true = negative, false = non-negative
|
|
negative: bool,
|
|
}
|
|
|
|
pub(crate) const HEEA_MAX_INDEX: usize = 129;
|
|
|
|
/// Implement the curve25519 hEEA variable-time algorithm.
|
|
///
|
|
/// 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) {
|
|
// Get L from the existing BASEPOINT_ORDER constant
|
|
let mut r0: I256 = (&constants::BASEPOINT_ORDER).into();
|
|
let mut r1 = v;
|
|
|
|
debug_assert!(r1.abs() < r0, "Input v must be less than the group order L");
|
|
|
|
let mut t0: I256 = I256::ZERO;
|
|
let mut t1: I256 = I256::ONE;
|
|
|
|
let mut bl_r0 = 253u32; // bit_length(L) = 253
|
|
let mut bl_r1 = bit_length_i256(r1);
|
|
|
|
// Main loop - continue until r1 is approximately half-size (~127 bits)
|
|
while bl_r1 > 127 {
|
|
// Compute shift amount
|
|
let s = bl_r0 - bl_r1;
|
|
|
|
// Check if signs are the same (cheap flag check)
|
|
let sign_r0 = r0.is_negative();
|
|
let sign_r1 = r1.is_negative();
|
|
|
|
let (r, t) = if sign_r0 == sign_r1 {
|
|
(r0.wrapping_sub(r1 << s), t0.wrapping_sub(t1 << s))
|
|
} else {
|
|
(r0.wrapping_add(r1 << s), t0.wrapping_add(t1 << s))
|
|
};
|
|
|
|
let bl_r = bit_length_i256(r);
|
|
|
|
if bl_r > bl_r1 {
|
|
// r grew, so keep it in r0
|
|
r0 = r;
|
|
t0 = t;
|
|
bl_r0 = bl_r;
|
|
} else {
|
|
// r shrunk, swap
|
|
r0 = r1;
|
|
r1 = r;
|
|
t0 = t1;
|
|
t1 = t;
|
|
bl_r0 = bl_r1;
|
|
bl_r1 = bl_r;
|
|
}
|
|
}
|
|
|
|
(r1, t1)
|
|
}
|
|
|
|
/// Compute bit length of I256 (magnitude, not including sign)
|
|
#[inline(always)]
|
|
fn bit_length_i256(v: I256) -> u32 {
|
|
for i in (0..4).rev() {
|
|
let limb = v.limbs[i];
|
|
if limb != 0 {
|
|
return (i as u32) * 64 + (64 - limb.leading_zeros());
|
|
}
|
|
}
|
|
|
|
0
|
|
}
|
|
|
|
impl I256 {
|
|
pub(crate) const ZERO: Self = I256 {
|
|
limbs: [0, 0, 0, 0],
|
|
negative: false,
|
|
};
|
|
|
|
const ONE: Self = I256 {
|
|
limbs: [1, 0, 0, 0],
|
|
negative: false,
|
|
};
|
|
|
|
#[inline(always)]
|
|
const fn abs(&self) -> Self {
|
|
I256 {
|
|
limbs: self.limbs,
|
|
negative: false,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn new(a: i128) -> Self {
|
|
let mag: u128 = a.unsigned_abs();
|
|
I256 {
|
|
limbs: [mag as u64, (mag >> 64) as u64, 0, 0],
|
|
negative: a.is_negative(),
|
|
}
|
|
}
|
|
|
|
/// Create from little-endian bytes
|
|
pub(crate) fn from_le_bytes(bytes: [u8; 32]) -> Self {
|
|
I256 {
|
|
limbs: [
|
|
u64::from_le_bytes([
|
|
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
|
]),
|
|
u64::from_le_bytes([
|
|
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
|
|
bytes[15],
|
|
]),
|
|
u64::from_le_bytes([
|
|
bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22],
|
|
bytes[23],
|
|
]),
|
|
u64::from_le_bytes([
|
|
bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30],
|
|
bytes[31],
|
|
]),
|
|
],
|
|
negative: false,
|
|
}
|
|
}
|
|
|
|
/// Convert to little-endian bytes
|
|
pub(crate) fn to_le_bytes(self) -> [u8; 32] {
|
|
let l = self.limbs;
|
|
[
|
|
l[0] as u8,
|
|
(l[0] >> 8) as u8,
|
|
(l[0] >> 16) as u8,
|
|
(l[0] >> 24) as u8,
|
|
(l[0] >> 32) as u8,
|
|
(l[0] >> 40) as u8,
|
|
(l[0] >> 48) as u8,
|
|
(l[0] >> 56) as u8,
|
|
l[1] as u8,
|
|
(l[1] >> 8) as u8,
|
|
(l[1] >> 16) as u8,
|
|
(l[1] >> 24) as u8,
|
|
(l[1] >> 32) as u8,
|
|
(l[1] >> 40) as u8,
|
|
(l[1] >> 48) as u8,
|
|
(l[1] >> 56) as u8,
|
|
l[2] as u8,
|
|
(l[2] >> 8) as u8,
|
|
(l[2] >> 16) as u8,
|
|
(l[2] >> 24) as u8,
|
|
(l[2] >> 32) as u8,
|
|
(l[2] >> 40) as u8,
|
|
(l[2] >> 48) as u8,
|
|
(l[2] >> 56) as u8,
|
|
l[3] as u8,
|
|
(l[3] >> 8) as u8,
|
|
(l[3] >> 16) as u8,
|
|
(l[3] >> 24) as u8,
|
|
(l[3] >> 32) as u8,
|
|
(l[3] >> 40) as u8,
|
|
(l[3] >> 48) as u8,
|
|
(l[3] >> 56) as u8,
|
|
]
|
|
}
|
|
|
|
/// Check if zero
|
|
#[inline(always)]
|
|
fn is_zero(&self) -> bool {
|
|
self.limbs[0] == 0 && self.limbs[1] == 0 && self.limbs[2] == 0 && self.limbs[3] == 0
|
|
}
|
|
|
|
/// Check if negative (< 0)
|
|
#[inline(always)]
|
|
fn is_negative(&self) -> bool {
|
|
self.negative && !self.is_zero()
|
|
}
|
|
|
|
/// Wrapping negation (two's complement)
|
|
#[inline(always)]
|
|
fn wrapping_neg(self) -> Self {
|
|
if self.is_zero() {
|
|
return Self::ZERO;
|
|
}
|
|
I256 {
|
|
limbs: self.limbs,
|
|
negative: !self.negative,
|
|
}
|
|
}
|
|
|
|
/// Wrapping addition
|
|
#[inline(always)]
|
|
fn wrapping_add(self, rhs: Self) -> Self {
|
|
// If signs are the same, add magnitudes
|
|
if self.negative == rhs.negative {
|
|
let (limbs, _overflow) = add_limbs(&self.limbs, &rhs.limbs);
|
|
I256 {
|
|
limbs,
|
|
negative: self.negative,
|
|
}
|
|
} else {
|
|
// Different signs: subtract the smaller magnitude from larger
|
|
let cmp = cmp_magnitude(&self.limbs, &rhs.limbs);
|
|
match cmp {
|
|
core::cmp::Ordering::Greater => {
|
|
let (limbs, _underflow) = sub_limbs(&self.limbs, &rhs.limbs);
|
|
I256 {
|
|
limbs,
|
|
negative: self.negative,
|
|
}
|
|
}
|
|
core::cmp::Ordering::Less => {
|
|
let (limbs, _underflow) = sub_limbs(&rhs.limbs, &self.limbs);
|
|
I256 {
|
|
limbs,
|
|
negative: rhs.negative,
|
|
}
|
|
}
|
|
core::cmp::Ordering::Equal => Self::ZERO,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wrapping subtraction
|
|
#[inline(always)]
|
|
fn wrapping_sub(self, rhs: Self) -> Self {
|
|
self.wrapping_add(rhs.wrapping_neg())
|
|
}
|
|
|
|
/// Left shift
|
|
#[inline(always)]
|
|
fn wrapping_shl(self, shift: u32) -> Self {
|
|
if shift == 0 {
|
|
return self;
|
|
}
|
|
if shift >= 256 {
|
|
return Self::ZERO;
|
|
}
|
|
|
|
let limb_shift = (shift / 64) as usize;
|
|
let bit_shift = shift & 63;
|
|
let l = self.limbs;
|
|
let mut result = [0u64; 4];
|
|
|
|
if bit_shift == 0 {
|
|
match limb_shift {
|
|
0 => return self,
|
|
1 => {
|
|
result[1] = l[0];
|
|
result[2] = l[1];
|
|
result[3] = l[2];
|
|
}
|
|
2 => {
|
|
result[2] = l[0];
|
|
result[3] = l[1];
|
|
}
|
|
3 => {
|
|
result[3] = l[0];
|
|
}
|
|
_ => {}
|
|
}
|
|
} else {
|
|
let carry = 64 - bit_shift;
|
|
match limb_shift {
|
|
0 => {
|
|
result[0] = l[0] << bit_shift;
|
|
result[1] = (l[1] << bit_shift) | (l[0] >> carry);
|
|
result[2] = (l[2] << bit_shift) | (l[1] >> carry);
|
|
result[3] = (l[3] << bit_shift) | (l[2] >> carry);
|
|
}
|
|
1 => {
|
|
result[1] = l[0] << bit_shift;
|
|
result[2] = (l[1] << bit_shift) | (l[0] >> carry);
|
|
result[3] = (l[2] << bit_shift) | (l[1] >> carry);
|
|
}
|
|
2 => {
|
|
result[2] = l[0] << bit_shift;
|
|
result[3] = (l[1] << bit_shift) | (l[0] >> carry);
|
|
}
|
|
3 => {
|
|
result[3] = l[0] << bit_shift;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
I256 {
|
|
limbs: result,
|
|
negative: self.negative,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Neg for I256 {
|
|
type Output = Self;
|
|
fn neg(self) -> <Self as Neg>::Output {
|
|
self.wrapping_neg()
|
|
}
|
|
}
|
|
|
|
// Helper: Add two magnitude arrays, returns (result, overflow_occurred)
|
|
#[inline(always)]
|
|
fn add_limbs(a: &[u64; 4], b: &[u64; 4]) -> ([u64; 4], bool) {
|
|
let mut result = [0u64; 4];
|
|
let (r0, c0) = a[0].overflowing_add(b[0]);
|
|
|
|
let (r1_base, c1a) = a[1].overflowing_add(b[1]);
|
|
let (r1, c1b) = r1_base.overflowing_add(c0 as u64);
|
|
let c1 = c1a || c1b;
|
|
|
|
let (r2_base, c2a) = a[2].overflowing_add(b[2]);
|
|
let (r2, c2b) = r2_base.overflowing_add(c1 as u64);
|
|
let c2 = c2a || c2b;
|
|
|
|
let (r3_base, c3a) = a[3].overflowing_add(b[3]);
|
|
let (r3, c3b) = r3_base.overflowing_add(c2 as u64);
|
|
let c3 = c3a || c3b;
|
|
|
|
result[0] = r0;
|
|
result[1] = r1;
|
|
result[2] = r2;
|
|
result[3] = r3;
|
|
|
|
(result, c3)
|
|
}
|
|
|
|
// Helper: Subtract b from a, returns (result, underflow)
|
|
// If underflow is true, then a < b and the result is the two's complement
|
|
#[inline(always)]
|
|
fn sub_limbs(a: &[u64; 4], b: &[u64; 4]) -> ([u64; 4], bool) {
|
|
let mut result = [0u64; 4];
|
|
let (r0, b0) = a[0].overflowing_sub(b[0]);
|
|
|
|
let (r1_base, b1a) = a[1].overflowing_sub(b[1]);
|
|
let (r1, b1b) = r1_base.overflowing_sub(b0 as u64);
|
|
let b1 = b1a || b1b;
|
|
|
|
let (r2_base, b2a) = a[2].overflowing_sub(b[2]);
|
|
let (r2, b2b) = r2_base.overflowing_sub(b1 as u64);
|
|
let b2 = b2a || b2b;
|
|
|
|
let (r3_base, b3a) = a[3].overflowing_sub(b[3]);
|
|
let (r3, b3b) = r3_base.overflowing_sub(b2 as u64);
|
|
let b3 = b3a || b3b;
|
|
|
|
result[0] = r0;
|
|
result[1] = r1;
|
|
result[2] = r2;
|
|
result[3] = r3;
|
|
|
|
(result, b3)
|
|
}
|
|
|
|
// Helper: Compare magnitudes of two limb arrays
|
|
#[inline(always)]
|
|
fn cmp_magnitude(a: &[u64; 4], b: &[u64; 4]) -> core::cmp::Ordering {
|
|
for i in (0..4).rev() {
|
|
match a[i].cmp(&b[i]) {
|
|
core::cmp::Ordering::Equal => continue,
|
|
other => return other,
|
|
}
|
|
}
|
|
core::cmp::Ordering::Equal
|
|
}
|
|
|
|
impl PartialOrd for I256 {
|
|
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for I256 {
|
|
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
|
use core::cmp::Ordering;
|
|
|
|
match (self.negative, other.negative) {
|
|
(true, false) => Ordering::Less,
|
|
(false, true) => Ordering::Greater,
|
|
(false, false) => cmp_magnitude(&self.limbs, &other.limbs),
|
|
(true, true) => cmp_magnitude(&other.limbs, &self.limbs),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl core::ops::Shl<u32> for I256 {
|
|
type Output = Self;
|
|
fn shl(self, rhs: u32) -> Self {
|
|
self.wrapping_shl(rhs)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[cfg(all(feature = "rand_core", feature = "digest"))]
|
|
use crate::{Scalar, digest::Update, traits::HEEADecomposition};
|
|
|
|
#[cfg(all(feature = "rand_core", feature = "digest"))]
|
|
use rand_core::RngCore;
|
|
|
|
#[test]
|
|
#[cfg(all(feature = "rand_core", feature = "digest"))]
|
|
fn test_heea_decompose_half_size_scalars() {
|
|
use rand::thread_rng;
|
|
use sha2::{Digest, Sha512};
|
|
|
|
let mut rng = thread_rng();
|
|
|
|
// Test with multiple random scalars
|
|
for _ in 0..1000 {
|
|
// Generate a random scalar by hashing random bytes
|
|
let mut random_bytes = [0u8; 64];
|
|
for byte in &mut random_bytes {
|
|
*byte = (rng.next_u32() & 0xff) as u8;
|
|
}
|
|
let h = Scalar::from_hash(Sha512::new().chain(random_bytes));
|
|
|
|
// Convert h to I256 to see the actual output
|
|
let h_i256 = (&h).into();
|
|
let (rho_i256, tau_i256) = curve25519_heea_vartime(h_i256);
|
|
|
|
// Check the magnitude of rho and tau in their signed representation
|
|
let rho_magnitude_bits = bit_length_i256(rho_i256);
|
|
let tau_magnitude_bits = bit_length_i256(tau_i256);
|
|
|
|
// Now convert to Scalars and verify the equation
|
|
let (rho, tau, flip) = h.heea_decompose();
|
|
|
|
// Verify the non-negative scalar relation reported by flip.
|
|
let computed_rho = tau * h;
|
|
let computed_rho = if flip { -computed_rho } else { computed_rho };
|
|
assert_eq!(
|
|
rho, computed_rho,
|
|
"rho should equal tau * h with flip applied"
|
|
);
|
|
|
|
// Check that they are non-zero
|
|
assert_ne!(rho, Scalar::ZERO, "rho should be non-zero");
|
|
assert_ne!(tau, Scalar::ZERO, "tau should be non-zero");
|
|
|
|
// Both magnitudes should be approximately half-size (~127 bits)
|
|
assert!(
|
|
rho_magnitude_bits <= 127,
|
|
"rho magnitude should be approximately half-size, got {} bits",
|
|
rho_magnitude_bits
|
|
);
|
|
assert!(
|
|
tau_magnitude_bits <= 128,
|
|
"tau magnitude should be approximately half-size, got {} bits",
|
|
tau_magnitude_bits
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_heea_edge_cases() {
|
|
// Test Zero: v = 0
|
|
let v_zero = I256::ZERO;
|
|
let (rho, tau) = curve25519_heea_vartime(v_zero);
|
|
// When v = 0, the algorithm should return (0, 1) since we start with r1=0, t1=1
|
|
// and the loop never executes (bl_r1 would be small)
|
|
assert_eq!(rho, I256::ZERO, "rho should be zero when v is zero");
|
|
assert_eq!(tau, I256::ONE, "tau should be 1 when v is zero");
|
|
|
|
// Test One: v = 1
|
|
let v_one = I256::ONE;
|
|
let (rho, tau) = curve25519_heea_vartime(v_one);
|
|
assert_eq!(rho, I256::ONE, "rho should be 1 when v is 1");
|
|
assert_eq!(tau, I256::ONE, "tau should be 1 when v is 1");
|
|
|
|
// Test Minus One: v = -1
|
|
let v_minus_one = -I256::ONE;
|
|
let (rho, tau) = curve25519_heea_vartime(v_minus_one);
|
|
assert_eq!(rho, -I256::ONE, "rho should be -1 when v is -1");
|
|
assert_eq!(tau, I256::ONE, "tau should be 1 when v is -1");
|
|
|
|
// Test Max i128 Boundary: v = 2^127 - 1
|
|
// This tests whether we handle values near the i128 boundary correctly
|
|
// i128::MAX = 2^127 - 1
|
|
let v_max_i128 = I256::new(i128::MAX);
|
|
let (rho, tau) = curve25519_heea_vartime(v_max_i128);
|
|
let rho_bits = bit_length_i256(rho);
|
|
assert!(
|
|
rho_bits <= 128,
|
|
"rho should be half-size for 2^127-1, got {} bits",
|
|
rho_bits
|
|
);
|
|
|
|
// Verify tau magnitude
|
|
let tau_bits = bit_length_i256(tau);
|
|
assert!(
|
|
tau_bits <= 128,
|
|
"tau should be approximately half-size, got {} bits",
|
|
tau_bits
|
|
);
|
|
|
|
// test v = 2^252
|
|
let v_252 = I256::ONE << 252;
|
|
let (rho, tau) = curve25519_heea_vartime(v_252);
|
|
let rho_bits = bit_length_i256(rho);
|
|
assert!(
|
|
rho_bits <= 128,
|
|
"rho should be half-size for 2^252, got {} bits",
|
|
rho_bits
|
|
);
|
|
assert_eq!(tau, -I256::ONE, "tau should be -1 for 2^252");
|
|
}
|
|
}
|