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 + +#include +*/ +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 +}