diff --git a/.yarn/patches/ssh2-npm-1.11.0-convertSha1.patch b/.yarn/patches/ssh2-npm-1.11.0-convertSha1.patch new file mode 100644 index 0000000..3e9f284 --- /dev/null +++ b/.yarn/patches/ssh2-npm-1.11.0-convertSha1.patch @@ -0,0 +1,96 @@ +diff --git a/lib/client.js b/lib/client.js +index 80f372a832b71f5bfd18277af7111bdb72930125..9712c5c3f74bb08890dc458efaf8020b988918b0 100644 +--- a/lib/client.js ++++ b/lib/client.js +@@ -388,8 +388,18 @@ class Client extends EventEmitter { + USERAUTH_PK_OK: (p) => { + if (curAuth.type === 'agent') { + const key = curAuth.agentCtx.currentKey(); ++ let algo; ++ if (key.type === 'ssh-rsa' && curAuth.convertSha1) { ++ if (this._protocol._remoteHostKeyAlgorithms.includes('rsa-sha2-512')) { ++ debug && debug('Client: USERAUTH_PK_OK: ssh-rsa key with convertSha1 enabled, switching to sha512'); ++ algo = 'sha512'; ++ } else if (this._protocol._remoteHostKeyAlgorithms.includes('rsa-sha2-256')) { ++ debug && debug('Client: USERAUTH_PK_OK: ssh-rsa key with convertSha1 enabled, switching to sha256'); ++ algo = 'sha256'; ++ } ++ } + proto.authPK(curAuth.username, key, (buf, cb) => { +- curAuth.agentCtx.sign(key, buf, {}, (err, signed) => { ++ curAuth.agentCtx.sign(key, buf, { hash: algo }, (err, signed) => { + if (err) { + err.level = 'agent'; + this.emit('error', err); +@@ -401,8 +411,18 @@ class Client extends EventEmitter { + }); + }); + } else if (curAuth.type === 'publickey') { ++ let algo; ++ if (curAuth.key.type === 'ssh-rsa' && curAuth.convertSha1) { ++ if (this._protocol._remoteHostKeyAlgorithms.includes('rsa-sha2-512')) { ++ debug && debug('Client: USERAUTH_PK_OK: ssh-rsa key with convertSha1 enabled, switching to sha512'); ++ algo = 'sha512'; ++ } else if (this._protocol._remoteHostKeyAlgorithms.includes('rsa-sha2-256')) { ++ debug && debug('Client: USERAUTH_PK_OK: ssh-rsa key with convertSha1 enabled, switching to sha256'); ++ algo = 'sha256'; ++ } ++ } + proto.authPK(curAuth.username, curAuth.key, (buf, cb) => { +- const signature = curAuth.key.sign(buf); ++ const signature = curAuth.key.sign(buf, algo); + if (signature instanceof Error) { + signature.message = + `Error signing data with key: ${signature.message}`; +@@ -881,7 +901,7 @@ class Client extends EventEmitter { + return skipAuth('Skipping invalid key auth attempt'); + if (!key.isPrivateKey()) + return skipAuth('Skipping non-private key'); +- nextAuth = { type, username, key }; ++ nextAuth = { type, username, key, convertSha1: nextAuth.convertSha1 }; + break; + } + case 'hostbased': { +@@ -906,7 +926,7 @@ class Client extends EventEmitter { + `Skipping invalid agent: ${nextAuth.agent}` + ); + } +- nextAuth = { type, username, agentCtx: new AgentContext(agent) }; ++ nextAuth = { type, username, agentCtx: new AgentContext(agent), convertSha1: nextAuth.convertSha1 }; + break; + } + case 'keyboard-interactive': { +diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js +index 94e12bc72b5c61094efd6862dfbce6ff852c5b26..e0cbb748bc80455bfa819cc20c672701c280409c 100644 +--- a/lib/protocol/Protocol.js ++++ b/lib/protocol/Protocol.js +@@ -616,7 +616,15 @@ class Protocol { + if (pubKey instanceof Error) + throw new Error('Invalid key'); + +- const keyType = pubKey.type; ++ let keyType = pubKey.type; ++ if (keyType === 'ssh-rsa') { ++ for (const algo of ['rsa-sha2-512', 'rsa-sha2-256']) { ++ if (this._remoteHostKeyAlgorithms.includes(algo)) { ++ keyType = algo; ++ break; ++ } ++ } ++ } + pubKey = pubKey.getPublicSSH(); + + const userLen = Buffer.byteLength(username); +diff --git a/lib/protocol/kex.js b/lib/protocol/kex.js +index 49b28f54677809c32b2141c99eec36e0c6d99e38..4ee69bd3b3b5685665d69c05114a94c14cd4b076 100644 +--- a/lib/protocol/kex.js ++++ b/lib/protocol/kex.js +@@ -196,6 +196,8 @@ function handleKexInit(self, payload) { + + const local = self._offer; + const remote = init; ++ ++ self._remoteHostKeyAlgorithms = remote.serverHostKey; + + let localKex = local.lists.kex.array; + if (self._compatFlags & COMPAT.BAD_DHGEX) { diff --git a/.yarn/yarn.lock b/.yarn/yarn.lock index ed53231..404b909 100644 --- a/.yarn/yarn.lock +++ b/.yarn/yarn.lock @@ -8799,7 +8799,7 @@ __metadata: languageName: node linkType: hard -"ssh2@npm:^1.11.0": +"ssh2@npm:1.11.0": version: 1.11.0 resolution: "ssh2@npm:1.11.0" dependencies: @@ -8816,6 +8816,23 @@ __metadata: languageName: node linkType: hard +"ssh2@patch:ssh2@npm%3A1.11.0#./.yarn/patches/ssh2-npm-1.11.0-convertSha1.patch::locator=vscode-sshfs%40workspace%3A.": + version: 1.11.0 + resolution: "ssh2@patch:ssh2@npm%3A1.11.0#./.yarn/patches/ssh2-npm-1.11.0-convertSha1.patch::version=1.11.0&hash=602c9b&locator=vscode-sshfs%40workspace%3A." + dependencies: + asn1: ^0.2.4 + bcrypt-pbkdf: ^1.0.2 + cpu-features: ~0.0.4 + nan: ^2.16.0 + dependenciesMeta: + cpu-features: + optional: true + nan: + optional: true + checksum: f33a0074637f195b261952cea4b8b02c15977375f59d985580c0c571ac6bac56cf523cd85ee99d1fdcdcce4e6547271c35b2450c0c14963f11ff88b0093d8b8e + languageName: node + linkType: hard + "ssri@npm:^9.0.0": version: 9.0.1 resolution: "ssri@npm:9.0.1" diff --git a/CHANGELOG.md b/CHANGELOG.md index 766120e..bc36f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog +## Unreleased + +### Changes + +- Apply a patch to ssh2 and make use of it to fix OpenSSH 8.8+ disabling `ssh-rsa` (SHA1) by default (#309) + - Patch file in `.yarn/patches` based on applied to `ssh2@1.11.0` + - The patch adds an option `convertSha1` to `publickey` and `agent` authentication methods on top of Eugeny's modifications + - When the option is present, `ssh-rsa` keys will be treated as `rsa-sha2-512` or `rsa-sha2-256`, if the server supports it + - Added a flag `OPENSSH-SHA1` (enabled by default) to pass this `convertSha1` flag when using `publickey` or `agent` auths + - Part of this change required creating a custom ssh2 `authHandler` (based on the built-in version) to pass the option if desired + ## v1.26.0 (2023-03-25) ### Changes diff --git a/common/src/ssh2.ts b/common/src/ssh2.ts index e49b3ca..ae7c3f4 100644 --- a/common/src/ssh2.ts +++ b/common/src/ssh2.ts @@ -110,6 +110,8 @@ declare module 'ssh2' { key: string | Buffer | ParsedKey; /** Optional passphrase in case `key` is an encrypted key */ passphrase?: string; + /** [PATCH:convertSha1#309] If true, make ssh-rsa keys use sha512/sha256 instead of sha1 if possible */ + convertSha1?: boolean; } export interface AuthHandlerHostBased { type: 'hostbased'; @@ -125,6 +127,8 @@ declare module 'ssh2' { type: 'agent'; username: string; agent: string | BaseAgent; + /** [PATCH:convertSha1#309] If true, make ssh-rsa keys use sha512/sha256 instead of sha1 if possible */ + convertSha1?: boolean; } export interface AuthHandlerKeyboardInteractive { type: 'keyboard-interactive'; diff --git a/package.json b/package.json index 26bc339..7fe9552 100644 --- a/package.json +++ b/package.json @@ -436,7 +436,8 @@ "winreg": "^1.2.4" }, "resolutions": { - "cpu-features": "npm:@favware/skip-dependency@1.1.3" + "cpu-features": "npm:@favware/skip-dependency@1.1.3", + "ssh2@^1.11.0": "patch:ssh2@npm%3A1.11.0#./.yarn/patches/ssh2-npm-1.11.0-convertSha1.patch" }, "workspaces": [ "./common", diff --git a/src/connect.ts b/src/connect.ts index 41f4f69..4f3c3d0 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -2,12 +2,12 @@ import type { FileSystemConfig } from 'common/fileSystemConfig'; import { readFile } from 'fs'; import { Socket } from 'net'; import { userInfo } from 'os'; -import { Client, ClientChannel, ConnectConfig } from 'ssh2'; +import { AuthHandlerFunction, AuthHandlerObject, Client, ClientChannel, ConnectConfig } from 'ssh2'; import { SFTP } from 'ssh2/lib/protocol/SFTP'; import * as vscode from 'vscode'; import { getConfig } from './config'; import { getFlagBoolean } from './flags'; -import { Logging } from './logging'; +import { Logger, Logging } from './logging'; import type { PuttySession } from './putty'; import { toPromise, validatePort } from './utils'; @@ -225,6 +225,34 @@ export async function createSocket(config: FileSystemConfig): Promise authsAllowed.shift() || false; +} + export async function createSSH(config: FileSystemConfig, sock?: NodeJS.ReadableStream): Promise { config = (await calculateActualConfig(config))!; if (!config) return null; @@ -253,7 +281,8 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable reject(error); }); try { - const finalConfig: ConnectConfig = { ...config, sock, ...DEFAULT_CONFIG }; + const finalConfig: FileSystemConfig = { ...config, sock, ...DEFAULT_CONFIG }; + finalConfig.authHandler = makeAuthHandler(finalConfig, logging); if (config.debug || getFlagBoolean('DEBUG_SSH2', false, config.flags)[0]) { const scope = Logging.scope(`ssh2(${config.name})`); finalConfig.debug = (msg: string) => scope.debug(msg); diff --git a/src/flags.ts b/src/flags.ts index 637fd47..941fa6a 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -8,6 +8,11 @@ import { catchingPromise } from './utils'; - Disables the 'diffie-hellman-group-exchange' kex algorithm as a default option - Originally for issue #239 - Automatically enabled for Electron v11.0, v11.1 and v11.2 + OPENSSH-SHA1 (boolean) (default=true) + - Patch for issue #309 where OpenSSH 8.8+ refuses `ssh-rsa` keys using SHA1 (which is what ssh2 uses) + - The patch (see `.yarn/patches/*-convertSha1.patch`) adds an option for `agent` and `publickey` authentications + - With this option enabled, the patch will, if the server supports it, make ssh2 use SHA512/SHA256 for `ssh-rsa` keys + / Mind that this option applies for every server, the patch doesn't (currently) check whether it's OpenSSH 8.8+ DEBUG_SSH2 (boolean) (default=false) - Enables debug logging in the ssh2 library (set at the start of each connection) WINDOWS_COMMAND_SEPARATOR (boolean) (default=false)