Skip to main content

signstar_request_signature/ssh/
known_hosts.rs

1//! SSH `known_hosts` format utilities.
2
3use hmac::KeyInit as _;
4use log::{info, warn};
5use russh::keys::{
6    PublicKey,
7    ssh_key::known_hosts::{Entry, HostPatterns},
8};
9
10/// Checks whether a set of server details can be found in SSH `known_hosts` data.
11///
12/// Based on a `host` and its `port`, this function evaluates whether a supplied `key` is part of a
13/// list of `entries` in the SSH known_hosts file format. Returns `true`, if the combination of
14/// `key`, `host` and `port` matches an entry in the list of `entries` and that entry is not a CA
15/// key or a revoked key. Returns `false` in all other cases.
16pub(crate) fn is_server_known<'a>(
17    entries: impl Iterator<Item = &'a Entry>,
18    host: &str,
19    port: u16,
20    key: &PublicKey,
21) -> bool {
22    for entry in entries {
23        if match entry.host_patterns() {
24            HostPatterns::Patterns(items) => items
25                .iter()
26                .any(|item| item == host || item == &format!("[{host}]:{port}")),
27            HostPatterns::HashedName { salt, hash } => {
28                use hmac::Mac;
29                if let Ok(mut mac) = hmac::Hmac::<sha1::Sha1>::new_from_slice(salt) {
30                    mac.update(host.as_bytes());
31                    mac.finalize().into_bytes()[..] == hash[..]
32                } else {
33                    warn!(
34                        "the salt {salt:?} was not of correct size so the entry for host {host} does not match"
35                    );
36                    false
37                }
38            }
39        } && entry.public_key() == key
40        {
41            return if let Some(marker) = entry.marker() {
42                info!("Found marker {marker} for host {host} but it is not supported.");
43                false
44            } else {
45                true
46            };
47        }
48    }
49    false
50}
51
52#[cfg(test)]
53mod tests {
54    use testresult::TestResult;
55
56    use super::*;
57
58    #[test]
59    fn test_single_entry() -> TestResult {
60        let entry: Entry = "gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
61
62        assert!(
63            is_server_known(
64                [entry].iter(),
65                "gitlab.archlinux.org",
66                22,
67                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
68                    .parse()?
69            ),
70            "server should be known since there's one matching entry"
71        );
72
73        Ok(())
74    }
75
76    #[test]
77    fn test_single_entry_with_port() -> TestResult {
78        let entry: Entry = "[gitlab.archlinux.org]:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
79
80        assert!(
81            is_server_known(
82                [entry].iter(),
83                "gitlab.archlinux.org",
84                22,
85                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
86                    .parse()?
87            ),
88            "server should be known since there's one matching entry"
89        );
90
91        Ok(())
92    }
93
94    #[test]
95    fn test_single_revoked_entry() -> TestResult {
96        let entry: Entry = "@revoked gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
97
98        assert!(
99            !is_server_known(
100                [entry].iter(),
101                "gitlab.archlinux.org",
102                22,
103                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
104                    .parse()?
105            ),
106            "server should not be known since there's one matching entry but it is revoked"
107        );
108
109        Ok(())
110    }
111
112    #[test]
113    fn test_single_cert_authority_entry() -> TestResult {
114        let entry: Entry = "@cert-authority gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
115
116        assert!(
117            !is_server_known(
118                [entry].iter(),
119                "gitlab.archlinux.org",
120                22,
121                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
122                    .parse()?
123            ),
124            "server should not be known since certification authorities are not supported"
125        );
126
127        Ok(())
128    }
129
130    #[test]
131    fn test_not_matching_entry() -> TestResult {
132        let entry: Entry = "gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K89dpNtltOK6vy".parse()?;
133
134        assert!(
135            !is_server_known(
136                [entry].iter(),
137                "gitlab.archlinux.org",
138                22,
139                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
140                    .parse()?
141            ),
142            "server should not be known since there are no matching entries"
143        );
144
145        Ok(())
146    }
147
148    #[test]
149    fn test_not_matching_port_entry() -> TestResult {
150        let entry: Entry = "[gitlab.archlinux.org]:23 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K89dpNtltOK6vy".parse()?;
151
152        assert!(
153            !is_server_known(
154                [entry].iter(),
155                "gitlab.archlinux.org",
156                22,
157                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
158                    .parse()?
159            ),
160            "server should not be known since there are no matching entries"
161        );
162
163        Ok(())
164    }
165
166    #[test]
167    fn test_hashed_entry() -> TestResult {
168        // entry generated using `ssh-keygen -H -F github.com`
169        let entry: Entry = "|1|b8LfkX9Y09oxr9MMnQyfC9CtciI=|MnTpZgaon9ON5+hrylyRlq/li3Q= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl".parse()?;
170        assert!(
171            is_server_known(
172                [entry].iter(),
173                "github.com",
174                22,
175                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
176                    .parse()?
177            ),
178            "server should be known since there's one matching entry"
179        );
180
181        Ok(())
182    }
183}