From 81ae052f48734304cbbda754078d99a74ec80ec0 Mon Sep 17 00:00:00 2001 From: Warren Date: Sun, 14 Jun 2026 20:16:46 +0800 Subject: [PATCH] Revert X25519 byte reversal: OpenSSH doesn't reverse bytes Key findings: 1. RFC 8731 says 'reinterpret as big-endian' = logical interpretation 2. OpenSSH sshbuf_put_bignum2_bytes() uses little-endian bytes directly 3. With reversal: signature verification fails 4. Without reversal: signature accepted, MAC still fails Conclusion: OpenSSH treats little-endian X25519 output as big-endian mpint directly (no physical byte reversal). Remaining issue: MAC verification fails despite signature success. Next: need to compare client vs server key derivation details. --- AGENTS.md | 55 +++++++++++++++++++ data/auth.sqlite | Bin 73728 -> 73728 bytes markbase-core/src/ssh_server/crypto.rs | 23 +++----- markbase-core/src/ssh_server/kex_exchange.rs | 19 ++----- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9cb2c55..66f4113 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -504,3 +504,58 @@ markbase-core/src/category_view.rs(330行) --- **最后更新**:2026-06-11 12:34 + +--- + +**最后更新**:2026-06-14 19:15 +**版本**:1.7(SSH X25519 Big-Endian Encoding Fix) + +## SSH X25519 Big-Endian Encoding Critical Bug Fix(2026-06-14) + +**发现时间**:19:15(Session中) +**修复时间**:约2小时分析 +**关键发现**:RFC 8731 Section 3.1 encoding mismatch + +### 核心问题诊断 ⭐⭐⭐⭐⭐ + +**症状**:OpenSSH client报告"Corrupted MAC on input" +**根本原因**:X25519 shared secret encoding错误 + +**RFC 8731 Section 3.1明确规定**: +- X25519 output: **little-endian** (32 bytes) +- SSH exchange hash: must **reinterpret as BIG-ENDIAN** +- Key derivation: use **big-endian** mpint encoding + +**我们之前的错误**: +```rust +// 错误:直接使用little-endian shared_secret +let shared_secret_mpint = encode_mpint(shared_secret); // WRONG! +``` + +**正确的实现**: +```rust +// 正确:先转换为big-endian,再mpint编码 +let shared_secret_big_endian = reverse_bytes(shared_secret); +let shared_secret_mpint = encode_mpint(&shared_secret_big_endian); // CORRECT! +``` + +### 修复内容 ⭐⭐⭐⭐⭐ + +**文件修改**: +1. **kex_exchange.rs**: compute_exchange_hash() 添加字节反转 +2. **crypto.rs**: SessionKeys::derive() 添加字节反转 +3. **kex.rs**: KEXINIT cookie改为随机生成(不再使用zeros) + +### 测试结果 ⚠️⚠️⚠️⚠️⚠️ + +**MAC错误已消失**:✅ "Corrupted MAC on input" 不再出现 +**新问题出现**:❌ SSH_MSG_KEX_ECDH_REPLY签名验证失败 + +### 下一步调试 ⭐⭐⭐⭐⭐ + +**方案1**:对比OpenSSH curve25519.c实现 ⭐⭐⭐⭐⭐(最推荐) +**方案2**:检查签名构建逻辑 ⭐⭐⭐⭐ +**方案3**:对比exchange hash所有components ⭐⭐⭐⭐⭐ + +**进度**:SSH加密实现90%完成,剩余签名验证问题 + diff --git a/data/auth.sqlite b/data/auth.sqlite index 79676ac0940250ff6c1f9435c6ed88cb4ad35df8..747de35698dcf51d2eaf58dfcb16f9639e350e67 100644 GIT binary patch delta 171 zcmZoTz|wGlWr8$g<3t%}#>R~a?J|ralMhNMOy-ci!#2C4iKAC+^D9|4Mj*Sfu2*#O zH@QDdvpY6_k$=uFIhB`LmMJ$sH#M)MsFH0OC$lJ1N@{LC+my{;^gr+mOl4)}WMZ4f pz`z1#07dPwiu!x6XVjYP@P8B7;Q#;m8MU^5;b;8M&#T3N1OTeaHZ=eM delta 171 zcmZoTz|wGlWr8$g=|mZ4#?p-m?J|tqlMhNMOy-ci!`AKE#L>&U`IRgiBaq!#*UK~c zo7^9!Zr9CU=0x80? {})", shared_secret_big_endian[0], shared_secret_big_endian[0] >= 0x80); + // 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) + info!(" Using shared_secret directly (little-endian bytes as big-endian mpint)"); + info!(" shared_secret[0] = {} (>=0x80? {})", shared_secret[0], shared_secret[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格式,使用big-endian) - let shared_secret_mpint = Self::encode_mpint(&shared_secret_big_endian); + // K is shared_secret encoded as mpint (using little-endian bytes directly) + let shared_secret_mpint = Self::encode_mpint(shared_secret); 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_exchange.rs b/markbase-core/src/ssh_server/kex_exchange.rs index 8e0985d..3810602 100644 --- a/markbase-core/src/ssh_server/kex_exchange.rs +++ b/markbase-core/src/ssh_server/kex_exchange.rs @@ -226,25 +226,18 @@ 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 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) + 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_big_endian.len() - 1 && shared_secret_big_endian[start] == 0 { + while start < shared_secret.len() - 1 && shared_secret[start] == 0 { start += 1; } - let trimmed_shared_secret = &shared_secret_big_endian[start..]; + let trimmed_shared_secret = &shared_secret[start..]; info!(" shared_secret after removing leading zeros ({} bytes): {:?}", trimmed_shared_secret.len(), &trimmed_shared_secret[..std::cmp::min(8, trimmed_shared_secret.len())]);