Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: commit signing using ssh and gpg #1146

Merged
merged 50 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b00db8b
feat: add support for commit signing
divanshu-go Oct 1, 2024
a65318e
hide signing key and show signing method
divanshu-go Oct 1, 2024
a442f1e
hide signing key and show signing method
divanshu-go Oct 1, 2024
c1bd88c
fix
divanshu-go Oct 1, 2024
c0079d6
fix
divanshu-go Oct 1, 2024
09e4eed
fix
divanshu-go Jan 5, 2024
c5043ba
verify ssh commits locally
divanshu-go Oct 3, 2024
1dddf9a
fix
divanshu-go Oct 3, 2024
b4aca6a
please test changes
divanshu-go Oct 4, 2024
9997aee
Merge branch 'main' into commits-git
divanshu-go Oct 4, 2024
9b3b721
fix
divanshu-go Oct 4, 2024
96a8f04
fix
divanshu-go Oct 4, 2024
e7cabe7
fix
divanshu-go Oct 5, 2024
a552ba4
refactor and add gpg setup functions
divanshu-go Oct 5, 2024
d95c622
fix
divanshu-go Oct 6, 2024
d039fbc
fix
divanshu-go Oct 6, 2024
c812e7f
refactor code and add mask bool to gp
divanshu-go Oct 7, 2024
c95e12f
fix
divanshu-go Oct 7, 2024
ed7f4ca
fix
divanshu-go Oct 7, 2024
b5829e7
refactor code
divanshu-go Oct 7, 2024
a28a422
fix
divanshu-go Oct 8, 2024
30acc35
refactor and check if gpg is installed
divanshu-go Oct 8, 2024
3018bf1
remove duplicate function
divanshu-go Oct 8, 2024
944a1d7
Merge branch 'daytonaio:main' into commits-git
divanshu-go Oct 8, 2024
87915fc
fix
divanshu-go Oct 8, 2024
1dbbec7
fix
divanshu-go Oct 9, 2024
aaf817b
fix
divanshu-go Oct 9, 2024
8858598
fix done
divanshu-go Oct 9, 2024
9bfbe6d
resolve merge conflicts
divanshu-go Oct 10, 2024
8f3b150
resolve issues
divanshu-go Oct 10, 2024
198c597
fix lint
divanshu-go Oct 10, 2024
1667dd6
Merge branch 'main' into commits-git
divanshu-go Oct 11, 2024
b23418d
fix
divanshu-go Oct 11, 2024
f3eb4c1
Merge branch 'main' into commits-git
divanshu-go Oct 11, 2024
b5c4bcd
lint
divanshu-go Oct 11, 2024
a722b33
lint
divanshu-go Oct 11, 2024
921366b
merge conflict resolved
divanshu-go Oct 11, 2024
b2419ff
refactor code
divanshu-go Oct 11, 2024
c41ba7a
resolve and refactor merge conflicts
divanshu-go Oct 11, 2024
d6d8314
refactor and update git api methods
divanshu-go Oct 12, 2024
7760bd3
fix lint
divanshu-go Oct 13, 2024
89a3434
fix
divanshu-go Oct 13, 2024
a2b699d
remove nil checks
divanshu-go Oct 14, 2024
1a3488f
fix
divanshu-go Oct 14, 2024
68ed24b
fix
divanshu-go Oct 14, 2024
3cb4924
lint
divanshu-go Oct 14, 2024
daa9f3e
fix
divanshu-go Oct 14, 2024
5258778
resolve conflicts
divanshu-go Oct 15, 2024
cad2122
add field signing method
divanshu-go Oct 15, 2024
0a09a10
use trace instead of warn
divanshu-go Oct 16, 2024
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
17 changes: 17 additions & 0 deletions cmd/daytona/config/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ func GetDocsLinkFromGitProvider(providerId string) string {
}
}

