@@ -13,7 +13,8 @@ import * as http from 'http';
13
13
import * as net from 'net' ;
14
14
import * as crypto from 'crypto' ;
15
15
import 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' ;
17
18
import * as tmp from 'tmp' ;
18
19
import * as path from 'path' ;
19
20
import * as vscode from 'vscode' ;
@@ -22,9 +23,12 @@ import { Disposable } from './common/dispose';
22
23
import { withServerApi } from './internalApi' ;
23
24
import TelemetryReporter from './telemetryReporter' ;
24
25
import { addHostToHostFile , checkNewHostInHostkeys } from './ssh/hostfile' ;
25
- import { checkDefaultIdentityFiles } from './ssh/identityFiles' ;
26
+ import { DEFAULT_IDENTITY_FILES } from './ssh/identityFiles' ;
26
27
import { HeartbeatManager } from './heartbeat' ;
27
28
import { getGitpodVersion , isFeatureSupported } from './featureSupport' ;
29
+ import SSHConfiguration from './ssh/sshConfig' ;
30
+ import { isWindows } from './common/platform' ;
31
+ import { untildify } from './common/files' ;
28
32
29
33
interface SSHConnectionParams {
30
34
workspaceId : string ;
@@ -429,6 +433,86 @@ export default class RemoteConnector extends Disposable {
429
433
}
430
434
}
431
435
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
+
432
516
private async getWorkspaceSSHDestination ( accessToken : string , { workspaceId, gitpodHost } : SSHConnectionParams ) : Promise < { destination : string ; password ?: string } > {
433
517
const serviceUrl = new URL ( gitpodHost ) ;
434
518
const gitpodVersion = await getGitpodVersion ( gitpodHost ) ;
@@ -504,39 +588,25 @@ export default class RemoteConnector extends Disposable {
504
588
this . logger . error ( `Couldn't write '${ sshDestInfo . hostName } ' host to known_hosts file:` , e ) ;
505
589
}
506
590
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 ) ;
509
593
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 ) ;
518
595
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' ) ;
524
598
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 ) ) ;
530
600
} else {
531
- if ( identityFilePaths . length ) {
601
+ if ( identityKeys . length ) {
532
602
sshDestInfo . user = `${ workspaceId } #${ ownerToken } ` ;
533
603
}
534
604
this . logger . warn ( `Registered SSH public keys not supported in ${ gitpodHost } , using version ${ gitpodVersion . version } ` ) ;
535
605
}
536
606
537
607
return {
538
608
destination : Buffer . from ( JSON . stringify ( sshDestInfo ) , 'utf8' ) . toString ( 'hex' ) ,
539
- password : identityFilePaths . length === 0 ? ownerToken : undefined
609
+ password : identityKeys . length === 0 ? ownerToken : undefined
540
610
} ;
541
611
}
542
612
0 commit comments