1use ssh_key::{HashAlg, PublicKey};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13pub enum SshKeyParseError {
14 #[error("key file not readable: {0}")]
15 Io(#[from] std::io::Error),
16 #[error("key blob not parseable: {0}")]
17 Parse(String),
18}
19
20#[derive(Debug, Clone)]
23pub struct ParsedKey {
24 pub key_type: String,
26 pub key_blob: Vec<u8>,
29 pub fingerprint: String,
32 pub comment: String,
34}
35
36pub fn parse_authorized_keys_line(line: &str) -> Result<ParsedKey, SshKeyParseError> {
43 let key =
44 PublicKey::from_openssh(line.trim()).map_err(|e| SshKeyParseError::Parse(e.to_string()))?;
45 let key_type = key.algorithm().as_str().to_string();
46 let key_blob = key
47 .to_bytes()
48 .map_err(|e| SshKeyParseError::Parse(e.to_string()))?;
49 let fingerprint = key.fingerprint(HashAlg::Sha256).to_string();
50 let comment = key.comment().to_string();
51 Ok(ParsedKey {
52 key_type,
53 key_blob,
54 fingerprint,
55 comment,
56 })
57}
58
59pub fn parse_public_key_file(path: &str) -> Result<ParsedKey, SshKeyParseError> {
68 let text = std::fs::read_to_string(path)?;
69 parse_authorized_keys_line(&text)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 const SAMPLE_PUB: &str = concat!(
80 "ssh-ed25519 ",
81 "AAAAC3NzaC1lZDI1NTE5AAAAIM4Prl5sFtRqvGeOYeKDx4f1HYpW2v0Z4V6yHlHWJaLi",
82 " alice@laptop",
83 );
84
85 #[test]
86 fn parses_ed25519_line() {
87 let parsed = parse_authorized_keys_line(SAMPLE_PUB).expect("valid ed25519 pubkey");
88 assert_eq!(parsed.key_type, "ssh-ed25519");
89 assert_eq!(parsed.comment, "alice@laptop");
90 assert!(parsed.fingerprint.starts_with("SHA256:"));
91 }
92
93 #[test]
94 fn fingerprint_matches_openssh_convention() {
95 let parsed = parse_authorized_keys_line(SAMPLE_PUB).unwrap();
96 let body = &parsed.fingerprint["SHA256:".len()..];
99 assert_eq!(body.len(), 43);
100 }
101
102 #[test]
103 fn rejects_garbage() {
104 let err = parse_authorized_keys_line("not-a-key").unwrap_err();
105 assert!(matches!(err, SshKeyParseError::Parse(_)));
106 }
107
108 #[test]
109 fn rejects_empty_input() {
110 assert!(matches!(
111 parse_authorized_keys_line(""),
112 Err(SshKeyParseError::Parse(_))
113 ));
114 }
115}