func GetDocsLinkForCommitSigning(providerId string) string {
switch providerId {
case "github", "github-enterprise-server":
return "https://docs.github.com/en/authentication/managing-commit-signature-verification"
case "gitlab", "gitlab-self-managed":
return "https://docs.gitlab.com/ee/user/project/repository/signed_commits"
case "gitea":
return "https://docs.gitea.com/administration/signing"
case "azure-devops":
return "https://learn.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops"
case "aws-codecommit":
return "https://docs.aws.amazon.com/codecommit/latest/userguide/setting-up-ssh-unixes.html"
default:
return ""
}
}

func GetRequiredScopesFromGitProviderId(providerId string) string {
switch providerId {
case "github":
Expand Down
174 changes: 143 additions & 31 deletions cmd/daytona/config/ssh_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
package config

import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"

log "github.com/sirupsen/logrus"
)

var sshHomeDir string
Expand Down Expand Up @@ -104,7 +108,7 @@ func UnlinkSshFiles() error {

// Add ssh entry

func generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsPath string) (string, error) {
func generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsPath string, gpgForward bool) (string, error) {
daytonaPath, err := os.Executable()
if err != nil {
return "", err
Expand All @@ -118,74 +122,157 @@ func generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsPath
tab+"StrictHostKeyChecking no\n"+
tab+"UserKnownHostsFile %s\n"+
tab+"ProxyCommand \"%s\" ssh-proxy %s %s %s\n"+
tab+"ForwardAgent yes\n\n", projectHostname, knownHostsPath, daytonaPath, profileId, workspaceId, projectName)
tab+"ForwardAgent yes\n", projectHostname, knownHostsPath, daytonaPath, profileId, workspaceId, projectName)

return config, nil
}
if gpgForward {
localSocket, err := getLocalGPGSocket()
if err != nil {
log.Warn(err)
return config, nil
}

func EnsureSshConfigEntryAdded(profileId, workspaceName, projectName string) error {
err := ensureSshFilesLinked()
if err != nil {
return err
}
remoteSocket, err := getRemoteGPGSocket(projectHostname)
if err != nil {
log.Warn(err)
return config, nil
}

knownHostsFile := "/dev/null"
if runtime.GOOS == "windows" {
knownHostsFile = "NUL"
config += fmt.Sprintf(
tab+"StreamLocalBindUnlink yes\n"+
tab+"RemoteForward %s:%s\n\n", remoteSocket, localSocket)
} else {
config += "\n"
}

data, err := generateSshConfigEntry(profileId, workspaceName, projectName, knownHostsFile)
return config, nil
}

func EnsureSshConfigEntryAdded(profileId, workspaceName, projectName string, gpgKey string) error {
err := ensureSshFilesLinked()
if err != nil {
return err
}

sshDir := filepath.Join(sshHomeDir, ".ssh")
configPath := filepath.Join(sshDir, "daytona_config")

knownHostsFile := getKnownHostsFile()

// Read existing content from the file
existingContent, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return err
}

if strings.Contains(string(existingContent), data) {
return nil
// Generate SSH config entry without GPG forwarding
newContent, err := appendSshConfigEntry(configPath, profileId, workspaceName, projectName, knownHostsFile, false, string(existingContent))
if err != nil {
return err
}

// Combine the new data with existing content
newData := data + string(existingContent)
if gpgKey != "" {
// Generate SSH config entry with GPG forwarding and override previous config
_, err := appendSshConfigEntry(configPath, profileId, workspaceName, projectName, knownHostsFile, true, newContent)
if err != nil {
return err
}
projectHostname := GetProjectHostname(profileId, workspaceName, projectName)
err = ExportGPGKey(gpgKey, projectHostname)
if err != nil {
return err
}
}

return nil
}

func getKnownHostsFile() string {
if runtime.GOOS == "windows" {
return "NUL"
}
return "/dev/null"
}

func appendSshConfigEntry(configPath, profileId, workspaceId, projectName, knownHostsFile string, gpgForward bool, existingContent string) (string, error) {
data, err := generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsFile, gpgForward)
if err != nil {
return "", err
}

if strings.Contains(existingContent, data) {
// Entry already exists in the file
return existingContent, nil
}

// We want to remove the config entry gpg counterpart
configCounterpart, err := generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsFile, !gpgForward)
if err != nil {
return "", err
}
updatedContent := strings.ReplaceAll(existingContent, configCounterpart, "")
updatedContent = data + updatedContent

