Skip to content

Commit 5293caa

Browse files
protocol: Check that base keys are torsion free and in range
Co-authored-by: Jordan Rose <[email protected]>
1 parent 5d37e8b commit 5293caa

File tree

5 files changed

+270
-1
lines changed

5 files changed

+270
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/core/src/curve.rs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod utils;
99
use std::cmp::Ordering;
1010
use std::fmt;
1111

12-
use curve25519_dalek::scalar;
12+
use curve25519_dalek::{MontgomeryPoint, scalar};
1313
use rand::{CryptoRng, Rng};
1414
use subtle::ConstantTimeEq;
1515

@@ -150,6 +150,33 @@ impl PublicKey {
150150
PublicKeyData::DjbPublicKey(_) => KeyType::Djb,
151151
}
152152
}
153+
154+
fn is_torsion_free(&self) -> bool {
155+
match &self.key {
156+
PublicKeyData::DjbPublicKey(k) => {
157+
let mont_point = MontgomeryPoint(*k);
158+
mont_point
159+
.to_edwards(0)
160+
.is_some_and(|ed| ed.is_torsion_free())
161+
}
162+
}
163+
}
164+
165+
fn scalar_is_in_range(&self) -> bool {
166+
match &self.key {
167+
PublicKeyData::DjbPublicKey(k) => {
168+
// it is not true that the scalar is greater than 2^255 - 19
169+
// specifically, it is not true that either the high bit is set
170+
// or that the high 247 bits are all 1 and the bottom byte is >(2^8 - 19)
171+
!(k[31] & 0b1000_0000_u8 != 0
172+
|| (k[0] >= 0u8.wrapping_sub(19) && k[1..31] == [0xFFu8; 30] && k[31] == 0x7F))
173+
}
174+
}
175+
}
176+
177+
pub fn is_canonical(&self) -> bool {
178+
self.is_torsion_free() && self.scalar_is_in_range()
179+
}
153180
}
154181

