Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions wacore/libsignal/src/core/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<PreparedEdwards>>; 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.
Expand Down Expand Up @@ -511,6 +598,72 @@ mod tests {
rand::make_rng::<rand::rngs::StdRng>()
}

/// 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();
Expand Down
67 changes: 40 additions & 27 deletions wacore/libsignal/src/core/curve/curve25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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] {
Expand Down
4 changes: 2 additions & 2 deletions wacore/libsignal/src/protocol/group_cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion wacore/libsignal/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
15 changes: 15 additions & 0 deletions wacore/libsignal/src/protocol/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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
Expand Down
48 changes: 48 additions & 0 deletions wacore/libsignal/src/protocol/sender_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PrivateKey>,
/// 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<crate::core::curve::PreparedVerifyingKey>,
}

// Manual impl with the signing key REDACTED: the protobuf state embeds the
Expand Down Expand Up @@ -224,16 +228,30 @@ 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,
}
}

pub(crate) fn from_protobuf(state: SenderKeyStateStructure) -> Self {
Self {
state,
signing_key_memo: std::sync::OnceLock::new(),
verifying_key_memo: std::sync::OnceLock::new(),
}
}

Expand Down Expand Up @@ -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<PrivateKey, InvalidSenderKeySessionError> {
if let Some(key) = self.signing_key_memo.get() {
return Ok(key.clone());
Expand Down Expand Up @@ -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");
Expand Down
Loading