diff --git a/cmd/agent/workspace/setup_gpg.go b/cmd/agent/workspace/setup_gpg.go index 26cb8841b..ecd546f66 100644 --- a/cmd/agent/workspace/setup_gpg.go +++ b/cmd/agent/workspace/setup_gpg.go @@ -135,10 +135,8 @@ func (cmd *SetupGPGCmd) Run(ctx context.Context, log log.Logger) error { if gpgConf.GitKey != "" { log.Debugf("Setup git signing key") - err = gitcredentials.SetupGpgGitKey(gpgConf.GitKey) - if err != nil { - log.Errorf("Setup git signing key: %v", err) - return err + if err := gitcredentials.SetupGpgGitKey(gpgConf.GitKey); err != nil { + log.Warnf("Setup git signing key failed (non-fatal): %v", err) } } diff --git a/cmd/ssh.go b/cmd/ssh.go index 453473871..82fd67b86 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -710,12 +710,7 @@ func (cmd *SSHCmd) setupGPGAgent( gpgExtraSocketPath := strings.TrimSpace(string(gpgExtraSocketBytes)) log.Debugf("[GPG] detected gpg-agent socket path %s", gpgExtraSocketPath) - gitGpgKey, err := exec.Command("git", []string{"config", "user.signingKey"}...).Output() - if err != nil { - log.Debugf("[GPG] no git signkey detected, skipping") - } else { - log.Debugf("[GPG] detected git sign key %s", gitGpgKey) - } + gitKey := gpgSigningKey(log) cmd.ReverseForwardPorts = append(cmd.ReverseForwardPorts, gpgExtraSocketPath) @@ -735,8 +730,7 @@ func (cmd *SSHCmd) setupGPGAgent( forwardAgent = append(forwardAgent, "--debug") } - if len(gitGpgKey) > 0 { - gitKey := strings.TrimSpace(string(gitGpgKey)) + if gitKey != "" { forwardAgent = append(forwardAgent, "--gitkey") forwardAgent = append(forwardAgent, gitKey) } @@ -770,6 +764,29 @@ func (cmd *SSHCmd) setupGPGAgent( return nil } +// gpgSigningKey returns the user's GPG signing key from git config, +// or empty string if no key is configured or the signing format is SSH +// (SSH signing keys are handled by the separate SSH signature helper). +func gpgSigningKey(log log.Logger) string { + format, err := exec.Command("git", "config", "--get", "gpg.format").Output() + if err == nil && strings.TrimSpace(string(format)) == "ssh" { + log.Debugf( + "[GPG] gpg.format is ssh, skipping GPG signing key (handled by SSH signing helper)", + ) + return "" + } + + key, err := exec.Command("git", "config", "--get", "user.signingKey").Output() + if err != nil { + log.Debugf("[GPG] no git signkey detected, skipping") + return "" + } + + result := strings.TrimSpace(string(key)) + log.Debugf("[GPG] detected git sign key %s", result) + return result +} + func startSSHKeepAlive( ctx context.Context, client *ssh.Client, diff --git a/cmd/ssh_test.go b/cmd/ssh_test.go new file mode 100644 index 000000000..a6783f7b3 --- /dev/null +++ b/cmd/ssh_test.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/skevetter/log" + "github.com/stretchr/testify/assert" +) + +func writeGitConfig(t *testing.T, content string) { + t.Helper() + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", home) + t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(home, ".gitconfig")) + err := os.WriteFile(filepath.Join(home, ".gitconfig"), []byte(content), 0o600) + assert.NoError(t, err) +} + +func TestGpgSigningKey_GPGFormat(t *testing.T) { + writeGitConfig(t, "[user]\n\tsigningKey = TESTKEY123\n") + result := gpgSigningKey(log.Discard) + assert.Equal(t, "TESTKEY123", result) +} + +func TestGpgSigningKey_SSHFormat_Skipped(t *testing.T) { + writeGitConfig( + t, + "[gpg]\n\tformat = ssh\n[user]\n\tsigningKey = /home/user/.ssh/id_ed25519.pub\n", + ) + result := gpgSigningKey(log.Discard) + assert.Empty(t, result) +} + +func TestGpgSigningKey_NoKeyConfigured(t *testing.T) { + writeGitConfig(t, "[user]\n\tname = Test\n") + result := gpgSigningKey(log.Discard) + assert.Empty(t, result) +} + +func TestGpgSigningKey_X509Format_Returned(t *testing.T) { + writeGitConfig(t, "[gpg]\n\tformat = x509\n[user]\n\tsigningKey = /path/to/cert\n") + result := gpgSigningKey(log.Discard) + assert.Equal(t, "/path/to/cert", result) +} diff --git a/e2e/tests/ssh/ssh.go b/e2e/tests/ssh/ssh.go index 7bc39200c..0bf53f424 100644 --- a/e2e/tests/ssh/ssh.go +++ b/e2e/tests/ssh/ssh.go @@ -61,6 +61,56 @@ var _ = ginkgo.Describe("devpod ssh test suite", ginkgo.Label("ssh"), ginkgo.Ord }, ) + ginkgo.It( + "should start workspace with GPG forwarding when host uses SSH signing format", + ginkgo.Label("gpg"), + ginkgo.SpecTimeout(5*time.Minute), + func(ctx ginkgo.SpecContext) { + if runtime.GOOS == osWindows { + ginkgo.Skip("skipping on windows") + } + + tempDir, err := framework.CopyToTempDir("tests/ssh/testdata/gpg-forwarding") + framework.ExpectNoError(err) + + f := framework.NewDefaultFramework(initialDir + "/bin") + _ = f.DevPodProviderAdd(ctx, "docker") + err = f.DevPodProviderUse(ctx, "docker") + framework.ExpectNoError(err) + + ginkgo.DeferCleanup(func(cleanupCtx context.Context) { + _ = f.DevPodWorkspaceDelete(cleanupCtx, tempDir) + framework.CleanupTempDir(initialDir, tempDir) + }) + + sshKeyDir := ginkgo.GinkgoT().TempDir() + + keyPath := filepath.Join(sshKeyDir, "id_ed25519") + // #nosec G204 -- test command with controlled arguments + err = exec.Command( + "ssh-keygen", "-t", "ed25519", "-f", keyPath, "-N", "", "-q", + ).Run() + framework.ExpectNoError(err) + + gitConfigDir := ginkgo.GinkgoT().TempDir() + gitConfigPath := filepath.Join(gitConfigDir, ".gitconfig") + gitConfigContent := "[gpg]\n\tformat = ssh\n[user]\n\tsigningKey = " + + keyPath + ".pub\n\tname = Test\n\temail = test@test.com\n" + err = os.WriteFile(gitConfigPath, []byte(gitConfigContent), 0o600) + framework.ExpectNoError(err) + ginkgo.GinkgoT().Setenv("GIT_CONFIG_GLOBAL", gitConfigPath) + + err = f.DevPodUp(ctx, tempDir, "--gpg-agent-forwarding") + framework.ExpectNoError(err) + + devpodSSHDeadline := time.Now().Add(20 * time.Second) + devpodSSHCtx, cancelSSH := context.WithDeadline(ctx, devpodSSHDeadline) + defer cancelSSH() + err = f.DevPodSSHEchoTestString(devpodSSHCtx, tempDir) + framework.ExpectNoError(err) + }, + ) + ginkgo.It( "should set up git SSH signature helper and sign a commit", ginkgo.SpecTimeout(7*time.Minute),