Skip to main content

ssh_vault/vault/
fingerprint.rs

1use crate::tools;
2use anyhow::{Context, Result};
3use rsa::{RsaPublicKey, pkcs8::EncodePublicKey};
4use ssh_key::{HashAlg, PublicKey};
5use std::{fmt, fs, path::Path};
6
7#[derive(Debug, Default, Clone, PartialEq, Eq)]
8pub struct Fingerprint {
9    pub key: String,
10    pub fingerprints: Vec<String>,
11    pub comment: String,
12    pub algorithm: String,
13}
14
15impl fmt::Display for Fingerprint {
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17        fn format_fingerprint(fp: &str, width: usize) -> String {
18            format!("{:>width$} {}", "", fp, width = width)
19        }
20
21        // Access custom width from the formatter arguments
22        let custom_width = f.width().unwrap_or(self.key.len());
23
24        writeln!(
25            f,
26            "{:>width$} Type: {} Comment: {}",
27            self.key,
28            self.algorithm,
29            self.comment,
30            width = custom_width
31        )?;
32
33        for fp in &self.fingerprints {
34            writeln!(f, "{}", format_fingerprint(fp, custom_width))?;
35        }
36
37        Ok(())
38    }
39}
40
41/// Collect fingerprints for all public keys in `~/.ssh`.
42///
43/// # Errors
44///
45/// Returns an error if any key cannot be read or parsed.
46pub fn fingerprints() -> Result<Vec<Fingerprint>> {
47    // Create a vector to store Fingerprint structs
48    let mut fingerprints: Vec<Fingerprint> = Vec::new();
49
50    let home = tools::get_home()?;
51    let ssh_home = Path::new(&home).join(".ssh");
52    if let Ok(entries) = fs::read_dir(ssh_home) {
53        for entry in entries.flatten() {
54            let path = entry.path();
55            if let Some(ext) = path.extension()
56                && ext == "pub"
57                && let Ok(key) = PublicKey::read_openssh_file(&path)
58            {
59                // Create a Fingerprint instance
60                let mut fingerprint = Fingerprint {
61                    key: path
62                        .file_name()
63                        .unwrap_or_default()
64                        .to_string_lossy()
65                        .to_string(),
66                    comment: key.comment().to_string(),
67                    algorithm: key.algorithm().to_string(),
68                    ..Default::default()
69                };
70
71                fingerprint
72                    .fingerprints
73                    .push(key.fingerprint(HashAlg::Sha256).to_string());
74
75                if let Some(key_data) = key.key_data().rsa() {
76                    let rsa_public_key = RsaPublicKey::try_from(key_data)?;
77                    fingerprint
78                        .fingerprints
79                        .push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
80                }
81
82                fingerprints.push(fingerprint);
83            }
84        }
85    }
86    Ok(fingerprints)
87}
88
89/// Compute fingerprints for a single public key.
90///
91/// # Errors
92///
93/// Returns an error if the key cannot be read or parsed.
94pub fn fingerprint(key: &str) -> Result<Fingerprint> {
95    let path = Path::new(&key);
96    let key = PublicKey::read_openssh_file(path)
97        .context("Ensure you are passing a valid openssh public key")?;
98    let mut fingerprint = Fingerprint {
99        key: path
100            .file_name()
101            .unwrap_or_default()
102            .to_string_lossy()
103            .to_string(),
104        comment: key.comment().to_string(),
105        algorithm: key.algorithm().to_string(),
106        ..Default::default()
107    };
108
109    fingerprint
110        .fingerprints
111        .push(key.fingerprint(HashAlg::Sha256).to_string());
112
113    if let Some(key_data) = key.key_data().rsa() {
114        let rsa_public_key = RsaPublicKey::try_from(key_data)?;
115        fingerprint
116            .fingerprints
117            .push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
118    }
119
120    Ok(fingerprint)
121}
122
123/// Fetch remote SSH keys and return their fingerprints.
124///
125/// # Errors
126///
127/// Returns an error if parsing or fingerprint generation fails.
128pub fn get_remote_fingerprints(keys: &str, key: Option<u32>) -> Result<Vec<Fingerprint>> {
129    // Get only SSH keys from the fetched keys
130    let keys = tools::filter_fetched_keys(keys)?;
131
132    // Create a vector to store Fingerprint structs
133    let mut fingerprints: Vec<Fingerprint> = Vec::new();
134
135    for (id, line) in keys.lines().enumerate() {
136        let u32_id = u32::try_from(id)?;
137
138        if let Some(mut key) = key {
139            key = key.saturating_sub(1);
140
141            if key >= u32::try_from(keys.lines().count())? {
142                Err(anyhow::anyhow!(
143                    "key index not found, try -k with a value between 1 and {}",
144                    keys.lines().count()
145                ))?;
146            }
147            if u32_id != key {
148                continue;
149            }
150        }
151
152        if let Ok(key) = PublicKey::from_openssh(line) {
153            // Create a Fingerprint instance
154            let mut fingerprint = Fingerprint {
155                key: format!("ID: {}", id + 1),
156                comment: key.comment().to_string(),
157                algorithm: key.algorithm().to_string(),
158                ..Default::default()
159            };
160
161            fingerprint
162                .fingerprints
163                .push(key.fingerprint(HashAlg::Sha256).to_string());
164
165            if let Some(key_data) = key.key_data().rsa() {
166                let rsa_public_key = RsaPublicKey::try_from(key_data)?;
167                fingerprint
168                    .fingerprints
169                    .push(format!("MD5 {}", md5_fingerprint(&rsa_public_key)?));
170            }
171
172            fingerprints.push(fingerprint);
173        }
174    }
175
176    Ok(fingerprints)
177}
178
179/// Calculate the MD5 fingerprint of a RSA public key and format it as a colon
180/// separated string.
181///
182/// # Errors
183///
184/// Returns an error if the key cannot be encoded.
185pub fn md5_fingerprint(public_key: &RsaPublicKey) -> Result<String> {
186    let public_key_der = public_key.to_public_key_der()?;
187    let md5_fingerprint = md5::compute(public_key_der.as_bytes());
188    let formatted_fingerprint = format!("{md5_fingerprint:x}")
189        .chars()
190        .collect::<Vec<char>>()
191        .chunks(2)
192        .map(|chunk| chunk.iter().collect::<String>())
193        .collect::<Vec<String>>()
194        .join(":");
195    Ok(formatted_fingerprint)
196}
197
198#[cfg(test)]
199#[allow(clippy::unwrap_used)]
200mod tests {
201    use super::*;
202
203    struct Test {
204        key: &'static str,
205        fingerprint: &'static str,
206    }
207
208    #[test]
209    fn test_fingerprints() {
210        let tests = [
211            Test {
212                key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDjjM4JEyg1T8j5YICtqslLNp2UGg80CppTM3ZYu73pEmDhMwbLfdhuI56AQZgWViFsF/7QHDJPcRY2Piu38b4kizTSM0QHEOC7CTo+vnzxptlKLGT1y2mcY1P9VXzCBMSWQN9/vGasgl/sUp1zcTvVT0CjjA6k1dJM6/+aDVtCsFa851VkwbeIsWl5BAHLyL+ur5BX93/BxYnRcYl7ooheuEWWokyWJ0IwEFToPMHAthTbDn1P17wYF43oscTORsFBfkP1JLBKHPDPJCGcBgQButL/srLJf6o44fScAYL99s1dQ/Qqv31aygDmwLdKEDldNnWEaJZ+iidEiIlPtAnLYGnVVA4u+NA2p3egrUrLWmpPjMX6XSb2VRHllzCcY4vZ4F2ud2TFaYG6N+9+vRCdxB+LFcHhm7ottI4vnC5P1bbMagjmFne0+TSKrAfMCw59eiQd8yZVMoE2yPXjFOQt6EOBvB4OHv1AaVt2q0PGqSkv5vIhgsKJWx/6IUj0Kz24hDiMipFb0jL3xstvizAllpC6yF26Ju/nwF03eJJGGxJjrxYd4P5/rY6SWY3yakiUN7pUBgUK2Ok3K3/+BTy5Aag8OXcvOZJumr2X2Wn9DweQeCRjC8UqFDKALqA/3vopZ2S59V4WOg3sV94hEig/KHLISNge1Uatn+qosK2sPw== test",
213                fingerprint: "55:cd:f2:7e:4c:0b:e5:a7:6e:6c:fc:6b:8e:58:9d:15",
214            },
215            Test {
216                key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXsxWj7gvLUHbkUDzB6g+DfTdJbIcjH5Ge8ZZcYrTFeZ3hFL/pEfsuDf0Ut87QR0QpTFwM8SHyjKAX1rnF10Y+9ezG3Z4btHFk7SVPW0qqBwoTHFYiRqjgOcQrfQoDAhn9p/h93RCHR6gQPwj5CmDMRmnUcPV9mzjiLyqaqecAjGZj6q6O99Z5/lY2It/fCUcNW0JXBc31SiquvkkYhNjQsQgJxI5KnBMUEdVhk3ItJp8XeDbk2Kq03w0L8XcAqS2BUl4nNF4a5eMgME/tCUjSVYMvqcFIpOUsZhYNE+rt0ElbsMuehdvdLCbb2EBt+n75JgfGOsZCd96JrZiPlq55e0r5uDPz0rVtqnAWQawTtmSwa/VY7GZCf/xB2FvuqoXozWpAgzM7pypVx3JTBZwHx0xe/a0m1RA6+laQ4cCKV6FZWPV8WwUcvvxPknbDsjCeXgVQAxlXMk3pYrcGl61IPv/GaOr1QNPtUFRUuQXfgWh0F5SaU5MeI6HSGvuzooM=",
217                fingerprint: "19:b9:77:30:3f:99:15:b7:53:98:0d:ef:d1:8f:33:58",
218            },
219            Test {
220                key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKdb5/i8sIEZ84k+LpJCAxRwxUZsP2MHFWApeB2TSUux ssh-vault",
221                fingerprint: "SHA256:HcSHlMDnxnmeh6dsxdTrqOGUPp8Ei78VaF9t3ED21S8",
222            },
223            Test {
224                key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINixf2m2nj8TDeazbWuemUY8ZHNg7znA7hVPN8TJLr2W",
225                fingerprint: "SHA256:hgIL5fEHz5zuOWY1CDlUuotdaUl4MvYG7vAgE4q4TzM",
226            },
227        ];
228
229        for test in &tests {
230            let mut fingerprint = String::new();
231            let public_key = PublicKey::from_openssh(test.key).unwrap();
232            if public_key.algorithm().as_str() == "ssh-rsa" {
233                let key_data = public_key.key_data().rsa().unwrap();
234                let rsa_public_key = RsaPublicKey::try_from(key_data).unwrap();
235                fingerprint = md5_fingerprint(&rsa_public_key).unwrap();
236            } else if public_key.algorithm().as_str() == "ssh-ed25519" {
237                fingerprint = public_key.fingerprint(HashAlg::Sha256).to_string();
238            }
239            assert_eq!(fingerprint, test.fingerprint);
240        }
241    }
242}