Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions cmd/agent/workspace/setup_gpg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
33 changes: 25 additions & 8 deletions cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions cmd/ssh_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
59 changes: 59 additions & 0 deletions e2e/tests/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,65 @@ 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)
})

// Configure host git to use SSH signing format with an SSH key,
// simulating the reporter's setup from issue #731.
sshKeyDir, err := os.MkdirTemp("", "devpod-gpg-ssh-test")
framework.ExpectNoError(err)
defer func() { _ = os.RemoveAll(sshKeyDir) }()

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)

// Create a temporary gitconfig with SSH signing format
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)

// Start workspace with GPG agent forwarding enabled.
// Before the fix, this would fail because setup-gpg would try
// to use the SSH key path as a GPG key and crash.
err = f.DevPodUp(ctx, tempDir, "--gpg-agent-forwarding")
framework.ExpectNoError(err)

// Verify SSH server is running and reachable
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),
Expand Down