@@ -13,7 +13,8 @@ import * as http from 'http';
1313import * as net from 'net' ;
1414import * as crypto from 'crypto' ;
1515import fetch , { Response } from 'node-fetch' ;
16- import { Client as sshClient , utils as sshUtils } from 'ssh2' ;
16+ import { Client as sshClient , OpenSSHAgent , utils as sshUtils } from 'ssh2' ;
17+ import { ParsedKey } from 'ssh2-streams' ;
1718import * as tmp from 'tmp' ;
1819import * as path from 'path' ;
1920import * as vscode from 'vscode' ;
@@ -22,9 +23,12 @@ import { Disposable } from './common/dispose';
2223import { withServerApi } from './internalApi' ;
2324import TelemetryReporter from './telemetryReporter' ;
2425import { addHostToHostFile , checkNewHostInHostkeys } from './ssh/hostfile' ;
25- import { checkDefaultIdentityFiles } from './ssh/identityFiles' ;
26+ import { DEFAULT_IDENTITY_FILES } from './ssh/identityFiles' ;
2627import { HeartbeatManager } from './heartbeat' ;
2728import { getGitpodVersion , isFeatureSupported } from './featureSupport' ;
29+ import SSHConfiguration from './ssh/sshConfig' ;
30+ import { isWindows } from './common/platform' ;
31+ import { untildify } from './common/files' ;
2832
2933interface SSHConnectionParams {
3034 workspaceId : string ;
@@ -429,6 +433,86 @@ export default class RemoteConnector extends Disposable {
429433 }
430434 }
431435
436+ // From https://github.com/openssh/openssh-portable/blob/acb2059febaddd71ee06c2ebf63dcf211d9ab9f2/sshconnect2.c#L1689-L1690
437+ private async getIdentityKeys ( hostConfig : Record < string , string > ) {
438+ const identityFiles : string [ ] = ( ( hostConfig [ 'IdentityFile' ] as unknown as string [ ] ) || [ ] ) . map ( untildify ) ;
439+ if ( identityFiles . length === 0 ) {
440+ identityFiles . push ( ...DEFAULT_IDENTITY_FILES ) ;
441+ }
442+
443+ const identityFileContentsResult = await Promise . allSettled ( identityFiles . map ( async path => fs . promises . readFile ( path + '.pub' ) ) ) ;
444+ const fileKeys = identityFileContentsResult . map ( ( result , i ) => {
445+ if ( result . status === 'rejected' ) {
446+ return undefined ;
447+ }
448+
449+ const parsedResult = sshUtils . parseKey ( result . value ) ;
450+ if ( parsedResult instanceof Error || ! parsedResult ) {
451+ this . logger . error ( `Error while parsing SSH public key ${ identityFiles [ i ] + '.pub' } :` , parsedResult ) ;
452+ return undefined ;
453+ }
454+
455+ const parsedKey = Array . isArray ( parsedResult ) ? parsedResult [ 0 ] : parsedResult ;
456+ const fingerprint = crypto . createHash ( 'sha256' ) . update ( parsedKey . getPublicSSH ( ) ) . digest ( 'base64' ) ;
457+
458+ return {
459+ filename : identityFiles [ i ] ,
460+ parsedKey,
461+ fingerprint
462+ } ;
463+ } ) . filter ( < T > ( v : T | undefined ) : v is T => ! ! v ) ;
464+
465+ let sshAgentParsedKeys : ParsedKey [ ] = [ ] ;
466+ try {
467+ let sshAgentSock = isWindows ? '\\\\.\\pipe\\openssh-ssh-agent' : ( hostConfig [ 'IdentityAgent' ] || process . env [ 'SSH_AUTH_SOCK' ] ) ;
468+ if ( ! sshAgentSock ) {
469+ throw new Error ( `SSH_AUTH_SOCK environment variable not defined` ) ;
470+ }
471+ sshAgentSock = untildify ( sshAgentSock ) ;
472+
473+ sshAgentParsedKeys = await new Promise < ParsedKey [ ] > ( ( resolve , reject ) => {
474+ const sshAgent = new OpenSSHAgent ( sshAgentSock ! ) ;
475+ sshAgent . getIdentities ( ( err , publicKeys ) => {
476+ if ( err ) {
477+ reject ( err ) ;
478+ } else {
479+ resolve ( publicKeys || [ ] ) ;
480+ }
481+ } ) ;
482+ } ) ;
483+ } catch ( e ) {
484+ this . logger . error ( `Couldn't get identities from OpenSSH agent` , e ) ;
485+ }
486+
487+ const sshAgentKeys = sshAgentParsedKeys . map ( parsedKey => {
488+ const fingerprint = crypto . createHash ( 'sha256' ) . update ( parsedKey . getPublicSSH ( ) ) . digest ( 'base64' ) ;
489+ return {
490+ filename : parsedKey . comment ,
491+ parsedKey,
492+ fingerprint
493+ } ;
494+ } ) ;
495+
496+ const identitiesOnly = ( hostConfig [ 'IdentitiesOnly' ] || '' ) . toLowerCase ( ) === 'yes' ;
497+ const agentKeys : { filename : string ; parsedKey : ParsedKey ; fingerprint : string } [ ] = [ ] ;
498+ const preferredIdentityKeys : { filename : string ; parsedKey : ParsedKey ; fingerprint : string } [ ] = [ ] ;
499+ for ( const agentKey of sshAgentKeys ) {
500+ const foundIdx = fileKeys . findIndex ( k => agentKey . parsedKey . type === k . parsedKey . type && agentKey . fingerprint === k . fingerprint ) ;
501+ if ( foundIdx >= 0 ) {
502+ preferredIdentityKeys . push ( fileKeys [ foundIdx ] ) ;
503+ fileKeys . splice ( foundIdx , 1 ) ;
504+ } else if ( ! identitiesOnly ) {
505+ agentKeys . push ( agentKey ) ;
506+ }
507+ }
508+ preferredIdentityKeys . push ( ...agentKeys ) ;
509+ preferredIdentityKeys . push ( ...fileKeys ) ;
510+
511+ this . logger . trace ( `Identity keys:` , preferredIdentityKeys . length ? preferredIdentityKeys . map ( k => `${ k . filename } ${ k . parsedKey . type } SHA256:${ k . fingerprint } ` ) . join ( '\n' ) : 'None' ) ;
512+
513+ return preferredIdentityKeys ;
514+ }
515+
432516 private async getWorkspaceSSHDestination ( accessToken : string , { workspaceId, gitpodHost } : SSHConnectionParams ) : Promise < { destination : string ; password ?: string } > {
433517 const serviceUrl = new URL ( gitpodHost ) ;
434518 const gitpodVersion = await getGitpodVersion ( gitpodHost ) ;
@@ -504,39 +588,25 @@ export default class RemoteConnector extends Disposable {
504588 this . logger . error ( `Couldn't write '${ sshDestInfo . hostName } ' host to known_hosts file:` , e ) ;
505589 }
506590
507- let identityFilePaths = await checkDefaultIdentityFiles ( ) ;
508- this . logger . trace ( `Default identity files:` , identityFilePaths . length ? identityFilePaths . toString ( ) : 'None' ) ;
591+ const sshConfiguration = await SSHConfiguration . loadFromFS ( ) ;
592+ const hostConfiguration = sshConfiguration . getHostConfiguration ( sshDestInfo . hostName ) ;
509593
510- if ( registeredSSHKeys ) {
511- const keyFingerprints = registeredSSHKeys . map ( i => i . fingerprint ) ;
512- const publickKeyFiles = await Promise . allSettled ( identityFilePaths . map ( path => fs . promises . readFile ( path + '.pub' ) ) ) ;
513- identityFilePaths = identityFilePaths . filter ( ( _ , index ) => {
514- const result = publickKeyFiles [ index ] ;
515- if ( result . status === 'rejected' ) {
516- return false ;
517- }
594+ let identityKeys = await this . getIdentityKeys ( hostConfiguration ) ;
518595
519- const parsedResult = sshUtils . parseKey ( result . value ) ;
520- if ( parsedResult instanceof Error || ! parsedResult ) {
521- this . logger . error ( `Error while parsing SSH public key${ identityFilePaths [ index ] + '.pub' } :` , parsedResult ) ;
522- return false ;
523- }
596+ if ( registeredSSHKeys ) {
597+ this . logger . trace ( `Registered public keys in Gitpod account:` , registeredSSHKeys . length ? registeredSSHKeys . map ( k => `${ k . name } SHA256:${ k . fingerprint } ` ) . join ( '\n' ) : 'None' ) ;
524598
525- const parsedKey = Array . isArray ( parsedResult ) ? parsedResult [ 0 ] : parsedResult ;
526- const fingerprint = crypto . createHash ( 'sha256' ) . update ( parsedKey . getPublicSSH ( ) ) . digest ( 'base64' ) ;
527- return keyFingerprints . includes ( fingerprint ) ;
528- } ) ;
529- this . logger . trace ( `Registered public keys in Gitpod account:` , identityFilePaths . length ? identityFilePaths . toString ( ) : 'None' ) ;
599+ identityKeys = identityKeys . filter ( k => ! ! registeredSSHKeys . find ( regKey => regKey . fingerprint === k . fingerprint ) ) ;
530600 } else {
531- if ( identityFilePaths . length ) {
601+ if ( identityKeys . length ) {
532602 sshDestInfo . user = `${ workspaceId } #${ ownerToken } ` ;
533603 }
534604 this . logger . warn ( `Registered SSH public keys not supported in ${ gitpodHost } , using version ${ gitpodVersion . version } ` ) ;
535605 }
536606
537607 return {
538608 destination : Buffer . from ( JSON . stringify ( sshDestInfo ) , 'utf8' ) . toString ( 'hex' ) ,
539- password : identityFilePaths . length === 0 ? ownerToken : undefined
609+ password : identityKeys . length === 0 ? ownerToken : undefined
540610 } ;
541611 }
542612
0 commit comments