Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion cmd/agent/git_ssh_signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type GitSSHSignatureCmd struct {
Namespace string
BufferFile string
Command string
UseAgent bool
}

// NewGitSSHSignatureCmd creates new git-ssh-signature command
Expand Down Expand Up @@ -50,13 +51,14 @@ func NewGitSSHSignatureCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd.BufferFile = args[len(args)-1]

return gitsshsigning.HandleGitSSHProgramCall(
cmd.CertPath, cmd.Namespace, cmd.BufferFile, logger)
cmd.CertPath, cmd.Namespace, cmd.BufferFile, cmd.UseAgent, logger)
},
}

gitSshSignatureCmd.Flags().StringVarP(&cmd.CertPath, "file", "f", "", "Path to the private key")
gitSshSignatureCmd.Flags().StringVarP(&cmd.Namespace, "namespace", "n", "", "Namespace")
gitSshSignatureCmd.Flags().StringVarP(&cmd.Command, "command", "Y", "sign", "Command - should be 'sign'")
gitSshSignatureCmd.Flags().BoolVarP(&cmd.UseAgent, "agent", "U", false, "Key resides in ssh-agent")

return gitSshSignatureCmd
}
15 changes: 8 additions & 7 deletions pkg/gitsshsigning/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (
)

// HandleGitSSHProgramCall implements logic handling call from git when signing a commit
func HandleGitSSHProgramCall(certPath, namespace, bufferFile string, log log.Logger) error {
func HandleGitSSHProgramCall(certPath, namespace, bufferFile string, useAgent bool, log log.Logger) error {
content, err := extractContentFromGitBuffer(bufferFile)
if err != nil {
return err
}

signature, err := requestContentSignature(content, certPath, log)
signature, err := requestContentSignature(content, certPath, useAgent, log)
if err != nil {
return err
}
Expand All @@ -37,8 +37,8 @@ func extractContentFromGitBuffer(bufferFile string) ([]byte, error) {
}

// requestContentSignature sends an HTTP request to the credentials server to sign the content
func requestContentSignature(content []byte, certPath string, log log.Logger) ([]byte, error) {
requestBody, err := createSignatureRequestBody(content, certPath)
func requestContentSignature(content []byte, certPath string, useAgent bool, log log.Logger) ([]byte, error) {
requestBody, err := createSignatureRequestBody(content, certPath, useAgent)
if err != nil {
return nil, err
}
Expand All @@ -61,10 +61,11 @@ func writeSignatureToFile(signature []byte, bufferFile string, log log.Logger) e
return nil
}

func createSignatureRequestBody(content []byte, certPath string) ([]byte, error) {
func createSignatureRequestBody(content []byte, certPath string, useAgent bool) ([]byte, error) {
request := &GitSSHSignatureRequest{
Content: string(content),
KeyPath: certPath,
Content: string(content),
KeyPath: certPath,
UseAgent: useAgent,
}
return json.Marshal(request)
}
Expand Down
13 changes: 10 additions & 3 deletions pkg/gitsshsigning/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
)

type GitSSHSignatureRequest struct {
Content string
KeyPath string
Content string
KeyPath string
UseAgent bool `json:"UseAgent,omitempty"`
}

type GitSSHSignatureResponse struct {
Expand All @@ -23,8 +24,14 @@ func (req *GitSSHSignatureRequest) Sign() (*GitSSHSignatureResponse, error) {
var commitBuffer bytes.Buffer
commitBuffer.WriteString(req.Content)

// Build ssh-keygen arguments
args := []string{"-Y", "sign", "-f", req.KeyPath, "-n", "git"}
if req.UseAgent {
args = append(args, "-U")
}

// Create the command to run ssh-keygen
cmd := exec.Command("ssh-keygen", "-Y", "sign", "-f", req.KeyPath, "-n", "git")
cmd := exec.Command("ssh-keygen", args...)
cmd.Stdin = &commitBuffer

// Capture the output of the command
Expand Down
137 changes: 137 additions & 0 deletions pkg/gitsshsigning/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package gitsshsigning

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

func TestSign_WithoutAgent(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "gitsshsigning-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()

// Generate a test key pair
keyPath := filepath.Join(tmpDir, "test_key")
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", keyPath, "-N", "", "-q")
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to generate test key: %v", err)
}

// Test signing without UseAgent (direct private key)
req := &GitSSHSignatureRequest{
Content: "test commit content\n",
KeyPath: keyPath,
UseAgent: false,
}

resp, err := req.Sign()
if err != nil {
t.Fatalf("Sign() failed: %v", err)
}

if len(resp.Signature) == 0 {
t.Error("Expected non-empty signature")
}

// Verify signature looks like an SSH signature
sigStr := string(resp.Signature)
if !strings.Contains(sigStr, "-----BEGIN SSH SIGNATURE-----") {
t.Errorf("Signature doesn't look like SSH signature: %s", sigStr)
}
}

func TestSign_WithAgent(t *testing.T) {
// Skip if SSH_AUTH_SOCK is not set (no agent running)
if os.Getenv("SSH_AUTH_SOCK") == "" {
t.Skip("Skipping agent test: SSH_AUTH_SOCK not set")
}

// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "gitsshsigning-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()

// Generate a test key pair
keyPath := filepath.Join(tmpDir, "test_key")
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", keyPath, "-N", "", "-q")
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to generate test key: %v", err)
}

// Add key to agent
cmd = exec.Command("ssh-add", keyPath)
if err := cmd.Run(); err != nil {
t.Skipf("Failed to add key to agent (agent may not be running): %v", err)
}

// Remove key from agent when done
defer func() {
_ = exec.Command("ssh-add", "-d", keyPath).Run()
}()

// Test signing with UseAgent (public key + agent lookup)
req := &GitSSHSignatureRequest{
Content: "test commit content\n",
KeyPath: keyPath + ".pub", // Use public key path
UseAgent: true,
}

resp, err := req.Sign()
if err != nil {
t.Fatalf("Sign() with UseAgent=true failed: %v", err)
}

if len(resp.Signature) == 0 {
t.Error("Expected non-empty signature")
}

// Verify signature looks like an SSH signature
sigStr := string(resp.Signature)
if !strings.Contains(sigStr, "-----BEGIN SSH SIGNATURE-----") {
t.Errorf("Signature doesn't look like SSH signature: %s", sigStr)
}
}

func TestSign_WithAgent_NoAgentFails(t *testing.T) {
// This test verifies that using UseAgent=true with a public key
// fails when no agent has the corresponding private key

// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "gitsshsigning-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()

// Generate a test key pair (not added to any agent)
keyPath := filepath.Join(tmpDir, "test_key")
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", keyPath, "-N", "", "-q")
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to generate test key: %v", err)
}

// Temporarily unset SSH_AUTH_SOCK to ensure no agent
oldSock := os.Getenv("SSH_AUTH_SOCK")
_ = os.Unsetenv("SSH_AUTH_SOCK")
defer func() { _ = os.Setenv("SSH_AUTH_SOCK", oldSock) }()

// Test signing with UseAgent but no agent available
req := &GitSSHSignatureRequest{
Content: "test commit content\n",
KeyPath: keyPath + ".pub",
UseAgent: true,
}

_, err = req.Sign()
if err == nil {
t.Error("Expected Sign() to fail when UseAgent=true but no agent available")
}
}