// Open the file for writing
file, err := os.Create(configPath)
if err != nil {
return err
return "", err
}
defer file.Close()

_, err = file.WriteString(newData)
_, err = file.WriteString(updatedContent)
return updatedContent, err
}

func getLocalGPGSocket() (string, error) {
// Check if gpg is installed
if _, err := exec.LookPath("gpg"); err != nil {
return "", fmt.Errorf("gpg is not installed: %v", err)
}

// Attempt to get the local GPG socket
cmd := exec.Command("gpgconf", "--list-dir", "agent-extra-socket")
output, err := cmd.Output()
if err != nil {
return err
return "", fmt.Errorf("failed to get local GPG socket: %v", err)
}
return strings.TrimSpace(string(output)), nil
}

return nil
func getRemoteGPGSocket(projectHostname string) (string, error) {
cmd := exec.Command("ssh", projectHostname, "gpgconf --list-dir agent-socket")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get remote GPG socket: %v", err)
}
return strings.TrimSpace(string(output)), nil
}

func RemoveWorkspaceSshEntries(profileId, workspaceId string) error {
sshDir := filepath.Join(sshHomeDir, ".ssh")
configPath := filepath.Join(sshDir, "daytona_config")
func ExportGPGKey(keyID, projectHostname string) error {
exportCmd := exec.Command("gpg", "--export", keyID)
var output bytes.Buffer
exportCmd.Stdout = &output

// Read existing content from the file
existingContent, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return nil
if err := exportCmd.Run(); err != nil {
return err
}

regex := regexp.MustCompile(fmt.Sprintf(`Host %s-%s-\w+\n(?:\t.*\n?)*`, profileId, workspaceId))
newContent := regex.ReplaceAllString(string(existingContent), "")
importCmd := exec.Command("ssh", projectHostname, "gpg --import")
importCmd.Stdin = &output

return importCmd.Run()
}

func readSshConfig(configPath string) (string, error) {
content, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return "", err
}
return string(content), nil
}

