Skip to content

Commit e25476b

Browse files
committed
libgit2: use provided host to validate public key
The callback from libgit2 only provides a hostname (without the port), but the `known_hosts` file indexes the public keys based on the full host (e.g. `[localhost]:123` for a host behind a specific port). As a result, it was unable to find the correct public key for the hostname when it was added to the `known_hosts` file with the port. To work around this, we add the user provided host that includes the port to the `PublicKeyAuth` strategy, and use this to find the right entry in the `known_hosts` file, after having validated that the hostname provided to the callback matches the hostname of the host provided by the user. Signed-off-by: Hidde Beydals <hello@hidde.co>
1 parent b501172 commit e25476b

File tree

2 files changed

+34
-12
lines changed

2 files changed

+34
-12
lines changed

pkg/git/libgit2/transport.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ import (
2222
"crypto/sha1"
2323
"crypto/x509"
2424
"fmt"
25+
"net"
2526
"net/url"
2627
"strings"
2728

28-
"golang.org/x/crypto/ssh"
29-
3029
git2go "github.com/libgit2/git2go/v31"
30+
"golang.org/x/crypto/ssh"
31+
"golang.org/x/crypto/ssh/knownhosts"
3132
corev1 "k8s.io/api/core/v1"
3233

3334
"github.com/fluxcd/source-controller/pkg/git"
@@ -43,7 +44,7 @@ func AuthSecretStrategyForURL(URL string) (git.AuthSecretStrategy, error) {
4344
case u.Scheme == "http", u.Scheme == "https":
4445
return &BasicAuth{}, nil
4546
case u.Scheme == "ssh":
46-
return &PublicKeyAuth{user: u.User.Username()}, nil
47+
return &PublicKeyAuth{user: u.User.Username(), host: u.Host}, nil
4748
default:
4849
return nil, fmt.Errorf("no auth secret strategy for scheme %s", u.Scheme)
4950
}
@@ -62,7 +63,7 @@ func (s *BasicAuth) Method(secret corev1.Secret) (*git.Auth, error) {
6263
password = string(d)
6364
}
6465
if username != "" && password != "" {
65-
credCallback = func(url string, username_from_url string, allowed_types git2go.CredType) (*git2go.Cred, error) {
66+
credCallback = func(url string, usernameFromURL string, allowedTypes git2go.CredType) (*git2go.Cred, error) {
6667
cred, err := git2go.NewCredUserpassPlaintext(username, password)
6768
if err != nil {
6869
return nil, err
@@ -97,11 +98,12 @@ func (s *BasicAuth) Method(secret corev1.Secret) (*git.Auth, error) {
9798

9899
type PublicKeyAuth struct {
99100
user string
101+
host string
100102
}
101103

102104
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
103105
if _, ok := secret.Data[git.CAFile]; ok {
104-
return nil, fmt.Errorf("found caFile key in secret '%s' but libgit2 SSH transport does not support custom certificates", secret.Name)
106+
return nil, fmt.Errorf("found %s key in secret '%s' but libgit2 SSH transport does not support custom certificates", git.CAFile, secret.Name)
105107
}
106108
identity := secret.Data["identity"]
107109
knownHosts := secret.Data["known_hosts"]
@@ -126,20 +128,40 @@ func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
126128
user = git.DefaultPublicKeyAuthUser
127129
}
128130

129-
credCallback := func(url string, username_from_url string, allowed_types git2go.CredType) (*git2go.Cred, error) {
131+
credCallback := func(url string, usernameFromURL string, allowedTypes git2go.CredType) (*git2go.Cred, error) {
130132
cred, err := git2go.NewCredSshKeyFromMemory(user, "", string(identity), "")
131133
if err != nil {
132134
return nil, err
133135
}
134136
return cred, nil
135137
}
136138
certCallback := func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
139+
// First, attempt to split the configured host and port to validate
140+
// the port-less hostname given to the callback.
141+
host, _, err := net.SplitHostPort(s.host)
142+
if err != nil {
143+
// SplitHostPort returns an error if the host is missing
144+
// a port, assume the host has no port.
145+
host = s.host
146+
}
147+
148+
// Check if the configured host matches the hostname to the
149+
// callback.
150+
if host != hostname {
151+
return git2go.ErrUser
152+
}
153+
154+
// We are now certain that the configured host and the hostname
155+
// given to the callback match. Use the configured host (that
156+
// includes the port), and normalize it so we can check if there
157+
// is an entry for the hostname _and_ port.
158+
host = knownhosts.Normalize(s.host)
137159
for _, k := range kk {
138-
if k.matches(hostname, cert.Hostkey.HashSHA1[:]) {
160+
if k.matches(s.host, cert.Hostkey.HashSHA1[:]) {
139161
return git2go.ErrOk
140162
}
141163
}
142-
return git2go.ErrGeneric
164+
return git2go.ErrCertificate
143165
}
144166

145167
return &git.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
@@ -151,7 +173,7 @@ type knownKey struct {
151173
}
152174

153175
func parseKnownHosts(s string) ([]knownKey, error) {
154-
knownHosts := []knownKey{}
176+
var knownHosts []knownKey
155177
scanner := bufio.NewScanner(strings.NewReader(s))
156178
for scanner.Scan() {
157179
_, hosts, pubKey, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
@@ -178,7 +200,7 @@ func (k knownKey) matches(host string, key []byte) bool {
178200
return false
179201
}
180202

181-
hash := sha1.Sum([]byte(k.key.Marshal()))
203+
hash := sha1.Sum(k.key.Marshal())
182204
if bytes.Compare(hash[:], key) != 0 {
183205
return false
184206
}

pkg/git/libgit2/transport_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ func TestAuthSecretStrategyForURL(t *testing.T) {
7373
}{
7474
{"HTTP", "http://git.example.com/org/repo.git", &BasicAuth{}, false},
7575
{"HTTPS", "https://git.example.com/org/repo.git", &BasicAuth{}, false},
76-
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{}, false},
77-
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example"}, false},
76+
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{host: "git.example.com:2222"}, false},
77+
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example", host: "git.example.com:2222"}, false},
7878
{"unsupported", "protocol://example.com", nil, true},
7979
}
8080
for _, tt := range tests {

0 commit comments

Comments
 (0)