telemt/src/proxy/handshake.rs

696 lines
24 KiB
Rust

//! MTProto Handshake
#![allow(dead_code)]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use tracing::{debug, warn, trace};
use zeroize::Zeroize;
use crate::crypto::{sha256, AesCtr, SecureRandom};
use rand::Rng;
use crate::protocol::constants::*;
use crate::protocol::tls;
use crate::stream::{FakeTlsReader, FakeTlsWriter, CryptoReader, CryptoWriter};
use crate::error::{ProxyError, HandshakeResult};
use crate::stats::ReplayChecker;
use crate::config::ProxyConfig;
use crate::tls_front::{TlsFrontCache, emulator};
fn decode_user_secrets(
config: &ProxyConfig,
preferred_user: Option<&str>,
) -> Vec<(String, Vec<u8>)> {
let mut secrets = Vec::with_capacity(config.access.users.len());
if let Some(preferred) = preferred_user
&& let Some(secret_hex) = config.access.users.get(preferred)
&& let Ok(bytes) = hex::decode(secret_hex)
{
secrets.push((preferred.to_string(), bytes));
}
for (name, secret_hex) in &config.access.users {
if preferred_user.is_some_and(|preferred| preferred == name.as_str()) {
continue;
}
if let Ok(bytes) = hex::decode(secret_hex) {
secrets.push((name.clone(), bytes));
}
}
secrets
}
/// Result of successful handshake
///
/// Key material (`dec_key`, `dec_iv`, `enc_key`, `enc_iv`) is
/// zeroized on drop.
#[derive(Debug, Clone)]
pub struct HandshakeSuccess {
/// Authenticated user name
pub user: String,
/// Target datacenter index
pub dc_idx: i16,
/// Protocol variant (abridged/intermediate/secure)
pub proto_tag: ProtoTag,
/// Decryption key and IV (for reading from client)
pub dec_key: [u8; 32],
pub dec_iv: u128,
/// Encryption key and IV (for writing to client)
pub enc_key: [u8; 32],
pub enc_iv: u128,
/// Client address
pub peer: SocketAddr,
/// Whether TLS was used
pub is_tls: bool,
}
impl Drop for HandshakeSuccess {
fn drop(&mut self) {
self.dec_key.zeroize();
self.dec_iv.zeroize();
self.enc_key.zeroize();
self.enc_iv.zeroize();
}
}
/// Handle fake TLS handshake
pub async fn handle_tls_handshake<R, W>(
handshake: &[u8],
reader: R,
mut writer: W,
peer: SocketAddr,
config: &ProxyConfig,
replay_checker: &ReplayChecker,
rng: &SecureRandom,
tls_cache: Option<Arc<TlsFrontCache>>,
) -> HandshakeResult<(FakeTlsReader<R>, FakeTlsWriter<W>, String), R, W>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
debug!(peer = %peer, handshake_len = handshake.len(), "Processing TLS handshake");
if handshake.len() < tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN + 1 {
debug!(peer = %peer, "TLS handshake too short");
return HandshakeResult::BadClient { reader, writer };
}
let digest = &handshake[tls::TLS_DIGEST_POS..tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN];
let digest_half = &digest[..tls::TLS_DIGEST_HALF_LEN];
if replay_checker.check_and_add_tls_digest(digest_half) {
warn!(peer = %peer, "TLS replay attack detected (duplicate digest)");
return HandshakeResult::BadClient { reader, writer };
}
let secrets = decode_user_secrets(config, None);
let validation = match tls::validate_tls_handshake(
handshake,
&secrets,
config.access.ignore_time_skew,
) {
Some(v) => v,
None => {
debug!(
peer = %peer,
ignore_time_skew = config.access.ignore_time_skew,
"TLS handshake validation failed - no matching user or time skew"
);
return HandshakeResult::BadClient { reader, writer };
}
};
let secret = match secrets.iter().find(|(name, _)| *name == validation.user) {
Some((_, s)) => s,
None => return HandshakeResult::BadClient { reader, writer },
};
let cached = if config.censorship.tls_emulation {
if let Some(cache) = tls_cache.as_ref() {
let selected_domain = if let Some(sni) = tls::extract_sni_from_client_hello(handshake) {
if cache.contains_domain(&sni).await {
sni
} else {
config.censorship.tls_domain.clone()
}
} else {
config.censorship.tls_domain.clone()
};
let cached_entry = cache.get(&selected_domain).await;
let use_full_cert_payload = cache
.take_full_cert_budget_for_ip(
peer.ip(),
Duration::from_secs(config.censorship.tls_full_cert_ttl_secs),
)
.await;
Some((cached_entry, use_full_cert_payload))
} else {
None
}
} else {
None
};
let alpn_list = if config.censorship.alpn_enforce {
tls::extract_alpn_from_client_hello(handshake)
} else {
Vec::new()
};
let selected_alpn = if config.censorship.alpn_enforce {
if alpn_list.iter().any(|p| p == b"h2") {
Some(b"h2".to_vec())
} else if alpn_list.iter().any(|p| p == b"http/1.1") {
Some(b"http/1.1".to_vec())
} else {
None
}
} else {
None
};
let response = if let Some((cached_entry, use_full_cert_payload)) = cached {
emulator::build_emulated_server_hello(
secret,
&validation.digest,
&validation.session_id,
&cached_entry,
use_full_cert_payload,
rng,
selected_alpn.clone(),
config.censorship.tls_new_session_tickets,
)
} else {
tls::build_server_hello(
secret,
&validation.digest,
&validation.session_id,
config.censorship.fake_cert_len,
rng,
selected_alpn.clone(),
config.censorship.tls_new_session_tickets,
)
};
// Optional anti-fingerprint delay before sending ServerHello.
if config.censorship.server_hello_delay_max_ms > 0 {
let min = config.censorship.server_hello_delay_min_ms;
let max = config.censorship.server_hello_delay_max_ms.max(min);
let delay_ms = if max == min {
max
} else {
rand::rng().random_range(min..=max)
};
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
debug!(peer = %peer, response_len = response.len(), "Sending TLS ServerHello");
if let Err(e) = writer.write_all(&response).await {
warn!(peer = %peer, error = %e, "Failed to write TLS ServerHello");
return HandshakeResult::Error(ProxyError::Io(e));
}
if let Err(e) = writer.flush().await {
warn!(peer = %peer, error = %e, "Failed to flush TLS ServerHello");
return HandshakeResult::Error(ProxyError::Io(e));
}
debug!(
peer = %peer,
user = %validation.user,
"TLS handshake successful"
);
HandshakeResult::Success((
FakeTlsReader::new(reader),
FakeTlsWriter::new(writer),
validation.user,
))
}
/// Handle MTProto obfuscation handshake
pub async fn handle_mtproto_handshake<R, W>(
handshake: &[u8; HANDSHAKE_LEN],
reader: R,
writer: W,
peer: SocketAddr,
config: &ProxyConfig,
replay_checker: &ReplayChecker,
is_tls: bool,
preferred_user: Option<&str>,
) -> HandshakeResult<(CryptoReader<R>, CryptoWriter<W>, HandshakeSuccess), R, W>
where
R: AsyncRead + Unpin + Send,
W: AsyncWrite + Unpin + Send,
{
// Log only the length — the raw bytes contain dec_prekey_iv (key-derivable material).
trace!(peer = %peer, handshake_len = handshake.len(), "MTProto handshake received");
let dec_prekey_iv = &handshake[SKIP_LEN..SKIP_LEN + PREKEY_LEN + IV_LEN];
if replay_checker.check_and_add_handshake(dec_prekey_iv) {
warn!(peer = %peer, "MTProto replay attack detected");
return HandshakeResult::BadClient { reader, writer };
}
let enc_prekey_iv: Vec<u8> = dec_prekey_iv.iter().rev().copied().collect();
let decoded_users = decode_user_secrets(config, preferred_user);
for (user, secret) in decoded_users {
let dec_prekey = &dec_prekey_iv[..PREKEY_LEN];
let dec_iv_bytes = &dec_prekey_iv[PREKEY_LEN..];
let mut dec_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
dec_key_input.extend_from_slice(dec_prekey);
dec_key_input.extend_from_slice(&secret);
let dec_key = sha256(&dec_key_input);
let mut dec_iv_arr = [0u8; IV_LEN];
dec_iv_arr.copy_from_slice(dec_iv_bytes);
let dec_iv = u128::from_be_bytes(dec_iv_arr);
let mut decryptor = AesCtr::new(&dec_key, dec_iv);
let decrypted = decryptor.decrypt(handshake);
let mut tag_bytes = [0u8; 4];
tag_bytes.copy_from_slice(&decrypted[PROTO_TAG_POS..PROTO_TAG_POS + 4]);
let proto_tag = match ProtoTag::from_bytes(tag_bytes) {
Some(tag) => tag,
None => continue,
};
let mode_ok = match proto_tag {
ProtoTag::Secure => {
if is_tls {
config.general.modes.tls || config.general.modes.secure
} else {
config.general.modes.secure || config.general.modes.tls
}
}
ProtoTag::Intermediate | ProtoTag::Abridged => config.general.modes.classic,
};
if !mode_ok {
debug!(peer = %peer, user = %user, proto = ?proto_tag, "Mode not enabled");
continue;
}
let mut dc_idx_bytes = [0u8; 2];
dc_idx_bytes.copy_from_slice(&decrypted[DC_IDX_POS..DC_IDX_POS + 2]);
let dc_idx = i16::from_le_bytes(dc_idx_bytes);
let enc_prekey = &enc_prekey_iv[..PREKEY_LEN];
let enc_iv_bytes = &enc_prekey_iv[PREKEY_LEN..];
let mut enc_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
enc_key_input.extend_from_slice(enc_prekey);
enc_key_input.extend_from_slice(&secret);
let enc_key = sha256(&enc_key_input);
let mut enc_iv_arr = [0u8; IV_LEN];
enc_iv_arr.copy_from_slice(enc_iv_bytes);
let enc_iv = u128::from_be_bytes(enc_iv_arr);
let encryptor = AesCtr::new(&enc_key, enc_iv);
let success = HandshakeSuccess {
user: user.clone(),
dc_idx,
proto_tag,
dec_key,
dec_iv,
enc_key,
enc_iv,
peer,
is_tls,
};
debug!(
peer = %peer,
user = %user,
dc = dc_idx,
proto = ?proto_tag,
tls = is_tls,
"MTProto handshake successful"
);
let max_pending = config.general.crypto_pending_buffer;
return HandshakeResult::Success((
CryptoReader::new(reader, decryptor),
CryptoWriter::new(writer, encryptor, max_pending),
success,
));
}
debug!(peer = %peer, "MTProto handshake: no matching user found");
HandshakeResult::BadClient { reader, writer }
}
/// Generate nonce for Telegram connection
// TEMPORARY: panic is the correct sentinel for catastrophic CSPRNG failure here; proper
// Result-propagation fix is tracked in .David_docs/deferred_generate_tg_nonce_panic.md.
#[allow(clippy::panic)]
pub fn generate_tg_nonce(
proto_tag: ProtoTag,
dc_idx: i16,
_client_dec_key: &[u8; 32],
_client_dec_iv: u128,
client_enc_key: &[u8; 32],
client_enc_iv: u128,
rng: &SecureRandom,
fast_mode: bool,
) -> ([u8; HANDSHAKE_LEN], [u8; 32], u128, [u8; 32], u128) {
// The probability of any single candidate being rejected is roughly 1/256
// (first-byte filter). After 1000 attempts without a valid nonce the CSPRNG
// has failed catastrophically; we must not proceed with a broken RNG.
for _ in 0..1000 {
let bytes = rng.bytes(HANDSHAKE_LEN);
let mut nonce = [0u8; HANDSHAKE_LEN];
nonce.copy_from_slice(&bytes);
if RESERVED_NONCE_FIRST_BYTES.contains(&nonce[0]) { continue; }
let first_four = [nonce[0], nonce[1], nonce[2], nonce[3]];
if RESERVED_NONCE_BEGINNINGS.contains(&first_four) { continue; }
let continue_four = [nonce[4], nonce[5], nonce[6], nonce[7]];
if RESERVED_NONCE_CONTINUES.contains(&continue_four) { continue; }
nonce[PROTO_TAG_POS..PROTO_TAG_POS + 4].copy_from_slice(&proto_tag.to_bytes());
// CRITICAL: write dc_idx so upstream DC knows where to route
nonce[DC_IDX_POS..DC_IDX_POS + 2].copy_from_slice(&dc_idx.to_le_bytes());
if fast_mode {
let mut key_iv = Vec::with_capacity(KEY_LEN + IV_LEN);
key_iv.extend_from_slice(client_enc_key);
key_iv.extend_from_slice(&client_enc_iv.to_be_bytes());
key_iv.reverse(); // Python/C behavior: reversed enc_key+enc_iv in nonce
nonce[SKIP_LEN..SKIP_LEN + KEY_LEN + IV_LEN].copy_from_slice(&key_iv);
}
let enc_key_iv = &nonce[SKIP_LEN..SKIP_LEN + KEY_LEN + IV_LEN];
let dec_key_iv: Vec<u8> = enc_key_iv.iter().rev().copied().collect();
let mut tg_enc_key = [0u8; KEY_LEN];
tg_enc_key.copy_from_slice(&enc_key_iv[..KEY_LEN]);
let mut tg_enc_iv_bytes = [0u8; IV_LEN];
tg_enc_iv_bytes.copy_from_slice(&enc_key_iv[KEY_LEN..]);
let tg_enc_iv = u128::from_be_bytes(tg_enc_iv_bytes);
let mut tg_dec_key = [0u8; KEY_LEN];
tg_dec_key.copy_from_slice(&dec_key_iv[..KEY_LEN]);
let mut tg_dec_iv_bytes = [0u8; IV_LEN];
tg_dec_iv_bytes.copy_from_slice(&dec_key_iv[KEY_LEN..]);
let tg_dec_iv = u128::from_be_bytes(tg_dec_iv_bytes);
return (nonce, tg_enc_key, tg_enc_iv, tg_dec_key, tg_dec_iv);
}
panic!("generate_tg_nonce: CSPRNG produced 1000 consecutive reserved-pattern nonces — RNG is compromised");
}
/// Encrypt nonce for sending to Telegram and return cipher objects with correct counter state
pub fn encrypt_tg_nonce_with_ciphers(nonce: &[u8; HANDSHAKE_LEN]) -> (Vec<u8>, AesCtr, AesCtr) {
let enc_key_iv = &nonce[SKIP_LEN..SKIP_LEN + KEY_LEN + IV_LEN];
let dec_key_iv: Vec<u8> = enc_key_iv.iter().rev().copied().collect();
let mut enc_key = [0u8; KEY_LEN];
enc_key.copy_from_slice(&enc_key_iv[..KEY_LEN]);
let mut enc_iv_bytes = [0u8; IV_LEN];
enc_iv_bytes.copy_from_slice(&enc_key_iv[KEY_LEN..]);
let enc_iv = u128::from_be_bytes(enc_iv_bytes);
let mut dec_key = [0u8; KEY_LEN];
dec_key.copy_from_slice(&dec_key_iv[..KEY_LEN]);
let mut dec_iv_bytes = [0u8; IV_LEN];
dec_iv_bytes.copy_from_slice(&dec_key_iv[KEY_LEN..]);
let dec_iv = u128::from_be_bytes(dec_iv_bytes);
let mut encryptor = AesCtr::new(&enc_key, enc_iv);
let encrypted_full = encryptor.encrypt(nonce); // counter: 0 → 4
let mut result = nonce[..PROTO_TAG_POS].to_vec();
result.extend_from_slice(&encrypted_full[PROTO_TAG_POS..]);
let decryptor = AesCtr::new(&dec_key, dec_iv);
(result, encryptor, decryptor)
}
/// Encrypt nonce for sending to Telegram (legacy function for compatibility)
pub fn encrypt_tg_nonce(nonce: &[u8; HANDSHAKE_LEN]) -> Vec<u8> {
let (encrypted, _, _) = encrypt_tg_nonce_with_ciphers(nonce);
encrypted
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_tg_nonce() {
let client_dec_key = [0x42u8; 32];
let client_dec_iv = 12345u128;
let client_enc_key = [0x24u8; 32];
let client_enc_iv = 54321u128;
let rng = SecureRandom::new();
let (nonce, _tg_enc_key, _tg_enc_iv, _tg_dec_key, _tg_dec_iv) =
generate_tg_nonce(
ProtoTag::Secure,
2,
&client_dec_key,
client_dec_iv,
&client_enc_key,
client_enc_iv,
&rng,
false,
);
assert_eq!(nonce.len(), HANDSHAKE_LEN);
let tag_bytes: [u8; 4] = nonce[PROTO_TAG_POS..PROTO_TAG_POS + 4].try_into().unwrap();
assert_eq!(ProtoTag::from_bytes(tag_bytes), Some(ProtoTag::Secure));
}
#[test]
fn test_encrypt_tg_nonce() {
let client_dec_key = [0x42u8; 32];
let client_dec_iv = 12345u128;
let client_enc_key = [0x24u8; 32];
let client_enc_iv = 54321u128;
let rng = SecureRandom::new();
let (nonce, _, _, _, _) =
generate_tg_nonce(
ProtoTag::Secure,
2,
&client_dec_key,
client_dec_iv,
&client_enc_key,
client_enc_iv,
&rng,
false,
);
let encrypted = encrypt_tg_nonce(&nonce);
assert_eq!(encrypted.len(), HANDSHAKE_LEN);
assert_eq!(&encrypted[..PROTO_TAG_POS], &nonce[..PROTO_TAG_POS]);
assert_ne!(&encrypted[PROTO_TAG_POS..], &nonce[PROTO_TAG_POS..]);
}
#[test]
fn test_handshake_success_zeroize_on_drop() {
let success = HandshakeSuccess {
user: "test".to_string(),
dc_idx: 2,
proto_tag: ProtoTag::Secure,
dec_key: [0xAA; 32],
dec_iv: 0xBBBBBBBB,
enc_key: [0xCC; 32],
enc_iv: 0xDDDDDDDD,
peer: "127.0.0.1:1234".parse().unwrap(),
is_tls: true,
};
assert_eq!(success.dec_key, [0xAA; 32]);
assert_eq!(success.enc_key, [0xCC; 32]);
drop(success);
// Drop impl zeroizes key material without panic
}
// ── generate_tg_nonce — correctness and security invariants ─────────────
fn make_nonce(proto_tag: ProtoTag, dc_idx: i16, fast_mode: bool) -> [u8; HANDSHAKE_LEN] {
let rng = SecureRandom::new();
let (nonce, _, _, _, _) = generate_tg_nonce(
proto_tag,
dc_idx,
&[0xAAu8; 32],
0u128,
&[0xBBu8; 32],
0u128,
&rng,
fast_mode,
);
nonce
}
// A censor probing the proxy can detect it if nonces contain protocol-
// discriminating reserved patterns that are never present in random data.
#[test]
fn nonce_first_byte_never_0xef() {
for _ in 0..500 {
let nonce = make_nonce(ProtoTag::Secure, 1, false);
assert_ne!(nonce[0], 0xef, "reserved first byte 0xef generated");
}
}
#[test]
fn nonce_first_four_bytes_never_a_reserved_beginning() {
for _ in 0..500 {
let nonce = make_nonce(ProtoTag::Intermediate, 2, false);
let first_four: [u8; 4] = nonce[..4].try_into().unwrap();
assert!(
!RESERVED_NONCE_BEGINNINGS.contains(&first_four),
"reserved 4-byte beginning generated: {:02x?}",
first_four
);
}
}
#[test]
fn nonce_bytes_4_to_7_never_all_zero() {
for _ in 0..500 {
let nonce = make_nonce(ProtoTag::Abridged, -1, false);
let cont: [u8; 4] = nonce[4..8].try_into().unwrap();
assert!(
!RESERVED_NONCE_CONTINUES.contains(&cont),
"reserved continue bytes [0,0,0,0] generated"
);
}
}
#[test]
fn nonce_embeds_proto_tag_at_correct_position() {
for tag in [ProtoTag::Secure, ProtoTag::Intermediate, ProtoTag::Abridged] {
let nonce = make_nonce(tag, 1, false);
let written: [u8; 4] = nonce[PROTO_TAG_POS..PROTO_TAG_POS + 4].try_into().unwrap();
assert_eq!(
ProtoTag::from_bytes(written),
Some(tag),
"proto_tag not written at PROTO_TAG_POS for {:?}",
tag
);
}
}
#[test]
fn nonce_embeds_dc_idx_at_correct_position_positive() {
for dc in [1i16, 2, 3, 4, 5] {
let nonce = make_nonce(ProtoTag::Secure, dc, false);
let written = i16::from_le_bytes(nonce[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap());
assert_eq!(written, dc, "dc_idx not written correctly for dc={}", dc);
}
}
#[test]
fn nonce_embeds_dc_idx_at_correct_position_negative() {
// Negative DC indices are used for test/cdn DCs in Telegram's protocol.
for dc in [-1i16, -2, -3, -4, -5] {
let nonce = make_nonce(ProtoTag::Intermediate, dc, false);
let written = i16::from_le_bytes(nonce[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap());
assert_eq!(written, dc, "negative dc_idx not written correctly for dc={}", dc);
}
}
#[test]
fn nonce_fast_mode_embeds_reversed_enc_key_iv_at_skip_pos() {
let enc_key = [0xCCu8; 32];
let enc_iv = 0xDEAD_BEEF_CAFE_BABEu128;
let rng = SecureRandom::new();
let (nonce, _, _, _, _) = generate_tg_nonce(
ProtoTag::Secure,
2,
&[0xAAu8; 32],
0u128,
&enc_key,
enc_iv,
&rng,
true,
);
// fast_mode writes reversed(enc_key || enc_iv.to_be_bytes()) at SKIP_LEN position
let mut expected = Vec::with_capacity(KEY_LEN + IV_LEN);
expected.extend_from_slice(&enc_key);
expected.extend_from_slice(&enc_iv.to_be_bytes());
expected.reverse();
assert_eq!(
&nonce[SKIP_LEN..SKIP_LEN + KEY_LEN + IV_LEN],
expected.as_slice(),
"fast_mode: reversed enc_key+enc_iv not written at SKIP_LEN"
);
}
#[test]
fn nonce_slow_mode_does_not_overwrite_random_bytes_at_skip_pos() {
// In non-fast_mode the key region at SKIP_LEN is random, not derived from client keys.
// Verify the nonce at that region is NOT the zero block (statistical sanity check).
let rng = SecureRandom::new();
let (nonce, _, _, _, _) = generate_tg_nonce(
ProtoTag::Secure,
1,
&[0u8; 32],
0,
&[0u8; 32],
0,
&rng,
false,
);
let region = &nonce[SKIP_LEN..SKIP_LEN + KEY_LEN + IV_LEN];
assert_ne!(region, &[0u8; KEY_LEN + IV_LEN], "SKIP_LEN region is all-zero — RNG suspiciously broken");
}
/// Regression: generate_tg_nonce must not loop more than 1000 times.
/// This test uses a deterministic seed to hit the panic threshold.
/// We can't inject a broken RNG, so we instead verify the normal path
/// terminates instantly (not a 1000-iteration hang).
#[test]
fn nonce_generation_terminates_quickly() {
use std::time::Instant;
let rng = SecureRandom::new();
let start = Instant::now();
for _ in 0..1000 {
let _ = generate_tg_nonce(ProtoTag::Secure, 1, &[0xAAu8; 32], 0, &[0xBBu8; 32], 0, &rng, false);
}
// 1000 calls should complete in well under 1 second on any hardware.
assert!(
start.elapsed().as_secs() < 1,
"generate_tg_nonce took suspiciously long — possible loop regression"
);
}
/// Guard: reserved patterns in RESERVED_NONCE_BEGINNINGS include all patterns
/// that a censor would recognise as non-proxy traffic (TLS, HTTP verbs, protocol tags).
#[test]
fn reserved_beginnings_cover_known_protocol_discriminators() {
// TLS ClientHello magic
assert!(RESERVED_NONCE_BEGINNINGS.contains(&[0x16, 0x03, 0x01, 0x02]));
// Intermediate / Secure protocol tags
assert!(RESERVED_NONCE_BEGINNINGS.contains(&[0xee, 0xee, 0xee, 0xee]));
assert!(RESERVED_NONCE_BEGINNINGS.contains(&[0xdd, 0xdd, 0xdd, 0xdd]));
}
}