Skip to content

Commit ea516d8

Browse files
authored
Merge pull request #783 from pjbgf/known_hosts_error
libgit2: improve known_hosts error messages
2 parents 2295134 + b490a6a commit ea516d8

File tree

4 files changed

+86
-33
lines changed

4 files changed

+86
-33
lines changed

pkg/git/libgit2/managed/ssh.go

+2-15
Original file line numberDiff line numberDiff line change
@@ -191,21 +191,8 @@ func (t *sshSmartSubtransport) Action(transportOptionsURL string, action git2go.
191191
}
192192

193193
sshConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
194-
marshaledKey := key.Marshal()
195-
cert := &git2go.Certificate{
196-
Kind: git2go.CertificateHostkey,
197-
Hostkey: git2go.HostkeyCertificate{
198-
Kind: git2go.HostkeySHA256 | git2go.HostkeyRaw,
199-
HashSHA256: sha256.Sum256(marshaledKey),
200-
Hostkey: marshaledKey,
201-
SSHPublicKey: key,
202-
},
203-
}
204-
205-
if len(opts.AuthOpts.KnownHosts) > 0 {
206-
return KnownHostsCallback(hostname, opts.AuthOpts.KnownHosts)(cert, true, hostname)
207-
}
208-
return nil
194+
keyHash := sha256.Sum256(key.Marshal())
195+
return CheckKnownHost(hostname, opts.AuthOpts.KnownHosts, keyHash[:])
209196
}
210197

