From 61916beebe85fd368f44c638a39749ac9c0f459a Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Sat, 22 Mar 2025 21:23:55 +0100 Subject: [PATCH 1/7] plumbing: re-add support for cert-authorities using skeema's HostKeyDB --- plumbing/transport/ssh/auth_method.go | 35 +++++---- plumbing/transport/ssh/auth_method_test.go | 89 +++++++++++++++++++++- plumbing/transport/ssh/common.go | 17 ++--- 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/plumbing/transport/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go index ac4e3583c..e11695bae 100644 --- a/plumbing/transport/ssh/auth_method.go +++ b/plumbing/transport/ssh/auth_method.go @@ -230,11 +230,23 @@ func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) { // ~/.ssh/known_hosts // /etc/ssh/ssh_known_hosts func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) { - kh, err := newKnownHosts(files...) - return ssh.HostKeyCallback(kh), err + kh, err := NewKnownHostsDb(files...) + return kh.HostKeyCallback(), err } -func newKnownHosts(files ...string) (knownhosts.HostKeyCallback, error) { +// NewKnownHostsDb returns knownhosts.HostKeyDB based on a file based on a +// known_hosts file. http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT +// +// If list of files is empty, then it will be read from the SSH_KNOWN_HOSTS +// environment variable, example: +// +// /home/foo/custom_known_hosts_file:/etc/custom_known/hosts_file +// +// If SSH_KNOWN_HOSTS is not set the following file locations will be used: +// +// ~/.ssh/known_hosts +// /etc/ssh/ssh_known_hosts +func NewKnownHostsDb(files ...string) (*knownhosts.HostKeyDB, error) { var err error if len(files) == 0 { @@ -247,7 +259,7 @@ func newKnownHosts(files ...string) (knownhosts.HostKeyCallback, error) { return nil, err } - return knownhosts.New(files...) + return knownhosts.NewDB(files...) } func getDefaultKnownHostsFiles() ([]string, error) { @@ -292,21 +304,16 @@ func filterKnownHostsFiles(files ...string) ([]string, error) { // configure HostKeyCallback into a ssh.ClientConfig. type HostKeyCallbackHelper struct { // HostKeyCallback is the function type used for verifying server keys. - // If nil default callback will be create using NewKnownHostsCallback + // If nil, a default callback will be created using NewKnownHostsDb // without argument. HostKeyCallback ssh.HostKeyCallback + } -// SetHostKeyCallback sets the field HostKeyCallback in the given cfg. If -// HostKeyCallback is empty a default callback is created using -// NewKnownHostsCallback. +// SetHostKeyCallback sets the field HostKeyCallback in the given cfg. +// If the host key callback is empty it is left empty. It will be handled +// by the dial method by falling back to knownhosts. func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { - var err error - if m.HostKeyCallback == nil { - if m.HostKeyCallback, err = NewKnownHostsCallback(); err != nil { - return cfg, err - } - } cfg.HostKeyCallback = m.HostKeyCallback return cfg, nil diff --git a/plumbing/transport/ssh/auth_method_test.go b/plumbing/transport/ssh/auth_method_test.go index b275018ae..5eaafafbb 100644 --- a/plumbing/transport/ssh/auth_method_test.go +++ b/plumbing/transport/ssh/auth_method_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "runtime" + "slices" "strings" "github.com/go-git/go-billy/v5/osfs" @@ -18,7 +19,8 @@ import ( type ( SuiteCommon struct{} - mockKnownHosts struct{} + mockKnownHosts struct{} + mockKnownHostsWithCert struct{} ) func (mockKnownHosts) host() string { return "github.com" } @@ -27,6 +29,19 @@ func (mockKnownHosts) knownHosts() []byte { } func (mockKnownHosts) Network() string { return "tcp" } func (mockKnownHosts) String() string { return "github.com:22" } +func (mockKnownHosts) Algorithms() []string { + return []string{ssh.KeyAlgoRSA, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512} +} + +func (mockKnownHostsWithCert) host() string { return "github.com" } +func (mockKnownHostsWithCert) knownHosts() []byte { + return []byte(`@cert-authority github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`) +} +func (mockKnownHostsWithCert) Network() string { return "tcp" } +func (mockKnownHostsWithCert) String() string { return "github.com:22" } +func (mockKnownHostsWithCert) Algorithms() []string { + return []string{ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSAv01} +} var _ = Suite(&SuiteCommon{}) @@ -230,3 +245,75 @@ func (*SuiteCommon) TestNewKnownHostsCallback(c *C) { err = clb(mock.String(), mock, hostKey) c.Assert(err, IsNil) } + +func (*SuiteCommon) TestNewKnownHostsDbWithoutCert(c *C) { + if runtime.GOOS == "js" { + c.Skip("not available in wasm") + } + + var mock = mockKnownHosts{} + + f, err := util.TempFile(osfs.Default, "", "known-hosts") + c.Assert(err, IsNil) + + _, err = f.Write(mock.knownHosts()) + c.Assert(err, IsNil) + + err = f.Close() + c.Assert(err, IsNil) + + defer util.RemoveAll(osfs.Default, f.Name()) + + f, err = osfs.Default.Open(f.Name()) + c.Assert(err, IsNil) + + defer f.Close() + + db, err := NewKnownHostsDb(f.Name()) + c.Assert(err, IsNil) + + algos := db.HostKeyAlgorithms(mock.String()) + c.Assert(algos, HasLen, len(mock.Algorithms())) + + for _, algorithm := range mock.Algorithms() { + if !slices.Contains(algos, algorithm) { + c.Error("algos does not contain ", algorithm) + } + } +} + +func (*SuiteCommon) TestNewKnownHostsDbWithCert(c *C) { + if runtime.GOOS == "js" { + c.Skip("not available in wasm") + } + + var mock = mockKnownHostsWithCert{} + + f, err := util.TempFile(osfs.Default, "", "known-hosts") + c.Assert(err, IsNil) + + _, err = f.Write(mock.knownHosts()) + c.Assert(err, IsNil) + + err = f.Close() + c.Assert(err, IsNil) + + defer util.RemoveAll(osfs.Default, f.Name()) + + f, err = osfs.Default.Open(f.Name()) + c.Assert(err, IsNil) + + defer f.Close() + + db, err := NewKnownHostsDb(f.Name()) + c.Assert(err, IsNil) + + algos := db.HostKeyAlgorithms(mock.String()) + c.Assert(algos, HasLen, len(mock.Algorithms())) + + for _, algorithm := range mock.Algorithms() { + if !slices.Contains(algos, algorithm) { + c.Error("algos does not contain ", algorithm) + } + } +} diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go index 05dea448f..ae6f2174a 100644 --- a/plumbing/transport/ssh/common.go +++ b/plumbing/transport/ssh/common.go @@ -11,7 +11,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/internal/common" - "github.com/skeema/knownhosts" "github.com/kevinburke/ssh_config" "golang.org/x/crypto/ssh" @@ -127,17 +126,17 @@ func (c *command) connect() error { } hostWithPort := c.getHostWithPort() if config.HostKeyCallback == nil { - kh, err := newKnownHosts() + db, err := NewKnownHostsDb() if err != nil { return err } - config.HostKeyCallback = kh.HostKeyCallback() - config.HostKeyAlgorithms = kh.HostKeyAlgorithms(hostWithPort) - } else if len(config.HostKeyAlgorithms) == 0 { - // Set the HostKeyAlgorithms based on HostKeyCallback. - // For background see https://github.com/go-git/go-git/issues/411 as well as - // https://github.com/golang/go/issues/29286 for root cause. - config.HostKeyAlgorithms = knownhosts.HostKeyAlgorithms(config.HostKeyCallback, hostWithPort) + config.HostKeyCallback = db.HostKeyCallback() + config.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort) + } else { + // If the user gave a custom HostKeyCallback, we do not try to detect host key algorithms + // based on knownhosts functionality, as the user may be requesting a FixedKey or using a + // different key approval strategy. In that case, the user is responsible for populating + // HostKeyAlgorithms appropriately } overrideConfig(c.config, config) From ba9d6937b68ab3c30a0889a9edea2436fd2a6f75 Mon Sep 17 00:00:00 2001 From: Javier Alvarez Date: Mon, 31 Mar 2025 23:22:03 +0200 Subject: [PATCH 2/7] plumbing: support setting custom host key algorithms along with host key callback --- plumbing/transport/ssh/auth_method.go | 25 ++++++++++++++----------- plumbing/transport/ssh/common_test.go | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/plumbing/transport/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go index e11695bae..6882e51bb 100644 --- a/plumbing/transport/ssh/auth_method.go +++ b/plumbing/transport/ssh/auth_method.go @@ -54,7 +54,7 @@ func (a *KeyboardInteractive) String() string { } func (a *KeyboardInteractive) ClientConfig() (*ssh.ClientConfig, error) { - return a.SetHostKeyCallback(&ssh.ClientConfig{ + return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ a.Challenge, @@ -78,7 +78,7 @@ func (a *Password) String() string { } func (a *Password) ClientConfig() (*ssh.ClientConfig, error) { - return a.SetHostKeyCallback(&ssh.ClientConfig{ + return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.Password(a.Password)}, }) @@ -101,7 +101,7 @@ func (a *PasswordCallback) String() string { } func (a *PasswordCallback) ClientConfig() (*ssh.ClientConfig, error) { - return a.SetHostKeyCallback(&ssh.ClientConfig{ + return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.PasswordCallback(a.Callback)}, }) @@ -150,7 +150,7 @@ func (a *PublicKeys) String() string { } func (a *PublicKeys) ClientConfig() (*ssh.ClientConfig, error) { - return a.SetHostKeyCallback(&ssh.ClientConfig{ + return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.PublicKeys(a.Signer)}, }) @@ -211,7 +211,7 @@ func (a *PublicKeysCallback) String() string { } func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) { - return a.SetHostKeyCallback(&ssh.ClientConfig{ + return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Callback)}, }) @@ -301,20 +301,23 @@ func filterKnownHostsFiles(files ...string) ([]string, error) { } // HostKeyCallbackHelper is a helper that provides common functionality to -// configure HostKeyCallback into a ssh.ClientConfig. +// configure HostKeyCallback and HostKeyAlgorithms into a ssh.ClientConfig. type HostKeyCallbackHelper struct { // HostKeyCallback is the function type used for verifying server keys. // If nil, a default callback will be created using NewKnownHostsDb // without argument. HostKeyCallback ssh.HostKeyCallback + // HostKeyAlgorithms is a list of supported host key algorithms that will + // be used for host key verification. + HostKeyAlgorithms []string } -// SetHostKeyCallback sets the field HostKeyCallback in the given cfg. -// If the host key callback is empty it is left empty. It will be handled -// by the dial method by falling back to knownhosts. -func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { - +// SetHostKeyCallbackAndAlgorithms sets the field HostKeyCallback and HostKeyAlgorithms in the given cfg. +// If the host key callback or algorithms is empty it is left empty. It will be handled by the dial method, +// falling back to knownhosts. +func (m *HostKeyCallbackHelper) SetHostKeyCallbackAndAlgorithms(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { cfg.HostKeyCallback = m.HostKeyCallback + cfg.HostKeyAlgorithms = m.HostKeyAlgorithms return cfg, nil } diff --git a/plumbing/transport/ssh/common_test.go b/plumbing/transport/ssh/common_test.go index a72493686..a4e29bef5 100644 --- a/plumbing/transport/ssh/common_test.go +++ b/plumbing/transport/ssh/common_test.go @@ -129,12 +129,35 @@ func (s *SuiteCommon) TestFixedHostKeyCallback(c *C) { c.Assert(err, IsNil) c.Assert(auth, NotNil) auth.HostKeyCallback = stdssh.FixedHostKey(hostKey.PublicKey()) + auth.HostKeyAlgorithms = []string{"ssh-ed25519"} ep := uploadPack.newEndpoint(c, "bar.git") ps, err := uploadPack.Client.NewUploadPackSession(ep, auth) c.Assert(err, IsNil) c.Assert(ps, NotNil) } +func (s *SuiteCommon) TestFixedHostKeyCallbackUnexpectedAlgorithm(c *C) { + hostKey, err := stdssh.ParsePrivateKey(testdata.PEMBytes["ed25519"]) + c.Assert(err, IsNil) + uploadPack := &UploadPackSuite{ + opts: []ssh.Option{ + ssh.HostKeyPEM(testdata.PEMBytes["rsa"]), + }, + } + uploadPack.SetUpSuite(c) + // Use the default client, which does not have a host key callback + uploadPack.Client = DefaultClient + auth, err := NewPublicKeys("foo", testdata.PEMBytes["rsa"], "") + c.Assert(err, IsNil) + c.Assert(auth, NotNil) + auth.HostKeyCallback = stdssh.FixedHostKey(hostKey.PublicKey()) + auth.HostKeyAlgorithms = []string{"ssh-ed25519"} + ep := uploadPack.newEndpoint(c, "bar.git") + ps, err := uploadPack.Client.NewUploadPackSession(ep, auth) + c.Assert(err, NotNil) + c.Assert(ps, IsNil) +} + func (s *SuiteCommon) TestFailHostKeyCallback(c *C) { uploadPack := &UploadPackSuite{ opts: []ssh.Option{ From 9996069e216745ffe9e0a4c624fe5553bb9744f9 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Thu, 10 Apr 2025 09:35:11 +0100 Subject: [PATCH 3/7] v5: Bump dependencies Signed-off-by: Paulo Gomes --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 2e1d2fe2e..f100ebeda 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.6 require ( dario.cat/mergo v1.0.0 - github.com/ProtonMail/go-crypto v1.1.5 + github.com/ProtonMail/go-crypto v1.1.6 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/elazarl/goproxy v1.7.2 github.com/emirpasic/gods v1.18.1 @@ -24,17 +24,17 @@ require ( github.com/skeema/knownhosts v1.3.1 github.com/stretchr/testify v1.10.0 github.com/xanzy/ssh-agent v0.3.3 - golang.org/x/crypto v0.35.0 - golang.org/x/net v0.35.0 - golang.org/x/sys v0.30.0 - golang.org/x/text v0.22.0 + golang.org/x/crypto v0.37.0 + golang.org/x/net v0.39.0 + golang.org/x/sys v0.32.0 + golang.org/x/text v0.24.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/cloudflare/circl v1.6.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect diff --git a/go.sum b/go.sum index d986c0940..5dc89814b 100644 --- a/go.sum +++ b/go.sum @@ -3,14 +3,14 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= -github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= -github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= @@ -70,27 +70,27 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From f3783f4abf49642326f67683f22d468db87a8822 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Sat, 12 Apr 2025 11:19:50 +0200 Subject: [PATCH 4/7] plumbing: support mTLS for HTTPS protocol Signed-off-by: Hidde Beydals --- options.go | 56 +++++++++++++++++++++++----- plumbing/transport/common.go | 12 +++++- plumbing/transport/http/common.go | 24 +++++++++++- plumbing/transport/http/transport.go | 6 ++- remote.go | 20 +++++----- repository.go | 3 ++ worktree.go | 3 ++ 7 files changed, 99 insertions(+), 25 deletions(-) diff --git a/options.go b/options.go index 3cd0f952c..e2c77edca 100644 --- a/options.go +++ b/options.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ProtonMail/go-crypto/openpgp" + "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" formatcfg "github.com/go-git/go-git/v5/plumbing/format/config" @@ -72,9 +73,16 @@ type CloneOptions struct { // Tags describe how the tags will be fetched from the remote repository, // by default is AllTags. Tags TagMode - // InsecureSkipTLS skips ssl verify if protocol is https + // InsecureSkipTLS skips SSL verification if protocol is HTTPS. InsecureSkipTLS bool - // CABundle specify additional ca bundle with system cert pool + // ClientCert is the client certificate to use for mutual TLS authentication + // over the HTTPS protocol. + ClientCert []byte + // ClientKey is the client key to use for mutual TLS authentication over + // the HTTPS protocol. + ClientKey []byte + // CABundle specifies an additional CA bundle to use together with the + // system cert pool. CABundle []byte // ProxyOptions provides info required for connecting to a proxy. ProxyOptions transport.ProxyOptions @@ -153,9 +161,16 @@ type PullOptions struct { // Force allows the pull to update a local branch even when the remote // branch does not descend from it. Force bool - // InsecureSkipTLS skips ssl verify if protocol is https + // InsecureSkipTLS skips SSL verification if protocol is HTTPS. InsecureSkipTLS bool - // CABundle specify additional ca bundle with system cert pool + // ClientCert is the client certificate to use for mutual TLS authentication + // over the HTTPS protocol. + ClientCert []byte + // ClientKey is the client key to use for mutual TLS authentication over + // the HTTPS protocol. + ClientKey []byte + // CABundle specifies an additional CA bundle to use together with the + // system cert pool. CABundle []byte // ProxyOptions provides info required for connecting to a proxy. ProxyOptions transport.ProxyOptions @@ -211,9 +226,16 @@ type FetchOptions struct { // Force allows the fetch to update a local branch even when the remote // branch does not descend from it. Force bool - // InsecureSkipTLS skips ssl verify if protocol is https + // InsecureSkipTLS skips SSL verification if protocol is HTTPS. InsecureSkipTLS bool - // CABundle specify additional ca bundle with system cert pool + // ClientCert is the client certificate to use for mutual TLS authentication + // over the HTTPS protocol. + ClientCert []byte + // ClientKey is the client key to use for mutual TLS authentication over + // the HTTPS protocol. + ClientKey []byte + // CABundle specifies an additional CA bundle to use together with the + // system cert pool. CABundle []byte // ProxyOptions provides info required for connecting to a proxy. ProxyOptions transport.ProxyOptions @@ -267,9 +289,16 @@ type PushOptions struct { // Force allows the push to update a remote branch even when the local // branch does not descend from it. Force bool - // InsecureSkipTLS skips ssl verify if protocol is https + // InsecureSkipTLS skips SSL verification if protocol is HTTPS. InsecureSkipTLS bool - // CABundle specify additional ca bundle with system cert pool + // ClientCert is the client certificate to use for mutual TLS authentication + // over the HTTPS protocol. + ClientCert []byte + // ClientKey is the client key to use for mutual TLS authentication over + // the HTTPS protocol. + ClientKey []byte + // CABundle specifies an additional CA bundle to use together with the + // system cert pool. CABundle []byte // RequireRemoteRefs only allows a remote ref to be updated if its current // value is the one specified here. @@ -693,9 +722,16 @@ func (o *CreateTagOptions) loadConfigTagger(r *Repository) error { type ListOptions struct { // Auth credentials, if required, to use with the remote repository. Auth transport.AuthMethod - // InsecureSkipTLS skips ssl verify if protocol is https + // InsecureSkipTLS skips SSL verification if protocol is HTTPS. InsecureSkipTLS bool - // CABundle specify additional ca bundle with system cert pool + // ClientCert is the client certificate to use for mutual TLS authentication + // over the HTTPS protocol. + ClientCert []byte + // ClientKey is the client key to use for mutual TLS authentication over + // the HTTPS protocol. + ClientKey []byte + // CABundle specifies an additional CA bundle to use together with the + // system cert pool. CABundle []byte // PeelingOption defines how peeled objects are handled during a // remote list. diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go index fae1aa98c..b4c5e98be 100644 --- a/plumbing/transport/common.go +++ b/plumbing/transport/common.go @@ -113,9 +113,17 @@ type Endpoint struct { Port int // Path is the repository path. Path string - // InsecureSkipTLS skips ssl verify if protocol is https + // InsecureSkipTLS skips SSL verification if Protocol is HTTPS. InsecureSkipTLS bool - // CaBundle specify additional ca bundle with system cert pool + // ClientCert specifies an optional client certificate to use for mutual + // TLS authentication if Protocol is HTTPS. + ClientCert []byte + // ClientKey specifies an optional client key to use for mutual TLS + // authentication if Protocol is HTTPS. + ClientKey []byte + // CaBundle specifies an optional CA bundle to use for SSL verification + // if Protocol is HTTPS. The bundle is added in addition to the system + // CA bundle. CaBundle []byte // Proxy provides info required for connecting to a proxy. Proxy ProxyOptions diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go index df01890b2..065ce215c 100644 --- a/plumbing/transport/http/common.go +++ b/plumbing/transport/http/common.go @@ -15,12 +15,13 @@ import ( "strings" "sync" + "github.com/golang/groupcache/lru" + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/protocol/packp" "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/utils/ioutil" - "github.com/golang/groupcache/lru" ) // it requires a bytes.Buffer, because we need to know the length @@ -185,6 +186,18 @@ func transportWithInsecureTLS(transport *http.Transport) { transport.TLSClientConfig.InsecureSkipVerify = true } +func transportWithClientCert(transport *http.Transport, cert, key []byte) error { + keyPair, err := tls.X509KeyPair(cert, key) + if err != nil { + return err + } + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.Certificates = []tls.Certificate{keyPair} + return nil +} + func transportWithCABundle(transport *http.Transport, caBundle []byte) error { rootCAs, err := x509.SystemCertPool() if err != nil { @@ -206,6 +219,11 @@ func transportWithProxy(transport *http.Transport, proxyURL *url.URL) { } func configureTransport(transport *http.Transport, ep *transport.Endpoint) error { + if len(ep.ClientCert) > 0 && len(ep.ClientKey) > 0 { + if err := transportWithClientCert(transport, ep.ClientCert, ep.ClientKey); err != nil { + return err + } + } if len(ep.CaBundle) > 0 { if err := transportWithCABundle(transport, ep.CaBundle); err != nil { return err @@ -230,7 +248,7 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* // We need to configure the http transport if there are transport specific // options present in the endpoint. - if len(ep.CaBundle) > 0 || ep.InsecureSkipTLS || ep.Proxy.URL != "" { + if len(ep.ClientKey) > 0 || len(ep.ClientCert) > 0 || len(ep.CaBundle) > 0 || ep.InsecureSkipTLS || ep.Proxy.URL != "" { var transport *http.Transport // if the client wasn't configured to have a cache for transports then just configure // the transport and use it directly, otherwise try to use the cache. @@ -245,6 +263,8 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* configureTransport(transport, ep) } else { transportOpts := transportOptions{ + clientCert: string(ep.ClientCert), + clientKey: string(ep.ClientKey), caBundle: string(ep.CaBundle), insecureSkipTLS: ep.InsecureSkipTLS, } diff --git a/plumbing/transport/http/transport.go b/plumbing/transport/http/transport.go index c8db38920..1991d0512 100644 --- a/plumbing/transport/http/transport.go +++ b/plumbing/transport/http/transport.go @@ -9,8 +9,10 @@ import ( type transportOptions struct { insecureSkipTLS bool // []byte is not comparable. - caBundle string - proxyURL url.URL + clientCert string + clientKey string + caBundle string + proxyURL url.URL } func (c *client) addTransport(opts transportOptions, transport *http.Transport) { diff --git a/remote.go b/remote.go index e2c734e75..730812797 100644 --- a/remote.go +++ b/remote.go @@ -114,7 +114,7 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) { o.RemoteURL = r.c.URLs[len(r.c.URLs)-1] } - s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions) + s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.ClientCert, o.ClientKey, o.CABundle, o.ProxyOptions) if err != nil { return err } @@ -416,7 +416,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen o.RemoteURL = r.c.URLs[0] } - s, err := newUploadPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions) + s, err := newUploadPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.ClientCert, o.ClientKey, o.CABundle, o.ProxyOptions) if err != nil { return nil, err } @@ -532,8 +532,8 @@ func depthChanged(before []plumbing.Hash, s storage.Storer) (bool, error) { return false, nil } -func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.UploadPackSession, error) { - c, ep, err := newClient(url, insecure, cabundle, proxyOpts) +func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, clientCert, clientKey, caBundle []byte, proxyOpts transport.ProxyOptions) (transport.UploadPackSession, error) { + c, ep, err := newClient(url, insecure, clientCert, clientKey, caBundle, proxyOpts) if err != nil { return nil, err } @@ -541,8 +541,8 @@ func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, return c.NewUploadPackSession(ep, auth) } -func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.ReceivePackSession, error) { - c, ep, err := newClient(url, insecure, cabundle, proxyOpts) +func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, clientCert, clientKey, caBundle []byte, proxyOpts transport.ProxyOptions) (transport.ReceivePackSession, error) { + c, ep, err := newClient(url, insecure, clientCert, clientKey, caBundle, proxyOpts) if err != nil { return nil, err } @@ -550,13 +550,15 @@ func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, ca return c.NewReceivePackSession(ep, auth) } -func newClient(url string, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.Transport, *transport.Endpoint, error) { +func newClient(url string, insecure bool, clientCert, clientKey, caBundle []byte, proxyOpts transport.ProxyOptions) (transport.Transport, *transport.Endpoint, error) { ep, err := transport.NewEndpoint(url) if err != nil { return nil, nil, err } ep.InsecureSkipTLS = insecure - ep.CaBundle = cabundle + ep.ClientCert = clientCert + ep.ClientKey = clientKey + ep.CaBundle = caBundle ep.Proxy = proxyOpts c, err := client.NewClient(ep) @@ -1356,7 +1358,7 @@ func (r *Remote) list(ctx context.Context, o *ListOptions) (rfs []*plumbing.Refe return nil, ErrEmptyUrls } - s, err := newUploadPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions) + s, err := newUploadPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.ClientCert, o.ClientKey, o.CABundle, o.ProxyOptions) if err != nil { return nil, err } diff --git a/repository.go b/repository.go index 200098e7a..401590544 100644 --- a/repository.go +++ b/repository.go @@ -19,6 +19,7 @@ import ( "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/util" + "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/internal/path_util" "github.com/go-git/go-git/v5/internal/revision" @@ -930,6 +931,8 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error { Tags: o.Tags, RemoteName: o.RemoteName, InsecureSkipTLS: o.InsecureSkipTLS, + ClientCert: o.ClientCert, + ClientKey: o.ClientKey, CABundle: o.CABundle, ProxyOptions: o.ProxyOptions, }, o.ReferenceName) diff --git a/worktree.go b/worktree.go index dded08e99..479904e0c 100644 --- a/worktree.go +++ b/worktree.go @@ -12,6 +12,7 @@ import ( "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/util" + "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" @@ -79,6 +80,8 @@ func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error { Progress: o.Progress, Force: o.Force, InsecureSkipTLS: o.InsecureSkipTLS, + ClientCert: o.ClientCert, + ClientKey: o.ClientKey, CABundle: o.CABundle, ProxyOptions: o.ProxyOptions, }) From 9bbc93b719b15a5e0955ed9c2f6aa008f13f7e86 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Sat, 12 Apr 2025 12:04:29 +0200 Subject: [PATCH 5/7] plumbing: fix unintended pointer mutation in test Signed-off-by: Hidde Beydals --- plumbing/transport/http/common_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plumbing/transport/http/common_test.go b/plumbing/transport/http/common_test.go index 822c860cf..7d073c2f2 100644 --- a/plumbing/transport/http/common_test.go +++ b/plumbing/transport/http/common_test.go @@ -111,12 +111,12 @@ func (s *ClientSuite) TestNewUnexpectedError(c *C) { func (s *ClientSuite) Test_newSession(c *C) { cl := NewClientWithOptions(nil, &ClientOptions{ - CacheMaxEntries: 2, + CacheMaxEntries: 3, }).(*client) - insecureEP := s.Endpoint + insecureEP := *s.Endpoint insecureEP.InsecureSkipTLS = true - session, err := newSession(cl, insecureEP, nil) + session, err := newSession(cl, &insecureEP, nil) c.Assert(err, IsNil) sessionTransport := session.client.Transport.(*http.Transport) @@ -131,7 +131,7 @@ func (s *ClientSuite) Test_newSession(c *C) { caEndpoint := insecureEP caEndpoint.CaBundle = []byte("this is the way") - session, err = newSession(cl, caEndpoint, nil) + session, err = newSession(cl, &caEndpoint, nil) c.Assert(err, IsNil) sessionTransport = session.client.Transport.(*http.Transport) @@ -146,7 +146,7 @@ func (s *ClientSuite) Test_newSession(c *C) { // cached transport should be the one that's used. c.Assert(sessionTransport, Equals, t) - session, err = newSession(cl, caEndpoint, nil) + session, err = newSession(cl, &caEndpoint, nil) c.Assert(err, IsNil) sessionTransport = session.client.Transport.(*http.Transport) // transport that's going to be used should be cached already. @@ -156,7 +156,7 @@ func (s *ClientSuite) Test_newSession(c *C) { // if the cache does not exist, the transport should still be correctly configured. cl.transports = nil - session, err = newSession(cl, insecureEP, nil) + session, err = newSession(cl, &insecureEP, nil) c.Assert(err, IsNil) sessionTransport = session.client.Transport.(*http.Transport) From 5320e1b6b4578aa7272de1fa535051b9b86fbbfb Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Sun, 13 Apr 2025 13:59:36 +0200 Subject: [PATCH 6/7] plumbing: surface transport configuration errors Signed-off-by: Hidde Beydals --- plumbing/transport/http/common.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go index 065ce215c..5dd2e311f 100644 --- a/plumbing/transport/http/common.go +++ b/plumbing/transport/http/common.go @@ -260,7 +260,9 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* } transport = tr.Clone() - configureTransport(transport, ep) + if err := configureTransport(transport, ep); err != nil { + return nil, err + } } else { transportOpts := transportOptions{ clientCert: string(ep.ClientCert), @@ -280,7 +282,9 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* if !found { transport = c.client.Transport.(*http.Transport).Clone() - configureTransport(transport, ep) + if err := configureTransport(transport, ep); err != nil { + return nil, err + } c.addTransport(transportOpts, transport) } } From beedd6bc1892182daa36dd45013ff28857320f58 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Mon, 14 Apr 2025 09:12:35 +0100 Subject: [PATCH 7/7] plumbing: transport, Reintroduce SetHostKeyCallback. Fix #1514 The PR #1482 renamed SetHostKeyCallback to SetHostKeyCallbackAndAlgorithms, due to the change in behaviour. For keeping backwards compatibility within existing v5, a new SetHostKeyCallback is being introduced that simply calls back to SetHostKeyCallbackAndAlgorithms. Some tests were introduced to enforce expected behaviour. Signed-off-by: Paulo Gomes --- plumbing/transport/ssh/auth_method.go | 32 ++++++- plumbing/transport/ssh/auth_method_test.go | 101 +++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/plumbing/transport/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go index 6882e51bb..a61610601 100644 --- a/plumbing/transport/ssh/auth_method.go +++ b/plumbing/transport/ssh/auth_method.go @@ -231,7 +231,10 @@ func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) { // /etc/ssh/ssh_known_hosts func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) { kh, err := NewKnownHostsDb(files...) - return kh.HostKeyCallback(), err + if err != nil { + return nil, err + } + return kh.HostKeyCallback(), nil } // NewKnownHostsDb returns knownhosts.HostKeyDB based on a file based on a @@ -311,13 +314,40 @@ type HostKeyCallbackHelper struct { // HostKeyAlgorithms is a list of supported host key algorithms that will // be used for host key verification. HostKeyAlgorithms []string + + // fallback allows for injecting the fallback call, which is called + // when a HostKeyCallback is not set. + fallback func(files ...string) (ssh.HostKeyCallback, error) } // SetHostKeyCallbackAndAlgorithms sets the field HostKeyCallback and HostKeyAlgorithms in the given cfg. // If the host key callback or algorithms is empty it is left empty. It will be handled by the dial method, // falling back to knownhosts. func (m *HostKeyCallbackHelper) SetHostKeyCallbackAndAlgorithms(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { + if cfg == nil { + cfg = &ssh.ClientConfig{} + } + + if m.HostKeyCallback == nil { + if m.fallback == nil { + m.fallback = NewKnownHostsCallback + } + + hkcb, err := m.fallback() + if err != nil { + return nil, fmt.Errorf("cannot create known hosts callback: %w", err) + } + + cfg.HostKeyCallback = hkcb + cfg.HostKeyAlgorithms = m.HostKeyAlgorithms + return cfg, err + } + cfg.HostKeyCallback = m.HostKeyCallback cfg.HostKeyAlgorithms = m.HostKeyAlgorithms return cfg, nil } + +func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { + return m.SetHostKeyCallbackAndAlgorithms(cfg) +} diff --git a/plumbing/transport/ssh/auth_method_test.go b/plumbing/transport/ssh/auth_method_test.go index 5eaafafbb..6117e0720 100644 --- a/plumbing/transport/ssh/auth_method_test.go +++ b/plumbing/transport/ssh/auth_method_test.go @@ -4,12 +4,16 @@ import ( "bufio" "fmt" "os" + "reflect" "runtime" "slices" "strings" + "testing" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/testdata" @@ -317,3 +321,100 @@ func (*SuiteCommon) TestNewKnownHostsDbWithCert(c *C) { } } } + +func TestHostKeyCallbackHelper(t *testing.T) { + cb1 := ssh.FixedHostKey(nil) + tests := []struct { + name string + cb ssh.HostKeyCallback + algos []string + fallback func(files ...string) (ssh.HostKeyCallback, error) + cc *ssh.ClientConfig + want *ssh.ClientConfig + wantErr string + }{ + { + name: "keep existing callback if set", + cb: cb1, + cc: &ssh.ClientConfig{}, + want: &ssh.ClientConfig{ + HostKeyCallback: cb1, + }, + }, + { + name: "create new client config is one isn't provided", + cb: cb1, + cc: nil, + want: &ssh.ClientConfig{ + HostKeyCallback: cb1, + }, + }, + { + name: "respect pre-set algos", + cb: cb1, + algos: []string{"foo"}, + cc: &ssh.ClientConfig{}, + want: &ssh.ClientConfig{ + HostKeyCallback: cb1, + HostKeyAlgorithms: []string{"foo"}, + }, + }, + { + name: "no callback is set, call fallback", + cc: &ssh.ClientConfig{}, + fallback: func(files ...string) (ssh.HostKeyCallback, error) { + return cb1, nil + }, + want: &ssh.ClientConfig{ + HostKeyCallback: cb1, + }, + }, + { + name: "no callback is set with nil client config", + fallback: func(files ...string) (ssh.HostKeyCallback, error) { + return cb1, nil + }, + want: &ssh.ClientConfig{ + HostKeyCallback: cb1, + }, + }, + { + name: "algos with no callback, call fallback", + algos: []string{"bar"}, + cc: &ssh.ClientConfig{}, + fallback: func(files ...string) (ssh.HostKeyCallback, error) { + return cb1, nil + }, + want: &ssh.ClientConfig{ + HostKeyCallback: cb1, + HostKeyAlgorithms: []string{"bar"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + helper := HostKeyCallbackHelper{ + HostKeyCallback: tc.cb, + HostKeyAlgorithms: tc.algos, + fallback: tc.fallback, + } + + got, gotErr := helper.SetHostKeyCallback(tc.cc) + + if tc.wantErr == "" { + require.NoError(t, gotErr) + require.NotNil(t, got) + + wantFunc := runtime.FuncForPC(reflect.ValueOf(tc.want.HostKeyCallback).Pointer()).Name() + gotFunc := runtime.FuncForPC(reflect.ValueOf(got.HostKeyCallback).Pointer()).Name() + assert.Equal(t, wantFunc, gotFunc) + + assert.Equal(t, tc.want.HostKeyAlgorithms, got.HostKeyAlgorithms) + } else { + assert.ErrorContains(t, gotErr, tc.wantErr) + assert.Nil(t, got) + } + }) + } +}