func writeSshConfig(configPath, newContent string) error {
newContent = strings.Trim(newContent, "\n")

// Open the file for writing
file, err := os.Create(configPath)
if err != nil {
return err
Expand All @@ -196,6 +283,31 @@ func RemoveWorkspaceSshEntries(profileId, workspaceId string) error {
if err != nil {
return err
}
return nil
}

// RemoveWorkspaceSshEntries removes all SSH entries for a given profileId and workspaceId
func RemoveWorkspaceSshEntries(profileId, workspaceId string) error {
sshDir := filepath.Join(os.Getenv("HOME"), ".ssh")
configPath := filepath.Join(sshDir, "daytona_config")

// Read existing content from the SSH config file
existingContent, err := readSshConfig(configPath)
if err != nil {
return err
}

// Define the regex pattern to match Host entries for the given profileId and workspaceId
regex := regexp.MustCompile(fmt.Sprintf(`Host %s-%s-\w+\s*\n(?:\t.*\n?)*`, profileId, workspaceId))

// Replace matched entries with an empty string
newContent := regex.ReplaceAllString(existingContent, "")

// Write the updated content back to the config file
err = writeSshConfig(configPath, newContent)
if err != nil {
return err
}

return nil
}
Expand Down
4 changes: 2 additions & 2 deletions internal/testing/git/mocks/gitservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func (m *MockGitService) RepositoryExists() (bool, error) {
return args.Bool(0), args.Error(1)
}

func (m *MockGitService) SetGitConfig(userData *gitprovider.GitUser) error {
args := m.Called(userData)
func (m *MockGitService) SetGitConfig(userData *gitprovider.GitUser, providerConfig *gitprovider.GitProviderConfig) error {
args := m.Called(userData, providerConfig)
return args.Error(0)
}

Expand Down
10 changes: 5 additions & 5 deletions internal/util/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"github.com/daytonaio/daytona/cmd/daytona/config"
)

func GetHomeDir(activeProfile config.Profile, workspaceId string, projectName string) (string, error) {
err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName)
func GetHomeDir(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string) (string, error) {
err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey)
if err != nil {
return "", err
}
Expand All @@ -27,8 +27,8 @@ func GetHomeDir(activeProfile config.Profile, workspaceId string, projectName st
return strings.TrimRight(string(homeDir), "\n"), nil
}

func GetProjectDir(activeProfile config.Profile, workspaceId string, projectName string) (string, error) {
err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName)
func GetProjectDir(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string) (string, error) {
err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey)
if err != nil {
return "", err
}
Expand All @@ -44,7 +44,7 @@ func GetProjectDir(activeProfile config.Profile, workspaceId string, projectName
return strings.TrimRight(string(daytonaProjectDir), "\n"), nil
}

homeDir, err := GetHomeDir(activeProfile, workspaceId, projectName)
homeDir, err := GetHomeDir(activeProfile, workspaceId, projectName, gpgKey)
if err != nil {
return "", err
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,14 @@ func (a *Agent) startProjectMode() error {
}
}

err = a.Git.SetGitConfig(gitUser)
var providerConfig *gitprovider.GitProviderConfig
if gitProvider != nil {
providerConfig = &gitprovider.GitProviderConfig{
SigningMethod: (*gitprovider.SigningMethod)(gitProvider.SigningMethod),
SigningKey: gitProvider.SigningKey,
}
}
err = a.Git.SetGitConfig(gitUser, providerConfig)
if err != nil {
log.Error(fmt.Sprintf("failed to set git config: %s", err))
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestAgent(t *testing.T) {

mockGitService := mock_git.NewMockGitService()
mockGitService.On("RepositoryExists").Return(true, nil)
mockGitService.On("SetGitConfig", mock.Anything).Return(nil)
mockGitService.On("SetGitConfig", mock.Anything, mock.Anything).Return(nil)
mockGitService.On("GetGitStatus").Return(gitStatus1, nil)

mockSshServer := mocks.NewMockSshServer()
Expand Down
18 changes: 12 additions & 6 deletions pkg/api/controllers/gitprovider/dto/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@

package dto

import (
"github.com/daytonaio/daytona/pkg/gitprovider"
)

type RepositoryUrl struct {
URL string `json:"url" validate:"required"`
} // @name RepositoryUrl

type SetGitProviderConfig struct {
Id string `json:"id" validate:"optional"`
ProviderId string `json:"providerId" validate:"required"`
Username *string `json:"username,omitempty" validate:"optional"`
Token string `json:"token" validate:"required"`
BaseApiUrl *string `json:"baseApiUrl,omitempty" validate:"optional"`
Alias *string `json:"alias,omitempty" validate:"optional"`
Id string `json:"id" validate:"optional"`
ProviderId string `json:"providerId" validate:"required"`
Username *string `json:"username,omitempty" validate:"optional"`
Token string `json:"token" validate:"required"`
BaseApiUrl *string `json:"baseApiUrl,omitempty" validate:"optional"`
Alias *string `json:"alias,omitempty" validate:"optional"`
SigningKey *string `json:"signingKey,omitempty" validate:"optional"`
SigningMethod *gitprovider.SigningMethod `json:"signingMethod,omitempty" validate:"optional"`
} // @name SetGitProviderConfig
Loading
Loading