// -*- mode: rust; -*- // // This file is part of solana-ed25519's ed_sigs module, forked from ed25519-zebra. // Original ed25519-zebra code: Copyright (c) Zcash Foundation contributors // Modifications for HEEA: Copyright (c) 2025 curve25519-sol contributors // See LICENSE-APACHE and LICENSE-MIT for licensing information. // // Modifications from ed25519-zebra: // - Added `verify_zebra`, an accelerated verification path using the HEEA // scalar decomposition from curve25519-sol's `HEEADecomposition` trait. // See "Accelerating EdDSA Signature Verification with Faster Scalar Size // Halving" (TCHES 2025) for the algorithm. // - `verify` dispatches to `verify_zebra`, preserving ZIP-215 semantics. use crate::{ edwards::{CompressedEdwardsY, EdwardsPoint}, scalar::Scalar, traits::{HEEADecomposition, IsIdentity}, }; use core::convert::{TryFrom, TryInto}; use sha2::{Sha512, digest::Update}; #[cfg(feature = "zeroize")] use zeroize::DefaultIsZeroes; use ed25519::{Signature, signature::Verifier}; #[cfg(feature = "pkcs8")] use pkcs8::der::asn1::BitStringRef; #[cfg(feature = "pkcs8")] use pkcs8::spki::{ AlgorithmIdentifierRef, DecodePublicKey, EncodePublicKey, Error as SpkiError, SubjectPublicKeyInfoRef, }; #[cfg(feature = "pkcs8")] use pkcs8::{Document, ObjectIdentifier}; use super::{Error, scalar_from_sha512}; /// The length of an ed25519 `VerificationKey`, in bytes. pub const VERIFICATION_KEY_LENGTH: usize = 32; #[cfg(feature = "pkcs8")] const OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112"); // RFC 8410 #[cfg(feature = "pkcs8")] const ALGORITHM_ID: AlgorithmIdentifierRef<'_> = AlgorithmIdentifierRef { oid: OID, parameters: None, }; const LEGACY_EXCLUDED_R_ENCODINGS: [[u8; 32]; 11] = [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ], [ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ], [ 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05, ], [ 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a, ], [ 0x13, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85, ], [ 0xb4, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa, ], [ 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, ], [ 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, ], [ 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, ], [ 0xd9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ], [ 0xda, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ], ]; /// A container for the 32-byte encoded form of an Ed25519 verification key. /// /// This type only checks or carries the byte length. It does not prove that the /// bytes decompress to a valid Ed25519 verification key. Convert it to /// [`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 /// following idiom: /// ``` /// use core::convert::TryFrom; /// # use curve25519::ed_sigs::*; /// # let msg = b"Zcash"; /// # let sk = SigningKey::from_bytes(&[1u8; 32]); /// # let sig = sk.sign(msg); /// # let vk_bytes = VerificationKeyBytes::from(&sk); /// VerificationKey::try_from(vk_bytes) /// .and_then(|vk| vk.verify(&sig, msg)); /// ``` #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct VerificationKeyBytes(pub(crate) [u8; VERIFICATION_KEY_LENGTH]); impl core::fmt::Debug for VerificationKeyBytes { fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fmt.debug_tuple("VerificationKeyBytes") .field(&self.0) .finish() } } impl AsRef<[u8]> for VerificationKeyBytes { fn as_ref(&self) -> &[u8] { &self.0[..] } } impl TryFrom<&[u8]> for VerificationKeyBytes { type Error = Error; fn try_from(slice: &[u8]) -> Result { if slice.len() == 32 { let mut bytes = [0u8; 32]; bytes[..].copy_from_slice(slice); Ok(bytes.into()) } else { Err(Error::InvalidSliceLength) } } } impl From<[u8; 32]> for VerificationKeyBytes { fn from(bytes: [u8; 32]) -> VerificationKeyBytes { VerificationKeyBytes(bytes) } } impl From for [u8; 32] { fn from(refined: VerificationKeyBytes) -> [u8; 32] { refined.0 } } #[cfg(feature = "pkcs8")] impl<'a> TryFrom> for VerificationKeyBytes { type Error = Error; fn try_from(spki: SubjectPublicKeyInfoRef<'a>) -> Result { verification_key_bytes_from_spki(spki).map_err(|_| Error::MalformedPublicKey) } } #[cfg(feature = "pkcs8")] fn verification_key_bytes_from_spki( spki: SubjectPublicKeyInfoRef<'_>, ) -> Result { if spki.algorithm.oid != OID { return Err(SpkiError::OidUnknown { oid: spki.algorithm.oid, }); } if spki.algorithm != ALGORITHM_ID { return Err(SpkiError::KeyMalformed); } let bytes = spki .subject_public_key .as_bytes() .ok_or(SpkiError::KeyMalformed)?; VerificationKeyBytes::try_from(bytes).map_err(|_| SpkiError::KeyMalformed) } /// A valid Ed25519 verification key. /// /// This is also called a public key by other implementations. /// /// This type holds decompressed state used in signature verification; if the /// verification key may not be used immediately, it is probably better to use /// [`VerificationKeyBytes`], which stores only the length-checked encoded bytes. /// /// ## Zcash-specific consensus properties /// /// Ed25519 checks are described in [§5.4.5][ps] of the Zcash protocol specification and in /// [ZIP 215]. The verification criteria for an (encoded) verification key `A_bytes` are: /// /// * `A_bytes` MUST be an encoding of a point `A` on the twisted Edwards form of /// Curve25519, and non-canonical encodings MUST be accepted; /// /// [ps]: https://zips.z.cash/protocol/protocol.pdf#concreteed25519 #[derive(PartialEq, Eq, Copy, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(try_from = "VerificationKeyBytes"))] #[cfg_attr(feature = "serde", serde(into = "VerificationKeyBytes"))] #[allow(non_snake_case)] pub struct VerificationKey { pub(crate) A_bytes: VerificationKeyBytes, pub(crate) minus_A: EdwardsPoint, } impl From for VerificationKeyBytes { fn from(vk: VerificationKey) -> VerificationKeyBytes { vk.A_bytes } } impl AsRef<[u8]> for VerificationKey { fn as_ref(&self) -> &[u8] { &self.A_bytes.0[..] } } impl Default for VerificationKey { fn default() -> VerificationKey { let identity: EdwardsPoint = Default::default(); let identity_bytes = identity.compress().to_bytes(); VerificationKey { A_bytes: VerificationKeyBytes::from(identity_bytes), minus_A: -identity, } } } #[cfg(feature = "zeroize")] impl DefaultIsZeroes for VerificationKey {} impl From for [u8; 32] { fn from(vk: VerificationKey) -> [u8; 32] { vk.A_bytes.0 } } impl TryFrom for VerificationKey { type Error = Error; #[allow(non_snake_case)] fn try_from(bytes: VerificationKeyBytes) -> Result { // * `A_bytes` and `R_bytes` MUST be encodings of points `A` and `R` respectively on the // twisted Edwards form of Curve25519, and non-canonical encodings MUST be accepted; let A = CompressedEdwardsY(bytes.0) .decompress() .ok_or(Error::MalformedPublicKey)?; Ok(VerificationKey { A_bytes: bytes, minus_A: -A, }) } } impl TryFrom<&[u8]> for VerificationKey { type Error = Error; fn try_from(slice: &[u8]) -> Result { VerificationKeyBytes::try_from(slice).and_then(|vkb| vkb.try_into()) } } impl TryFrom<[u8; 32]> for VerificationKey { type Error = Error; fn try_from(bytes: [u8; 32]) -> Result { VerificationKeyBytes::from(bytes).try_into() } } #[cfg(feature = "pkcs8")] impl EncodePublicKey for VerificationKey { /// Serialize [`VerificationKey`] to an ASN.1 DER-encoded document. fn to_public_key_der(&self) -> pkcs8::spki::Result { SubjectPublicKeyInfoRef { algorithm: ALGORITHM_ID, subject_public_key: BitStringRef::from_bytes(&self.A_bytes.0[..])?, } .try_into() } } #[cfg(feature = "pkcs8")] impl DecodePublicKey for VerificationKey { /// Deserialize [`VerificationKey`] from ASN.1 DER bytes (32 bytes). fn from_public_key_der(bytes: &[u8]) -> Result { let spki = SubjectPublicKeyInfoRef::try_from(bytes)?; let pk_bytes = verification_key_bytes_from_spki(spki)?; Self::try_from(pk_bytes).map_err(|_| SpkiError::KeyMalformed) } } impl Verifier for VerificationKey { /// Verify a [`Signature`] object against a given [`VerificationKey`]. fn verify( &self, message: &[u8], signature: &Signature, ) -> Result<(), ed25519::signature::Error> { self.verify(signature, message) .map_err(|_| ed25519::signature::Error::new()) } } impl VerificationKey { fn challenge_scalar(&self, signature: &Signature, msg: &[u8]) -> Scalar { scalar_from_sha512( 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 /// [ZIP215]. The verification criteria for an (encoded) signature `(R_bytes, s_bytes)` with /// (encoded) verification key `A_bytes` are: /// /// * `A_bytes` and `R_bytes` MUST be encodings of points `A` and `R` respectively on the /// twisted Edwards form of Curve25519, and non-canonical encodings MUST be accepted; /// /// * `s_bytes` MUST represent an integer `s` less than `l`, the order of the prime-order /// subgroup of Curve25519; /// /// * the verification equation `[8][s]B = [8]R + [8][k]A` MUST be satisfied; /// /// * the alternate verification equation `[s]B = R + [k]A`, allowed by RFC 8032, MUST NOT be /// used. /// /// [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> { self.verify_zebra(signature, msg) } /// 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). /// /// The decomposition returns ρ and τ such that either ρ ≡ τh (mod ℓ) or /// ρ ≡ -τ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. /// /// We then decompose τs into two 128-bit scalars: /// τs = τs_hi * 2^128 + τs_lo /// /// The resulting equation can be checked with a 4-variable MSM with /// half-size scalars. #[allow(non_snake_case)] 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 τ. If flip_h is false, then // ρ ≡ τh (mod ℓ). If flip_h is true, then ρ ≡ -τh (mod ℓ), so the // sign of A is flipped below. let (rho, tau, flip_h) = h.heea_decompose(); // Extract s from the signature let s = Option::::from(Scalar::from_canonical_bytes(*signature.s_bytes())) .ok_or(Error::InvalidSignature)?; // Decode R from the signature let neg_R = -CompressedEdwardsY(*signature.r_bytes()) .decompress() .ok_or(Error::InvalidSignature)?; // Standard verification checks: sB = R + hA. // // We verify: // [8] τs B + [8] τ (-R) + [8] ρ A_term == 0 // where A_term is -A when ρ ≡ τh and A when ρ ≡ -τh. // Compute τs let ts = tau * s; let A = if flip_h { -self.minus_A } else { self.minus_A }; // HEEA decomposition guarantees tau and rho fit the optimized // 128/128/256-bit multiplication path. let result = crate::backend::vartime_triple_base_mul_128_128_256_prechecked( &tau, &neg_R, &rho, &A, &ts, ); if result.mul_by_cofactor().is_identity() { Ok(()) } else { Err(Error::InvalidSignature) } } /// Verify a signature with dalek-style canonical-`R` byte comparison. /// /// This recomputes the expected canonical `R` encoding and compares it to the /// signature's `R` bytes. /// /// 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 /// original `R` encoding needed for the byte comparison. #[allow(non_snake_case)] 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> { if self.A_bytes.0 == [0; 32] || LEGACY_EXCLUDED_R_ENCODINGS.contains(signature.r_bytes()) { return Err(Error::InvalidSignature); } let s = Option::::from(Scalar::from_canonical_bytes(*signature.s_bytes())) .ok_or(Error::InvalidSignature)?; let expected_R = EdwardsPoint::vartime_double_scalar_mul_basepoint(&h, &self.minus_A, &s).compress(); if expected_R.as_bytes() == signature.r_bytes() { Ok(()) } else { Err(Error::InvalidSignature) } } } // --------------------------------------------------------------------------- // 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) } } }