21
21
//! this file.
22
22
23
23
use crate :: util:: config:: { Definition , Value } ;
24
- use git2:: cert:: Cert ;
24
+ use git2:: cert:: { Cert , SshHostKeyType } ;
25
25
use git2:: CertificateCheckStatus ;
26
26
use std:: collections:: HashSet ;
27
27
use std:: fmt:: Write ;
@@ -49,15 +49,15 @@ enum KnownHostError {
49
49
/// The host key was not found.
50
50
HostKeyNotFound {
51
51
hostname : String ,
52
- key_type : git2 :: cert :: SshHostKeyType ,
52
+ key_type : SshHostKeyType ,
53
53
remote_host_key : String ,
54
54
remote_fingerprint : String ,
55
55
other_hosts : Vec < KnownHost > ,
56
56
} ,
57
57
/// The host key was found, but does not match the remote's key.
58
58
HostKeyHasChanged {
59
59
hostname : String ,
60
- key_type : git2 :: cert :: SshHostKeyType ,
60
+ key_type : SshHostKeyType ,
61
61
old_known_host : KnownHost ,
62
62
remote_host_key : String ,
63
63
remote_fingerprint : String ,
@@ -238,11 +238,6 @@ fn check_ssh_known_hosts(
238
238
return Err ( anyhow:: format_err!( "remote host key is not available" ) . into ( ) ) ;
239
239
} ;
240
240
let remote_key_type = cert_host_key. hostkey_type ( ) . unwrap ( ) ;
241
- // `changed_key` keeps track of any entries where the key has changed.
242
- let mut changed_key = None ;
243
- // `other_hosts` keeps track of any entries that have an identical key,
244
- // but a different hostname.
245
- let mut other_hosts = Vec :: new ( ) ;
246
241
247
242
// Collect all the known host entries from disk.
248
243
let mut known_hosts = Vec :: new ( ) ;
@@ -293,6 +288,21 @@ fn check_ssh_known_hosts(
293
288
} ) ;
294
289
}
295
290
}
291
+ check_ssh_known_hosts_loaded ( & known_hosts, host, remote_key_type, remote_host_key)
292
+ }
293
+
294
+ /// Checks a host key against a loaded set of known hosts.
295
+ fn check_ssh_known_hosts_loaded (
296
+ known_hosts : & [ KnownHost ] ,
297
+ host : & str ,
298
+ remote_key_type : SshHostKeyType ,
299
+ remote_host_key : & [ u8 ] ,
300
+ ) -> Result < ( ) , KnownHostError > {
301
+ // `changed_key` keeps track of any entries where the key has changed.
302
+ let mut changed_key = None ;
303
+ // `other_hosts` keeps track of any entries that have an identical key,
304
+ // but a different hostname.
305
+ let mut other_hosts = Vec :: new ( ) ;
296
306
297
307
for known_host in known_hosts {
298
308
// The key type from libgit2 needs to match the key type from the host file.
@@ -301,7 +311,6 @@ fn check_ssh_known_hosts(
301
311
}
302
312
let key_matches = known_host. key == remote_host_key;
303
313
if !known_host. host_matches ( host) {
304
- // `name` can be None for hashed hostnames (which libgit2 does not expose).
305
314
if key_matches {
306
315
other_hosts. push ( known_host. clone ( ) ) ;
307
316
}
@@ -434,7 +443,7 @@ impl KnownHost {
434
443
return false ;
435
444
}
436
445
} else {
437
- match_found = pattern == host;
446
+ match_found | = pattern == host;
438
447
}
439
448
}
440
449
match_found
@@ -444,6 +453,10 @@ impl KnownHost {
444
453
/// Loads an OpenSSH known_hosts file.
445
454
fn load_hostfile ( path : & Path ) -> Result < Vec < KnownHost > , anyhow:: Error > {
446
455
let contents = cargo_util:: paths:: read ( path) ?;
456
+ Ok ( load_hostfile_contents ( path, & contents) )
457
+ }
458
+
459
+ fn load_hostfile_contents ( path : & Path , contents : & str ) -> Vec < KnownHost > {
447
460
let entries = contents
448
461
. lines ( )
449
462
. enumerate ( )
@@ -455,13 +468,13 @@ fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> {
455
468
parse_known_hosts_line ( line, location)
456
469
} )
457
470
. collect ( ) ;
458
- Ok ( entries)
471
+ entries
459
472
}
460
473
461
474
fn parse_known_hosts_line ( line : & str , location : KnownHostLocation ) -> Option < KnownHost > {
462
475
let line = line. trim ( ) ;
463
476
// FIXME: @revoked and @cert-authority is currently not supported.
464
- if line. is_empty ( ) || line. starts_with ( '#' ) || line . starts_with ( '@' ) {
477
+ if line. is_empty ( ) || line. starts_with ( [ '#' , '@' , '|' ] ) {
465
478
return None ;
466
479
}
467
480
let mut parts = line. split ( [ ' ' , '\t' ] ) . filter ( |s| !s. is_empty ( ) ) ;
@@ -476,3 +489,126 @@ fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<Kno
476
489
key,
477
490
} )
478
491
}
492
+
493
+ #[ cfg( test) ]
494
+ mod tests {
495
+ use super :: * ;
496
+
497
+ static COMMON_CONTENTS : & str = r#"
498
+ # Comments allowed at start of line
499
+
500
+ example.com,rust-lang.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5MzWIpZwpkpDjyCNiTIEVFhSA9OUUQvjFo7CgZBGCAj/cqeUIgiLsgtfmtBsfWIkAECQpM7ePP7NLZFGJcHvoyg5jXJiIX5s0eKo9IlcuTLLrMkW5MkHXE7bNklVbW1WdCfF2+y7Ao25B4L8FFRokMh0yp/H6+8xZ7PdVwL3FRPEg8ftZ5R0kuups6xiMHPRX+f/07vfJzA47YDPmXfhkn+JK8kL0JYw8iy8BtNBfRQL99d9iXJzWXnNce5NHMuKD5rOonD3aQHLDlwK+KhrFRrdaxQEM8ZWxNti0ux8yT4Dl5jJY0CrIu3Xl6+qroVgTqJGNkTbhs5DGWdFh6BLPTTH15rN4buisg7uMyLyHqx06ckborqD33gWu+Jig7O+PV6KJmL5mp1O1HXvZqkpBdTiT6GiDKG3oECCIXkUk0BSU9VG9VQcrMxxvgiHlyoXUAfYQoXv/lnxkTnm+Sr36kutsVOs7n5B43ZKAeuaxyQ11huJZpxamc0RA1HM641s= eric@host
501
+ Example.net ssh-dss AAAAB3NzaC1kc3MAAACBAK2Ek3jVxisXmz5UcZ7W65BAj/nDJCCVvSe0Aytndn4PH6k7sVesut5OoY6PdksZ9tEfuFjjS9HR5SJb8j1GW0GxtaSHHbf+rNc36PeU75bffzyIWwpA8uZFONt5swUAXJXcsHOoapNbUFuhHsRhB2hXxz9QGNiiwIwRJeSHixKRAAAAFQChKfxO1z9H2/757697xP5nJ/Z5dwAAAIEAoc+HIWas+4WowtB/KtAp6XE0B9oHI+55wKtdcGwwb7zHKK9scWNXwxIcMhSvyB3Oe2I7dQQlvyIWxsdZlzOkX0wdsTHjIAnBAP68MyvMv4kq3+I5GAVcFsqoLZfZvh0dlcgUq1/YNYZwKlt89tnzk8Fp4KLWmuw8Bd8IShYVa78AAACAL3qd8kNTY7CthgsQ8iWdjbkGSF/1KCeFyt8UjurInp9wvPDjqagwakbyLOzN7y3/ItTPCaGuX+RjFP0zZTf8i9bsAVyjFJiJ7vzRXcWytuFWANrpzLTn1qzPfh63iK92Aw8AVBYvEA/4bxo+XReAvhNBB/m78G6OedTeu6ZoTsI= eric@host
502
+ [example.net]:2222 ssh-dss AAAAB3NzaC1kc3MAAACBAJJN5kLZEpOJpXWyMT4KwYvLAj+b9ErNtglxOi86C6Kw7oZeYdDMCfD3lc3PJyX64udQcWGfO4abSESMiYdY43yFAZH279QGH5Q/B5CklVvTqYpfAUR+1r9TQxy3OVQHk7FB2wOi4xNQ3myO0vaYlBOB9il+P223aERbXx4JTWdvAAAAFQCTHWTcXxLK5Z6ZVPmfdSDyHzkF2wAAAIEAhp41/mTnM0Y0EWSyCXuETMW1QSpKGF8sqoZKp6wdzyhLXu0i32gLdXj4p24em/jObYh93hr+MwgxqWq+FHgD+D80Qg5f6vj4yEl4Uu5hqtTpCBFWUQoyEckbUkPf8uZ4/XzAne+tUSjZm09xATCmK9U2IGqZE+D+90eBkf1Svc8AAACAeKhi4EtfwenFYqKz60ZoEEhIsE1yI2jH73akHnfHpcW84w+fk3YlwjcfDfyYso+D0jZBdJeK5qIdkbUWhAX8wDjJVO0WL6r/YPr4yu/CgEyW1H59tAbujGJ4NR0JDqioulzYqNHnxpiw1RJukZnPBfSFKzRElvPOCq/NkQM/Mwk= eric@host
503
+ nistp256.example.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ4iYGCcJrUIfrHfzlsv8e8kaF36qpcUpe3VNAKVCZX/BDptIdlEe8u8vKNRTPgUO9jqS0+tjTcPiQd8/8I9qng= eric@host
504
+ nistp384.example.org ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNuGT3TqMz2rcwOt2ZqkiNqq7dvWPE66W2qPCoZsh0pQhVU3BnhKIc6nEr6+Wts0Z3jdF3QWwxbbTjbVTVhdr8fMCFhDCWiQFm9xLerYPKnu9qHvx9K87/fjc5+0pu4hLA== eric@host
505
+ nistp521.example.org ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAD35HH6OsK4DN75BrKipVj/GvZaUzjPNa1F8wMjUdPB1JlVcUfgzJjWSxrhmaNN3u0soiZw8WNRFINsGPCw5E7DywF1689WcIj2Ye2rcy99je15FknScTzBBD04JgIyOI50mCUaPCBoF14vFlN6BmO00cFo+yzy5N8GuQ2sx9kr21xmFQ== eric@host
506
+ # Revoked not yet supported.
507
+ @revoked * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host
508
+ example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
509
+ 192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
510
+ # Hash not yet supported.
511
+ |1|7CMSYgzdwruFLRhwowMtKx0maIE=|Tlff1GFqc3Ao+fUWxMEVG8mJiyk= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host
512
+ # Negation isn't terribly useful without globs.
513
+ neg.example.com,!neg.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXfUnaAHTlo1Qi//rNk26OcmHikmkns1Z6WW/UuuS3K eric@host
514
+ "# ;
515
+
516
+ #[ test]
517
+ fn known_hosts_parse ( ) {
518
+ let kh_path = Path :: new ( "/home/abc/.known_hosts" ) ;
519
+ let khs = load_hostfile_contents ( kh_path, COMMON_CONTENTS ) ;
520
+ assert_eq ! ( khs. len( ) , 9 ) ;
521
+ match & khs[ 0 ] . location {
522
+ KnownHostLocation :: File { path, lineno } => {
523
+ assert_eq ! ( path, kh_path) ;
524
+ assert_eq ! ( * lineno, 4 ) ;
525
+ }
526
+ _ => panic ! ( "unexpected" ) ,
527
+ }
528
+ assert_eq ! ( khs[ 0 ] . patterns, "example.com,rust-lang.org" ) ;
529
+ assert_eq ! ( khs[ 0 ] . key_type, "ssh-rsa" ) ;
530
+ assert_eq ! ( khs[ 0 ] . key. len( ) , 407 ) ;
531
+ assert_eq ! ( & khs[ 0 ] . key[ ..30 ] , b"\x00 \x00 \x00 \x07 ssh-rsa\x00 \x00 \x00 \x03 \x01 \x00 \x01 \x00 \x00 \x01 \x81 \x00 \xb9 35\x88 \xa5 \x9c )" ) ;
532
+ match & khs[ 1 ] . location {
533
+ KnownHostLocation :: File { path, lineno } => {
534
+ assert_eq ! ( path, kh_path) ;
535
+ assert_eq ! ( * lineno, 5 ) ;
536
+ }
537
+ _ => panic ! ( "unexpected" ) ,
538
+ }
539
+ assert_eq ! ( khs[ 2 ] . patterns, "[example.net]:2222" ) ;
540
+ assert_eq ! ( khs[ 3 ] . patterns, "nistp256.example.org" ) ;
541
+ assert_eq ! ( khs[ 7 ] . patterns, "192.168.42.12" ) ;
542
+ }
543
+
544
+ #[ test]
545
+ fn host_matches ( ) {
546
+ let kh_path = Path :: new ( "/home/abc/.known_hosts" ) ;
547
+ let khs = load_hostfile_contents ( kh_path, COMMON_CONTENTS ) ;
548
+ assert ! ( khs[ 0 ] . host_matches( "example.com" ) ) ;
549
+ assert ! ( khs[ 0 ] . host_matches( "rust-lang.org" ) ) ;
550
+ assert ! ( khs[ 0 ] . host_matches( "EXAMPLE.COM" ) ) ;
551
+ assert ! ( khs[ 1 ] . host_matches( "example.net" ) ) ;
552
+ assert ! ( !khs[ 0 ] . host_matches( "example.net" ) ) ;
553
+ assert ! ( khs[ 2 ] . host_matches( "[example.net]:2222" ) ) ;
554
+ assert ! ( !khs[ 2 ] . host_matches( "example.net" ) ) ;
555
+ assert ! ( !khs[ 8 ] . host_matches( "neg.example.com" ) ) ;
556
+ }
557
+
558
+ #[ test]
559
+ fn check_match ( ) {
560
+ let kh_path = Path :: new ( "/home/abc/.known_hosts" ) ;
561
+ let khs = load_hostfile_contents ( kh_path, COMMON_CONTENTS ) ;
562
+
563
+ assert ! ( check_ssh_known_hosts_loaded(
564
+ & khs,
565
+ "example.com" ,
566
+ SshHostKeyType :: Rsa ,
567
+ & khs[ 0 ] . key
568
+ )
569
+ . is_ok( ) ) ;
570
+
571
+ match check_ssh_known_hosts_loaded ( & khs, "example.com" , SshHostKeyType :: Dss , & khs[ 0 ] . key ) {
572
+ Err ( KnownHostError :: HostKeyNotFound {
573
+ hostname,
574
+ remote_fingerprint,
575
+ other_hosts,
576
+ ..
577
+ } ) => {
578
+ assert_eq ! (
579
+ remote_fingerprint,
580
+ "yn+pONDn0EcgdOCVptgB4RZd/wqmsVKrPnQMLtrvhw8"
581
+ ) ;
582
+ assert_eq ! ( hostname, "example.com" ) ;
583
+ assert_eq ! ( other_hosts. len( ) , 0 ) ;
584
+ }
585
+ _ => panic ! ( "unexpected" ) ,
586
+ }
587
+
588
+ match check_ssh_known_hosts_loaded (
589
+ & khs,
590
+ "foo.example.com" ,
591
+ SshHostKeyType :: Rsa ,
592
+ & khs[ 0 ] . key ,
593
+ ) {
594
+ Err ( KnownHostError :: HostKeyNotFound { other_hosts, .. } ) => {
595
+ assert_eq ! ( other_hosts. len( ) , 1 ) ;
596
+ assert_eq ! ( other_hosts[ 0 ] . patterns, "example.com,rust-lang.org" ) ;
597
+ }
598
+ _ => panic ! ( "unexpected" ) ,
599
+ }
600
+
601
+ let mut modified_key = khs[ 0 ] . key . clone ( ) ;
602
+ modified_key[ 0 ] = 1 ;
603
+ match check_ssh_known_hosts_loaded ( & khs, "example.com" , SshHostKeyType :: Rsa , & modified_key)
604
+ {
605
+ Err ( KnownHostError :: HostKeyHasChanged { old_known_host, .. } ) => {
606
+ assert ! ( matches!(
607
+ old_known_host. location,
608
+ KnownHostLocation :: File { lineno: 4 , .. }
609
+ ) ) ;
610
+ }
611
+ _ => panic ! ( "unexpected" ) ,
612
+ }
613
+ }
614
+ }
0 commit comments