211198
if t.connected {

pkg/git/libgit2/managed/ssh_test.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ limitations under the License.
1717
package managed
1818

1919
import (
20+
"net/url"
2021
"os"
2122
"path/filepath"
2223
"testing"
24+
"time"
2325

2426
"github.com/fluxcd/pkg/ssh"
2527
"github.com/fluxcd/source-controller/pkg/git"
@@ -97,13 +99,20 @@ func TestSSHManagedTransport_E2E(t *testing.T) {
9799
err = server.InitRepo("../../testdata/git/repo", git.DefaultBranch, repoPath)
98100
g.Expect(err).ToNot(HaveOccurred())
99101

102+
u, err := url.Parse(server.SSHAddress())
103+
g.Expect(err).NotTo(HaveOccurred())
104+
g.Expect(u.Host).ToNot(BeEmpty())
105+
knownhosts, err := ssh.ScanHostKey(u.Host, 5*time.Second, git.HostKeyAlgos, false)
106+
g.Expect(err).NotTo(HaveOccurred())
107+
100108
transportOptsURL := "ssh://git@fake-url"
101109
sshAddress := server.SSHAddress() + "/" + repoPath
102110
AddTransportOptions(transportOptsURL, TransportOptions{
103111
TargetURL: sshAddress,
104112
AuthOpts: &git.AuthOptions{
105-
Username: "user",
106-
Identity: kp.PrivateKey,
113+
Username: "user",
114+
Identity: kp.PrivateKey,
115+
KnownHosts: knownhosts,
107116
},
108117
})
109118

pkg/git/libgit2/managed/transport.go

+29-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package managed
22

33
import (
4+
"encoding/base64"
45
"fmt"
56
"net"
67

@@ -14,11 +15,6 @@ import (
1415
// git.SSH Transports.
1516
func KnownHostsCallback(host string, knownHosts []byte) git2go.CertificateCheckCallback {
1617
return func(cert *git2go.Certificate, valid bool, hostname string) error {
17-
kh, err := pkgkh.ParseKnownHosts(string(knownHosts))
18-
if err != nil {
19-
return fmt.Errorf("failed to parse known_hosts: %w", err)
20-
}
21-
2218
// First, attempt to split the configured host and port to validate
2319
// the port-less hostname given to the callback.
2420
hostWithoutPort, _, err := net.SplitHostPort(host)
@@ -47,18 +43,36 @@ func KnownHostsCallback(host string, knownHosts []byte) git2go.CertificateCheckC
4743
return fmt.Errorf("invalid host key kind, expected to be of kind SHA256")
4844
}
4945

50-
// We are now certain that the configured host and the hostname
51-
// given to the callback match. Use the configured host (that
52-
// includes the port), and normalize it, so we can check if there
53-
// is an entry for the hostname _and_ port.
54-
h := knownhosts.Normalize(host)
55-
for _, k := range kh {
56-
if k.Matches(h, fingerprint) {
57-
return nil
58-
}
46+
return CheckKnownHost(host, knownHosts, fingerprint)
47+
}
48+
}
49+
50+
// CheckKnownHost checks whether the host being connected to is
51+
// part of the known_hosts, and if so, it ensures the host
52+
// fingerprint matches the fingerprint of the known host with
53+
// the same name.
54+
func CheckKnownHost(host string, knownHosts []byte, fingerprint []byte) error {
55+
kh, err := pkgkh.ParseKnownHosts(string(knownHosts))
56+
if err != nil {
57+
return fmt.Errorf("failed to parse known_hosts: %w", err)
58+
}
59+
60+
if len(kh) == 0 {
61+
return fmt.Errorf("hostkey verification aborted: no known_hosts found")
62+
}
63+
64+
// We are now certain that the configured host and the hostname
65+
// given to the callback match. Use the configured host (that
66+
// includes the port), and normalize it, so we can check if there
67+
// is an entry for the hostname _and_ port.
68+
h := knownhosts.Normalize(host)
69+
for _, k := range kh {
70+
if k.Matches(h, fingerprint) {
71+
return nil
5972
}
60-
return fmt.Errorf("hostkey could not be verified")
6173
}
74+
return fmt.Errorf("no entries in known_hosts match host '%s' with fingerprint '%s'",
75+
h, base64.RawStdEncoding.EncodeToString(fingerprint))
6276
}
6377

6478
// RemoteCallbacks constructs git2go.RemoteCallbacks with dummy callbacks.

pkg/git/libgit2/managed/transport_test.go

+44-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAA
1616
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
1717
`
1818

19+
// To fetch latest knownhosts for source.developers.google.com run:
20+
// ssh-keyscan -p 2022 source.developers.google.com
21+
//
22+
// Expected hash (used in the cases) can get found with:
23+
// ssh-keyscan -p 2022 source.developers.google.com | ssh-keygen -l -f -
24+
var knownHostsFixtureWithPort = `[source.developers.google.com]:2022 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB5Iy4/cq/gt/fPqe3uyMy4jwv1Alc94yVPxmnwNhBzJqEV5gRPiRk5u4/JJMbbu9QUVAguBABxL7sBZa5PH/xY=`
25+
26+
// This is an incorrect known hosts entry, that does not aligned with
27+
// the normalized format and therefore won't match.
28+
var knownHostsFixtureUnormalized = `source.developers.google.com:2022 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB5Iy4/cq/gt/fPqe3uyMy4jwv1Alc94yVPxmnwNhBzJqEV5gRPiRk5u4/JJMbbu9QUVAguBABxL7sBZa5PH/xY=`
29+
1930
func TestKnownHostsCallback(t *testing.T) {
2031
tests := []struct {
2132
name string
@@ -25,6 +36,38 @@ func TestKnownHostsCallback(t *testing.T) {
2536
hostkey git2go.HostkeyCertificate
2637
want error
2738
}{
39+
{
40+
name: "Empty",
41+
host: "source.developers.google.com",
42+
knownHosts: []byte(""),
43+
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
44+
expectedHost: "source.developers.google.com:2022",
45+
want: fmt.Errorf("hostkey verification aborted: no known_hosts found"),
46+
},
47+
{
48+
name: "Mismatch incorrect known_hosts",
49+
host: "source.developers.google.com",
50+
knownHosts: []byte(knownHostsFixtureUnormalized),
51+
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
52+
expectedHost: "source.developers.google.com:2022",
53+
want: fmt.Errorf("no entries in known_hosts match host '[source.developers.google.com]:2022' with fingerprint 'AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434'"),
54+
},
55+
{
56+
name: "Match when host has port",
57+
host: "source.developers.google.com:2022",
58+
knownHosts: []byte(knownHostsFixtureWithPort),
59+
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
60+
expectedHost: "source.developers.google.com:2022",
61+
want: nil,
62+
},
63+
{
64+
name: "Match even when host does not have port",
65+
host: "source.developers.google.com",
66+
knownHosts: []byte(knownHostsFixtureWithPort),
67+
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
68+
expectedHost: "source.developers.google.com:2022",
69+
want: nil,
70+
},
2871
{
2972
name: "Match",
3073
host: "github.com",
@@ -66,7 +109,7 @@ func TestKnownHostsCallback(t *testing.T) {
66109
knownHosts: []byte(knownHostsFixture),
67110
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("ROQFvPThGrW4RuWLoL9tq9I9zJ42fK4XywyRtbOz/EQ")},
68111
expectedHost: "github.com",
69-
want: fmt.Errorf("hostkey could not be verified"),
112+
want: fmt.Errorf("no entries in known_hosts match host 'github.com' with fingerprint 'ROQFvPThGrW4RuWLoL9tq9I9zJ42fK4XywyRtbOz/EQ'"),
70113
},
71114
}
72115
for _, tt := range tests {

0 commit comments

Comments
 (0)