Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "C"
import (
"crypto/rand"
"errors"
"runtime"
"fmt"
"unsafe"

"golang.org/x/crypto/ssh"
Expand Down Expand Up @@ -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)
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
23 changes: 15 additions & 8 deletions remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"strings"
"sync"
"unsafe"

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

// RemoteCreateOptionsFlag is Remote creation options flags
Expand Down Expand Up @@ -256,20 +258,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 {
Expand Down
1 change: 1 addition & 0 deletions script/build-libgit2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}" \
Expand Down
237 changes: 237 additions & 0 deletions ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package git

/*
#include <git2.h>

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
}
5 changes: 5 additions & 0 deletions wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,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);
Expand Down