Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
142 changes: 142 additions & 0 deletions wacore/libsignal/src/core/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,82 @@ 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).
cached: [OnceLock<Option<PreparedEdwards>>; 2],
}

impl PreparedVerifyingKey {
pub fn new(key: &PublicKey) -> Self {
let PublicKeyData::DjbPublicKey(mont) = key.key;
Self {
mont,
cached: [OnceLock::new(), OnceLock::new()],
}
}

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 +587,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
36 changes: 36 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,22 @@ impl SenderKeyState {
key.precompute_signing_cache();
let _ = signing_key_memo.set(key);
}
let verifying_key_memo = std::sync::OnceLock::new();
let _ = verifying_key_memo.set(crate::core::curve::PreparedVerifyingKey::new(
&signature_key,
));
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 +286,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 +666,15 @@ mod tests {
.has_warm_signing_cache()
);

// Verifier memo: seeded at creation, rebuilt lazily after a cold
// load, and clones carry it.
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