Add support for managed SSH transport #minor (#814)

This change drops the (hard) dependency on libssh2 and instead uses Go's
implementation of SSH when libgit2 is not built with it.
This commit is contained in:
lhchavez 2021-09-05 17:04:40 -07:00 committed by GitHub
parent b983e1daeb
commit 70e5e419cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 274 additions and 5 deletions

View File

@ -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()

5
git.go
View File

@ -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

View File

@ -17,6 +17,8 @@ import (
"strings"
"sync"
"unsafe"
"golang.org/x/crypto/ssh"
)
// RemoteCreateOptionsFlag is Remote creation options flags
@ -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 {

View File

@ -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}" \

242
ssh.go Normal file
View File

@ -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
}