Skip to content

Commit 1b42fa5

Browse files
committed
fix(mtproto): harden PFS and session ownership
1 parent 8ccd22e commit 1b42fa5

31 files changed

Lines changed: 1955 additions & 432 deletions

‎native/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.

‎native/mtproto-rs/src/auth/auth_key.rs‎

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use tellers_mtproto_session::{Clock, Snapshot};
1919
use tellers_mtproto_transport::{Connection, Framing, PaddedIntermediate};
2020

2121
use crate::MtprotoError;
22+
use zeroize::Zeroize;
2223

2324
const PROD_RSA_PEM: &str = "-----BEGIN RSA PUBLIC KEY-----\n\
2425
MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g\n\
@@ -129,6 +130,15 @@ struct AuthAdapter {
129130
g_b: Option<Vec<u8>>,
130131
auth_key: Option<Vec<u8>>,
131132
expires_in: Option<i32>,
133+
expires_at: Option<i32>,
134+
time_offset_micros: Option<i64>,
135+
clock: fn() -> i64,
136+
}
137+
138+
impl Drop for AuthAdapter {
139+
fn drop(&mut self) {
140+
self.auth_key.zeroize();
141+
}
132142
}
133143

134144
impl AuthAdapter {
@@ -150,6 +160,9 @@ impl AuthAdapter {
150160
g_b: None,
151161
auth_key: None,
152162
expires_in: None,
163+
expires_at: None,
164+
time_offset_micros: None,
165+
clock: || SystemClock.unix_micros(),
153166
})
154167
}
155168
}
@@ -272,6 +285,9 @@ impl AuthorizationAdapter for AuthAdapter {
272285
let server_nonce = self.server_nonce.clone().ok_or_else(|| {
273286
tellers_mtproto_engine::Error::Authorization("missing server_nonce".into())
274287
})?;
288+
if ok.nonce != self.nonce || ok.server_nonce != server_nonce {
289+
return Err(tellers_mtproto_engine::Error::Authorization("DH envelope nonce mismatch".into()));
290+
}
275291
let new_nonce = self.new_nonce.clone().ok_or_else(|| {
276292
tellers_mtproto_engine::Error::Authorization("missing new_nonce".into())
277293
})?;
@@ -299,6 +315,18 @@ impl AuthorizationAdapter for AuthAdapter {
299315
));
300316
};
301317

