From f3783f4abf49642326f67683f22d468db87a8822 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Sat, 12 Apr 2025 11:19:50 +0200 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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) + } + }) + } +}