155182
impl TryFrom<&[u8]> for PublicKey {
@@ -356,6 +383,8 @@ impl TryFrom<PrivateKey> for KeyPair {
356383
#[cfg(test)]
357384
mod tests {
358385
use assert_matches::assert_matches;
386+
use const_str::hex;
387+
use curve25519_dalek::constants::EIGHT_TORSION;
359388
use rand::TryRngCore as _;
360389
use rand::rngs::OsRng;
361390

@@ -433,4 +462,100 @@ mod tests {
433462
let error = Box::new(error) as Box<dyn std::error::Error>;
434463
assert_matches!(error.downcast_ref(), Some(CurveError::BadKeyType(_)));
435464
}
465+
466+
#[test]
467+
fn honest_keys_are_torsion_free() {
468+
let mut csprng = OsRng.unwrap_err();
469+
let key_pair = KeyPair::generate(&mut csprng);
470+
assert!(key_pair.public_key.is_torsion_free());
471+
}
472+
473+
#[test]
474+
fn tweaked_keys_are_not_torsion_free() {
475+
let mut csprng = OsRng.unwrap_err();
476+
let key_pair = KeyPair::generate(&mut csprng);
477+
let pk_bytes: [u8; 32] = key_pair.public_key.public_key_bytes().try_into().unwrap();
478+
let mont_pt = MontgomeryPoint(pk_bytes);
479+
let ed_pt = mont_pt.to_edwards(0).unwrap();
480+
for t in EIGHT_TORSION.iter().skip(1) {
481+
let tweaked = ed_pt + *t; // add a torsion point
482+
let tweaked_mont = tweaked.to_montgomery();
483+
let tweaked_pk_bytes: [u8; 32] = tweaked_mont.to_bytes();
484+
let tweaked_pk = PublicKey::from_djb_public_key_bytes(&tweaked_pk_bytes).unwrap();
485+
assert!(!tweaked_pk.is_torsion_free());
486+
}
487+
}
488+
489+
#[test]
490+
fn keys_with_the_high_bit_set_are_out_of_range() {
491+
assert!(
492+
PublicKey::from_djb_public_key_bytes(&[0; 32])
493+
.expect("structurally valid")
494+
.scalar_is_in_range(),
495+
"0 should be in range"
496+
);
497+
assert!(
498+
!PublicKey::from_djb_public_key_bytes(&hex!(
499+
"0000000000000000000000000000000000000000000000000000000000000080"
500+
))
501+
.expect("structurally valid")
502+
.scalar_is_in_range(),
503+
"2^255 should be out of range"
504+
);
505+
assert!(
506+
!PublicKey::from_djb_public_key_bytes(&[0xFF; 32])
507+
.expect("structurally valid")
508+
.scalar_is_in_range(),
509+
"2^256 - 1 should be out of range"
510+
);
511+
{
512+
let mut csprng = OsRng.unwrap_err();
513+
let key_pair = KeyPair::generate(&mut csprng);
514+
assert!(key_pair.public_key.scalar_is_in_range());
515+
let mut pk_bytes: [u8; 32] = key_pair.public_key.public_key_bytes().try_into().unwrap();
516+
assert!(pk_bytes[31] & 0x80 == 0);
517+
pk_bytes[31] |= 0x80;
518+
assert!(
519+
!PublicKey::from_djb_public_key_bytes(&pk_bytes)
520+
.expect("structurally valid")
521+
.scalar_is_in_range(),
522+
">2^255 should be out of range"
523+
);
524+
}
525+
}
526+
527+
#[test]
528+
fn keys_above_the_prime_modulus_are_out_of_range() {
529+
// Curve25519 scalars use a little-endian representation.
530+
let two_to_the_255_minus_one =
531+
hex!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f");
532+
533+
for i in 1..=19 {
534+
let mut pk_bytes = two_to_the_255_minus_one;
535+
pk_bytes[0] -= i;
536+
pk_bytes[0] += 1; // because our original literal was 2^255 - 1
537+
assert!(
538+
!PublicKey::from_djb_public_key_bytes(&pk_bytes)
539+
.expect("structurally valid")
540+
.scalar_is_in_range(),
541+
"2^255 - {i} should be out of range",
542+
);
543+
544+
let mut canonical_representative = [0; 32];
545+
canonical_representative[0] = 19 - i;
546+
547+
assert_eq!(
548+
MontgomeryPoint(pk_bytes),
549+
MontgomeryPoint(canonical_representative)
550+
);
551+
}
552+
553+
let mut pk_bytes = two_to_the_255_minus_one;
554+
pk_bytes[0] -= 19; // resulting in the value 2^255 - 20
555+
assert!(
556+
PublicKey::from_djb_public_key_bytes(&pk_bytes)
557+
.expect("structurally valid")
558+
.scalar_is_in_range()
559+
);
560+
}
436561
}

rust/protocol/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ mlkem1024 = []
5656
[dev-dependencies]
5757
clap = { workspace = true, features = ["derive"] }
5858
criterion = { workspace = true }
59+
curve25519-dalek = { workspace = true, features = ["digest"] }
5960
env_logger = { workspace = true }
6061
futures-util = { workspace = true }
6162
proptest = { workspace = true }

rust/protocol/src/ratchet.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ pub(crate) fn initialize_alice_session<R: Rng + CryptoRng>(
139139
pub(crate) fn initialize_bob_session(
140140
parameters: &BobSignalProtocolParameters,
141141
) -> Result<SessionState> {
142+
// validate their base key
143+
if !parameters.their_base_key().is_canonical() {
144+
return Err(SignalProtocolError::InvalidMessage(
145+
crate::CiphertextMessageType::PreKey,
146+
"incoming base key is invalid",
147+
));
148+
}
149+
142150
let local_identity = parameters.our_identity_key_pair().identity_key();
143151

144152
let mut secrets = Vec::with_capacity(32 * 6);

rust/protocol/tests/ratchet.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use libsignal_protocol::*;
77

88
mod support;
9+
use curve25519_dalek::constants::EIGHT_TORSION;
10+
use curve25519_dalek::montgomery::MontgomeryPoint;
911
use rand::TryRngCore as _;
1012
use support::*;
1113

@@ -77,3 +79,135 @@ fn test_alice_and_bob_agree_on_chain_keys_with_kyber() -> Result<(), SignalProto
7779

7880
Ok(())
7981
}
82+
83+
#[test]
84+
fn test_bob_rejects_torsioned_basekey() -> Result<(), SignalProtocolError> {
85+
let mut csprng = rand::rngs::OsRng.unwrap_err();
86+
87+
let alice_identity_key_pair = IdentityKeyPair::generate(&mut csprng);
88+
let alice_base_key_pair = KeyPair::generate(&mut csprng);
89+
90+
let bob_ephemeral_key_pair = KeyPair::generate(&mut csprng);
91+
let bob_identity_key_pair = IdentityKeyPair::generate(&mut csprng);
92+
let bob_signed_pre_key_pair = KeyPair::generate(&mut csprng);
93+
94+
let bob_kyber_pre_key_pair = kem::KeyPair::generate(kem::KeyType::Kyber1024, &mut csprng);
95+
96+
let alice_parameters = AliceSignalProtocolParameters::new(
97+
alice_identity_key_pair,
98+
alice_base_key_pair,
99+
*bob_identity_key_pair.identity_key(),
100+
bob_signed_pre_key_pair.public_key,
101+
bob_ephemeral_key_pair.public_key,
102+
bob_kyber_pre_key_pair.public_key.clone(),
103+
UsePQRatchet::Yes,
104+
);
105+
106+
let alice_record = initialize_alice_session_record(&alice_parameters, &mut csprng)?;
107+
108+
assert_eq!(
109+
KYBER_AWARE_MESSAGE_VERSION,
110+
alice_record.session_version().expect("must have a version")
111+
);
112+
113+
let kyber_ciphertext = alice_record
114+
.get_kyber_ciphertext()
115+
.expect("must have session")
116+
.expect("must have kyber ciphertext")
117+
.clone()
118+
.into_boxed_slice();
119+
120+
let tweaked_alice_base_key = {
121+
let pk_bytes: [u8; 32] = alice_base_key_pair
122+
.public_key
123+
.public_key_bytes()
124+
.try_into()
125+
.unwrap();
126+
let mont_pt = MontgomeryPoint(pk_bytes);
127+
let ed_pt = mont_pt.to_edwards(0).unwrap();
128+
let tweaked_ed = ed_pt + EIGHT_TORSION[1];
129+
let tweaked_mont = tweaked_ed.to_montgomery();
130+
let tweaked_pk_bytes: [u8; 32] = tweaked_mont.to_bytes();
131+
PublicKey::from_djb_public_key_bytes(&tweaked_pk_bytes).unwrap()
132+
};
133+
134+
let bob_parameters = BobSignalProtocolParameters::new(
135+
bob_identity_key_pair,
136+
bob_signed_pre_key_pair,
137+
None,
138+
bob_ephemeral_key_pair,
139+
bob_kyber_pre_key_pair,
140+
*alice_identity_key_pair.identity_key(),
141+
tweaked_alice_base_key,
142+
&kyber_ciphertext,
143+
UsePQRatchet::Yes,
144+
);
145+
146+
assert!(initialize_bob_session_record(&bob_parameters).is_err());
147+
148+
Ok(())
149+
}
150+
151+
#[test]
152+
fn test_bob_rejects_highbit_basekey() -> Result<(), SignalProtocolError> {
153+
let mut csprng = rand::rngs::OsRng.unwrap_err();
154+
155+
let alice_identity_key_pair = IdentityKeyPair::generate(&mut csprng);
156+
let alice_base_key_pair = KeyPair::generate(&mut csprng);
157+
158+
let bob_ephemeral_key_pair = KeyPair::generate(&mut csprng);
159+
let bob_identity_key_pair = IdentityKeyPair::generate(&mut csprng);
160+
let bob_signed_pre_key_pair = KeyPair::generate(&mut csprng);
161+
162+
let bob_kyber_pre_key_pair = kem::KeyPair::generate(kem::KeyType::Kyber1024, &mut csprng);
163+
164+
let alice_parameters = AliceSignalProtocolParameters::new(
165+
alice_identity_key_pair,
166+
alice_base_key_pair,
167+
*bob_identity_key_pair.identity_key(),
168+
bob_signed_pre_key_pair.public_key,
169+
bob_ephemeral_key_pair.public_key,
170+
bob_kyber_pre_key_pair.public_key.clone(),
171+
UsePQRatchet::Yes,
172+
);
173+
174+
let alice_record = initialize_alice_session_record(&alice_parameters, &mut csprng)?;
175+
176+
assert_eq!(
177+
KYBER_AWARE_MESSAGE_VERSION,
178+
alice_record.session_version().expect("must have a version")
179+
);
180+
181+
let kyber_ciphertext = alice_record
182+
.get_kyber_ciphertext()
183+
.expect("must have session")
184+
.expect("must have kyber ciphertext")
185+
.clone()
186+
.into_boxed_slice();
187+
188+
let tweaked_alice_base_key = {
189+
let mut pk_bytes: [u8; 32] = alice_base_key_pair
190+
.public_key
191+
.public_key_bytes()
192+
.try_into()
193+
.unwrap();
194+
pk_bytes[31] |= 0b1000_0000_u8;
195+
PublicKey::from_djb_public_key_bytes(&pk_bytes).unwrap()
196+
};
197+
198+
let bob_parameters = BobSignalProtocolParameters::new(
199+
bob_identity_key_pair,
200+
bob_signed_pre_key_pair,
201+
None,
202+
bob_ephemeral_key_pair,
203+
bob_kyber_pre_key_pair,
204+
*alice_identity_key_pair.identity_key(),
205+
tweaked_alice_base_key,
206+
&kyber_ciphertext,
207+
UsePQRatchet::Yes,
208+
);
209+
210+
assert!(initialize_bob_session_record(&bob_parameters).is_err());
211+
212+
Ok(())
213+
}

0 commit comments

Comments
 (0)