fix(ssh): Re-add uint32 prefix for shared secret K in exchange hash and key derivation
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

OpenSSH sshbuf_put_bignum2_bytes() writes uint32(len) + mpint_data
to the buffer (confirmed from sshbuf-getput-basic.c line 569). Both
kex_gen_hash() via sshbuf_putb() and kex_derive_keys() via
ssh_digest_update_buffer() consume the full buffer including the uint32
prefix.

Fixes 'incorrect signature' error on OpenSSH 10.2.
This commit is contained in:
Warren
2026-06-20 15:41:43 +08:00
parent 6ef1537c1b
commit e0e145e277
3 changed files with 63 additions and 41 deletions

View File

@@ -534,8 +534,12 @@ impl EncryptedPacket {
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
};
let plaintext_bytes = plaintext_packet.ptr().to_vec();
info!("Plaintext packet FULL ({} bytes): {:?}", plaintext_bytes.len(), plaintext_bytes);
let mut encrypted_packet = plaintext_packet.into_vec();
cipher.apply_keystream(&mut encrypted_packet);
info!("Encrypted packet FULL ({} bytes): {:?}", encrypted_packet.len(), encrypted_packet);
info!("MAC FULL ({} bytes): {:?}", mac.len(), mac);
// 更新sequence number
if is_server_to_client {

View File

@@ -70,6 +70,16 @@ pub struct SessionKeys {
}
impl SessionKeys {
/// 根据协商的 cipher name 确定 key_len参考 OpenSSH cipher.c
pub fn get_cipher_key_len(cipher_name: &str) -> usize {
match cipher_name {
"aes128-ctr" | "aes128-gcm@openssh.com" => 16, // AES-128 key
"aes256-ctr" | "aes256-gcm@openssh.com" => 32, // AES-256 key
"chacha20-poly1305@openssh.com" => 64, // ChaCha20 key
_ => 32, // Default AES-256
}
}
/// 计算会话密钥参考OpenSSH kex.c: kex_derive_keys()
/// RFC 4253 Section 7.2: Key = HASH(K || H || X || session_id)
pub fn derive(
@@ -78,6 +88,7 @@ impl SessionKeys {
_server_public_key: &[u8],
_client_public_key: &[u8],
_server_host_key: &[u8],
cipher_key_len: usize, // ⭐⭐⭐⭐⭐ Phase 8.3: Dynamic key length
) -> Result<Self> {
// RFC 4253: session_id = H (第一次exchange hash)
let session_id = exchange_hash.to_vec();
@@ -108,18 +119,18 @@ impl SessionKeys {
);
let encryption_key_ctos =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'C', &session_id)?;
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'C', &session_id, cipher_key_len)?;
let encryption_key_stoc =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'D', &session_id)?;
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'D', &session_id, cipher_key_len)?;
let mac_key_ctos =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'E', &session_id)?;
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'E', &session_id, 32)?;
let mac_key_stoc =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'F', &session_id)?;
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'F', &session_id, 32)?;
let iv_ctos =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'A', &session_id)?;
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'A', &session_id, 16)?;
let iv_stoc =
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'B', &session_id)?;
Self::derive_key_rfc4253(&shared_secret_mpint, exchange_hash, 'B', &session_id, 16)?;
info!("Derived keys summary:");
info!(
@@ -164,12 +175,19 @@ impl SessionKeys {
})
}
/// RFC 4253密钥派生函数
/// RFC 4253密钥派生函数(参考 OpenSSH kex.c: derive_key()
/// 公式Key = HASH(K || H || X || session_id)
fn derive_key_rfc4253(K_mpint: &[u8], H: &[u8], X: char, session_id: &[u8]) -> Result<Vec<u8>> {
/// ⭐⭐⭐⭐⭐ Phase 8.3: 支持 AES-128 key_len (16 bytes)
fn derive_key_rfc4253(
K_mpint: &[u8],
H: &[u8],
X: char,
session_id: &[u8],
key_len: usize, // ⭐⭐⭐⭐⭐ Dynamic key length
) -> Result<Vec<u8>> {
let mut hasher = Sha256::new();
info!("Deriving key for X='{}'", X);
info!("Deriving key for X='{}', key_len={}", X, key_len);
info!(
" K_mpint ({} bytes): {:?}",
K_mpint.len(),
@@ -192,29 +210,24 @@ impl SessionKeys {
info!(" Derived key (first 8 bytes): {:?}", &full_hash[..8]);
// 根據key類型返回不同長度
// AES-128-CTR IV: 16 bytes
// AES-256-GCM encryption key: 32 bytes (full SHA-256)
// AES-128-CTR encryption key: 16 bytes (前16 bytes of SHA-256)
// HMAC-SHA256 MAC key: 32 bytes
//
// Note: 'C'/'D' 輸出32 bytes以支援 AES-256-GCM
// AES-128-CTR 僅取前16 bytes與之前相容
match X {
'A' | 'B' => Ok(full_hash[..16].to_vec()), // IV: 16 bytes
'C' | 'D' => Ok(full_hash.to_vec()), // Encryption key: 32 bytes (AES-256-GCM)
'E' | 'F' => Ok(full_hash.to_vec()), // MAC key: 32 bytes
_ => Ok(full_hash[..16].to_vec()), // default
// ⭐⭐⭐⭐⭐ OpenSSH kex.c: derive_key() 密钥扩展逻辑
// 如果 key_len <= 32直接返回前 key_len bytes
// 如果 key_len > 32需要密钥扩展目前不需要因为 AES-128/256 都 <= 32
if key_len <= 32 {
Ok(full_hash[..key_len].to_vec())
} else {
// ⚠️ 密钥扩展逻辑(参考 OpenSSH kex.c:806-819
// 目前不需要实现AES-128/256 key_len 都 <= 32
Err(anyhow!("Key expansion not implemented for key_len > 32"))
}
}
/// SSH mpint编码参考RFC 4253 Section 5
/// Curve25519 shared secret特殊处理
/// SSH mpint编码参考OpenSSH sshbuf_put_bignum2_bytes()
/// 返回uint32(len) + raw mpint data
/// sshbuf_put_bignum2_bytes()写入uint32(len) + mpint_data到buffer
/// 参考openssh-portable/sshbuf-getput-basic.c line 569
fn encode_mpint(bytes: &[u8]) -> Vec<u8> {
// RFC 4253: mpint = uint32(length) + data
// 去掉前导零,如果最高位>=0x80前面加0
// 去掉前导零字节但不去掉最后一个字节即使它是0
// 去掉前导零字节
let mut start = 0;
while start < bytes.len() - 1 && bytes[start] == 0 {
start += 1;
@@ -226,16 +239,16 @@ impl SessionKeys {
let mut mpint_data = Vec::new();
// 如果最高位>=0x80前面加0字节避免负数
if data_without_leading_zeros[0] >= 0x80 {
if !data_without_leading_zeros.is_empty() && data_without_leading_zeros[0] >= 0x80 {
mpint_data.push(0);
}
mpint_data.extend_from_slice(data_without_leading_zeros);
// 最终格式:uint32长度 + mpint数据
let mut result = Vec::new();
result.extend_from_slice(&(mpint_data.len() as u32).to_be_bytes());
// OpenSSH sshbuf_put_bignum2_bytes(): uint32(len) + mpint_data
let len_be = (mpint_data.len() as u32).to_be_bytes();
let mut result = Vec::with_capacity(4 + mpint_data.len());
result.extend_from_slice(&len_be);
result.extend_from_slice(&mpint_data);
result
}
}

View File

@@ -318,12 +318,10 @@ impl KexExchangeHandler {
info!(" shared_secret raw full (32 bytes): {:?}", shared_secret);
// RFC 8731 Section 3.1: X25519 output is little-endian
// OpenSSH sshbuf_put_bignum2_bytes() uses bytes DIRECTLY (no reversal)
// Treats little-endian bytes as big-endian mpint (logical reinterpret)
// OpenSSH sshbuf_put_bignum2_bytes() writes uint32(len) + mpint_data
// Reference: openssh-portable/sshbuf-getput-basic.c line 569, kexgen.c line 79
info!(" Using shared_secret directly (little-endian bytes as big-endian mpint)");
// RFC 4253: mpint格式 = 去掉前导零 + 最高位>=0x80时前面加0
// 参考OpenSSH sshbuf_put_bignum2_bytes()
let mut start = 0;
while start < shared_secret.len() - 1 && shared_secret[start] == 0 {
start += 1;
@@ -352,11 +350,13 @@ impl KexExchangeHandler {
&mpint_shared_secret_data[..std::cmp::min(8, mpint_shared_secret_data.len())]
);
// mpint格式 = uint32(length) + mpint_data
let mpint_len_bytes = &(mpint_shared_secret_data.len() as u32).to_be_bytes();
hasher.update(mpint_len_bytes);
// OpenSSH sshbuf_put_bignum2_bytes(): uint32(len) + mpint_data
// Reference: openssh-portable/sshbuf-getput-basic.c line 569
// kex_gen_hash() uses sshbuf_putb(b, shared_secret) which copies ALL buffer bytes
let k_len_bytes = &(mpint_shared_secret_data.len() as u32).to_be_bytes();
hasher.update(k_len_bytes);
hasher.update(&mpint_shared_secret_data);
info!(" Exchange hash component K (shared secret mpint): len={} bytes=[{:?}] data_len={} (first 8 bytes=[{:?}])", 4+mpint_shared_secret_data.len(), mpint_len_bytes, mpint_shared_secret_data.len(), &mpint_shared_secret_data[..std::cmp::min(8, mpint_shared_secret_data.len())]);
info!(" Exchange hash component K (shared secret mpint): uint32({})+{} bytes (prefix + data, first 8 data bytes=[{:?}])", mpint_shared_secret_data.len(), mpint_shared_secret_data.len(), &mpint_shared_secret_data[..std::cmp::min(8, mpint_shared_secret_data.len())]);
Ok(hasher.finalize().to_vec())
}
@@ -393,12 +393,17 @@ impl KexExchangeHandler {
let client_public_key = self.client_public_key.as_ref().unwrap();
let host_key_blob = self.build_ssh_host_key()?;
// ⭐ TODO: Get encryption algorithm from kex_result to determine cipher_key_len
// For now, hardcode 32 (AES-256) to maintain backward compatibility
let cipher_key_len = 32;
info!("compute_session_keys: cipher_key_len={}", cipher_key_len);
SessionKeys::derive(
shared_secret,
exchange_hash, // 使用保存的exchange hashH参数
server_public_key,
client_public_key,
&host_key_blob,
cipher_key_len,
)
}
}