Skip to content

Commit

Permalink
Add support for managed SSH transport #minor (#814)
Browse files Browse the repository at this point in the history
This change drops the (hard) dependency on libssh2 and instead uses Go's
implementation of SSH when libgit2 is not built with it.
  • Loading branch information
lhchavez committed Sep 6, 2021
1 parent b983e1d commit 70e5e41
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 5 deletions.
15 changes: 15 additions & 0 deletions credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ func (o *Credential) GetUserpassPlaintext() (username, password string, err erro
return
}

// GetSSHKey returns the SSH-specific key information from the Cred object.
func (o *Credential) GetSSHKey() (username, publickey, privatekey, passphrase string, err error) {
if o.Type() != CredentialTypeSSHKey && o.Type() != CredentialTypeSSHMemory {
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 NewCredentialUsername(username string) (*Credential, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
Expand Down
5 changes: 5 additions & 0 deletions git.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,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
Expand Down
16 changes: 11 additions & 5 deletions remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"strings"
"sync"
"unsafe"

"golang.org/x/crypto/ssh"
)

// RemoteCreateOptionsFlag is Remote creation options flags
Expand Down Expand Up @@ -257,21 +259,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
HostkeySHA256 HostkeyKind = C.GIT_CERT_SSH_SHA256
HostkeyRaw HostkeyKind = 1 << 3
)

// Server host key information. A bitmask containing the available fields.
// Check for combinations of: HostkeyMD5, HostkeySHA1, HostkeySHA256.
// Check for combinations of: HostkeyMD5, HostkeySHA1, HostkeySHA256, HostkeyRaw.
type HostkeyCertificate struct {
Kind HostkeyKind
HashMD5 [16]byte
HashSHA1 [20]byte
HashSHA256 [32]byte
Kind HostkeyKind
HashMD5 [16]byte
HashSHA1 [20]byte
HashSHA256 [32]byte
Hostkey []byte
SSHPublicKey ssh.PublicKey
}

type PushOptions struct {
Expand Down
1 change: 1 addition & 0 deletions script/build-libgit2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ cmake -DTHREADSAFE=ON \
-DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \
-DREGEX_BACKEND=builtin \
-DUSE_HTTPS=OFF \
-DUSE_SSH=OFF \
-DCMAKE_C_FLAGS=-fPIC \
-DCMAKE_BUILD_TYPE="RelWithDebInfo" \
-DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \
Expand Down
242 changes: 242 additions & 0 deletions ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package git

/*
#include <git2.h>
#include <git2/sys/credential.h>
*/
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("", CredentialTypeSSHKey|CredentialTypeSSHMemory)
if err != nil {
return nil, err
}
defer cred.Free()

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 *Credential) (*ssh.ClientConfig, error) {
switch cred.Type() {
case CredentialTypeSSHCustom:
credSSHCustom := (*C.git_credential_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, err := cred.GetSSHKey()
if err != nil {
return nil, err
}

var pemBytes []byte
if cred.Type() == CredentialTypeSSHMemory {
pemBytes = []byte(privatekey)
} else {
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
}

0 comments on commit 70e5e41

Please sign in to comment.