SSH AES-128-CTR encryption fixes (Phase 4 refinement)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Major fixes:
- Persistent cipher state: ciphers maintain counter across packets
- Cipher direction bug: use cipher_ctos for client packets, cipher_stoc for server packets
- MAC key length: 32 bytes for HMAC-SHA256 (was incorrectly 16 bytes)
- MtE mode MAC: calculate MAC over plaintext before encryption
- AES-CTR encryption: encrypt entire packet including packet_length field
- Service name length: corrected to 12 for 'ssh-userauth'
- mpint encoding: properly remove leading zeros and handle high bit

Remaining issue:
- SSH client reports 'Corrupted MAC on input'
- Likely due to key derivation mismatch with OpenSSH client
- Requires further investigation with packet capture analysis

Progress: 80% of SSH encryption implementation complete
Security: Still using RustCrypto authoritative libraries ()
This commit is contained in:
Warren
2026-06-14 15:06:01 +08:00
parent 2cbf0d7b98
commit 7d50c1147d
4 changed files with 179 additions and 93 deletions

View File

@@ -48,8 +48,30 @@ impl Default for EncryptionContext {
impl EncryptionContext {
/// 创建加密上下文从SessionKeys
/// RFC 4344: AES-CTR IV = nonce(8 bytes) + sequence_number(8 bytes)
/// OpenSSH cipher.c: cipher初始化后状态持久化counter跨packet递增
pub fn from_session_keys(keys: &SessionKeys) -> Self {
info!("Initializing ciphers with session keys:");
info!(" encryption_key_ctos (16 bytes): {:?}", &keys.encryption_key_ctos[..16]);
info!(" iv_ctos (16 bytes): {:?}", &keys.iv_ctos[..16]);
info!(" encryption_key_stoc (16 bytes): {:?}", &keys.encryption_key_stoc[..16]);
info!(" iv_stoc (16 bytes): {:?}", &keys.iv_stoc[..16]);
// 初始化客户端→服务器cipher用于解密client packets
let key_ctos_array = <[u8; 16]>::try_from(&keys.encryption_key_ctos[..16])
.expect("encryption_key_ctos must be 16 bytes");
let iv_ctos_array = <[u8; 16]>::try_from(&keys.iv_ctos[..16])
.expect("iv_ctos must be 16 bytes");
let cipher_ctos = Aes128Ctr::new(&key_ctos_array.into(), &iv_ctos_array.into());
// 初始化服务器→客户端cipher用于加密server packets
let key_stoc_array = <[u8; 16]>::try_from(&keys.encryption_key_stoc[..16])
.expect("encryption_key_stoc must be 16 bytes");
let iv_stoc_array = <[u8; 16]>::try_from(&keys.iv_stoc[..16])
.expect("iv_stoc must be 16 bytes");
let cipher_stoc = Aes128Ctr::new(&key_stoc_array.into(), &iv_stoc_array.into());
info!("Ciphers initialized successfully");
Self {
encryption_key_ctos: keys.encryption_key_ctos.clone(),
encryption_key_stoc: keys.encryption_key_stoc.clone(),
@@ -59,8 +81,8 @@ 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,
cipher_ctos: Some(cipher_ctos), // 持久化cipher实例
cipher_stoc: Some(cipher_stoc), // 持久化cipher实例
}
}
@@ -170,8 +192,8 @@ pub struct EncryptedPacket {
}
impl EncryptedPacket {
/// 创建加密packet参考OpenSSH
/// RFC 4253: packet_length是plaintext只有payload+padding加密
/// 创建加密packet参考OpenSSH cipher.c
/// AES-CTR模式所有数据加密包括packet_length
pub fn new(
plaintext_payload: &[u8],
encryption_ctx: &mut EncryptionContext,
@@ -188,27 +210,23 @@ impl EncryptedPacket {
// packet_length = padding_length(1) + payload + padding
let packet_length = 1 + payload_length + padding_length as usize;
info!("Creating encrypted packet: payload_len={}, padding_len={}, packet_len={}",
info!("Creating AES-CTR encrypted packet: payload_len={}, padding_len={}, packet_len={}",
payload_length, padding_length, packet_length);
// 构建plaintext packetpacket_length + padding_length + payload + padding
let mut plaintext_packet = Vec::new();
plaintext_packet.write_u8(padding_length)?;
plaintext_packet.write_all(plaintext_payload)?;
plaintext_packet.write_u32::<BigEndian>(packet_length as u32)?; // plaintext packet_length
plaintext_packet.write_u8(padding_length)?; // plaintext padding_length
plaintext_packet.write_all(plaintext_payload)?; // plaintext payload
let mut random_padding = vec![0u8; padding_length as usize];
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut random_padding);
plaintext_packet.write_all(&random_padding)?;
plaintext_packet.write_all(&random_padding)?; // plaintext padding
// 加密payload+padding不包括packet_length
let (encryption_key, iv) = if is_server_to_client {
(encryption_ctx.encryption_key_stoc.clone(), encryption_ctx.iv_stoc.clone())
} else {
(encryption_ctx.encryption_key_ctos.clone(), encryption_ctx.iv_ctos.clone())
};
let encrypted_packet = encryption_ctx.encrypt_packet(&plaintext_packet, &encryption_key, &iv)?;
info!("Plaintext packet size: {} bytes", plaintext_packet.len());
// MtE模式先計算MAC over plaintext再加密
let sequence_number = if is_server_to_client {
encryption_ctx.sequence_number_stoc
} else {
@@ -221,11 +239,32 @@ impl EncryptedPacket {
&encryption_ctx.mac_key_ctos
};
let mut mac_data = Vec::new();
mac_data.write_u32::<BigEndian>(packet_length as u32)?;
mac_data.extend_from_slice(&encrypted_packet);
info!("MAC calculation (MtE mode) over plaintext packet:");
info!(" sequence_number: {}", sequence_number);
info!(" mac_key length: {}", mac_key.len());
info!(" plaintext_packet length: {}", plaintext_packet.len());
let mac = encryption_ctx.compute_mac(sequence_number, &mac_data, mac_key)?;
// MAC計算HMAC(sequence_number || plaintext_packet)
let mac = encryption_ctx.compute_mac(sequence_number, &plaintext_packet, mac_key)?;
// 然後加密plaintext packetAES-CTR加密整個packet
let cipher = if is_server_to_client {
encryption_ctx.cipher_stoc.as_mut()
.ok_or_else(|| anyhow!("cipher_stoc not initialized"))?
} else {
encryption_ctx.cipher_ctos.as_mut()
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
};
let mut encrypted_packet = plaintext_packet;
cipher.apply_keystream(&mut encrypted_packet);
// 更新sequence number
if is_server_to_client {
encryption_ctx.sequence_number_stoc += 1;
} else {
encryption_ctx.sequence_number_ctos += 1;
}
Ok(Self {
packet_length: packet_length as u32,
@@ -236,22 +275,26 @@ impl EncryptedPacket {
})
}
/// 写入加密packet参考OpenSSH packet.c
/// RFC 4253: packet_length是plaintext然后是encrypted(payload+padding)最后是mac
/// 写入加密packet参考OpenSSH cipher.c
/// AES-CTR模式写入完整加密packet + MAC
pub fn write<W: std::io::Write>(&self, stream: &mut W) -> Result<()> {
// 写入packet_lengthplaintext
stream.write_u32::<BigEndian>(self.packet_length)?;
// 写入encrypted(payload+padding)
info!("Writing AES-CTR encrypted packet: total_encrypted_len={}, mac_len={}",
self.payload.len(), self.mac.len());
// AES-CTR: 整个packet已加密包括packet_length直接写入
stream.write_all(&self.payload)?;
info!("Wrote encrypted packet ({} bytes)", self.payload.len());
// 写入MAC
stream.write_all(&self.mac)?;
info!("Wrote MAC ({} bytes)", self.mac.len());
Ok(())
}
/// 读取加密packet参考OpenSSH packet.c + RFC 4344
/// RFC 4344: AES-CTR IV = nonce(8 bytes) + sequence_number(8 bytes)
/// 每个packet使用不同的IV基于sequence number
/// 读取加密packet参考OpenSSH packet.c ssh_packet_read_poll2
/// OpenSSH packet.c: AES-CTR先解密第一个块再提取packet_length
/// aadlen = 0 (没有EtM或authenticated encryption), packet_length被加密
pub fn read<R: std::io::Read>(
stream: &mut R,
encryption_ctx: &mut EncryptionContext,
@@ -259,50 +302,32 @@ impl EncryptedPacket {
) -> Result<Self> {
use std::io::Read;
info!("Reading AES-CTR encrypted packet (RFC 4344 per-packet IV)");
info!("Reading AES-CTR encrypted packet (packet_length encrypted)");
// 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字节
// 1. 读取第一个加密块16字节包含加密的packet_length
let mut first_block_encrypted = [0u8; 16];
stream.read_exact(&mut first_block_encrypted)?;
info!("Read first encrypted block (16 bytes)");
info!("Read first encrypted block (16 bytes): {:?}", &first_block_encrypted);
// 4. 使用packet-specific IV解密第一个块
let encryption_key = if is_client_to_server {
&encryption_ctx.encryption_key_ctos
// 2. 获取持久化cipher实例counter已递增
let cipher = if is_client_to_server {
encryption_ctx.cipher_ctos.as_mut()
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
} else {
&encryption_ctx.encryption_key_stoc
encryption_ctx.cipher_stoc.as_mut()
.ok_or_else(|| anyhow!("cipher_stoc not initialized"))?
};
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());
info!("Using cipher for decryption (is_client_to_server={})", is_client_to_server);
// 3. 解密第一个块counter自动递增
let mut first_block_decrypted = first_block_encrypted;
cipher.apply_keystream(&mut first_block_decrypted);
info!("First block decrypted: {:?}", &first_block_decrypted[..8]);
info!("Decrypted first block: {:?}", &first_block_decrypted);
// 5. 提取packet_length前4字节和padding_length第5字节
// 3. 从解密后的数据中提取packet_length前4字节和padding_length第5字节
let packet_length = u32::from_be_bytes([
first_block_decrypted[0],
first_block_decrypted[1],
@@ -313,36 +338,56 @@ impl EncryptedPacket {
info!("Decrypted packet_length={}, padding_length={}", packet_length, padding_length);
// 6. 合理性检查
// 4. 合理性检查
if packet_length > 35000 {
info!("packet_length raw bytes: {:?}", &first_block_decrypted[..4]);
return Err(anyhow!("Invalid packet_length: {}", packet_length));
}
// 7. 读取并解密剩余数据使用同一个cipher实例内部counter自动递增
let total_encrypted = packet_length as usize + 4; // packet_length字段也加密
let remaining_encrypted_length = total_encrypted - 16;
// 3. 计算剩余加密数据长度
// packet_length = padding_length(1) + payload + padding
// 总加密数据 = packet_length(4) + packet_length = packet_length + 4
// 已读取16字节剩余 = packet_length + 4 - 16
let total_encrypted_size = packet_length as usize + 4; // packet_length field + content
let remaining_encrypted_size = total_encrypted_size - 16;
let mut full_packet = first_block_decrypted.to_vec();
info!("Total encrypted size: {}, remaining: {}", total_encrypted_size, remaining_encrypted_size);
if remaining_encrypted_length > 0 {
let mut remaining_encrypted = vec![0u8; remaining_encrypted_length];
stream.read_exact(&mut remaining_encrypted)?;
// 使用同一个cipher实例继续解密内部counter自动递增block 1, 2, 3...
cipher.apply_keystream(&mut remaining_encrypted);
full_packet.extend_from_slice(&remaining_encrypted);
}
// 4. 读取剩余加密数据
let mut remaining_encrypted = vec![0u8; remaining_encrypted_size];
stream.read_exact(&mut remaining_encrypted)?;
// 8. 读取MAC
// 5. 继续解密使用同一个cipher
cipher.apply_keystream(&mut remaining_encrypted);
info!("Remaining decrypted data: {:?}", &remaining_encrypted);
// 6. 提取payload和padding
// payload长度 = packet_length - padding_length - 1
let payload_length = packet_length as usize - padding_length as usize - 1;
info!("Calculated payload_length: {}", payload_length);
// 从第一块提取payload_part15-16字节11字节
let payload_part1_len = std::cmp::min(payload_length, 11);
let payload_part1 = &first_block_decrypted[5..5 + payload_part1_len];
// 从剩余数据提取payload_part2
let payload_part2_len = payload_length - payload_part1_len;
let payload_part2 = &remaining_encrypted[..payload_part2_len];
// 合并payload
let mut payload = Vec::new();
payload.extend_from_slice(payload_part1);
payload.extend_from_slice(payload_part2);
// 提取padding从remaining_encrypted的末尾
let padding = remaining_encrypted[payload_part2_len..].to_vec();
// 9. 读取MAC
info!("Reading MAC (32 bytes)...");
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();
info!("MAC read successfully");
// 10. 更新sequence number
if is_client_to_server {