diff --git a/curve25519/solana-ed25519/Cargo.toml b/curve25519/solana-ed25519/Cargo.toml index 7cea938..25d1367 100644 --- a/curve25519/solana-ed25519/Cargo.toml +++ b/curve25519/solana-ed25519/Cargo.toml @@ -90,4 +90,5 @@ check-cfg = [ 'cfg(curve25519_backend, values("simd"))', 'cfg(curve25519_diagnostics, values("build"))', 'cfg(curve25519_bits, values("64"))', + 'cfg(curve25519_serial_only)', ] diff --git a/curve25519/solana-ed25519/src/backend.rs b/curve25519/solana-ed25519/src/backend.rs index ee71035..5f71765 100644 --- a/curve25519/solana-ed25519/src/backend.rs +++ b/curve25519/solana-ed25519/src/backend.rs @@ -39,19 +39,19 @@ use crate::Scalar; pub mod serial; -#[cfg(target_arch = "x86_64")] +#[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] pub mod vector; #[derive(Copy, Clone)] enum BackendKind { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] Avx2, Serial, } #[inline] fn get_selected_backend() -> BackendKind { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] { cpufeatures::new!(cpuid_avx2, "avx2"); let token_avx2: cpuid_avx2::InitToken = cpuid_avx2::init(); @@ -74,7 +74,7 @@ where use crate::traits::VartimeMultiscalarMul; match get_selected_backend() { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] BackendKind::Avx2 => { vector::scalar_mul::pippenger::spec_avx2::Pippenger::optional_multiscalar_mul::( scalars, points, @@ -90,7 +90,7 @@ where #[cfg(feature = "alloc")] pub(crate) enum VartimePrecomputedStraus { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] Avx2(vector::scalar_mul::precomputed_straus::spec_avx2::VartimePrecomputedStraus), Scalar(serial::scalar_mul::precomputed_straus::VartimePrecomputedStraus), } @@ -105,7 +105,7 @@ impl VartimePrecomputedStraus { use crate::traits::VartimePrecomputedMultiscalarMul; match get_selected_backend() { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] BackendKind::Avx2 => VartimePrecomputedStraus::Avx2( vector::scalar_mul::precomputed_straus::spec_avx2::VartimePrecomputedStraus::new( static_points, @@ -124,7 +124,7 @@ impl VartimePrecomputedStraus { use crate::traits::VartimePrecomputedMultiscalarMul; match self { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] VartimePrecomputedStraus::Avx2(inner) => inner.len(), VartimePrecomputedStraus::Scalar(inner) => inner.len(), } @@ -135,7 +135,7 @@ impl VartimePrecomputedStraus { use crate::traits::VartimePrecomputedMultiscalarMul; match self { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] VartimePrecomputedStraus::Avx2(inner) => inner.is_empty(), VartimePrecomputedStraus::Scalar(inner) => inner.is_empty(), } @@ -157,7 +157,7 @@ impl VartimePrecomputedStraus { use crate::traits::VartimePrecomputedMultiscalarMul; match self { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] VartimePrecomputedStraus::Avx2(inner) => inner.optional_mixed_multiscalar_mul( static_scalars, dynamic_scalars, @@ -184,7 +184,7 @@ where use crate::traits::MultiscalarMul; match get_selected_backend() { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] BackendKind::Avx2 => { vector::scalar_mul::straus::spec_avx2::Straus::multiscalar_mul::(scalars, points) } @@ -205,7 +205,7 @@ where use crate::traits::VartimeMultiscalarMul; match get_selected_backend() { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] BackendKind::Avx2 => { vector::scalar_mul::straus::spec_avx2::Straus::optional_multiscalar_mul::( scalars, points, @@ -220,7 +220,7 @@ where /// Perform constant-time, variable-base scalar multiplication. pub fn variable_base_mul(point: &EdwardsPoint, scalar: &Scalar) -> EdwardsPoint { match get_selected_backend() { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] BackendKind::Avx2 => vector::scalar_mul::variable_base::spec_avx2::mul(point, scalar), BackendKind::Serial => serial::scalar_mul::variable_base::mul(point, scalar), } @@ -230,7 +230,7 @@ pub fn variable_base_mul(point: &EdwardsPoint, scalar: &Scalar) -> EdwardsPoint #[allow(non_snake_case)] pub fn vartime_double_base_mul(a: &Scalar, A: &EdwardsPoint, b: &Scalar) -> EdwardsPoint { match get_selected_backend() { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] BackendKind::Avx2 => vector::scalar_mul::vartime_double_base::spec_avx2::mul(a, A, b), BackendKind::Serial => serial::scalar_mul::vartime_double_base::mul(a, A, b), } @@ -267,7 +267,7 @@ pub(crate) fn vartime_triple_base_mul_128_128_256_prechecked( b: &Scalar, ) -> EdwardsPoint { match get_selected_backend() { - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] BackendKind::Avx2 => { vector::scalar_mul::vartime_triple_base::spec_avx2::mul_128_128_256_prechecked( a1, A1, a2, A2, b, diff --git a/curve25519/solana-ed25519/src/ed_sigs.rs b/curve25519/solana-ed25519/src/ed_sigs.rs index 8764723..184dd4e 100644 --- a/curve25519/solana-ed25519/src/ed_sigs.rs +++ b/curve25519/solana-ed25519/src/ed_sigs.rs @@ -36,3 +36,19 @@ pub(crate) fn scalar_from_sha512(hash: Sha512) -> Scalar { scalar } + +/// AENEAS-COMPAT: single-call SHA-512 oracle for the verified verification +/// path. Semantically `Sha512(r || a || m)` — identical to hashing via the +/// incremental `Digest` API. A single monomorphic function whose signature +/// carries no foreign types lets the extractor treat the whole hash +/// computation as one opaque oracle. +pub(crate) fn sha512_hash3(r: &[u8], a: &[u8], m: &[u8]) -> [u8; 64] { + let mut h = Sha512::default(); + Digest::update(&mut h, r); + Digest::update(&mut h, a); + Digest::update(&mut h, m); + let output = h.finalize(); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(output.as_slice()); + bytes +} diff --git a/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs b/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs index 5dd4a97..1a4afc6 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs @@ -462,3 +462,132 @@ impl VerificationKey { } } } + +// --------------------------------------------------------------------------- +// AENEAS-COMPAT verified-verification entry points. +// +// `verify_sha512` is semantically `verify_dalek` with each step spelled in +// extractor-friendly form: the derived array `PartialEq`/`contains` become +// explicit index loops, and `Scalar::from_canonical_bytes` (whose `subtle` +// internals defeat the extractor) becomes an explicit `s < l` byte compare +// followed by `Scalar::from_bytes_mod_order` (the identity on canonical +// bytes). The verification path is variable-time throughout, as upstream's. +// --------------------------------------------------------------------------- + +/// `bytes` interpreted little-endian is a canonical scalar (< l)? If so the +/// scalar itself; value-level semantics identical to +/// `Scalar::from_canonical_bytes(bytes).into()`. +fn check_scalar_canonical(bytes: [u8; 32]) -> Result { + /// l = 2^252 + 27742317777372353535851937790883648493, little-endian. + const L_BYTES: [u8; 32] = [ + 237, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, + 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, + ]; + // bytes < l, most-significant byte first; the first differing byte decides. + let mut lt = false; + let mut decided = false; + let mut i = 32; + while i > 0 { + let j = i - 1; + if !decided { + if bytes[j] < L_BYTES[j] { + lt = true; + decided = true; + } else if bytes[j] > L_BYTES[j] { + decided = true; + } + } + i -= 1; + } + if lt { + Ok(Scalar::from_bytes_mod_order(bytes)) + } else { + Err(Error::InvalidSignature) + } +} + +/// Explicit-loop `LEGACY_EXCLUDED_R_ENCODINGS.contains(r)`. +fn is_legacy_excluded_r(r: &[u8; 32]) -> bool { + let mut found = false; + let mut i = 0; + while i < 11 { + let mut eq = true; + let mut j = 0; + while j < 32 { + if LEGACY_EXCLUDED_R_ENCODINGS[i][j] != r[j] { + eq = false; + } + j += 1; + } + if eq { + found = true; + } + i += 1; + } + found +} + +impl VerificationKey { + /// Explicit-loop `self.A_bytes.0 != [0; 32]`. + fn a_bytes_nonzero(&self) -> bool { + let mut nonzero = false; + let mut i = 0; + while i < 32 { + if self.A_bytes.0[i] != 0 { + nonzero = true; + } + i += 1; + } + nonzero + } + + /// Recompute the expected canonical `R` encoding: + /// `compress([k]*(-A) + [s]*B)` with `k = SHA-512(R || A || msg) mod l`. + fn recompute_r_sha512( + &self, + r_bytes: &[u8; 32], + s: &Scalar, + msg: &[u8], + ) -> CompressedEdwardsY { + let k = Scalar::from_bytes_mod_order_wide(&super::sha512_hash3( + &r_bytes[..], + &self.A_bytes.0[..], + msg, + )); + EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &self.minus_A, s).compress() + } + + /// Semantically identical to [`Self::verify_dalek`]; see the module + /// comment above for the extractor-friendly spellings. The signature's + /// `R`/`s` accessors are each called exactly once. + pub fn verify_sha512(&self, sig: &Signature, msg: &[u8]) -> Result<(), Error> { + // (parameter named `sig`: the extractor's generated code would + // otherwise shadow the `signature::` crate namespace) + let r_bytes: [u8; 32] = *sig.r_bytes(); + let s_bytes: [u8; 32] = *sig.s_bytes(); + + if !self.a_bytes_nonzero() { + return Err(Error::InvalidSignature); + } + if is_legacy_excluded_r(&r_bytes) { + return Err(Error::InvalidSignature); + } + let s = check_scalar_canonical(s_bytes)?; + + let expected_r = self.recompute_r_sha512(&r_bytes, &s, msg); + let e = expected_r.as_bytes(); + let mut equal = true; + let mut k = 0; + while k < 32 { + if e[k] != r_bytes[k] { + equal = false; + } + k += 1; + } + if equal { + Ok(()) + } else { + Err(Error::InvalidSignature) + } + } +} diff --git a/curve25519/solana-ed25519/src/scalar.rs b/curve25519/solana-ed25519/src/scalar.rs index d739fa4..162a3bd 100644 --- a/curve25519/solana-ed25519/src/scalar.rs +++ b/curve25519/solana-ed25519/src/scalar.rs @@ -243,7 +243,7 @@ impl Scalar { /// Construct a `Scalar` from bytes that are known to be canonical. #[inline] - #[cfg(target_arch = "x86_64")] + #[cfg(all(target_arch = "x86_64", not(curve25519_serial_only)))] pub(crate) const fn from_canonical_bytes_unchecked(bytes: [u8; 32]) -> Scalar { Scalar { bytes } }