summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--credentials.go15
-rw-r--r--git.go5
-rw-r--r--remote.go16
-rwxr-xr-xscript/build-libgit2.sh1
-rw-r--r--ssh.go242
5 files changed, 274 insertions, 5 deletions
diff --git a/credentials.go b/credentials.go
index 273de2f..2fb65d5 100644
--- a/credentials.go
+++ b/credentials.go
@@ -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()
diff --git a/git.go b/git.go
index 9ad1ffc..62cf5d9 100644
--- a/git.go
+++ b/git.go
@@ -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
diff --git a/remote.go b/remote.go
index 3a435d1..275d4d9 100644
--- a/remote.go
+++ b/remote.go
@@ -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 {
diff --git a/script/build-libgit2.sh b/script/build-libgit2.sh
index 90a225c..e490b4b 100755
--- a/script/build-libgit2.sh
+++ b/script/build-libgit2.sh
@@ -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}" \
diff --git a/ssh.go b/ssh.go
new file mode 100644
index 0000000..dd2725e
--- /dev/null
+++ b/ssh.go
@@ -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
+}