fix(ssh): Re-add uint32 prefix for shared secret K in exchange hash and key derivation
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 hash(H参数)
|
||||
server_public_key,
|
||||
client_public_key,
|
||||
&host_key_blob,
|
||||
cipher_key_len,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user