Skip to content

Commit 8b50367

Browse files
author
Paulo Gomes
committed
libgit2: Add support for hashed known_hosts
Hashed known_hosts was previously only supported when using go-git. Now both Git implementations benefit from this features, and the code coverage across them can ensure no future regression. Signed-off-by: Paulo Gomes <[email protected]>
1 parent 6a40770 commit 8b50367

File tree

8 files changed

+207
-13
lines changed

8 files changed

+207
-13
lines changed

controllers/gitrepository_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
478478
u, err := url.Parse(obj.Spec.URL)
479479
g.Expect(err).NotTo(HaveOccurred())
480480
g.Expect(u.Host).ToNot(BeEmpty())
481-
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos)
481+
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
482482
g.Expect(err).NotTo(HaveOccurred())
483483
secret.Data["known_hosts"] = knownHosts
484484
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ require (
2626
github.com/fluxcd/pkg/helmtestserver v0.7.2
2727
github.com/fluxcd/pkg/lockedfile v0.1.0
2828
github.com/fluxcd/pkg/runtime v0.15.1
29-
github.com/fluxcd/pkg/ssh v0.3.3
29+
github.com/fluxcd/pkg/ssh v0.3.4
3030
github.com/fluxcd/pkg/testserver v0.2.0
3131
github.com/fluxcd/pkg/untar v0.1.0
3232
github.com/fluxcd/pkg/version v0.1.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ github.com/fluxcd/pkg/lockedfile v0.1.0/go.mod h1:EJLan8t9MiOcgTs8+puDjbE6I/KAfH
366366
github.com/fluxcd/pkg/runtime v0.13.0-rc.6/go.mod h1:4oKUO19TeudXrnCRnxCfMSS7EQTYpYlgfXwlQuDJ/Eg=
367367
github.com/fluxcd/pkg/runtime v0.15.1 h1:PKooYqlZM+KLhnNz10sQnBH0AHllS40PIDHtiRH/BGU=
368368
github.com/fluxcd/pkg/runtime v0.15.1/go.mod h1:TPAoOEgUFG60FXBA4ID41uaPldxuXCEI4jt3qfd5i5Q=
369-
github.com/fluxcd/pkg/ssh v0.3.3 h1:/tc7W7LO1VoVUI5jB+p9ZHCA+iQaXTkaSCDZJsxcZ9k=
370-
github.com/fluxcd/pkg/ssh v0.3.3/go.mod h1:+bKhuv0/pJy3HZwkK54Shz68sNv1uf5aI6wtPaEHaYk=
369+
github.com/fluxcd/pkg/ssh v0.3.4 h1:Ko+MUNiiQG3evyoMO19iRk7d4X0VJ6w6+GEeVQ1jLC0=
370+
github.com/fluxcd/pkg/ssh v0.3.4/go.mod h1:KGgOUOy1uI6RC6+qxIBLvP1AeOOs/nLB25Ca6TZMIXE=
371371
github.com/fluxcd/pkg/testserver v0.2.0 h1:Mj0TapmKaywI6Fi5wvt1LAZpakUHmtzWQpJNKQ0Krt4=
372372
github.com/fluxcd/pkg/testserver v0.2.0/go.mod h1:bgjjydkXsZTeFzjz9Cr4heGANr41uTB1Aj1Q5qzuYVk=
373373
github.com/fluxcd/pkg/untar v0.1.0 h1:k97V/xV5hFrAkIkVPuv5AVhyxh1ZzzAKba/lbDfGo6o=

pkg/git/gogit/checkout_test.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ func Test_KeyTypes(t *testing.T) {
461461
g.Expect(err).NotTo(HaveOccurred())
462462
g.Expect(u.Host).ToNot(BeEmpty())
463463

464-
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos)
464+
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
465465
g.Expect(err).ToNot(HaveOccurred())
466466

467467
for _, tt := range tests {
@@ -600,7 +600,7 @@ func Test_KeyExchangeAlgos(t *testing.T) {
600600
g.Expect(err).NotTo(HaveOccurred())
601601
g.Expect(u.Host).ToNot(BeEmpty())
602602

603-
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos)
603+
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
604604
g.Expect(err).ToNot(HaveOccurred())
605605

606606
// No authentication is required for this test, but it is
@@ -644,6 +644,7 @@ func TestHostKeyAlgos(t *testing.T) {
644644
name string
645645
keyType ssh.KeyPairType
646646
ClientHostKeyAlgos []string
647+
hashHostNames bool
647648
}{
648649
{
649650
name: "support for hostkey: ssh-rsa",
@@ -680,6 +681,48 @@ func TestHostKeyAlgos(t *testing.T) {
680681
keyType: ssh.ED25519,
681682
ClientHostKeyAlgos: []string{"ssh-ed25519"},
682683
},
684+
{
685+
name: "support for hostkey: ssh-rsa with hashed host names",
686+
keyType: ssh.RSA_4096,
687+
ClientHostKeyAlgos: []string{"ssh-rsa"},
688+
hashHostNames: true,
689+
},
690+
{
691+
name: "support for hostkey: rsa-sha2-256 with hashed host names",
692+
keyType: ssh.RSA_4096,
693+
ClientHostKeyAlgos: []string{"rsa-sha2-256"},
694+
hashHostNames: true,
695+
},
696+
{
697+
name: "support for hostkey: rsa-sha2-512 with hashed host names",
698+
keyType: ssh.RSA_4096,
699+
ClientHostKeyAlgos: []string{"rsa-sha2-512"},
700+
hashHostNames: true,
701+
},
702+
{
703+
name: "support for hostkey: ecdsa-sha2-nistp256 with hashed host names",
704+
keyType: ssh.ECDSA_P256,
705+
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp256"},
706+
hashHostNames: true,
707+
},
708+
{
709+
name: "support for hostkey: ecdsa-sha2-nistp384 with hashed host names",
710+
keyType: ssh.ECDSA_P384,
711+
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp384"},
712+
hashHostNames: true,
713+
},
714+
{
715+
name: "support for hostkey: ecdsa-sha2-nistp521 with hashed host names",
716+
keyType: ssh.ECDSA_P521,
717+
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp521"},
718+
hashHostNames: true,
719+
},
720+
{
721+
name: "support for hostkey: ssh-ed25519 with hashed host names",
722+
keyType: ssh.ED25519,
723+
ClientHostKeyAlgos: []string{"ssh-ed25519"},
724+
hashHostNames: true,
725+
},
683726
}
684727

685728
for _, tt := range tests {
@@ -727,7 +770,7 @@ func TestHostKeyAlgos(t *testing.T) {
727770
g.Expect(err).NotTo(HaveOccurred())
728771
g.Expect(u.Host).ToNot(BeEmpty())
729772

730-
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos)
773+
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, tt.hashHostNames)
731774
g.Expect(err).ToNot(HaveOccurred())
732775

733776
// No authentication is required for this test, but it is

pkg/git/libgit2/managed_test.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func Test_ManagedSSH_KeyTypes(t *testing.T) {
9696
g.Expect(err).NotTo(HaveOccurred())
9797
g.Expect(u.Host).ToNot(BeEmpty())
9898

99-
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos)
99+
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
100100
g.Expect(err).ToNot(HaveOccurred())
101101

102102
for _, tt := range tests {
@@ -238,7 +238,7 @@ func Test_ManagedSSH_KeyExchangeAlgos(t *testing.T) {
238238
g.Expect(err).NotTo(HaveOccurred())
239239
g.Expect(u.Host).ToNot(BeEmpty())
240240

241-
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos)
241+
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
242242
g.Expect(err).ToNot(HaveOccurred())
243243

244244
// No authentication is required for this test, but it is
@@ -282,6 +282,7 @@ func Test_ManagedSSH_HostKeyAlgos(t *testing.T) {
282282
name string
283283
keyType ssh.KeyPairType
284284
ClientHostKeyAlgos []string
285+
hashHostNames bool
285286
}{
286287
{
287288
name: "support for hostkey: ssh-rsa",
@@ -318,6 +319,48 @@ func Test_ManagedSSH_HostKeyAlgos(t *testing.T) {
318319
keyType: ssh.ED25519,
319320
ClientHostKeyAlgos: []string{"ssh-ed25519"},
320321
},
322+
{
323+
name: "support for hostkey: ssh-rsa with hashed host names",
324+
keyType: ssh.RSA_4096,
325+
ClientHostKeyAlgos: []string{"ssh-rsa"},
326+
hashHostNames: true,
327+
},
328+
{
329+
name: "support for hostkey: rsa-sha2-256 with hashed host names",
330+
keyType: ssh.RSA_4096,
331+
ClientHostKeyAlgos: []string{"rsa-sha2-256"},
332+
hashHostNames: true,
333+
},
334+
{
335+
name: "support for hostkey: rsa-sha2-512 with hashed host names",
336+
keyType: ssh.RSA_4096,
337+
ClientHostKeyAlgos: []string{"rsa-sha2-512"},
338+
hashHostNames: true,
339+
},
340+
{
341+
name: "support for hostkey: ecdsa-sha2-nistp256 with hashed host names",
342+
keyType: ssh.ECDSA_P256,
343+
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp256"},
344+
hashHostNames: true,
345+
},
346+
{
347+
name: "support for hostkey: ecdsa-sha2-nistp384 with hashed host names",
348+
keyType: ssh.ECDSA_P384,
349+
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp384"},
350+
hashHostNames: true,
351+
},
352+
{
353+
name: "support for hostkey: ecdsa-sha2-nistp521 with hashed host names",
354+
keyType: ssh.ECDSA_P521,
355+
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp521"},
356+
hashHostNames: true,
357+
},
358+
{
359+
name: "support for hostkey: ssh-ed25519 with hashed host names",
360+
keyType: ssh.ED25519,
361+
ClientHostKeyAlgos: []string{"ssh-ed25519"},
362+
hashHostNames: true,
363+
},
321364
}
322365

323366
for _, tt := range tests {
@@ -368,7 +411,7 @@ func Test_ManagedSSH_HostKeyAlgos(t *testing.T) {
368411
g.Expect(err).NotTo(HaveOccurred())
369412
g.Expect(u.Host).ToNot(BeEmpty())
370413

371-
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, tt.ClientHostKeyAlgos)
414+
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, tt.ClientHostKeyAlgos, tt.hashHostNames)
372415
g.Expect(err).ToNot(HaveOccurred())
373416

374417
// No authentication is required for this test, but it is

pkg/git/libgit2/transport.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import (
2020
"bufio"
2121
"bytes"
2222
"context"
23+
"crypto/hmac"
2324
"crypto/md5"
2425
"crypto/sha1"
2526
"crypto/sha256"
2627
"crypto/x509"
28+
"encoding/base64"
2729
"fmt"
2830
"hash"
2931
"io"
@@ -288,10 +290,54 @@ func (k knownKey) matches(host string, hostkey git2go.HostkeyCertificate) bool {
288290
}
289291

290292
func containsHost(hosts []string, host string) bool {
291-
for _, h := range hosts {
292-
if h == host {
293+
for _, kh := range hosts {
294+
// hashed host must start with a pipe
295+
if kh[0] == '|' {
296+
match, _ := MatchHashedHost(kh, host)
297+
if match {
298+
return true
299+
}
300+
301+
} else if kh == host { // unhashed host check
293302
return true
294303
}
295304
}
296305
return false
297306
}
307+
308+
// MatchHashedHost tries to match a hashed known host (kh) to
309+
// host.
310+
//
311+
// Note that host is not hashed, but it is rather hashed during
312+
// the matching process using the same salt used when hashing
313+
// the known host.
314+
func MatchHashedHost(kh, host string) (bool, error) {
315+
if kh == "" || kh[0] != '|' {
316+
return false, fmt.Errorf("hashed known host must begin with '|': '%s'", kh)
317+
}
318+
319+
components := strings.Split(kh, "|")
320+
if len(components) != 4 {
321+
return false, fmt.Errorf("invalid format for hashed known host: '%s'", kh)
322+
}
323+
324+
if components[1] != "1" {
325+
return false, fmt.Errorf("unsupported hash type '%s'", components[1])
326+
}
327+
328+
hkSalt, err := base64.StdEncoding.DecodeString(components[2])
329+
if err != nil {
330+
return false, fmt.Errorf("cannot decode hashed known host: '%w'", err)
331+
}
332+
333+
hkHash, err := base64.StdEncoding.DecodeString(components[3])
334+
if err != nil {
335+
return false, fmt.Errorf("cannot decode hashed known host: '%w'", err)
336+
}
337+
338+
mac := hmac.New(sha1.New, hkSalt)
339+
mac.Write([]byte(host))
340+
hostHash := mac.Sum(nil)
341+
342+
return bytes.Equal(hostHash, hkHash), nil
343+
}

pkg/git/libgit2/transport_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,68 @@ func Test_pushTransferProgressCallback(t *testing.T) {
522522
}
523523
}
524524

525+
func TestMatchHashedHost(t *testing.T) {
526+
tests := []struct {
527+
name string
528+
knownHost string
529+
host string
530+
match bool
531+
wantErr string
532+
}{
533+
{
534+
name: "match valid known host",
535+
knownHost: "|1|vApZG0Ybr4rHfTb69+cjjFIGIv0=|M5sSXen14encOvQAy0gseRahnJw=",
536+
host: "[127.0.0.1]:44167",
537+
match: true,
538+
},
539+
{
540+
name: "empty known host errors",
541+
wantErr: "hashed known host must begin with '|'",
542+
},
543+
{
544+
name: "unhashed known host errors",
545+
knownHost: "[127.0.0.1]:44167",
546+
wantErr: "hashed known host must begin with '|'",
547+
},
548+
{
549+
name: "invalid known host format errors",
550+
knownHost: "|1M5sSXen14encOvQAy0gseRahnJw=",
551+
wantErr: "invalid format for hashed known host",
552+
},
553+
{
554+
name: "invalid hash type errors",
555+
knownHost: "|2|vApZG0Ybr4rHfTb69+cjjFIGIv0=|M5sSXen14encOvQAy0gseRahnJw=",
556+
wantErr: "unsupported hash type",
557+
},
558+
{
559+
name: "invalid base64 component[2] errors",
560+
knownHost: "|1|azz|M5sSXen14encOvQAy0gseRahnJw=",
561+
wantErr: "cannot decode hashed known host",
562+
},
563+
{
564+
name: "invalid base64 component[3] errors",
565+
knownHost: "|1|M5sSXen14encOvQAy0gseRahnJw=|azz",
566+
wantErr: "cannot decode hashed known host",
567+
},
568+
}
569+
570+
for _, tt := range tests {
571+
t.Run(tt.name, func(t *testing.T) {
572+
g := NewWithT(t)
573+
574+
matched, err := MatchHashedHost(tt.knownHost, tt.host)
575+
576+
if tt.wantErr == "" {
577+
g.Expect(err).NotTo(HaveOccurred())
578+
g.Expect(matched).To(Equal(tt.match))
579+
} else {
580+
g.Expect(err).To(HaveOccurred())
581+
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
582+
}
583+
})
584+
}
585+
}
586+
525587
func md5Fingerprint(in string) [16]byte {
526588
var out [16]byte
527589
copy(out[:], in)

pkg/git/strategy/strategy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func TestCheckoutStrategyForImplementation_Auth(t *testing.T) {
9797
return getSSHRepoURL(srv.SSHAddress(), repoPath)
9898
},
9999
authOptsFunc: func(g *WithT, u *url.URL, user, pswd string, ca []byte) *git.AuthOptions {
100-
knownhosts, err := ssh.ScanHostKey(u.Host, 5*time.Second, git.HostKeyAlgos)
100+
knownhosts, err := ssh.ScanHostKey(u.Host, 5*time.Second, git.HostKeyAlgos, false)
101101
g.Expect(err).ToNot(HaveOccurred())
102102

103103
keygen := ssh.NewRSAGenerator(2048)

0 commit comments

Comments
 (0)