318+
if inner.nonce != self.nonce || inner.server_nonce != server_nonce {
319+
return Err(tellers_mtproto_engine::Error::Authorization("DH inner nonce mismatch".into()));
320+
}
321+
let received_at = (self.clock)();
322+
if inner.server_time <= 0 {
323+
return Err(tellers_mtproto_engine::Error::Authorization("invalid DH server time".into()));
324+
}
325+
let expires_at = self.expires_in.map(|seconds| inner.server_time.checked_add(seconds)
326+
.filter(|expiry| *expiry > inner.server_time)
327+
.ok_or_else(|| tellers_mtproto_engine::Error::Authorization("invalid temporary key expiry".into())))
328+
.transpose()?;
329+
302330
use num_bigint::BigUint;
303331
let prime = BigUint::from_bytes_be(&inner.dh_prime);
304332
let g_a = BigUint::from_bytes_be(&inner.g_a);
@@ -310,7 +338,7 @@ impl AuthorizationAdapter for AuthAdapter {
310338
let prime_bytes = usize::try_from(prime.bits().div_ceil(8))
311339
.unwrap_or(256)
312340
.max(256);
313-
let mut b_bytes = vec![0_u8; prime_bytes];
341+
let mut b_bytes = zeroize::Zeroizing::new(vec![0_u8; prime_bytes]);
314342
fill_random(&mut b_bytes)
315343
.map_err(|e| tellers_mtproto_engine::Error::Authorization(e.to_string()))?;
316344
let b = BigUint::from_bytes_be(&b_bytes);
@@ -322,6 +350,8 @@ impl AuthorizationAdapter for AuthAdapter {
322350
let auth_key = derive_auth_key(&g_a, &b, &prime)
323351
.map_err(|e| tellers_mtproto_engine::Error::Authorization(e.to_string()))?;
324352
self.auth_key = Some(auth_key);
353+
self.expires_at = expires_at;
354+
self.time_offset_micros = Some((i64::from(inner.server_time) * 1_000_000).saturating_sub(received_at));
325355

326356
let client_inner = tellers_mtproto::transport::ClientDhInnerDataConstructor {
327357
nonce: self.nonce.clone(),
@@ -357,7 +387,7 @@ impl AuthorizationAdapter for AuthAdapter {
357387
) -> Result<(Vec<u8>, i64), tellers_mtproto_engine::Error> {
358388
let answer = decode_boxed::<SetClientDhParamsAnswer>(response)
359389
.map_err(|e| tellers_mtproto_engine::Error::Authorization(e.to_string()))?;
360-
let auth_key = self.auth_key.clone().ok_or_else(|| {
390+
let auth_key = self.auth_key.as_ref().ok_or_else(|| {
361391
tellers_mtproto_engine::Error::Authorization("missing auth_key".into())
362392
})?;
363393
let new_nonce = self.new_nonce.clone().ok_or_else(|| {
@@ -368,6 +398,9 @@ impl AuthorizationAdapter for AuthAdapter {
368398
})?;
369399
match answer {
370400
SetClientDhParamsAnswer::DhGenOk(ok) => {
401+
if ok.nonce != self.nonce || ok.server_nonce != server_nonce {
402+
return Err(tellers_mtproto_engine::Error::Authorization("DH completion nonce mismatch".into()));
403+
}
371404
let expected = new_nonce_hash(&i256_to_bytes(&new_nonce), &auth_key, 1);
372405
let got = i128_to_bytes(&ok.new_nonce_hash1);
373406
if got != expected {
@@ -377,13 +410,17 @@ impl AuthorizationAdapter for AuthAdapter {
377410
}
378411
let salt =
379412
initial_server_salt(&i256_to_bytes(&new_nonce), &i128_to_bytes(&server_nonce));
380-
Ok((auth_key, salt))
413+
Ok((self.auth_key.take().expect("validated auth key"), salt))
381414
}
382415
_ => Err(tellers_mtproto_engine::Error::Authorization(
383416
"dh_gen not ok".into(),
384417
)),
385418
}
386419
}
420+
421+
fn time_offset_micros(&self) -> Option<i64> {
422+
self.time_offset_micros
423+
}
387424
}
388425

389426
fn send_plain(
@@ -418,6 +455,9 @@ fn recv_plain(
418455
if n == 0 {
419456
return Err(MtprotoError::Message("connection closed".into()));
420457
}
458+
if input.len().saturating_add(n) > 1024 * 1024 + 4 {
459+
return Err(MtprotoError::Message("authorization response exceeds size limit".into()));
460+
}
421461
input.extend_from_slice(&buf[..n]);
422462
match framing.decode(&input) {
423463
Ok(packet) => {
@@ -446,10 +486,11 @@ pub fn create_temp_auth_key(
446486
framing: &mut PaddedIntermediate,
447487
snapshot: &mut Snapshot,
448488
expires_in: i32,
449-
) -> Result<(), MtprotoError> {
489+
) -> Result<i32, MtprotoError> {
450490
let mut adapter = AuthAdapter::with_pem(snapshot.dc_id, rsa_pem().to_string())?;
451491
adapter.expires_in = Some(expires_in);
452-
run_exchange(adapter, conn, framing, snapshot)
492+
run_exchange(adapter, conn, framing, snapshot)?
493+
.ok_or_else(|| MtprotoError::Message("missing temporary key expiry".into()))
453494
}
454495

455496
pub fn create_auth_key_with_pem(
@@ -459,15 +500,15 @@ pub fn create_auth_key_with_pem(
459500
pem: &str,
460501
) -> Result<(), MtprotoError> {
461502
let adapter = AuthAdapter::with_pem(snapshot.dc_id, pem.to_string())?;
462-
run_exchange(adapter, conn, framing, snapshot)
503+
run_exchange(adapter, conn, framing, snapshot).map(|_| ())
463504
}
464505

465506
fn run_exchange(
466507
adapter: AuthAdapter,
467508
conn: &mut dyn Connection,
468509
framing: &mut PaddedIntermediate,
469510
snapshot: &mut Snapshot,
470-
) -> Result<(), MtprotoError> {
511+
) -> Result<Option<i32>, MtprotoError> {
471512
let clock = SystemClock;
472513
let mut auth = Authorization::new(adapter);
473514

@@ -498,7 +539,7 @@ fn run_exchange(
498539
if next.is_some() || auth.phase() != AuthorizationPhase::Authorized {
499540
return Err(MtprotoError::Message("auth key not installed".into()));
500541
}
501-
Ok(())
542+
Ok(auth.into_adapter().expires_at)
502543
}
503544

504545
/// Padded-intermediate returns payload + 0..=15 extra bytes. Plain envelopes

0 commit comments

Comments
 (0)