From e434188460e222070aac95bb820d8d1c92151dd9 Mon Sep 17 00:00:00 2001 From: lhchavez Date: Sun, 5 Sep 2021 18:27:52 -0700 Subject: [PATCH] Add support for managed SSH transport #minor (#814) (#817) This change drops the (hard) dependency on libssh2 and instead uses Go's implementation of SSH when libgit2 is not built with it. --- credentials.go | 20 +++- git.go | 5 + remote.go | 23 ++-- script/build-libgit2.sh | 1 + ssh.go | 237 ++++++++++++++++++++++++++++++++++++++++ wrapper.c | 5 + 6 files changed, 279 insertions(+), 12 deletions(-) create mode 100644 ssh.go diff --git a/credentials.go b/credentials.go index d7b9fcd..79ed37f 100644 --- a/credentials.go +++ b/credentials.go @@ -9,7 +9,7 @@ import "C" import ( "crypto/rand" "errors" - "runtime" + "fmt" "unsafe" "golang.org/x/crypto/ssh" @@ -57,10 +57,22 @@ func (o *Cred) GetUserpassPlaintext() (username, password string, err error) { return } -func NewCredUsername(username string) (int, Cred) { - runtime.LockOSThread() - defer runtime.UnlockOSThread() +// GetSSHKey returns the SSH-specific key information from the Cred object. +func (o *Cred) GetSSHKey() (username, publickey, privatekey, passphrase string, err error) { + if o.Type() != CredTypeSshKey { + err = fmt.Errorf("credential is not an SSH key: %v", o.Type()) + return + } + sshKeyCredPtr := (*C.git_cred_ssh_key)(unsafe.Pointer(o.ptr)) + username = C.GoString(sshKeyCredPtr.username) + publickey = C.GoString(sshKeyCredPtr.publickey) + privatekey = C.GoString(sshKeyCredPtr.privatekey) + passphrase = C.GoString(sshKeyCredPtr.passphrase) + return +} + +func NewCredUsername(username string) (int, Cred) { cred := Cred{} cusername := C.CString(username) ret := C.git_cred_username_new(&cred.ptr, cusername) diff --git a/git.go b/git.go index 758d5c9..5796cdf 100644 --- a/git.go +++ b/git.go @@ -157,6 +157,11 @@ func initLibGit2() { // they're the only ones setting it up. C.git_openssl_set_locking() } + if features&FeatureSSH == 0 { + if err := registerManagedSSH(); err != nil { + panic(err) + } + } } // Shutdown frees all the resources acquired by libgit2. Make sure no diff --git a/remote.go b/remote.go index 10eab2b..7ae7e06 100644 --- a/remote.go +++ b/remote.go @@ -16,6 +16,8 @@ import ( "strings" "sync" "unsafe" + + "golang.org/x/crypto/ssh" ) type TransferProgress struct { @@ -239,20 +241,25 @@ type Certificate struct { Hostkey HostkeyCertificate } +// HostkeyKind is a bitmask of the available hashes in HostkeyCertificate. type HostkeyKind uint const ( - HostkeyMD5 HostkeyKind = C.GIT_CERT_SSH_MD5 - HostkeySHA1 HostkeyKind = C.GIT_CERT_SSH_SHA1 + HostkeyMD5 HostkeyKind = C.GIT_CERT_SSH_MD5 + HostkeySHA1 HostkeyKind = C.GIT_CERT_SSH_SHA1 + HostkeySHA256 HostkeyKind = 1 << 2 + HostkeyRaw HostkeyKind = 1 << 3 ) -// Server host key information. If Kind is HostkeyMD5 the MD5 field -// will be filled. If Kind is HostkeySHA1, then HashSHA1 will be -// filled. +// Server host key information. A bitmask containing the available fields. +// Check for combinations of: HostkeyMD5, HostkeySHA1, HostkeySHA256, HostkeyRaw. type HostkeyCertificate struct { - Kind HostkeyKind - HashMD5 [16]byte - HashSHA1 [20]byte + Kind HostkeyKind + HashMD5 [16]byte + HashSHA1 [20]byte + HashSHA256 [32]byte + Hostkey []byte + SSHPublicKey ssh.PublicKey } type PushOptions struct { diff --git a/script/build-libgit2.sh b/script/build-libgit2.sh index 11d956d..4dd57f6 100755 --- a/script/build-libgit2.sh +++ b/script/build-libgit2.sh @@ -59,6 +59,7 @@ cmake -DTHREADSAFE=ON \ -DBUILD_CLAR=OFF \ -DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \ -DUSE_HTTPS=OFF \ + -DUSE_SSH=OFF \ -DCMAKE_C_FLAGS=-fPIC \ -DCMAKE_BUILD_TYPE="RelWithDebInfo" \ -DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \ diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..65dfbcf --- /dev/null +++ b/ssh.go @@ -0,0 +1,237 @@ +package git + +/* +#include + +void _go_git_credential_free(git_cred *cred); +*/ +import "C" +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/url" + "runtime" + "unsafe" + + "golang.org/x/crypto/ssh" +) + +// RegisterManagedSSHTransport registers a Go-native implementation of an SSH +// transport that doesn't rely on any system libraries (e.g. libssh2). +// +// If Shutdown or ReInit are called, make sure that the smart transports are +// freed before it. +func RegisterManagedSSHTransport(protocol string) (*RegisteredSmartTransport, error) { + return NewRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory) +} + +func registerManagedSSH() error { + globalRegisteredSmartTransports.Lock() + defer globalRegisteredSmartTransports.Unlock() + + for _, protocol := range []string{"ssh", "ssh+git", "git+ssh"} { + if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok { + continue + } + managed, err := newRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory, true) + if err != nil { + return fmt.Errorf("failed to register transport for %q: %v", protocol, err) + } + globalRegisteredSmartTransports.transports[protocol] = managed + } + return nil +} + +func sshSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) { + return &sshSmartSubtransport{ + transport: transport, + }, nil +} + +type sshSmartSubtransport struct { + transport *Transport + + lastAction SmartServiceAction + client *ssh.Client + session *ssh.Session + stdin io.WriteCloser + stdout io.Reader + currentStream *sshSmartSubtransportStream +} + +func (t *sshSmartSubtransport) Action(urlString string, action SmartServiceAction) (SmartSubtransportStream, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + u, err := url.Parse(urlString) + if err != nil { + return nil, err + } + + var cmd string + switch action { + case SmartServiceActionUploadpackLs, SmartServiceActionUploadpack: + if t.currentStream != nil { + if t.lastAction == SmartServiceActionUploadpackLs { + return t.currentStream, nil + } + t.Close() + } + cmd = fmt.Sprintf("git-upload-pack %q", u.Path) + + case SmartServiceActionReceivepackLs, SmartServiceActionReceivepack: + if t.currentStream != nil { + if t.lastAction == SmartServiceActionReceivepackLs { + return t.currentStream, nil + } + t.Close() + } + cmd = fmt.Sprintf("git-receive-pack %q", u.Path) + + default: + return nil, fmt.Errorf("unexpected action: %v", action) + } + + cred, err := t.transport.SmartCredentials("", CredTypeSshKey) + if err != nil { + return nil, err + } + defer C._go_git_credential_free(cred.ptr) + + sshConfig, err := getSSHConfigFromCredential(cred) + if err != nil { + return nil, err + } + sshConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { + marshaledKey := key.Marshal() + cert := &Certificate{ + Kind: CertificateHostkey, + Hostkey: HostkeyCertificate{ + Kind: HostkeySHA1 | HostkeyMD5 | HostkeySHA256 | HostkeyRaw, + HashMD5: md5.Sum(marshaledKey), + HashSHA1: sha1.Sum(marshaledKey), + HashSHA256: sha256.Sum256(marshaledKey), + Hostkey: marshaledKey, + SSHPublicKey: key, + }, + } + + return t.transport.SmartCertificateCheck(cert, true, hostname) + } + + var addr string + if u.Port() != "" { + addr = fmt.Sprintf("%s:%s", u.Hostname(), u.Port()) + } else { + addr = fmt.Sprintf("%s:22", u.Hostname()) + } + + t.client, err = ssh.Dial("tcp", addr, sshConfig) + if err != nil { + return nil, err + } + + t.session, err = t.client.NewSession() + if err != nil { + return nil, err + } + + t.stdin, err = t.session.StdinPipe() + if err != nil { + return nil, err + } + + t.stdout, err = t.session.StdoutPipe() + if err != nil { + return nil, err + } + + if err := t.session.Start(cmd); err != nil { + return nil, err + } + + t.lastAction = action + t.currentStream = &sshSmartSubtransportStream{ + owner: t, + } + + return t.currentStream, nil +} + +func (t *sshSmartSubtransport) Close() error { + t.currentStream = nil + if t.client != nil { + t.stdin.Close() + t.session.Wait() + t.session.Close() + t.client = nil + } + return nil +} + +func (t *sshSmartSubtransport) Free() { +} + +type sshSmartSubtransportStream struct { + owner *sshSmartSubtransport +} + +func (stream *sshSmartSubtransportStream) Read(buf []byte) (int, error) { + return stream.owner.stdout.Read(buf) +} + +func (stream *sshSmartSubtransportStream) Write(buf []byte) (int, error) { + return stream.owner.stdin.Write(buf) +} + +func (stream *sshSmartSubtransportStream) Free() { +} + +func getSSHConfigFromCredential(cred *Cred) (*ssh.ClientConfig, error) { + switch cred.Type() { + case CredTypeSshCustom: + credSSHCustom := (*C.git_cred_ssh_custom)(unsafe.Pointer(cred.ptr)) + data, ok := pointerHandles.Get(credSSHCustom.payload).(*credentialSSHCustomData) + if !ok { + return nil, errors.New("unsupported custom SSH credentials") + } + return &ssh.ClientConfig{ + User: C.GoString(credSSHCustom.username), + Auth: []ssh.AuthMethod{ssh.PublicKeys(data.signer)}, + }, nil + } + + username, _, privatekey, passphrase, ret := cred.GetSSHKey() + if ret != nil { + return nil, ret + } + + pemBytes, err := ioutil.ReadFile(privatekey) + if err != nil { + return nil, err + } + + var key ssh.Signer + if passphrase != "" { + key, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(passphrase)) + if err != nil { + return nil, err + } + } else { + key, err = ssh.ParsePrivateKey(pemBytes) + if err != nil { + return nil, err + } + } + + return &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ssh.PublicKeys(key)}, + }, nil +} diff --git a/wrapper.c b/wrapper.c index c65f674..8c970c4 100644 --- a/wrapper.c +++ b/wrapper.c @@ -434,6 +434,11 @@ void _go_git_populate_credential_ssh_custom(git_cred_ssh_custom *cred) cred->sign_callback = credential_ssh_sign_callback; } +void _go_git_credential_free(git_cred *cred) +{ + cred->free(cred); +} + int _go_git_odb_write_pack(git_odb_writepack **out, git_odb *db, void *progress_payload) { return git_odb_write_pack(out, db, transfer_progress_callback, progress_payload);