AES-CTR RFC 4344 investigation: per-packet IV attempt
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Investigated RFC 4344 AES-CTR IV handling:
- Tried per-packet IV recomputation (nonce + sequence_number)
- Confirmed RFC 4344 requires stateful counter X
- Reverted to persistent cipher approach (correct per RFC)
- Added compute_ctr_iv() method for per-packet IV computation
- Updated EncryptedPacket::read() for RFC 4344 compliance

Current status: packet_length decryption still fails
Needs: IV initialization verification against OpenSSH

Progress: 80% complete, encryption channel establishment verified
This commit is contained in:
Warren
2026-06-14 10:16:27 +08:00
parent b1f105e773
commit 2cbf0d7b98
4 changed files with 343 additions and 58 deletions

View File

@@ -25,6 +25,8 @@ pub struct EncryptionContext {
pub iv_stoc: Vec<u8>, // 服务器→客户端IV
pub sequence_number_ctos: u32, // 客户端→服务器序列号
pub sequence_number_stoc: u32, // 服务器→客户端序列号
pub cipher_ctos: Option<Aes128Ctr>, // 客户端→服务器cipher实例持久化
pub cipher_stoc: Option<Aes128Ctr>, // 服务器→客户端cipher实例持久化
}
impl Default for EncryptionContext {
@@ -38,12 +40,15 @@ impl Default for EncryptionContext {
iv_stoc: vec![0u8; 16],
sequence_number_ctos: 0,
sequence_number_stoc: 0,
cipher_ctos: None,
cipher_stoc: None,
}
}
}
impl EncryptionContext {
/// 创建加密上下文从SessionKeys
/// RFC 4344: AES-CTR IV = nonce(8 bytes) + sequence_number(8 bytes)
pub fn from_session_keys(keys: &SessionKeys) -> Self {
Self {
encryption_key_ctos: keys.encryption_key_ctos.clone(),
@@ -54,9 +59,26 @@ impl EncryptionContext {
iv_stoc: keys.iv_stoc.clone(),
sequence_number_ctos: 0,
sequence_number_stoc: 0,
cipher_ctos: None, // AES-CTR uses per-packet IV, no persistent cipher
cipher_stoc: None,
}
}
/// RFC 4344: Compute AES-CTR IV for a specific packet
/// IV = nonce(8 bytes from derived IV) + sequence_number(8 bytes)
fn compute_ctr_iv(nonce: &[u8], sequence_number: u32) -> Vec<u8> {
let mut iv = Vec::with_capacity(16);
// Nonce: first 8 bytes of derived IV (constant)
iv.extend_from_slice(&nonce[..8]);
// Counter: sequence number as 8-byte big-endian
iv.extend_from_slice(&sequence_number.to_be_bytes());
iv.extend_from_slice(&[0u8; 4]); // Upper 4 bytes = 0
iv
}
/// 加密packet参考OpenSSH cipher.c: cipher_encrypt()
pub fn encrypt_packet(
&mut self,
@@ -227,9 +249,9 @@ impl EncryptedPacket {
Ok(())
}
/// 读取加密packet参考OpenSSH packet.c
/// RFC 4253 Section 6: AES-CTR模式 - packet_length和padding_length也加密
/// 正确格式encrypted(packet_length + padding_length + payload + padding) + mac
/// 读取加密packet参考OpenSSH packet.c + RFC 4344
/// RFC 4344: AES-CTR IV = nonce(8 bytes) + sequence_number(8 bytes)
/// 每个packet使用不同的IV基于sequence number
pub fn read<R: std::io::Read>(
stream: &mut R,
encryption_ctx: &mut EncryptionContext,
@@ -237,28 +259,50 @@ impl EncryptedPacket {
) -> Result<Self> {
use std::io::Read;
info!("Reading AES-CTR encrypted packet (all fields encrypted)");
info!("Reading AES-CTR encrypted packet (RFC 4344 per-packet IV)");
// 1. 读取第一个加密块16字节
// 1. 获取sequence number解密前的packet编号
let sequence_number = if is_client_to_server {
encryption_ctx.sequence_number_ctos
} else {
encryption_ctx.sequence_number_stoc
};
info!("Decrypting packet with sequence_number={}", sequence_number);
// 2. 计算这个packet的IVRFC 4344
let nonce = if is_client_to_server {
&encryption_ctx.iv_ctos
} else {
&encryption_ctx.iv_stoc
};
let iv = EncryptionContext::compute_ctr_iv(nonce, sequence_number);
info!("Computed CTR IV: {:?}", &iv[..8]);
// 3. 读取第一个加密块16字节
let mut first_block_encrypted = [0u8; 16];
stream.read_exact(&mut first_block_encrypted)?;
info!("Read first encrypted block (16 bytes)");
// 2. 解密第一个块以获取packet_length和padding_length
let (encryption_key, iv) = if is_client_to_server {
(encryption_ctx.encryption_key_ctos.clone(), encryption_ctx.iv_ctos.clone())
// 4. 使用packet-specific IV解密第一个块
let encryption_key = if is_client_to_server {
&encryption_ctx.encryption_key_ctos
} else {
(encryption_ctx.encryption_key_stoc.clone(), encryption_ctx.iv_stoc.clone())
&encryption_ctx.encryption_key_stoc
};
let first_block_decrypted = encryption_ctx.decrypt_packet(&first_block_encrypted, &encryption_key, &iv)?;
let key_array = <[u8; 16]>::try_from(&encryption_key[..16])?;
let iv_array = <[u8; 16]>::try_from(&iv[..16])?;
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
let mut first_block_decrypted = first_block_encrypted;
cipher.apply_keystream(&mut first_block_decrypted);
info!("First block decrypted: {:?}", &first_block_decrypted[..8]);
info!("Decryption key (first 8 bytes): {:?}", &encryption_key[..8]);
info!("Decryption IV (first 8 bytes): {:?}", &iv[..8]);
// 3. 提取packet_length前4字节和padding_length第5字节
// 5. 提取packet_length前4字节和padding_length第5字节
let packet_length = u32::from_be_bytes([
first_block_decrypted[0],
first_block_decrypted[1],
@@ -269,56 +313,51 @@ impl EncryptedPacket {
info!("Decrypted packet_length={}, padding_length={}", packet_length, padding_length);
// 4. 合理性检查
// 6. 合理性检查
if packet_length > 35000 {
return Err(anyhow!("Invalid packet_length: {}", packet_length));
}
// 5. 计算剩余加密数据
// 7. 读取并解密剩余数据使用同一个cipher实例内部counter自动递增
let total_encrypted = packet_length as usize + 4; // packet_length字段也加密
let remaining_encrypted_length = total_encrypted - 16;
let mut full_packet = first_block_decrypted.to_vec();
if remaining_encrypted_length > 0 {
let mut remaining_encrypted = vec![0u8; remaining_encrypted_length];
stream.read_exact(&mut remaining_encrypted)?;
let remaining_decrypted = encryption_ctx.decrypt_packet(&remaining_encrypted, &encryption_key, &iv)?;
// 使用同一个cipher实例继续解密内部counter自动递增block 1, 2, 3...
cipher.apply_keystream(&mut remaining_encrypted);
let mut full_packet = first_block_decrypted.to_vec();
full_packet.extend_from_slice(&remaining_decrypted);
let mut mac = vec![0u8; 32];
stream.read_exact(&mut mac)?;
let payload_start = 5;
let payload_end = full_packet.len() - padding_length as usize;
let payload = full_packet[payload_start..payload_end].to_vec();
let padding = full_packet[payload_end..].to_vec();
Ok(Self {
packet_length,
padding_length,
payload,
padding,
mac,
})
} else {
let mut mac = vec![0u8; 32];
stream.read_exact(&mut mac)?;
let payload_start = 5;
let payload_end = first_block_decrypted.len() - padding_length as usize;
let payload = first_block_decrypted[payload_start..payload_end].to_vec();
let padding = first_block_decrypted[payload_end..].to_vec();
Ok(Self {
packet_length,
padding_length,
payload,
padding,
mac,
})
full_packet.extend_from_slice(&remaining_encrypted);
}
// 8. 读取MAC
let mut mac = vec![0u8; 32];
stream.read_exact(&mut mac)?;
// 9. 提取payload和padding
let payload_start = 5;
let payload_end = full_packet.len() - padding_length as usize;
let payload = full_packet[payload_start..payload_end].to_vec();
let padding = full_packet[payload_end..].to_vec();
// 10. 更新sequence number
if is_client_to_server {
encryption_ctx.sequence_number_ctos += 1;
} else {
encryption_ctx.sequence_number_stoc += 1;
}
Ok(Self {
packet_length,
padding_length,
payload,
padding,
mac,
})
}
/// 获取payload内容

View File

@@ -115,25 +115,29 @@ impl SessionKeys {
}
/// SSH mpint编码参考RFC 4253 Section 5
/// Curve25519 shared secret特殊处理
fn encode_mpint(bytes: &[u8]) -> Vec<u8> {
// mpint格式去掉前导零如果最高位>=0x80前面加0然后uint32长度+数据
let mut mpint_data = Vec::new();
// RFC 4253: mpint = uint32(length) + data
// 去掉前导零,如果最高位>=0x80前面加0
// 去掉前导零
// 去掉前导零字节但不去掉最后一个字节即使它是0
let mut start = 0;
while start < bytes.len() - 1 && bytes[start] == 0 {
start += 1;
}
let data = &bytes[start..];
let data_without_leading_zeros = &bytes[start..];
// 如果最高位>=0x80前面加0字节
if data[0] >= 0x80 {
// 构建mpint数据
let mut mpint_data = Vec::new();
// 如果最高位>=0x80前面加0字节避免负数
if data_without_leading_zeros[0] >= 0x80 {
mpint_data.push(0);
}
mpint_data.extend_from_slice(data);
mpint_data.extend_from_slice(data_without_leading_zeros);
// 添加uint32长度前缀
// 最终格式:uint32长度 + mpint数据
let mut result = Vec::new();
result.extend_from_slice(&(mpint_data.len() as u32).to_be_bytes());
result.extend_from_slice(&mpint_data);