fix: 修复 SSH 协议实现的多个关键 bug
- ECDH_INIT 发送原始 EC 点而非 SSH 公钥 blob - Exchange hash 计算使用 encodeString 包装所有字段 - NEWKEYS 以明文发送后再启用加密 - AES-GCM 模式下 MAC 算法改为 none - 修复 WebSocket 发送 Uint8Array 时 buffer 偏移问题
This commit is contained in:
1 parent
cbafaef1ac
commit
e49f7fe4ef
3 files changed
+43
-69
No files matched your search
+17
-43
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user