diff --git a/wacore/libsignal/src/core/curve.rs b/wacore/libsignal/src/core/curve.rs index c2e8ee1c3..505dd7d5c 100644 --- a/wacore/libsignal/src/core/curve.rs +++ b/wacore/libsignal/src/core/curve.rs @@ -237,6 +237,93 @@ use curve25519_dalek::edwards::CompressedEdwardsY; use curve25519_dalek::scalar::Scalar; use std::sync::OnceLock; +use curve25519_dalek::edwards::EdwardsPoint; +use curve25519_dalek::montgomery::MontgomeryPoint; + +/// A verifying view of a [`PublicKey`] that caches the per-key XEdDSA +/// derivations: the Montgomery-to-Edwards conversion and its compressed +/// encoding, per sign bit (the bit travels in each signature, and a given +/// signer always produces the same one). Repeat verifications of one key, +/// e.g. every incoming message under a group sender key, skip two field +/// inversions per signature. +/// +/// Clones carry initialized entries, so a warmed instance stays warm through +/// the per-use clones of whatever memoizes it. +/// `(-A, A_compressed)` for one sign bit of a verifying key. +type PreparedEdwards = (EdwardsPoint, [u8; 32]); + +#[derive(Clone)] +pub struct PreparedVerifyingKey { + mont: [u8; 32], + /// Per sign bit; `None` when the key has no Edwards form for that bit + /// (such a signature can never verify). Behind an `Arc` so the many + /// per-use clones of a memoizing holder share one allocation and one + /// warm state, instead of each clone copying (or re-deriving) entries. + cached: std::sync::Arc<[OnceLock>; 2]>, +} + +impl PreparedVerifyingKey { + pub fn new(key: &PublicKey) -> Self { + let PublicKeyData::DjbPublicKey(mont) = key.key; + Self { + mont, + cached: std::sync::Arc::new([OnceLock::new(), OnceLock::new()]), + } + } + + /// Derives both sign-bit entries now. The signature's sign bit is fixed + /// per signer but unknowable from the Montgomery key alone, so a + /// receive-side holder warms both once instead of paying the derivation + /// inside the first verification. + pub fn precompute(&self) { + let _ = self.entry(0); + let _ = self.entry(1); + } + + fn entry(&self, sign: u8) -> Option<&(EdwardsPoint, [u8; 32])> { + self.cached[usize::from(sign & 1)] + .get_or_init(|| { + MontgomeryPoint(self.mont) + .to_edwards(sign) + .map(|point| (-point, point.compress().to_bytes())) + }) + .as_ref() + } + + pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> bool { + self.verify_signature_for_multipart_message(&[message], signature) + } + + pub fn verify_signature_for_multipart_message( + &self, + message: &[&[u8]], + signature: &[u8], + ) -> bool { + let Ok(signature) = <&[u8; 64]>::try_from(signature) else { + return false; + }; + let sign = (signature[63] & 0b1000_0000_u8) >> 7; + let Some((minus_cap_a, cap_a_bytes)) = self.entry(sign) else { + return false; + }; + curve25519::verify_signature_prepared(minus_cap_a, cap_a_bytes, message, signature) + } +} + +impl From<&PublicKey> for PreparedVerifyingKey { + fn from(key: &PublicKey) -> Self { + Self::new(key) + } +} + +impl std::fmt::Debug for PreparedVerifyingKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PreparedVerifyingKey") + .field("mont", &hex::encode(self.mont)) + .finish_non_exhaustive() + } +} + /// Cached data for XEdDSA signing. /// This avoids an expensive scalar multiplication on every signature /// and caches the scalar representation to avoid repeated modular reduction. @@ -511,6 +598,72 @@ mod tests { rand::make_rng::() } + /// The cached verifier must agree with the plain path on every input: + /// valid signatures under both sign bits, corrupted signatures and + /// messages, garbage keys, and wrong-length signatures. + #[test] + fn prepared_verifier_matches_plain_verify() { + let mut csprng = rng(); + let message: &[u8] = b"mensagem para verificar"; + + let mut seen_signs = [false; 2]; + for _ in 0..32 { + let keypair = KeyPair::generate(&mut csprng); + let prepared = PreparedVerifyingKey::new(&keypair.public_key); + let signature = keypair + .calculate_signature(message, &mut csprng) + .expect("sign"); + seen_signs[usize::from(signature[63] >> 7)] = true; + + assert!(keypair.public_key.verify_signature(message, &signature)); + assert!(prepared.verify_signature(message, &signature)); + + // Corrupted signature and corrupted message reject on both paths. + let mut bad_sig = signature; + bad_sig[7] ^= 0x40; + assert!(!keypair.public_key.verify_signature(message, &bad_sig)); + assert!(!prepared.verify_signature(message, &bad_sig)); + assert!(!keypair.public_key.verify_signature(b"outra", &signature)); + assert!(!prepared.verify_signature(b"outra", &signature)); + + // Flipped sign bit must agree between paths too. + let mut flipped = signature; + flipped[63] ^= 0x80; + assert_eq!( + keypair.public_key.verify_signature(message, &flipped), + prepared.verify_signature(message, &flipped) + ); + + // Wrong-length signatures reject on both paths. + assert!( + !keypair + .public_key + .verify_signature(message, &signature[..63]) + ); + assert!(!prepared.verify_signature(message, &signature[..63])); + + // A warmed CLONE must verify identically (the memoized instance + // hands out its state by reference, but clones must stay correct). + assert!(prepared.clone().verify_signature(message, &signature)); + } + assert!( + seen_signs[0] && seen_signs[1], + "corpus must exercise both signature sign bits, got {seen_signs:?}" + ); + + // Garbage key bytes: both paths must reject the same way without panicking. + let mut garbage = [0u8; 33]; + garbage[0] = 0x05; + garbage[1..].fill(0xFF); + let bad_key = PublicKey::deserialize(&garbage).expect("type-tagged bytes parse"); + let prepared_bad = PreparedVerifyingKey::new(&bad_key); + let some_sig = [0x11u8; 64]; + assert_eq!( + bad_key.verify_signature(message, &some_sig), + prepared_bad.verify_signature(message, &some_sig) + ); + } + #[test] fn test_signature_with_lazy_cache() { let mut csprng = rng(); diff --git a/wacore/libsignal/src/core/curve/curve25519.rs b/wacore/libsignal/src/core/curve/curve25519.rs index fbd72fb24..ff6154434 100644 --- a/wacore/libsignal/src/core/curve/curve25519.rs +++ b/wacore/libsignal/src/core/curve/curve25519.rs @@ -46,6 +46,45 @@ pub struct PrivateKey { sign_bit: u8, } +/// The XEdDSA verify equation with the per-key derivations precomputed: +/// `minus_cap_a` is the negated Edwards form of the signer's public key for +/// the signature's sign bit, and `cap_a_bytes` its compressed encoding. The +/// single source of truth for verification; `PrivateKey::verify_signature` +/// derives the inputs per call, while cached verifiers reuse them. +pub(crate) fn verify_signature_prepared( + minus_cap_a: &EdwardsPoint, + cap_a_bytes: &[u8; 32], + message: &[&[u8]], + signature: &[u8; SIGNATURE_LENGTH], +) -> bool { + let mut cap_r = [0u8; 32]; + cap_r.copy_from_slice(&signature[..32]); + let mut s = [0u8; 32]; + s.copy_from_slice(&signature[32..]); + s[31] &= 0b0111_1111_u8; + if (s[31] & 0b1110_0000_u8) != 0 { + return false; + } + + let mut hash = Sha512::new(); + // Explicitly pass a slice to avoid generating multiple versions of update(). + hash.update(&cap_r[..]); + hash.update(cap_a_bytes); + for message_piece in message { + hash.update(message_piece); + } + let h = Scalar::from_bytes_mod_order_wide(&hash.finalize().into()); + + let cap_r_check_point = EdwardsPoint::vartime_double_scalar_mul_basepoint( + &h, + minus_cap_a, + &Scalar::from_bytes_mod_order(s), + ); + let cap_r_check = cap_r_check_point.compress(); + + bool::from(cap_r_check.as_bytes().ct_eq(&cap_r)) +} + impl PrivateKey { /// Computes the cached scalar, Edwards public key, and sign bit from a StaticSecret. #[inline] @@ -256,33 +295,7 @@ impl PrivateKey { None => return false, }; let cap_a = ed_pub_key_point.compress(); - let mut cap_r = [0u8; 32]; - cap_r.copy_from_slice(&signature[..32]); - let mut s = [0u8; 32]; - s.copy_from_slice(&signature[32..]); - s[31] &= 0b0111_1111_u8; - if (s[31] & 0b1110_0000_u8) != 0 { - return false; - } - let minus_cap_a = -ed_pub_key_point; - - let mut hash = Sha512::new(); - // Explicitly pass a slice to avoid generating multiple versions of update(). - hash.update(&cap_r[..]); - hash.update(cap_a.as_bytes()); - for message_piece in message { - hash.update(message_piece); - } - let h = Scalar::from_bytes_mod_order_wide(&hash.finalize().into()); - - let cap_r_check_point = EdwardsPoint::vartime_double_scalar_mul_basepoint( - &h, - &minus_cap_a, - &Scalar::from_bytes_mod_order(s), - ); - let cap_r_check = cap_r_check_point.compress(); - - bool::from(cap_r_check.as_bytes().ct_eq(&cap_r)) + verify_signature_prepared(&-ed_pub_key_point, cap_a.as_bytes(), message, signature) } pub fn derive_public_key_bytes(&self) -> [u8; PUBLIC_KEY_LENGTH] { diff --git a/wacore/libsignal/src/protocol/group_cipher.rs b/wacore/libsignal/src/protocol/group_cipher.rs index cebee62eb..13cae7ba2 100644 --- a/wacore/libsignal/src/protocol/group_cipher.rs +++ b/wacore/libsignal/src/protocol/group_cipher.rs @@ -210,9 +210,9 @@ pub async fn group_decrypt( } let signing_key = sender_key_state - .signing_key_public() + .signing_key_verifier() .map_err(|_| SignalProtocolError::InvalidSenderKeySession)?; - if !skm.verify_signature(&signing_key)? { + if !skm.verify_signature_prepared(signing_key)? { return Err(SignalProtocolError::SignatureValidationFailed); } diff --git a/wacore/libsignal/src/protocol/mod.rs b/wacore/libsignal/src/protocol/mod.rs index f3ce96ec5..18a2e6526 100644 --- a/wacore/libsignal/src/protocol/mod.rs +++ b/wacore/libsignal/src/protocol/mod.rs @@ -32,7 +32,7 @@ mod state; mod storage; mod stores; mod timestamp; -pub use crate::core::curve::{CurveError, KeyPair, PrivateKey, PublicKey}; +pub use crate::core::curve::{CurveError, KeyPair, PreparedVerifyingKey, PrivateKey, PublicKey}; pub use crate::core::{ Aci, DeviceId, Pni, ProtocolAddress, ServiceId, ServiceIdFixedWidthBinaryBytes, ServiceIdKind, }; diff --git a/wacore/libsignal/src/protocol/protocol.rs b/wacore/libsignal/src/protocol/protocol.rs index 5f076e1fd..34021a251 100644 --- a/wacore/libsignal/src/protocol/protocol.rs +++ b/wacore/libsignal/src/protocol/protocol.rs @@ -504,6 +504,21 @@ impl SenderKeyMessage { Ok(valid) } + /// Like [`Self::verify_signature`], against a cached verifier: the + /// per-key Edwards derivations are reused across messages instead of + /// recomputed per signature. + pub fn verify_signature_prepared( + &self, + signature_key: &crate::core::curve::PreparedVerifyingKey, + ) -> Result { + let valid = signature_key.verify_signature( + &self.serialized[..self.serialized.len() - Self::SIGNATURE_LEN], + &self.serialized[self.serialized.len() - Self::SIGNATURE_LEN..], + ); + + Ok(valid) + } + #[inline] pub fn message_version(&self) -> u8 { self.message_version diff --git a/wacore/libsignal/src/protocol/sender_keys.rs b/wacore/libsignal/src/protocol/sender_keys.rs index 5239906aa..684e2fc45 100644 --- a/wacore/libsignal/src/protocol/sender_keys.rs +++ b/wacore/libsignal/src/protocol/sender_keys.rs @@ -177,6 +177,10 @@ pub struct SenderKeyState { /// rebuilt lazily after a cold load. If a signing-key setter is ever /// added, it must reset this memo. signing_key_memo: std::sync::OnceLock, + /// Receive-side mirror of `signing_key_memo`: cached verifier whose + /// Edwards derivations are reused across every incoming message under + /// this sender key. Same lifecycle rules as above. + verifying_key_memo: std::sync::OnceLock, } // Manual impl with the signing key REDACTED: the protobuf state embeds the @@ -224,9 +228,22 @@ impl SenderKeyState { key.precompute_signing_cache(); let _ = signing_key_memo.set(key); } + let verifying_key_memo = std::sync::OnceLock::new(); + if signing_key_memo.get().is_none() { + // Receive-side state (no private key): this key will verify every + // incoming message, so build the verifier and derive its Edwards + // entries here, at SKDM processing, once per sender rotation. + // Send-side states never verify their own messages, so they skip + // even the verifier allocation; the memo builds lazily if ever + // asked. + let verifier = crate::core::curve::PreparedVerifyingKey::new(&signature_key); + verifier.precompute(); + let _ = verifying_key_memo.set(verifier); + } Self { state, signing_key_memo, + verifying_key_memo, } } @@ -234,6 +251,7 @@ impl SenderKeyState { Self { state, signing_key_memo: std::sync::OnceLock::new(), + verifying_key_memo: std::sync::OnceLock::new(), } } @@ -276,6 +294,23 @@ impl SenderKeyState { } } + /// Cached verifier for this sender's signing key; the Edwards + /// derivations warm on first use and persist with the in-memory state. + pub fn signing_key_verifier( + &self, + ) -> Result<&crate::core::curve::PreparedVerifyingKey, InvalidSenderKeySessionError> { + if let Some(verifier) = self.verifying_key_memo.get() { + return Ok(verifier); + } + let verifier = crate::core::curve::PreparedVerifyingKey::new(&self.signing_key_public()?); + // Benign race: concurrent firsts compute the same value. + let _ = self.verifying_key_memo.set(verifier); + Ok(self + .verifying_key_memo + .get() + .expect("set on the line above")) + } + pub fn signing_key_private(&self) -> Result { if let Some(key) = self.signing_key_memo.get() { return Ok(key.clone()); @@ -639,6 +674,19 @@ mod tests { .has_warm_signing_cache() ); + // Verifier memo: send-side states (private key present) skip even + // the allocation; it builds lazily if asked, is seeded eagerly only + // on receive-side creation, rebuilds after a cold load, and clones + // carry it. + assert!(state.verifying_key_memo.get().is_none()); + let _ = state.signing_key_verifier().expect("lazy build works"); + assert!(state.verifying_key_memo.get().is_some()); + let cold = SenderKeyState::from_protobuf(state.as_protobuf()); + assert!(cold.verifying_key_memo.get().is_none()); + let _ = cold.signing_key_verifier().expect("verifier"); + assert!(cold.verifying_key_memo.get().is_some()); + assert!(cold.clone().verifying_key_memo.get().is_some()); + // The memoized key still signs correctly. let msg = b"skmsg"; let sig = key.calculate_signature(msg, &mut rng).expect("sign");