fix: 修复 SSH 协议实现的多个关键 bug

- ECDH_INIT 发送原始 EC 点而非 SSH 公钥 blob
- Exchange hash 计算使用 encodeString 包装所有字段
- NEWKEYS 以明文发送后再启用加密
- AES-GCM 模式下 MAC 算法改为 none
- 修复 WebSocket 发送 Uint8Array 时 buffer 偏移问题
This commit is contained in:
IT committed 2026-06-12 18:22:02 +08:00
1 parent cbafaef1ac
commit e49f7fe4ef
3 files changed
+43 -69

No files matched your search

+17 -43
View File
@@ -1,5 +1,5 @@
import { SSH_MSG_KEX_ECDH_INIT } from '../types';
import { concat, readUint32, encodeUint32, encodeString, toSSHMPInt, extractRawECDHPoint } from './utils';
import { concat, readUint32, encodeString, toSSHMPInt } from './utils';
export class ECDHKeyExchange {
static async generateKeyPair(): Promise<CryptoKeyPair> {
@@ -10,44 +10,22 @@ export class ECDHKeyExchange {
);
}
static async exportPublicKeyForSSH(keyPair: CryptoKeyPair): Promise<Uint8Array> {
const rawKey = new Uint8Array(
static async exportRawPublicKey(keyPair: CryptoKeyPair): Promise<Uint8Array> {
return new Uint8Array(
await crypto.subtle.exportKey('raw', keyPair.publicKey)
);
const parts: Uint8Array[] = [];
const keyType = new TextEncoder().encode('ecdsa-sha2-nistp256');
const keyTypeLen = new Uint8Array(4);
new DataView(keyTypeLen.buffer).setUint32(0, keyType.length, false);
const curveName = new TextEncoder().encode('nistp256');
const curveNameLen = new Uint8Array(4);
new DataView(curveNameLen.buffer).setUint32(0, curveName.length, false);
const pointLen = new Uint8Array(4);
new DataView(pointLen.buffer).setUint32(0, rawKey.length, false);
const totalLen = keyTypeLen.length + keyType.length +
curveNameLen.length + curveName.length +
pointLen.length + rawKey.length;
const totalLenBytes = new Uint8Array(4);
new DataView(totalLenBytes.buffer).setUint32(0, totalLen, false);
return concat(totalLenBytes, keyTypeLen, keyType,
curveNameLen, curveName, pointLen, rawKey);
}
static buildInit(clientPublicKeySSH: Uint8Array): Uint8Array {
static buildInit(clientRawPublicKey: Uint8Array): Uint8Array {
return concat(
new Uint8Array([SSH_MSG_KEX_ECDH_INIT]),
clientPublicKeySSH
encodeString(clientRawPublicKey)
);
}
static parseReply(data: Uint8Array): {
hostKey: Uint8Array;
serverPublicKey: Uint8Array;
serverRawPublicKey: Uint8Array;
signature: Uint8Array;
} {
let offset = 1;
@@ -59,25 +37,23 @@ export class ECDHKeyExchange {
const qSLen = readUint32(data, offset);
offset += 4;
const serverPublicKey = data.slice(offset, offset + qSLen);
const serverRawPublicKey = data.slice(offset, offset + qSLen);
offset += qSLen;
const sigLen = readUint32(data, offset);
offset += 4;
const signature = data.slice(offset, offset + sigLen);
return { hostKey, serverPublicKey, signature };
return { hostKey, serverRawPublicKey, signature };
}
static async computeSharedSecret(
privateKey: CryptoKey,
serverPublicKeyRaw: Uint8Array
serverRawPublicKey: Uint8Array
): Promise<Uint8Array> {
const rawPoint = extractRawECDHPoint(serverPublicKeyRaw);
const serverKey = await crypto.subtle.importKey(
'raw',
rawPoint,
serverRawPublicKey,
{ name: 'ECDH', namedCurve: 'P-256' },
false,
[]
@@ -98,20 +74,18 @@ export class ECDHKeyExchange {
clientKEXInit: Uint8Array,
serverKEXInit: Uint8Array,
hostKey: Uint8Array,
clientPublicKeySSH: Uint8Array,
serverPublicKeySSH: Uint8Array,
clientRawPublicKey: Uint8Array,
serverRawPublicKey: Uint8Array,
sharedSecret: Uint8Array
): Promise<Uint8Array> {
const data = concat(
encodeString(clientVersion),
encodeString(serverVersion),
encodeUint32(clientKEXInit.length),
clientKEXInit,
encodeUint32(serverKEXInit.length),
serverKEXInit,
hostKey,
clientPublicKeySSH,
serverPublicKeySSH,
encodeString(clientKEXInit),
encodeString(serverKEXInit),
encodeString(hostKey),
encodeString(clientRawPublicKey),
encodeString(serverRawPublicKey),
sharedSecret
);
+2 -2
View File
@@ -16,8 +16,8 @@ export class KEXInitBuilder {
'rsa-sha2-256,ssh-rsa',
'aes256-gcm@openssh.com',
'aes256-gcm@openssh.com',
'hmac-sha2-256-etm@openssh.com,hmac-sha2-256',
'hmac-sha2-256-etm@openssh.com,hmac-sha2-256',
'none',
'none',
'none',
'none',
'',
+24 -24
View File
@@ -34,6 +34,7 @@ export class SSHSession {
private channel: SSHChannel;
private encryptCipher: SSHAESGCMCipher | null = null;
private decryptCipher: SSHAESGCMCipher | null = null;
private derivedKeys: any = null;
private seqNumSend: number = 0;
private seqNumRecv: number = 0;
@@ -43,7 +44,7 @@ export class SSHSession {
private kexInitRemote: Uint8Array | null = null;
private ecdhKeyPair!: CryptoKeyPair;
private ecdhPublicKeySSH!: Uint8Array;
private ecdhRawPublicKey!: Uint8Array;
private state: 'connecting' | 'version' | 'kex' | 'auth' | 'shell' | 'ready'
= 'connecting';
@@ -127,12 +128,12 @@ export class SSHSession {
this.ecdhKeyPair = await ECDHKeyExchange.generateKeyPair();
console.log('[KEX] ECDH key pair generated');
this.ecdhPublicKeySSH = await ECDHKeyExchange.exportPublicKeyForSSH(
this.ecdhRawPublicKey = await ECDHKeyExchange.exportRawPublicKey(
this.ecdhKeyPair
);
console.log('[KEX] Public key exported, length:', this.ecdhPublicKeySSH.length);
console.log('[KEX] Raw public key exported, length:', this.ecdhRawPublicKey.length);
const ecdhInit = ECDHKeyExchange.buildInit(this.ecdhPublicKeySSH);
const ecdhInit = ECDHKeyExchange.buildInit(this.ecdhRawPublicKey);
const ecdhPacket = SSHPacketBuilder.build(
ecdhInit, 8, null, this.seqNumSend++
);
@@ -200,19 +201,18 @@ export class SSHSession {
break;
case SSH_MSG_NEWKEYS:
console.log('[KEX] Received NEWKEYS');
await this.enableEncryption();
console.log('[KEX] Encryption enabled');
console.log('[KEX] Received NEWKEYS from server');
const newKeys = new Uint8Array([SSH_MSG_NEWKEYS]);
const packet = SSHPacketBuilder.build(
newKeys, 16,
(data, seq) => this.encryptCipher!.encrypt(data, seq) as any,
this.seqNumSend++
newKeys, 8, null, this.seqNumSend++
);
await this.writeSocket(packet);
console.log('[KEX] NEWKEYS sent');
await this.enableEncryption();
console.log('[KEX] Encryption enabled');
this.state = 'auth';
console.log('[SSH] State changed to: auth');
await this.authenticate();
@@ -222,16 +222,16 @@ export class SSHSession {
private async handleECDHReply(payload: Uint8Array): Promise<void> {
console.log('[KEX] Parsing ECDH_REPLY...');
const { hostKey, serverPublicKey, signature } =
const { hostKey, serverRawPublicKey, signature } =
ECDHKeyExchange.parseReply(payload);
console.log('[KEX] ECDH_REPLY parsed');
console.log('[KEX] ECDH_REPLY parsed, hostKey:', hostKey.length, 'serverPubKey:', serverRawPublicKey.length, 'sig:', signature.length);
console.log('[KEX] Computing shared secret...');
const sharedSecret = await ECDHKeyExchange.computeSharedSecret(
this.ecdhKeyPair.privateKey,
serverPublicKey
serverRawPublicKey
);
console.log('[KEX] Shared secret computed');
console.log('[KEX] Shared secret computed, length:', sharedSecret.length);
console.log('[KEX] Computing exchange hash...');
const H = await ECDHKeyExchange.computeExchangeHash(
@@ -240,8 +240,8 @@ export class SSHSession {
this.kexInitLocal!,
this.kexInitRemote!,
hostKey,
this.ecdhPublicKeySSH,
serverPublicKey,
this.ecdhRawPublicKey,
serverRawPublicKey,
sharedSecret
);
console.log('[KEX] Exchange hash computed');
@@ -252,10 +252,14 @@ export class SSHSession {
}
console.log('[KEX] Deriving keys...');
const keys = await KeyDerivation.deriveKeys(sharedSecret, H, this.sessionID!);
console.log('[KEX] Keys derived');
this.derivedKeys = await KeyDerivation.deriveKeys(sharedSecret, H, this.sessionID!);
console.log('[KEX] Keys derived, waiting for NEWKEYS...');
}
private async enableEncryption(): Promise<void> {
console.log('[KEX] Enabling encryption...');
const keys = this.derivedKeys;
console.log('[KEX] Initializing encryption ciphers...');
this.encryptCipher = new SSHAESGCMCipher(
keys.encKeyClientToServer,
keys.ivClientToServer
@@ -270,10 +274,6 @@ export class SSHSession {
console.log('[KEX] Encryption ciphers ready');
}
private async enableEncryption(): Promise<void> {
console.log('[KEX] Enabling encryption...');
}
private async authenticate(): Promise<void> {
console.log('[AUTH] Sending password authentication...');
const authRequest = SSHAuth.buildPasswordAuthRequest(
@@ -347,7 +347,7 @@ export class SSHSession {
case SSH_MSG_CHANNEL_DATA:
const outputData = this.channel.handleChannelData(payload);
this.ws.send(outputData.buffer);
this.ws.send(outputData.slice().buffer);
break;
case SSH_MSG_CHANNEL_WINDOW_ADJUST: