Skip to content

Commit 70e5e41

Browse files
authored
Add support for managed SSH transport #minor (libgit2#814)
This change drops the (hard) dependency on libssh2 and instead uses Go's implementation of SSH when libgit2 is not built with it.
1 parent b983e1d commit 70e5e41

File tree

5 files changed

+274
-5
lines changed

5 files changed

+274
-5
lines changed

credentials.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ func (o *Credential) GetUserpassPlaintext() (username, password string, err erro
120120
return
121121
}
122122

123+
// GetSSHKey returns the SSH-specific key information from the Cred object.
124+
func (o *Credential) GetSSHKey() (username, publickey, privatekey, passphrase string, err error) {
125+
if o.Type() != CredentialTypeSSHKey && o.Type() != CredentialTypeSSHMemory {
126+
err = fmt.Errorf("credential is not an SSH key: %v", o.Type())
127+
return
128+
}
129+
130+
sshKeyCredPtr := (*C.git_cred_ssh_key)(unsafe.Pointer(o.ptr))
131+
username = C.GoString(sshKeyCredPtr.username)
132+
publickey = C.GoString(sshKeyCredPtr.publickey)
133+
privatekey = C.GoString(sshKeyCredPtr.privatekey)
134+
passphrase = C.GoString(sshKeyCredPtr.passphrase)
135+
return
136+
}
137+
123138
func NewCredentialUsername(username string) (*Credential, error) {
124139
runtime.LockOSThread()
125140
defer runtime.UnlockOSThread()

git.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ func initLibGit2() {
161161
// they're the only ones setting it up.
162162
C.git_openssl_set_locking()
163163
}
164+
if features&FeatureSSH == 0 {
165+
if err := registerManagedSSH(); err != nil {
166+
panic(err)
167+
}
168+
}
164169
}
165170

166171
// Shutdown frees all the resources acquired by libgit2. Make sure no

remote.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"strings"
1818
"sync"
1919
"unsafe"
20+
21+
"golang.org/x/crypto/ssh"
2022
)
2123

2224
// RemoteCreateOptionsFlag is Remote creation options flags
@@ -257,21 +259,25 @@ type Certificate struct {
257259
Hostkey HostkeyCertificate
258260
}
259261

262+
// HostkeyKind is a bitmask of the available hashes in HostkeyCertificate.
260263
type HostkeyKind uint
261264

262265
const (
263266
HostkeyMD5 HostkeyKind = C.GIT_CERT_SSH_MD5
264267
HostkeySHA1 HostkeyKind = C.GIT_CERT_SSH_SHA1
265268
HostkeySHA256 HostkeyKind = C.GIT_CERT_SSH_SHA256
269+
HostkeyRaw HostkeyKind = 1 << 3
266270
)
267271

268272
// Server host key information. A bitmask containing the available fields.
269-
// Check for combinations of: HostkeyMD5, HostkeySHA1, HostkeySHA256.
273+
// Check for combinations of: HostkeyMD5, HostkeySHA1, HostkeySHA256, HostkeyRaw.
270274
type HostkeyCertificate struct {
271-
Kind HostkeyKind
272-
HashMD5 [16]byte
273-
HashSHA1 [20]byte
274-
HashSHA256 [32]byte
275+
Kind HostkeyKind
276+
HashMD5 [16]byte
277+
HashSHA1 [20]byte
278+
HashSHA256 [32]byte
279+
Hostkey []byte
280+
SSHPublicKey ssh.PublicKey
275281
}
276282

277283
type PushOptions struct {

script/build-libgit2.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ cmake -DTHREADSAFE=ON \
6868
-DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \
6969
-DREGEX_BACKEND=builtin \
7070
-DUSE_HTTPS=OFF \
71+
-DUSE_SSH=OFF \
7172
-DCMAKE_C_FLAGS=-fPIC \
7273
-DCMAKE_BUILD_TYPE="RelWithDebInfo" \
7374
-DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \

ssh.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package git
2+
3+
/*
4+
#include <git2.h>
5+
6+
#include <git2/sys/credential.h>
7+
*/
8+
import "C"
9+
import (
10+
"crypto/md5"
11+
"crypto/sha1"
12+
"crypto/sha256"
13+
"errors"
14+
"fmt"
15+
"io"
16+
"io/ioutil"
17+
"net"
18+
"net/url"
19+
"runtime"
20+
"unsafe"
21+
22+
"golang.org/x/crypto/ssh"
23+
)
24+
25+
// RegisterManagedSSHTransport registers a Go-native implementation of an SSH
26+
// transport that doesn't rely on any system libraries (e.g. libssh2).
27+
//
28+
// If Shutdown or ReInit are called, make sure that the smart transports are
29+
// freed before it.
30+
func RegisterManagedSSHTransport(protocol string) (*RegisteredSmartTransport, error) {
31+
return NewRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory)
32+
}
33+
34+
func registerManagedSSH() error {
35+
globalRegisteredSmartTransports.Lock()
36+
defer globalRegisteredSmartTransports.Unlock()
37+
38+
for _, protocol := range []string{"ssh", "ssh+git", "git+ssh"} {
39+
if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok {
40+
continue
41+
}
42+
managed, err := newRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory, true)
43+
if err != nil {
44+
return fmt.Errorf("failed to register transport for %q: %v", protocol, err)
45+
}
46+
globalRegisteredSmartTransports.transports[protocol] = managed
47+
}
48+
return nil
49+
}
50+
51+
func sshSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) {
52+
return &sshSmartSubtransport{
53+
transport: transport,
54+
}, nil
55+
}
56+
57+
type sshSmartSubtransport struct {
58+
transport *Transport
59+
60+
lastAction SmartServiceAction
61+
client *ssh.Client
62+
session *ssh.Session
63+
stdin io.WriteCloser
64+
stdout io.Reader
65+
currentStream *sshSmartSubtransportStream
66+
}
67+
68+
func (t *sshSmartSubtransport) Action(urlString string, action SmartServiceAction) (SmartSubtransportStream, error) {
69+
runtime.LockOSThread()
70+
defer runtime.UnlockOSThread()
71+
72+
u, err := url.Parse(urlString)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
var cmd string
78+
switch action {
79+
case SmartServiceActionUploadpackLs, SmartServiceActionUploadpack:
80+
if t.currentStream != nil {
81+
if t.lastAction == SmartServiceActionUploadpackLs {
82+
return t.currentStream, nil
83+
}
84+
t.Close()
85+
}
86+
cmd = fmt.Sprintf("git-upload-pack %q", u.Path)
87+
88+
case SmartServiceActionReceivepackLs, SmartServiceActionReceivepack:
89+
if t.currentStream != nil {
90+
if t.lastAction == SmartServiceActionReceivepackLs {
91+
return t.currentStream, nil
92+
}
93+
t.Close()
94+
}
95+
cmd = fmt.Sprintf("git-receive-pack %q", u.Path)
96+
97+
default:
98+
return nil, fmt.Errorf("unexpected action: %v", action)
99+
}
100+
101+
cred, err := t.transport.SmartCredentials("", CredentialTypeSSHKey|CredentialTypeSSHMemory)
102+
if err != nil {
103+
return nil, err
104+
}
105+
defer cred.Free()
106+
107+
sshConfig, err := getSSHConfigFromCredential(cred)
108+
if err != nil {
109+
return nil, err
110+
}
111+
sshConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
112+
marshaledKey := key.Marshal()
113+
cert := &Certificate{
114+
Kind: CertificateHostkey,
115+
Hostkey: HostkeyCertificate{
116+
Kind: HostkeySHA1 | HostkeyMD5 | HostkeySHA256 | HostkeyRaw,
117+
HashMD5: md5.Sum(marshaledKey),
118+
HashSHA1: sha1.Sum(marshaledKey),
119+
HashSHA256: sha256.Sum256(marshaledKey),
120+
Hostkey: marshaledKey,
121+
SSHPublicKey: key,
122+
},
123+
}
124+
125+
return t.transport.SmartCertificateCheck(cert, true, hostname)
126+
}
127+
128+
var addr string
129+
if u.Port() != "" {
130+
addr = fmt.Sprintf("%s:%s", u.Hostname(), u.Port())
131+
} else {
132+
addr = fmt.Sprintf("%s:22", u.Hostname())
133+
}
134+
135+
t.client, err = ssh.Dial("tcp", addr, sshConfig)
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
t.session, err = t.client.NewSession()
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
t.stdin, err = t.session.StdinPipe()
146+
if err != nil {
147+
return nil, err
148+
}
149+
150+
t.stdout, err = t.session.StdoutPipe()
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
if err := t.session.Start(cmd); err != nil {
156+
return nil, err
157+
}
158+
159+
t.lastAction = action
160+
t.currentStream = &sshSmartSubtransportStream{
161+
owner: t,
162+
}
163+
164+
return t.currentStream, nil
165+
}
166+
167+
func (t *sshSmartSubtransport) Close() error {
168+
t.currentStream = nil
169+
if t.client != nil {
170+
t.stdin.Close()
171+
t.session.Wait()
172+
t.session.Close()
173+
t.client = nil
174+
}
175+
return nil
176+
}
177+
178+
func (t *sshSmartSubtransport) Free() {
179+
}
180+
181+
type sshSmartSubtransportStream struct {
182+
owner *sshSmartSubtransport
183+
}
184+
185+
func (stream *sshSmartSubtransportStream) Read(buf []byte) (int, error) {
186+
return stream.owner.stdout.Read(buf)
187+
}
188+
189+
func (stream *sshSmartSubtransportStream) Write(buf []byte) (int, error) {
190+
return stream.owner.stdin.Write(buf)
191+
}
192+
193+
func (stream *sshSmartSubtransportStream) Free() {
194+
}
195+
196+
func getSSHConfigFromCredential(cred *Credential) (*ssh.ClientConfig, error) {
197+
switch cred.Type() {
198+
case CredentialTypeSSHCustom:
199+
credSSHCustom := (*C.git_credential_ssh_custom)(unsafe.Pointer(cred.ptr))
200+
data, ok := pointerHandles.Get(credSSHCustom.payload).(*credentialSSHCustomData)
201+
if !ok {
202+
return nil, errors.New("unsupported custom SSH credentials")
203+
}
204+
return &ssh.ClientConfig{
205+
User: C.GoString(credSSHCustom.username),
206+
Auth: []ssh.AuthMethod{ssh.PublicKeys(data.signer)},
207+
}, nil
208+
}
209+
210+
username, _, privatekey, passphrase, err := cred.GetSSHKey()
211+
if err != nil {
212+
return nil, err
213+
}
214+
215+
var pemBytes []byte
216+
if cred.Type() == CredentialTypeSSHMemory {
217+
pemBytes = []byte(privatekey)
218+
} else {
219+
pemBytes, err = ioutil.ReadFile(privatekey)
220+
if err != nil {
221+
return nil, err
222+
}
223+
}
224+
225+
var key ssh.Signer
226+
if passphrase != "" {
227+
key, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(passphrase))
228+
if err != nil {
229+
return nil, err
230+
}
231+
} else {
232+
key, err = ssh.ParsePrivateKey(pemBytes)
233+
if err != nil {
234+
return nil, err
235+
}
236+
}
237+
238+
return &ssh.ClientConfig{
239+
User: username,
240+
Auth: []ssh.AuthMethod{ssh.PublicKeys(key)},
241+
}, nil
242+
}

0 commit comments

Comments
 (0)