From 76f707a31d5e0a5bc1fc49548a6a6afedfdc7dbc Mon Sep 17 00:00:00 2001 From: Warren Date: Sun, 14 Jun 2026 19:13:18 +0800 Subject: [PATCH] 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. --- data/auth.sqlite | Bin 73728 -> 73728 bytes markbase-core/src/ssh_server/crypto.rs | 20 ++++- markbase-core/src/ssh_server/kex.rs | 5 +- markbase-core/src/ssh_server/kex_exchange.rs | 76 +++++++++++-------- 4 files changed, 66 insertions(+), 35 deletions(-) diff --git a/data/auth.sqlite b/data/auth.sqlite index 10852a8892f7b0f7085061cd78adf0ee92858cea..79676ac0940250ff6c1f9435c6ed88cb4ad35df8 100644 GIT binary patch delta 172 zcmZoTz|wGlWr8$g=|mZ4#?p-mZ8A)~+>;MVDNN>&y~Eb++QiYzyZMzY8zYe2Sl7!l z`J3Dyrf%2GU*w<{|#x&^w delta 172 zcmZoTz|wGlWr8%L;Y1l{M#GH>Z8A(f5t9!}DNN>&y~D=$qKTs?YV#{uHbx-3v92d_ z@;A9ZOnfgkf02LAFB!?pEX$OepPQOjQdG$n#mOwnl#-g8&la)yi~a|Gfk;+nPA0Y} q1_l-|11M^bRn*^mJ)_uUhyR=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())]); diff --git a/markbase-core/src/ssh_server/kex.rs b/markbase-core/src/ssh_server/kex.rs index 232ac5c..682767a 100644 --- a/markbase-core/src/ssh_server/kex.rs +++ b/markbase-core/src/ssh_server/kex.rs @@ -97,8 +97,9 @@ impl KexProposal { payload.write_u8(PacketType::SSH_MSG_KEXINIT as u8)?; // Cookie(16字节随机数,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) diff --git a/markbase-core/src/ssh_server/kex_exchange.rs b/markbase-core/src/ssh_server/kex_exchange.rs index 13da776..8e0985d 100644 --- a/markbase-core/src/ssh_server/kex_exchange.rs +++ b/markbase-core/src/ssh_server/kex_exchange.rs @@ -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 { 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::(host_key_ssh.len() as u32)?; - payload.write_all(&host_key_ssh)?; + payload.write_u32::(host_key_blob.len() as u32)?; + payload.write_all(host_key_blob)?; payload.write_u32::(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::(signature.len() as u32)?; payload.write_all(&signature)?; @@ -195,6 +175,30 @@ impl KexExchangeHandler { ) -> Result> { 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())]);