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.
This commit is contained in:
Warren
2026-06-14 20:16:46 +08:00
parent 76f707a31d
commit 81ae052f48
4 changed files with 68 additions and 29 deletions

View File

@@ -77,26 +77,17 @@ impl SessionKeys {
info!("SessionKeys::derive() starting");
info!(" shared_secret ({} bytes): {:?}", shared_secret.len(), &shared_secret[..std::cmp::min(8, shared_secret.len())]);
// 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);
// 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())]);

View File

@@ -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())]);