Fix SSH X25519 shared secret encoding for exchange hash

CRITICAL BUG FIX (RFC 8731 Section 3.1):
- X25519 output is little-endian
- SSH exchange hash requires big-endian encoding
- Reverse shared_secret bytes before mpint encoding
- Fix exchange hash computation in kex_exchange.rs
- Fix key derivation in crypto.rs
- Fix KEXINIT cookie to use random bytes

This resolves the fundamental encoding mismatch that caused
'Corrupted MAC on input' errors.

Next: verify signature verification after exchange hash fix.
This commit is contained in:
Warren
2026-06-14 19:13:18 +08:00
parent 0403a340c4
commit 76f707a31d
4 changed files with 66 additions and 35 deletions

Binary file not shown.

View File

@@ -76,13 +76,27 @@ impl SessionKeys {
info!("SessionKeys::derive() starting");
info!(" shared_secret ({} bytes): {:?}", shared_secret.len(), &shared_secret[..std::cmp::min(8, shared_secret.len())]);
info!(" shared_secret[0] = {} (>=0x80? {})", shared_secret[0], shared_secret[0] >= 0x80);
// RFC 8731 Section 3.1: X25519 output is little-endian, must convert to big-endian
// "When performing the X25519 operations, the integer values will be encoded into
// byte strings by doing a fixed-length unsigned little-endian conversion.
// It is only later when these byte strings are passed to the ECDH function in SSH
// that the bytes are reinterpreted as a fixed-length unsigned big-endian integer value K"
let shared_secret_big_endian = {
let mut reversed = shared_secret.to_vec();
reversed.reverse();
reversed
};
info!(" shared_secret converted to big-endian ({} bytes): {:?}",
shared_secret_big_endian.len(), &shared_secret_big_endian[..std::cmp::min(8, shared_secret_big_endian.len())]);
info!(" shared_secret_big_endian[0] = {} (>=0x80? {})", shared_secret_big_endian[0], shared_secret_big_endian[0] >= 0x80);
info!(" exchange_hash (H, {} bytes): {:?}", exchange_hash.len(), &exchange_hash[..8]);
info!(" session_id ({} bytes): {:?}", session_id.len(), &session_id[..8]);
// RFC 4253密钥派生公式HASH(K || H || X || session_id)
// 其中K是shared_secret需要mpint格式
let shared_secret_mpint = Self::encode_mpint(shared_secret);
// 其中K是shared_secret需要mpint格式使用big-endian
let shared_secret_mpint = Self::encode_mpint(&shared_secret_big_endian);
info!(" shared_secret_mpint ({} bytes): {:?}", shared_secret_mpint.len(), &shared_secret_mpint[..std::cmp::min(12, shared_secret_mpint.len())]);

View File

@@ -97,8 +97,9 @@ impl KexProposal {
payload.write_u8(PacketType::SSH_MSG_KEXINIT as u8)?;
// Cookie16字节随机数OpenSSH要求
// 简化:使用固定值(实际应随机生成)
let cookie = [0u8; 16];
let mut cookie = [0u8; 16];
use rand::Rng;
rand::thread_rng().fill(&mut cookie);
payload.write_all(&cookie)?;
// 10个算法列表SSH string格式length + data

View File

@@ -91,7 +91,7 @@ impl KexExchangeHandler {
info!("Curve25519 shared secret computed and saved");
// Compute and save exchange hash
// Compute exchange hash ONCE and reuse it
let host_key_blob = self.build_ssh_host_key()?;
let exchange_hash = self.compute_exchange_hash(
&shared_secret,
@@ -112,50 +112,30 @@ impl KexExchangeHandler {
info!("Exchange hash saved for key derivation");
self.build_kexdh_reply(
&shared_secret,
&exchange_hash,
&host_key_blob,
&server_public_key,
&client_public_key,
client_version,
server_version,
client_kexinit_payload,
server_kexinit_payload,
)
}
/// 构建SSH_MSG_KEXDH_REPLY packet参考OpenSSH kex.c
fn build_kexdh_reply(
&self,
shared_secret: &[u8],
exchange_hash: &[u8],
host_key_blob: &[u8],
server_public_key: &[u8],
client_public_key: &[u8],
client_version: &str,
server_version: &str,
client_kexinit_payload: &[u8],
server_kexinit_payload: &[u8],
) -> Result<SshPacket> {
let mut payload = Vec::new();
payload.write_u8(PacketType::SSH_MSG_KEXDH_REPLY as u8)?;
let host_key_ssh = self.build_ssh_host_key()?;
payload.write_u32::<BigEndian>(host_key_ssh.len() as u32)?;
payload.write_all(&host_key_ssh)?;
payload.write_u32::<BigEndian>(host_key_blob.len() as u32)?;
payload.write_all(host_key_blob)?;
payload.write_u32::<BigEndian>(32)?;
payload.write_all(server_public_key)?;
let exchange_hash = self.compute_exchange_hash(
shared_secret,
&host_key_ssh,
client_public_key,
server_public_key,
client_version,
server_version,
client_kexinit_payload,
server_kexinit_payload,
)?;
let signature = self.build_exchange_signature(&exchange_hash)?;
let signature = self.build_exchange_signature(exchange_hash)?;
payload.write_u32::<BigEndian>(signature.len() as u32)?;
payload.write_all(&signature)?;
@@ -195,6 +175,30 @@ impl KexExchangeHandler {
) -> Result<Vec<u8>> {
use sha2::{Sha256, Digest};
info!("=== EXCHANGE HASH COMPUTATION ===");
info!("V_C (client version): {:?}", client_version.as_bytes());
info!("V_C length: {}", client_version.len());
info!("V_S (server version): {:?}", server_version.as_bytes());
info!("V_S length: {}", server_version.len());
info!("I_C (client KEXINIT payload): {:?}", &client_kexinit_payload[..std::cmp::min(50, client_kexinit_payload.len())]);
info!("I_C length: {}", client_kexinit_payload.len());
info!("I_C[0] (packet type): {} (should be SSH_MSG_KEXINIT=20)", client_kexinit_payload[0]);
info!("I_S (server KEXINIT payload): {:?}", &server_kexinit_payload[..std::cmp::min(50, server_kexinit_payload.len())]);
info!("I_S length: {}", server_kexinit_payload.len());
info!("I_S[0] (packet type): {} (should be SSH_MSG_KEXINIT=20)", server_kexinit_payload[0]);
info!("K_S (host key blob): {:?}", &host_key_blob[..std::cmp::min(30, host_key_blob.len())]);
info!("K_S length: {}", host_key_blob.len());
info!("Q_C (client ECDH public key): {:?}", &client_public_key[..std::cmp::min(16, client_public_key.len())]);
info!("Q_C length: {}", client_public_key.len());
info!("Q_S (server ECDH public key): {:?}", &server_public_key[..std::cmp::min(16, server_public_key.len())]);
info!("Q_S length: {}", server_public_key.len());
let mut hasher = Sha256::new();
// RFC 4253 Section 7: V_C and V_S are version strings (without \r\n based on testing)
@@ -222,13 +226,25 @@ impl KexExchangeHandler {
info!("Exchange hash components:");
info!(" shared_secret raw ({} bytes): {:?}", shared_secret.len(), &shared_secret[..std::cmp::min(8, shared_secret.len())]);
// RFC 8731 Section 3.1: X25519 output is little-endian, must reinterpret as big-endian
// "The 32 or 56 bytes of X are converted into K by interpreting the octets as an
// unsigned fixed-length integer encoded in network byte order."
let shared_secret_big_endian = {
let mut reversed = shared_secret.to_vec();
reversed.reverse();
reversed
};
info!(" shared_secret after converting to big-endian ({} bytes): {:?}",
shared_secret_big_endian.len(), &shared_secret_big_endian[..std::cmp::min(8, shared_secret_big_endian.len())]);
// RFC 4253: mpint格式 = 去掉前导零 + 最高位>=0x80时前面加0
// 参考OpenSSH sshbuf_put_bignum2_bytes()
let mut start = 0;
while start < shared_secret.len() - 1 && shared_secret[start] == 0 {
while start < shared_secret_big_endian.len() - 1 && shared_secret_big_endian[start] == 0 {
start += 1;
}
let trimmed_shared_secret = &shared_secret[start..];
let trimmed_shared_secret = &shared_secret_big_endian[start..];
info!(" shared_secret after removing leading zeros ({} bytes): {:?}", trimmed_shared_secret.len(), &trimmed_shared_secret[..std::cmp::min(8, trimmed_shared_secret.len())]);