@@ -293,20 +293,21 @@ func TestSSH(t *testing.T) {
293
293
pty .WriteLine ("exit" )
294
294
<- cmdDone
295
295
})
296
+ }
296
297
297
- //nolint:paralleltest // This test uses t.Setenv.
298
- t . Run ( "ForwardGPG" , func (t * testing.T ) {
299
- if runtime .GOOS == "windows" {
300
- // While GPG forwarding from a Windows client works, we currently do
301
- // not support forwarding to a Windows workspace. Our tests use the
302
- // same platform for the "client" and "workspace" as they run in the
303
- // same process.
304
- t .Skip ("Test not supported on windows" )
305
- }
298
+ //nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel .
299
+ func TestSSH_ForwardGPG (t * testing.T ) {
300
+ if runtime .GOOS == "windows" {
301
+ // While GPG forwarding from a Windows client works, we currently do
302
+ // not support forwarding to a Windows workspace. Our tests use the
303
+ // same platform for the "client" and "workspace" as they run in the
304
+ // same process.
305
+ t .Skip ("Test not supported on windows" )
306
+ }
306
307
307
- // This key is for [email protected] .
308
- const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"
309
- const randPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
308
+ // This key is for [email protected] .
309
+ const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"
310
+ const randPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
310
311
311
312
mQINBF6SWkEBEADB8sAhBaT36VQ6HEhAmtKexLldu1HUdXNw16rdF+1wiBzSFfJN
312
313
aPeX4Y9iFIZgC2wU0wOjJ04BpioyOLtJngbThI5WpeoQ/1yQZOpnDaCMPPLp+uJ+
@@ -359,40 +360,40 @@ p7KeSZdlk47pMBGOfnvEmoQ=
359
360
=OxHv
360
361
-----END PGP PUBLIC KEY BLOCK-----`
361
362
362
- ctx , cancel := context .WithTimeout (context .Background (), testutil .WaitLong )
363
- defer cancel ()
363
+ ctx , cancel := context .WithTimeout (context .Background (), testutil .WaitLong )
364
+ defer cancel ()
364
365
365
- gpgPath , err := exec .LookPath ("gpg" )
366
- if err != nil {
367
- t .Skip ("gpg not found" )
368
- }
369
- gpgConfPath , err := exec .LookPath ("gpgconf" )
370
- if err != nil {
371
- t .Skip ("gpgconf not found" )
372
- }
373
- gpgAgentPath , err := exec .LookPath ("gpg-agent" )
374
- if err != nil {
375
- t .Skip ("gpg-agent not found" )
376
- }
366
+ gpgPath , err := exec .LookPath ("gpg" )
367
+ if err != nil {
368
+ t .Skip ("gpg not found" )
369
+ }
370
+ gpgConfPath , err := exec .LookPath ("gpgconf" )
371
+ if err != nil {
372
+ t .Skip ("gpgconf not found" )
373
+ }
374
+ gpgAgentPath , err := exec .LookPath ("gpg-agent" )
375
+ if err != nil {
376
+ t .Skip ("gpg-agent not found" )
377
+ }
377
378
378
- // Setup GPG home directory on the "client".
379
- gnupgHomeClient := tempDirUnixSocket (t )
380
- t .Setenv ("GNUPGHOME" , gnupgHomeClient )
379
+ // Setup GPG home directory on the "client".
380
+ gnupgHomeClient := tempDirUnixSocket (t )
381
+ t .Setenv ("GNUPGHOME" , gnupgHomeClient )
381
382
382
- // Get the agent extra socket path.
383
- var (
384
- stdout = bytes .NewBuffer (nil )
385
- stderr = bytes .NewBuffer (nil )
386
- )
387
- c := exec .CommandContext (ctx , gpgConfPath , "--list-dir" , "agent-extra-socket" )
388
- c .Stdout = stdout
389
- c .Stderr = stderr
390
- err = c .Run ()
391
- require .NoError (t , err , "get extra socket path failed: %s" , stderr .String ())
392
- extraSocketPath := strings .TrimSpace (stdout .String ())
393
-
394
- // Generate private key non-interactively.
395
- genKeyScript := `
383
+ // Get the agent extra socket path.
384
+ var (
385
+ stdout = bytes .NewBuffer (nil )
386
+ stderr = bytes .NewBuffer (nil )
387
+ )
388
+ c := exec .CommandContext (ctx , gpgConfPath , "--list-dir" , "agent-extra-socket" )
389
+ c .Stdout = stdout
390
+ c .Stderr = stderr
391
+ err = c .Run ()
392
+ require .NoError (t , err , "get extra socket path failed: %s" , stderr .String ())
393
+ extraSocketPath := strings .TrimSpace (stdout .String ())
394
+
395
+ // Generate private key non-interactively.
396
+ genKeyScript := `
396
397
Key-Type: 1
397
398
Key-Length: 2048
398
399
Subkey-Type: 1
402
403
Expire-Date: 0
403
404
%no-protection
404
405
`
405
- c = exec .CommandContext (ctx , gpgPath , "--batch" , "--gen-key" )
406
- c .Stdin = strings .NewReader (genKeyScript )
407
- out , err := c .CombinedOutput ()
408
- require .NoError (t , err , "generate key failed: %s" , out )
409
-
410
- // Import a random public key.
411
- stdin := strings .NewReader (randPublicKey + "\n " )
412
- c = exec .CommandContext (ctx , gpgPath , "--import" , "-" )
413
- c .Stdin = stdin
414
- out , err = c .CombinedOutput ()
415
- require .NoError (t , err , "import key failed: %s" , out )
416
-
417
- // Set ultimate trust on imported key.
418
- stdin = strings .NewReader (randPublicKeyFingerprint + ":6:\n " )
419
- c = exec .CommandContext (ctx , gpgPath , "--import-ownertrust" )
420
- c .Stdin = stdin
421
- out , err = c .CombinedOutput ()
422
- require .NoError (t , err , "import ownertrust failed: %s" , out )
423
-
424
- // Start the GPG agent.
425
- agentCmd := exec .CommandContext (ctx , gpgAgentPath , "--no-detach" , "--extra-socket" , extraSocketPath )
426
- agentCmd .Env = append (agentCmd .Env , "GNUPGHOME=" + gnupgHomeClient )
427
- agentPTY , agentProc , err := pty .Start (agentCmd , pty .WithPTYOption (pty .WithGPGTTY ()))
428
- require .NoError (t , err , "launch agent failed" )
429
- defer func () {
430
- _ = agentProc .Kill ()
431
- _ = agentPTY .Close ()
432
- }()
433
-
434
- // Get the agent socket path in the "workspace".
435
- gnupgHomeWorkspace := tempDirUnixSocket (t )
436
-
437
- stdout = bytes .NewBuffer (nil )
438
- stderr = bytes .NewBuffer (nil )
439
- c = exec .CommandContext (ctx , gpgConfPath , "--list-dir" , "agent-socket" )
440
- c .Env = append (c .Env , "GNUPGHOME=" + gnupgHomeWorkspace )
441
- c .Stdout = stdout
442
- c .Stderr = stderr
443
- err = c .Run ()
444
- require .NoError (t , err , "get agent socket path in workspace failed: %s" , stderr .String ())
445
- workspaceAgentSocketPath := strings .TrimSpace (stdout .String ())
446
- require .NotEqual (t , extraSocketPath , workspaceAgentSocketPath , "socket path should be different" )
447
-
448
- client , workspace , agentToken := setupWorkspaceForAgent (t , nil )
449
-
450
- agentClient := codersdk .New (client .URL )
451
- agentClient .SetSessionToken (agentToken )
452
- agentCloser := agent .New (agent.Options {
453
- Client : agentClient ,
454
- EnvironmentVariables : map [string ]string {
455
- "GNUPGHOME" : gnupgHomeWorkspace ,
456
- },
457
- Logger : slogtest .Make (t , nil ).Named ("agent" ),
458
- })
459
- defer agentCloser .Close ()
460
-
461
- cmd , root := clitest .New (t ,
462
- "ssh" ,
463
- workspace .Name ,
464
- "--forward-gpg" ,
465
- )
466
- clitest .SetupConfig (t , client , root )
467
- pty := ptytest .New (t )
468
- cmd .SetIn (pty .Input ())
469
- cmd .SetOut (pty .Output ())
470
- cmd .SetErr (pty .Output ())
471
- cmdDone := tGo (t , func () {
472
- err := cmd .ExecuteContext (ctx )
473
- assert .NoError (t , err , "ssh command failed" )
474
- })
475
- // Prevent the test from hanging if the asserts below kill the test
476
- // early. This will cause the command to exit with an error, which will
477
- // let the t.Cleanup'd `<-done` inside of `tGo` exit and not hang.
478
- // Without this, the test will hang forever on failure, preventing the
479
- // real error from being printed.
480
- t .Cleanup (cancel )
481
-
482
- // Wait for the prompt or any output really to indicate the command has
483
- // started and accepting input on stdin.
484
- _ = pty .Peek (ctx , 1 )
485
-
486
- pty .WriteLine ("echo hello 'world'" )
487
- pty .ExpectMatch ("hello world" )
488
-
489
- // Check the GNUPGHOME was correctly inherited via shell.
490
- pty .WriteLine ("env && echo env-''-command-done" )
491
- match := pty .ExpectMatch ("env--command-done" )
492
- require .Contains (t , match , "GNUPGHOME=" + gnupgHomeWorkspace , match )
493
-
494
- // Get the agent extra socket path in the "workspace" via shell.
495
- pty .WriteLine ("gpgconf --list-dir agent-socket && echo gpgconf-''-agentsocket-command-done" )
496
- pty .ExpectMatch (workspaceAgentSocketPath )
497
- pty .ExpectMatch ("gpgconf--agentsocket-command-done" )
498
-
499
- // List the keys in the "workspace".
500
- pty .WriteLine ("gpg --list-keys && echo gpg-''-listkeys-command-done" )
501
- listKeysOutput := pty .ExpectMatch ("gpg--listkeys-command-done" )
502
- require .
Contains (
t ,
listKeysOutput ,
"[ultimate] Coder Test <[email protected] >" )
503
- require .
Contains (
t ,
listKeysOutput ,
"[ultimate] Dean Sheather (work key) <[email protected] >" )
504
-
505
- // Try to sign something. This demonstrates that the forwarding is
506
- // working as expected, since the workspace doesn't have access to the
507
- // private key directly and must use the forwarded agent.
508
- pty .WriteLine ("echo 'hello world' | gpg --clearsign && echo gpg-''-sign-command-done" )
509
- pty .ExpectMatch ("BEGIN PGP SIGNED MESSAGE" )
510
- pty .ExpectMatch ("Hash:" )
511
- pty .ExpectMatch ("hello world" )
512
- pty .ExpectMatch ("gpg--sign-command-done" )
406
+ c = exec .CommandContext (ctx , gpgPath , "--batch" , "--gen-key" )
407
+ c .Stdin = strings .NewReader (genKeyScript )
408
+ out , err := c .CombinedOutput ()
409
+ require .NoError (t , err , "generate key failed: %s" , out )
410
+
411
+ // Import a random public key.
412
+ stdin := strings .NewReader (randPublicKey + "\n " )
413
+ c = exec .CommandContext (ctx , gpgPath , "--import" , "-" )
414
+ c .Stdin = stdin
415
+ out , err = c .CombinedOutput ()
416
+ require .NoError (t , err , "import key failed: %s" , out )
417
+
418
+ // Set ultimate trust on imported key.
419
+ stdin = strings .NewReader (randPublicKeyFingerprint + ":6:\n " )
420
+ c = exec .CommandContext (ctx , gpgPath , "--import-ownertrust" )
421
+ c .Stdin = stdin
422
+ out , err = c .CombinedOutput ()
423
+ require .NoError (t , err , "import ownertrust failed: %s" , out )
424
+
425
+ // Start the GPG agent.
426
+ agentCmd := exec .CommandContext (ctx , gpgAgentPath , "--no-detach" , "--extra-socket" , extraSocketPath )
427
+ agentCmd .Env = append (agentCmd .Env , "GNUPGHOME=" + gnupgHomeClient )
428
+ agentPTY , agentProc , err := pty .Start (agentCmd , pty .WithPTYOption (pty .WithGPGTTY ()))
429
+ require .NoError (t , err , "launch agent failed" )
430
+ defer func () {
431
+ _ = agentProc .Kill ()
432
+ _ = agentPTY .Close ()
433
+ }()
513
434
514
- // And we're done.
515
- pty .WriteLine ("exit" )
516
- <- cmdDone
435
+ // Get the agent socket path in the "workspace".
436
+ gnupgHomeWorkspace := tempDirUnixSocket (t )
437
+
438
+ stdout = bytes .NewBuffer (nil )
439
+ stderr = bytes .NewBuffer (nil )
440
+ c = exec .CommandContext (ctx , gpgConfPath , "--list-dir" , "agent-socket" )
441
+ c .Env = append (c .Env , "GNUPGHOME=" + gnupgHomeWorkspace )
442
+ c .Stdout = stdout
443
+ c .Stderr = stderr
444
+ err = c .Run ()
445
+ require .NoError (t , err , "get agent socket path in workspace failed: %s" , stderr .String ())
446
+ workspaceAgentSocketPath := strings .TrimSpace (stdout .String ())
447
+ require .NotEqual (t , extraSocketPath , workspaceAgentSocketPath , "socket path should be different" )
448
+
449
+ client , workspace , agentToken := setupWorkspaceForAgent (t , nil )
450
+
451
+ agentClient := codersdk .New (client .URL )
452
+ agentClient .SetSessionToken (agentToken )
453
+ agentCloser := agent .New (agent.Options {
454
+ Client : agentClient ,
455
+ EnvironmentVariables : map [string ]string {
456
+ "GNUPGHOME" : gnupgHomeWorkspace ,
457
+ },
458
+ Logger : slogtest .Make (t , nil ).Named ("agent" ),
459
+ })
460
+ defer agentCloser .Close ()
461
+
462
+ cmd , root := clitest .New (t ,
463
+ "ssh" ,
464
+ workspace .Name ,
465
+ "--forward-gpg" ,
466
+ )
467
+ clitest .SetupConfig (t , client , root )
468
+ tpty := ptytest .New (t )
469
+ cmd .SetIn (tpty .Input ())
470
+ cmd .SetOut (tpty .Output ())
471
+ cmd .SetErr (tpty .Output ())
472
+ cmdDone := tGo (t , func () {
473
+ err := cmd .ExecuteContext (ctx )
474
+ assert .NoError (t , err , "ssh command failed" )
517
475
})
476
+ // Prevent the test from hanging if the asserts below kill the test
477
+ // early. This will cause the command to exit with an error, which will
478
+ // let the t.Cleanup'd `<-done` inside of `tGo` exit and not hang.
479
+ // Without this, the test will hang forever on failure, preventing the
480
+ // real error from being printed.
481
+ t .Cleanup (cancel )
482
+
483
+ // Wait for the prompt or any output really to indicate the command has
484
+ // started and accepting input on stdin.
485
+ _ = tpty .Peek (ctx , 1 )
486
+
487
+ tpty .WriteLine ("echo hello 'world'" )
488
+ tpty .ExpectMatch ("hello world" )
489
+
490
+ // Check the GNUPGHOME was correctly inherited via shell.
491
+ tpty .WriteLine ("env && echo env-''-command-done" )
492
+ match := tpty .ExpectMatch ("env--command-done" )
493
+ require .Contains (t , match , "GNUPGHOME=" + gnupgHomeWorkspace , match )
494
+
495
+ // Get the agent extra socket path in the "workspace" via shell.
496
+ tpty .WriteLine ("gpgconf --list-dir agent-socket && echo gpgconf-''-agentsocket-command-done" )
497
+ tpty .ExpectMatch (workspaceAgentSocketPath )
498
+ tpty .ExpectMatch ("gpgconf--agentsocket-command-done" )
499
+
500
+ // List the keys in the "workspace".
501
+ tpty .WriteLine ("gpg --list-keys && echo gpg-''-listkeys-command-done" )
502
+ listKeysOutput := tpty .ExpectMatch ("gpg--listkeys-command-done" )
503
+ require .
Contains (
t ,
listKeysOutput ,
"[ultimate] Coder Test <[email protected] >" )
504
+ require .
Contains (
t ,
listKeysOutput ,
"[ultimate] Dean Sheather (work key) <[email protected] >" )
505
+
506
+ // Try to sign something. This demonstrates that the forwarding is
507
+ // working as expected, since the workspace doesn't have access to the
508
+ // private key directly and must use the forwarded agent.
509
+ tpty .WriteLine ("echo 'hello world' | gpg --clearsign && echo gpg-''-sign-command-done" )
510
+ tpty .ExpectMatch ("BEGIN PGP SIGNED MESSAGE" )
511
+ tpty .ExpectMatch ("Hash:" )
512
+ tpty .ExpectMatch ("hello world" )
513
+ tpty .ExpectMatch ("gpg--sign-command-done" )
514
+
515
+ // And we're done.
516
+ tpty .WriteLine ("exit" )
517
+ <- cmdDone
518
518
}
519
519
520
520
// tGoContext runs fn in a goroutine passing a context that will be
0 commit comments