openssh_keys/
lib.rs

1//! A pure-Rust library to handle OpenSSH public keys.
2//!
3//! This crate supports parsing, manipulation, and some basic validation of
4//! SSH keys. It provides a struct for encapsulation of SSH keys in projects.
5//!
6//! `openssh-keys` does not have the ability to generate SSH keys. However,
7//! it does allow to construct RSA and DSA keys from their components, so if you
8//! generate the keys with another library (say, rust-openssl), then you can
9//! output the SSH public keys with this library.
10//!
11//! # Example
12//!
13//! ```rust
14//! use std::{env, fs, io, path};
15//! use std::io::BufRead;
16//!
17//! fn inspect_rsa() {
18//!     let home = env::home_dir().unwrap_or(path::PathBuf::from("/home/core/"));
19//!     let pub_path = home.join(".ssh").join("id_rsa.pub");
20//!     println!("Inspecting '{}':", pub_path.to_string_lossy());
21//!     let file = fs::File::open(&pub_path).expect("unable to open RSA pubkey");
22//!     let reader = io::BufReader::new(file);
23//!
24//!     for (i, line) in reader.lines().enumerate() {
25//!         let line = line.expect(&format!("unable to read key at line {}", i + 1));
26//!         let pubkey = openssh_keys::PublicKey::parse(&line).expect("unable to parse RSA pubkey");
27//!         println!(" * Pubkey #{} -> {}", i + 1, pubkey.to_fingerprint_string());
28//!     }
29//! }
30
31mod reader;
32mod writer;
33
34pub mod errors {
35    use thiserror::Error;
36
37    pub type Result<T> = std::result::Result<T, OpenSSHKeyError>;
38
39    #[derive(Error, Debug)]
40    pub enum OpenSSHKeyError {
41        #[error("I/O error")]
42        IO {
43            #[from]
44            source: std::io::Error,
45        },
46
47        #[error("invalid UTF-8")]
48        InvalidUtf8 {
49            #[from]
50            source: std::str::Utf8Error,
51        },
52
53        // keep base64::DecodeError out of the public API
54        #[error("invalid base64: {detail}")]
55        InvalidBase64 { detail: String },
56
57        #[error("invalid key format")]
58        InvalidFormat,
59
60        #[error("unsupported keytype: {keytype}")]
61        UnsupportedKeyType { keytype: String },
62
63        #[error("unsupported curve: {curve}")]
64        UnsupportedCurve { curve: String },
65    }
66}
67
68use crate::errors::*;
69
70use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
71use md5::Md5;
72use sha2::{Digest, Sha256};
73
74use crate::reader::Reader;
75use crate::writer::Writer;
76
77use std::fmt;
78use std::io::{BufRead, BufReader, Read};
79
80const SSH_RSA: &str = "ssh-rsa";
81const SSH_DSA: &str = "ssh-dss";
82const SSH_ED25519: &str = "ssh-ed25519";
83const SSH_ED25519_SK: &str = "sk-ssh-ed25519@openssh.com";
84const SSH_ECDSA_256: &str = "ecdsa-sha2-nistp256";
85const SSH_ECDSA_384: &str = "ecdsa-sha2-nistp384";
86const SSH_ECDSA_521: &str = "ecdsa-sha2-nistp521";
87const SSH_ECDSA_SK: &str = "sk-ecdsa-sha2-nistp256@openssh.com";
88const NISTP_256: &str = "nistp256";
89const NISTP_384: &str = "nistp384";
90const NISTP_521: &str = "nistp521";
91
92/// Curves for ECDSA
93#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
94pub enum Curve {
95    Nistp256,
96    Nistp384,
97    Nistp521,
98}
99
100impl Curve {
101    /// get converts a curve name of the type in the format described in
102    /// https://tools.ietf.org/html/rfc5656#section-10 and returns a curve
103    /// object.
104    fn get(curve: &str) -> Result<Self> {
105        Ok(match curve {
106            NISTP_256 => Curve::Nistp256,
107            NISTP_384 => Curve::Nistp384,
108            NISTP_521 => Curve::Nistp521,
109            _ => {
110                return Err(OpenSSHKeyError::UnsupportedCurve {
111                    curve: curve.to_string(),
112                })
113            }
114        })
115    }
116
117    /// curvetype gets the curve name in the format described in
118    /// https://tools.ietf.org/html/rfc5656#section-10
119    fn curvetype(self) -> &'static str {
120        match self {
121            Curve::Nistp256 => NISTP_256,
122            Curve::Nistp384 => NISTP_384,
123            Curve::Nistp521 => NISTP_521,
124        }
125    }
126}
127
128impl fmt::Display for Curve {
129    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
130        write!(f, "{}", self.curvetype())
131    }
132}
133
134/// Data is the representation of the data section of an ssh public key. it is
135/// an enum with all the different supported key algorithms.
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub enum Data {
138    Rsa {
139        exponent: Vec<u8>,
140        modulus: Vec<u8>,
141    },
142    Dsa {
143        p: Vec<u8>,
144        q: Vec<u8>,
145        g: Vec<u8>,
146        pub_key: Vec<u8>,
147    },
148    Ed25519 {
149        key: Vec<u8>,
150    },
151    Ed25519Sk {
152        key: Vec<u8>,
153        application: Vec<u8>,
154    },
155    Ecdsa {
156        curve: Curve,
157        key: Vec<u8>,
158    },
159    EcdsaSk {
160        curve: Curve,
161        key: Vec<u8>,
162        application: Vec<u8>,
163    },
164}
165
166/// `PublicKey` is the struct representation of an ssh public key.
167#[derive(Clone, Debug, Eq)]
168pub struct PublicKey {
169    pub options: Option<String>,
170    pub data: Data,
171    pub comment: Option<String>,
172}
173
174impl fmt::Display for PublicKey {
175    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176        write!(f, "{}", self.to_key_format())
177    }
178}
179
180/// Two public keys are equivalent if their data sections are equivalent,
181/// ignoring their comment section.
182impl core::cmp::PartialEq for PublicKey {
183    fn eq(&self, other: &PublicKey) -> bool {
184        self.data == other.data
185    }
186}
187
188impl std::str::FromStr for PublicKey {
189    type Err = OpenSSHKeyError;
190    fn from_str(s: &str) -> Result<Self> {
191        PublicKey::parse(s)
192    }
193}
194
195impl PublicKey {
196    /// parse takes a string and parses it as a public key from an authorized
197    /// keys file. the format it expects is described here
198    /// https://tools.ietf.org/html/rfc4253#section-6.6 and here
199    /// https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT
200    ///
201    /// sshd describes an additional, optional "options" field for public keys
202    /// in the authorized_keys file. This field allows for passing of options to
203    /// sshd that only apply to that particular public key. This means that a
204    /// public key in an authorized keys file is a strict superset of the public
205    /// key format described in rfc4253. Another superset of a public key is
206    /// what is present in the known_hosts file. This file has a hostname as the
207    /// first thing on the line. This parser treats the hostname the same as an
208    /// option field. When one of these things is found at the beginning of a
209    /// line, it is treated as a semi-opaque string that is carried with the
210    /// public key and reproduced when the key is printed. It is not entirely
211    /// opaque, since the parser needs to be aware of quoting semantics within
212    /// the option fields, since options surrounded by double quotes can contain
213    /// spaces, which are otherwise the main delimiter of the parts of a public
214    /// key.
215    ///
216    /// You can parse and output ssh keys like this
217    ///
218    /// ```
219    /// let rsa_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcMCOEryBa8IkxXacjIawaQPp08hR5h7+4vZePZ7DByTG3tqKgZYRJ86BaR+4fmdikFoQjvLJVUmwniq3wixhkP7VLCbqip3YHzxXrzxkbPC3w3O1Bdmifwn9cb8RcZXfXncCsSu+h5XCtQ5BOi41Iit3d13gIe/rfXVDURmRanV6R7Voljxdjmp/zyReuzc2/w5SI6Boi4tmcUlxAI7sFuP1kA3pABDhPtc3TDgAcPUIBoDCoY8q2egI197UuvbgsW2qraUcuQxbMvJOMSFg2FQrE2bpEqC4CtBn7+HiJrkVOHjV7bvSv7jd1SuX5XqkwMCRtdMuRpJr7CyZoFL5n demos@anduin";
220    /// let key = openssh_keys::PublicKey::parse(rsa_key).unwrap();
221    /// let out = key.to_string();
222    /// assert_eq!(rsa_key, out);
223    /// ```
224    ///
225    /// parse somewhat attempts to keep track of comments, but it doesn't fully
226    /// comply with the rfc in that regard.
227    pub fn parse(key: &str) -> Result<Self> {
228        // trim leading and trailing whitespace
229        let key = key.trim();
230        // try just parsing the keys straight up
231        PublicKey::try_key_parse(key).or_else(|e| {
232            // remove the preceeding string
233            let mut key_start = 0;
234            let mut escape = false;
235            let mut quote = false;
236            let mut marker = key.starts_with('@');
237            for (i, c) in key.chars().enumerate() {
238                if c == '\\' {
239                    escape = true;
240                    continue;
241                }
242                if escape {
243                    escape = false;
244                    continue;
245                }
246                if c == '"' {
247                    quote = !quote;
248                }
249                if !quote && (c == ' ' || c == '\t') {
250                    if marker {
251                        marker = false;
252                        continue;
253                    } else {
254                        key_start = i + 1;
255                        break;
256                    }
257                }
258            }
259            let mut parsed = PublicKey::try_key_parse(&key[key_start..]).map_err(|_| e)?;
260            parsed.options = Some(key[..key_start - 1].into());
261            Ok(parsed)
262        })
263    }
264
265    fn try_key_parse(key: &str) -> Result<Self> {
266        // then parse the key according to rfc4253
267        let (keytype, remaining) = key
268            .split_once(char::is_whitespace)
269            .ok_or(OpenSSHKeyError::InvalidFormat)?;
270
271        let (data, comment) = remaining
272            .split_once(char::is_whitespace)
273            .unwrap_or((remaining, ""));
274
275        let comment = comment.trim();
276        if comment.contains('\n') {
277            return Err(OpenSSHKeyError::InvalidFormat);
278        }
279        let comment = if comment.is_empty() {
280            None
281        } else {
282            Some(comment.to_owned())
283        };
284
285        let buf = BASE64
286            .decode(data)
287            .map_err(|e| OpenSSHKeyError::InvalidBase64 {
288                detail: format!("{}", e),
289            })?;
290        let mut reader = Reader::new(&buf);
291        let data_keytype = reader.read_string()?;
292        if keytype != data_keytype {
293            return Err(OpenSSHKeyError::InvalidFormat);
294        }
295
296        let data = match keytype {
297            SSH_RSA => {
298                // the data for an rsa key consists of three pieces:
299                //    ssh-rsa public-exponent modulus
300                // see ssh-rsa format in https://tools.ietf.org/html/rfc4253#section-6.6
301                let e = reader.read_mpint()?;
302                let n = reader.read_mpint()?;
303                Data::Rsa {
304                    exponent: e.into(),
305                    modulus: n.into(),
306                }
307            }
308            SSH_DSA => {
309                // the data stored for a dsa key is, in order
310                //    ssh-dsa p q g public-key
311                // p and q are primes
312                // g = h^((p-1)/q) where 1 < h < p-1
313                // public-key is the value that is actually generated in
314                // relation to the secret key
315                // see https://en.wikipedia.org/wiki/Digital_Signature_Algorithm
316                // and ssh-dss format in https://tools.ietf.org/html/rfc4253#section-6.6
317                // and https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L743
318                let p = reader.read_mpint()?;
319                let q = reader.read_mpint()?;
320                let g = reader.read_mpint()?;
321                let pub_key = reader.read_mpint()?;
322                Data::Dsa {
323                    p: p.into(),
324                    q: q.into(),
325                    g: g.into(),
326                    pub_key: pub_key.into(),
327                }
328            }
329            SSH_ED25519 => {
330                // the data stored for an ed25519 is just the point on the curve
331                // for now the exact specification of the point on that curve is
332                // a mystery to me, instead of having to compute it, we just
333                // assume the key we got is correct and copy that verbatim. this
334                // also means we have to disallow arbitrary construction until
335                // furthur notice.
336                // see https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L772
337                let key = reader.read_bytes()?;
338                Data::Ed25519 { key: key.into() }
339            }
340            SSH_ED25519_SK => {
341                // same as above
342                let key = reader.read_bytes()?;
343                let application = reader.read_bytes()?;
344                Data::Ed25519Sk {
345                    key: key.into(),
346                    application: application.into(),
347                }
348            }
349            SSH_ECDSA_256 | SSH_ECDSA_384 | SSH_ECDSA_521 => {
350                // ecdsa is of the form
351                //    ecdsa-sha2-[identifier] [identifier] [data]
352                // the identifier is one of nistp256, nistp384, nistp521
353                // the data is some weird thing described in section 2.3.4 and
354                // 2.3.4 of https://www.secg.org/sec1-v2.pdf so for now we
355                // aren't going to bother actually computing it and instead we
356                // will just not let you construct them.
357                //
358                // see the data definition at
359                // https://tools.ietf.org/html/rfc5656#section-3.1
360                // and the openssh output
361                // https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L753
362                // and the openssh buffer writer implementation
363                // https://github.com/openssh/openssh-portable/blob/master/sshbuf-getput-crypto.c#L192
364                // and the openssl point2oct implementation
365                // https://github.com/openssl/openssl/blob/aa8f3d76fcf1502586435631be16faa1bef3cdf7/crypto/ec/ec_oct.c#L82
366                let curve = reader.read_string()?;
367                let key = reader.read_bytes()?;
368                Data::Ecdsa {
369                    curve: Curve::get(curve)?,
370                    key: key.into(),
371                }
372            }
373            SSH_ECDSA_SK => {
374                // same as above (like there, we don't assert that the curve matches what was specified in the keytype)
375                let curve = reader.read_string()?;
376                let key = reader.read_bytes()?;
377                let application = reader.read_bytes()?;
378                Data::EcdsaSk {
379                    curve: Curve::get(curve)?,
380                    key: key.into(),
381                    application: application.into(),
382                }
383            }
384            _ => {
385                return Err(OpenSSHKeyError::UnsupportedKeyType {
386                    keytype: keytype.to_string(),
387                })
388            }
389        };
390
391        Ok(PublicKey {
392            options: None,
393            data,
394            comment,
395        })
396    }
397
398    /// read_keys takes a reader and parses it as an authorized_keys file. it
399    /// returns an error if it can't read or parse any of the public keys in the
400    /// list.
401    pub fn read_keys<R>(r: R) -> Result<Vec<Self>>
402    where
403        R: Read,
404    {
405        let keybuf = BufReader::new(r);
406        // authorized_keys files are newline-separated lists of public keys
407        let mut keys = vec![];
408        for key in keybuf.lines() {
409            let key = key?;
410            // skip any empty lines and any comment lines (prefixed with '#')
411            if !key.is_empty() && !(key.trim().starts_with('#')) {
412                keys.push(PublicKey::parse(&key)?);
413            }
414        }
415        Ok(keys)
416    }
417
418    /// get an ssh public key from rsa components
419    pub fn from_rsa(e: Vec<u8>, n: Vec<u8>) -> Self {
420        PublicKey {
421            options: None,
422            data: Data::Rsa {
423                exponent: e,
424                modulus: n,
425            },
426            comment: None,
427        }
428    }
429
430    /// get an ssh public key from dsa components
431    pub fn from_dsa(p: Vec<u8>, q: Vec<u8>, g: Vec<u8>, pkey: Vec<u8>) -> Self {
432        PublicKey {
433            options: None,
434            data: Data::Dsa {
435                p,
436                q,
437                g,
438                pub_key: pkey,
439            },
440            comment: None,
441        }
442    }
443
444    /// keytype returns the type of key in the format described by rfc4253
445    /// The output will be ssh-{type} where type is [rsa,ed25519,ecdsa,dsa]
446    pub fn keytype(&self) -> &'static str {
447        match self.data {
448            Data::Rsa { .. } => SSH_RSA,
449            Data::Dsa { .. } => SSH_DSA,
450            Data::Ed25519 { .. } => SSH_ED25519,
451            Data::Ed25519Sk { .. } => SSH_ED25519_SK,
452            Data::Ecdsa { ref curve, .. } => match *curve {
453                Curve::Nistp256 => SSH_ECDSA_256,
454                Curve::Nistp384 => SSH_ECDSA_384,
455                Curve::Nistp521 => SSH_ECDSA_521,
456            },
457            Data::EcdsaSk { .. } => SSH_ECDSA_SK,
458        }
459    }
460
461    /// data returns the data section of the key in the format described by rfc4253
462    /// the contents of the data section depend on the keytype. For RSA keys it
463    /// contains the keytype, exponent, and modulus in that order. Other types
464    /// have other data sections. This function doesn't base64 encode the data,
465    /// that task is left to the consumer of the output.
466    pub fn data(&self) -> Vec<u8> {
467        let mut writer = Writer::new();
468        writer.write_string(self.keytype());
469        match self.data {
470            Data::Rsa {
471                ref exponent,
472                ref modulus,
473            } => {
474                // the data for an rsa key consists of three pieces:
475                //    ssh-rsa public-exponent modulus
476                // see ssh-rsa format in https://tools.ietf.org/html/rfc4253#section-6.6
477                writer.write_mpint(exponent.clone());
478                writer.write_mpint(modulus.clone());
479            }
480            Data::Dsa {
481                ref p,
482                ref q,
483                ref g,
484                ref pub_key,
485            } => {
486                writer.write_mpint(p.clone());
487                writer.write_mpint(q.clone());
488                writer.write_mpint(g.clone());
489                writer.write_mpint(pub_key.clone());
490            }
491            Data::Ed25519 { ref key } => {
492                writer.write_bytes(key.clone());
493            }
494            Data::Ed25519Sk {
495                ref key,
496                ref application,
497            } => {
498                writer.write_bytes(key.clone());
499                writer.write_bytes(application.clone());
500            }
501            Data::Ecdsa { ref curve, ref key } => {
502                writer.write_string(curve.curvetype());
503                writer.write_bytes(key.clone());
504            }
505            Data::EcdsaSk {
506                ref curve,
507                ref key,
508                ref application,
509            } => {
510                writer.write_string(curve.curvetype());
511                writer.write_bytes(key.clone());
512                writer.write_bytes(application.clone());
513            }
514        }
515        writer.into_vec()
516    }
517
518    pub fn set_comment(&mut self, comment: &str) {
519        self.comment = Some(comment.to_string());
520    }
521
522    /// to_key_format returns a string representation of the ssh key. this string
523    /// output is appropriate to use as a public key file. it adheres to the
524    /// format described in https://tools.ietf.org/html/rfc4253#section-6.6
525    ///
526    /// an ssh key consists of four pieces:
527    ///
528    ///    [options] ssh-keytype data comment
529    ///
530    /// the output of the data section is described in the documentation for the
531    /// data function. the options section is optional, and is not part of the
532    /// spec. rather, it is a field present in authorized_keys files or
533    /// known_hosts files.
534    pub fn to_key_format(&self) -> String {
535        let key = format!(
536            "{} {} {}",
537            self.keytype(),
538            BASE64.encode(self.data()),
539            self.comment.clone().unwrap_or_default()
540        );
541        if let Some(ref options) = self.options {
542            format!("{} {}", options, key)
543        } else {
544            key
545        }
546    }
547
548    /// size returns the size of the stored ssh key. for rsa keys this is
549    /// determined by the number of bits in the modulus. for dsa keys it's the
550    /// number of bits in the prime p.
551    ///
552    /// see https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L261
553    /// for more details
554    pub fn size(&self) -> usize {
555        match self.data {
556            Data::Rsa { ref modulus, .. } => modulus.len() * 8,
557            Data::Dsa { ref p, .. } => p.len() * 8,
558            Data::Ed25519 { .. } | Data::Ed25519Sk { .. } => 256, // ??
559            Data::Ecdsa { ref curve, .. } | Data::EcdsaSk { ref curve, .. } => match *curve {
560                Curve::Nistp256 => 256,
561                Curve::Nistp384 => 384,
562                Curve::Nistp521 => 521,
563            },
564        }
565    }
566
567    /// fingerprint returns a string representing the fingerprint of the ssh key
568    /// the format of the fingerprint is described tersely in
569    /// https://tools.ietf.org/html/rfc4716#page-6. This uses the ssh-keygen
570    /// defaults of a base64 encoded SHA256 hash.
571    pub fn fingerprint(&self) -> String {
572        let data = self.data();
573        let mut hasher = Sha256::new();
574        hasher.update(&data);
575        let hashed = hasher.finalize();
576        let mut fingerprint = BASE64.encode(hashed);
577        // trim padding characters off the end. I'm not clear on exactly what
578        // this is doing but they do it here and the test fails without it
579        // https://github.com/openssh/openssh-portable/blob/643c2ad82910691b2240551ea8b14472f60b5078/sshkey.c#L918
580        if let Some(l) = fingerprint.find('=') {
581            fingerprint.truncate(l);
582        };
583        fingerprint
584    }
585
586    /// to_fingerprint_string prints out the fingerprint in the same format used
587    /// by `ssh-keygen -l -f key`, specifically the implementation here -
588    /// https://github.com/openssh/openssh-portable/blob/master/ssh-keygen.c#L842
589    /// right now it just sticks with the defaults of a base64 encoded SHA256
590    /// hash.
591    pub fn to_fingerprint_string(&self) -> String {
592        let keytype = match self.data {
593            Data::Rsa { .. } => "RSA",
594            Data::Dsa { .. } => "DSA",
595            Data::Ed25519 { .. } => "ED25519",
596            Data::Ed25519Sk { .. } => "ED25519_SK",
597            Data::Ecdsa { .. } => "ECDSA",
598            Data::EcdsaSk { .. } => "ECDSA_SK",
599        };
600
601        let comment = self
602            .comment
603            .clone()
604            .unwrap_or_else(|| "no comment".to_string());
605        format!(
606            "{} SHA256:{} {} ({})",
607            self.size(),
608            self.fingerprint(),
609            comment,
610            keytype
611        )
612    }
613
614    /// fingerprint_m5 returns a string representing the fingerprint of the ssh key
615    /// the format of the fingerprint is MD5, and the output looks like,
616    /// `fb:a0:5b:a0:21:01:47:33:3b:8d:9e:14:1a:4c:db:6d` .
617    pub fn fingerprint_md5(&self) -> String {
618        let mut sh = Md5::default();
619        sh.update(self.data());
620
621        let md5: Vec<String> = sh.finalize().iter().map(|n| format!("{:02x}", n)).collect();
622        md5.join(":")
623    }
624
625    /// to_fingerprint_m5_string prints out the fingerprint in the in hex format used
626    /// by `ssh-keygen -l -E md5 -f key`, and the output looks like,
627    /// `2048 MD5:fb:a0:5b:a0:21:01:47:33:3b:8d:9e:14:1a:4c:db:6d demos@anduin (RSA)` .
628    pub fn to_fingerprint_md5_string(&self) -> String {
629        let keytype = match self.data {
630            Data::Rsa { .. } => "RSA",
631            Data::Dsa { .. } => "DSA",
632            Data::Ed25519 { .. } => "ED25519",
633            Data::Ed25519Sk { .. } => "ED25519_SK",
634            Data::Ecdsa { .. } => "ECDSA",
635            Data::EcdsaSk { .. } => "ECDSA_SK",
636        };
637
638        let comment = self
639            .comment
640            .clone()
641            .unwrap_or_else(|| "no comment".to_string());
642        format!(
643            "{} MD5:{} {} ({})",
644            self.size(),
645            self.fingerprint_md5(),
646            comment,
647            keytype
648        )
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    const TEST_RSA_KEY: &str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCYH3vPUJThzriVlVKmKOg71EOVYm274oRa5KLWEoK0HmjMc9ru0j4ofouoeW/AVmRVujxfaIGR/8en/lUPkiv5DSeM6aXnDz5cExNptrAy/sMPLQhVALRrqQ+dkS9Ct/YA+A1Le5LPh4MJu79hCDLTwqSdKqDuUcYQzR0M7APslaDCR96zY+VUL4lKObUUd4wsP3opdTQ6G20qXEer14EPGr9N53S/u+JJGLoPlb1uPIH96oKY4t/SeLIRQsocdViRaiF/Aq7kPzWd/yCLVdXJSRt3CftboV4kLBHGteTS551J32MJoqjEi4Q/DucWYrQfx5H3qXVB+/G2HurKPIHL demos@siril";
657    const TEST_RSA_COMMENT_KEY: &str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCYH3vPUJThzriVlVKmKOg71EOVYm274oRa5KLWEoK0HmjMc9ru0j4ofouoeW/AVmRVujxfaIGR/8en/lUPkiv5DSeM6aXnDz5cExNptrAy/sMPLQhVALRrqQ+dkS9Ct/YA+A1Le5LPh4MJu79hCDLTwqSdKqDuUcYQzR0M7APslaDCR96zY+VUL4lKObUUd4wsP3opdTQ6G20qXEer14EPGr9N53S/u+JJGLoPlb1uPIH96oKY4t/SeLIRQsocdViRaiF/Aq7kPzWd/yCLVdXJSRt3CftboV4kLBHGteTS551J32MJoqjEi4Q/DucWYrQfx5H3qXVB+/G2HurKPIHL test";
658    const TEST_DSA_KEY: &str = "ssh-dss AAAAB3NzaC1kc3MAAACBAIkd9CkqldM2St8f53rfJT7kPgiA8leZaN7hdZd48hYJyKzVLoPdBMaGFuOwGjv0Im3JWqWAewANe0xeLceQL0rSFbM/mZV+1gc1nm1WmtVw4KJIlLXl3gS7NYfQ9Ith4wFnZd/xhRz9Q+MBsA1DgXew1zz4dLYI46KmFivJ7XDzAAAAFQC8z4VIhI4HlHTvB7FdwAfqWsvcOwAAAIBEqPIkW3HHDTSEhUhhV2AlIPNwI/bqaCXy2zYQ6iTT3oUh+N4xlRaBSvW+h2NC97U8cxd7Y0dXIbQKPzwNzRX1KA1F9WAuNzrx9KkpCg2TpqXShhp+Sseb+l6uJjthIYM6/0dvr9cBDMeExabPPgBo3Eii2NLbFSqIe86qav8hZAAAAIBk5AetZrG8varnzv1khkKh6Xq/nX9r1UgIOCQos2XOi2ErjlB9swYCzReo1RT7dalITVi7K9BtvJxbutQEOvN7JjJnPJs+M3OqRMMF+anXPdCWUIBxZUwctbkAD5joEjGDrNXHQEw9XixZ9p3wudbISnPFgZhS1sbS9Rlw5QogKg== demos@siril";
659    const TEST_ED25519_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril";
660    const TEST_ED25519_SK_KEY: &str = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIEX/dQ0v4127bEo8eeG1EV0ApO2lWbSnN6RWusn/NjqIAAAABHNzaDo= demos@siril";
661    const TEST_ECDSA256_KEY: &str = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIhfLQrww4DlhYzbSWXoX3ctOQ0jVosvfHfW+QWVotksbPzM2YgkIikTpoHUfZrYpJKWx7WYs5aqeLkdCDdk+jk= demos@siril";
662    const TEST_ECDSA_SK_KEY: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBDZ+f5tSRhlB7EN39f93SscTN5PUvbD3UQsNrlE1ZdbwPMMRul2zlPiUvwAvnJitW0jlD/vwZOW2YN+q+iZ5c0MAAAAEc3NoOg== demos@siril";
663
664    #[test]
665    fn rsa_parse_to_string() {
666        let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
667        let out = key.to_string();
668        assert_eq!(TEST_RSA_KEY, out);
669    }
670
671    #[test]
672    fn rsa_size() {
673        let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
674        assert_eq!(2048, key.size());
675    }
676
677    #[test]
678    fn rsa_keytype() {
679        let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
680        assert_eq!("ssh-rsa", key.keytype());
681    }
682
683    #[test]
684    fn rsa_fingerprint() {
685        let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
686        assert_eq!(
687            "YTw/JyJmeAAle1/7zuZkPP0C73BQ+6XrFEt2/Wy++2o",
688            key.fingerprint()
689        );
690    }
691
692    #[test]
693    fn rsa_fingerprint_string() {
694        let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
695        assert_eq!(
696            "2048 SHA256:YTw/JyJmeAAle1/7zuZkPP0C73BQ+6XrFEt2/Wy++2o demos@siril (RSA)",
697            key.to_fingerprint_string()
698        );
699    }
700
701    #[test]
702    fn rsa_fingerprint_md5() {
703        let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
704        assert_eq!(
705            "e9:a1:5b:cd:a3:69:d2:d9:17:cb:09:3e:78:e1:0d:dd",
706            key.fingerprint_md5()
707        );
708    }
709
710    #[test]
711    fn rsa_fingerprint_md5_string() {
712        let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
713        assert_eq!(
714            "2048 MD5:e9:a1:5b:cd:a3:69:d2:d9:17:cb:09:3e:78:e1:0d:dd demos@siril (RSA)",
715            key.to_fingerprint_md5_string()
716        );
717    }
718
719    #[test]
720    fn rsa_set_comment() {
721        let mut key = PublicKey::parse(TEST_RSA_KEY).unwrap();
722        key.set_comment("test");
723        let out = key.to_string();
724        assert_eq!(TEST_RSA_COMMENT_KEY, out);
725    }
726
727    #[test]
728    fn dsa_parse_to_string() {
729        let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
730        let out = key.to_string();
731        assert_eq!(TEST_DSA_KEY, out);
732    }
733
734    #[test]
735    fn dsa_size() {
736        let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
737        assert_eq!(1024, key.size());
738    }
739
740    #[test]
741    fn dsa_keytype() {
742        let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
743        assert_eq!("ssh-dss", key.keytype());
744    }
745
746    #[test]
747    fn dsa_fingerprint() {
748        let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
749        assert_eq!(
750            "/Pyxrjot1Hs5PN2Dpg/4pK2wxxtP9Igc3sDTAWIEXT4",
751            key.fingerprint()
752        );
753    }
754
755    #[test]
756    fn dsa_fingerprint_string() {
757        let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
758        assert_eq!(
759            "1024 SHA256:/Pyxrjot1Hs5PN2Dpg/4pK2wxxtP9Igc3sDTAWIEXT4 demos@siril (DSA)",
760            key.to_fingerprint_string()
761        );
762    }
763
764    #[test]
765    fn ed25519_parse_to_string() {
766        let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
767        let out = key.to_string();
768        assert_eq!(TEST_ED25519_KEY, out);
769    }
770
771    #[test]
772    fn ed25519_size() {
773        let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
774        assert_eq!(256, key.size());
775    }
776
777    #[test]
778    fn ed25519_keytype() {
779        let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
780        assert_eq!("ssh-ed25519", key.keytype());
781    }
782
783    #[test]
784    fn ed25519_fingerprint() {
785        let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
786        assert_eq!(
787            "A/lHzXxsgbp11dcKKfSDyNQIdep7EQgZEoRYVDBfNdI",
788            key.fingerprint()
789        );
790    }
791
792    #[test]
793    fn ed25519_fingerprint_string() {
794        let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
795        assert_eq!(
796            "256 SHA256:A/lHzXxsgbp11dcKKfSDyNQIdep7EQgZEoRYVDBfNdI demos@siril (ED25519)",
797            key.to_fingerprint_string()
798        );
799    }
800
801    #[test]
802    fn ed25519_sk_parse_to_string() {
803        let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
804        let out = key.to_string();
805        assert_eq!(TEST_ED25519_SK_KEY, out);
806    }
807
808    #[test]
809    fn ed25519_sk_size() {
810        let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
811        assert_eq!(256, key.size());
812    }
813
814    #[test]
815    fn ed25519_sk_keytype() {
816        let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
817        assert_eq!("sk-ssh-ed25519@openssh.com", key.keytype());
818    }
819
820    #[test]
821    fn ed25519_sk_fingerprint() {
822        let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
823        assert_eq!(
824            "U8IKRkIHed6vFMTflwweA3HhIf2DWgZ8EFTm9fgwOUk",
825            key.fingerprint()
826        );
827    }
828
829    #[test]
830    fn ed25519_sk_fingerprint_string() {
831        let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
832        assert_eq!(
833            "256 SHA256:U8IKRkIHed6vFMTflwweA3HhIf2DWgZ8EFTm9fgwOUk demos@siril (ED25519_SK)",
834            key.to_fingerprint_string()
835        );
836    }
837
838    #[test]
839    fn ecdsa256_parse_to_string() {
840        let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
841        let out = key.to_string();
842        assert_eq!(TEST_ECDSA256_KEY, out);
843    }
844
845    #[test]
846    fn ecdsa256_size() {
847        let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
848        assert_eq!(256, key.size());
849    }
850
851    #[test]
852    fn ecdsa256_keytype() {
853        let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
854        assert_eq!("ecdsa-sha2-nistp256", key.keytype());
855    }
856
857    #[test]
858    fn ecdsa256_fingerprint() {
859        let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
860        assert_eq!(
861            "BzS5YXMW/d2vFk8Oqh+nKmvKr8X/FTLBfJgDGLu5GAs",
862            key.fingerprint()
863        );
864    }
865
866    #[test]
867    fn ecdsa256_fingerprint_string() {
868        let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
869        assert_eq!(
870            "256 SHA256:BzS5YXMW/d2vFk8Oqh+nKmvKr8X/FTLBfJgDGLu5GAs demos@siril (ECDSA)",
871            key.to_fingerprint_string()
872        );
873    }
874
875    #[test]
876    fn ecdsa_sk_parse_to_string() {
877        let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
878        let out = key.to_string();
879        assert_eq!(TEST_ECDSA_SK_KEY, out);
880    }
881
882    #[test]
883    fn ecdsa_sk_size() {
884        let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
885        assert_eq!(256, key.size());
886    }
887
888    #[test]
889    fn ecdsa_sk_keytype() {
890        let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
891        assert_eq!("sk-ecdsa-sha2-nistp256@openssh.com", key.keytype());
892    }
893
894    #[test]
895    fn ecdsa_sk_fingerprint() {
896        let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
897        assert_eq!(
898            "N0sNKBgWKK8usPuPegtgzHQQA9vQ/dRhAEhwFDAnLA4",
899            key.fingerprint()
900        );
901    }
902
903    #[test]
904    fn ecdsa_sk_fingerprint_string() {
905        let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
906        assert_eq!(
907            "256 SHA256:N0sNKBgWKK8usPuPegtgzHQQA9vQ/dRhAEhwFDAnLA4 demos@siril (ECDSA_SK)",
908            key.to_fingerprint_string()
909        );
910    }
911
912    #[test]
913    fn option_parse() {
914        let key = PublicKey::parse("agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
915        assert_eq!(Some("agent-forwarding".into()), key.options);
916        assert_eq!("agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
917        let key = PublicKey::parse("from=\"*.sales.example.net,!pc.sales.example.net\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
918        assert_eq!(
919            Some("from=\"*.sales.example.net,!pc.sales.example.net\"".into()),
920            key.options
921        );
922        assert_eq!("from=\"*.sales.example.net,!pc.sales.example.net\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
923        let key = PublicKey::parse("permitopen=\"192.0.2.1:80\",permitopen=\"192.0.2.2:25\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
924        assert_eq!(
925            Some("permitopen=\"192.0.2.1:80\",permitopen=\"192.0.2.2:25\"".into()),
926            key.options
927        );
928        assert_eq!("permitopen=\"192.0.2.1:80\",permitopen=\"192.0.2.2:25\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
929        let key = PublicKey::parse("command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
930        assert_eq!(
931            Some("command=\"echo \\\"holy shell escaping batman\\\"\"".into()),
932            key.options
933        );
934        assert_eq!("command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
935        let key = PublicKey::parse("command=\"dump /home\",no-pty,no-port-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
936        assert_eq!(
937            Some("command=\"dump /home\",no-pty,no-port-forwarding".into()),
938            key.options
939        );
940        assert_eq!("command=\"dump /home\",no-pty,no-port-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
941    }
942
943    #[test]
944    fn hostname_parse() {
945        let key = PublicKey::parse("ec2-52-53-211-129.us-west-1.compute.amazonaws.com,52.53.211.129 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFHnC16I49ccjBo68lvN1+zpnAuTGbZjHFi2JRgPZK5o02UDCrFYCUhuS3oCh75+6YmVyReLZAyAM7S/5wjMzTY=").unwrap();
946        assert_eq!(
947            Some("ec2-52-53-211-129.us-west-1.compute.amazonaws.com,52.53.211.129".into()),
948            key.options
949        );
950        assert_eq!("ec2-52-53-211-129.us-west-1.compute.amazonaws.com,52.53.211.129 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFHnC16I49ccjBo68lvN1+zpnAuTGbZjHFi2JRgPZK5o02UDCrFYCUhuS3oCh75+6YmVyReLZAyAM7S/5wjMzTY=", key.to_string().trim());
951        let key = PublicKey::parse("[fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAopjUBQqif5ILeoMHjJ9wGlGs2eNHEv3+OAiiEDHCapNm3guNa+T/ZMtedaC/0P8bLBCXMiyNQU04N/IRyN3Mp/SGhtGJl1PDENXzPB9aoxsB2HHc8s8P7mxal1G4BtCT/fJM5XywEHWAcHkzW91iTK+ApAdqt6AHj35ogil9maFlUNKcXz2aW27hdbDtC0fautvWd9RIITHPq00rdvaHjRcc2msv8LddhBkStP8FrB39RPu9M+ikBkTwdQTSGcIBDYJgt3la2KMwmU1F81cq17wb21lPriBwr626lBiir/WdrBsoAsANeZfyzpAm8K4ssI3eu9eklxpEKdAdNRJbpQ==").unwrap();
952        assert_eq!(
953            Some("[fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090".into()),
954            key.options
955        );
956        assert_eq!("[fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAopjUBQqif5ILeoMHjJ9wGlGs2eNHEv3+OAiiEDHCapNm3guNa+T/ZMtedaC/0P8bLBCXMiyNQU04N/IRyN3Mp/SGhtGJl1PDENXzPB9aoxsB2HHc8s8P7mxal1G4BtCT/fJM5XywEHWAcHkzW91iTK+ApAdqt6AHj35ogil9maFlUNKcXz2aW27hdbDtC0fautvWd9RIITHPq00rdvaHjRcc2msv8LddhBkStP8FrB39RPu9M+ikBkTwdQTSGcIBDYJgt3la2KMwmU1F81cq17wb21lPriBwr626lBiir/WdrBsoAsANeZfyzpAm8K4ssI3eu9eklxpEKdAdNRJbpQ==", key.to_string().trim());
957        let key = PublicKey::parse("@revoked [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAopjUBQqif5ILeoMHjJ9wGlGs2eNHEv3+OAiiEDHCapNm3guNa+T/ZMtedaC/0P8bLBCXMiyNQU04N/IRyN3Mp/SGhtGJl1PDENXzPB9aoxsB2HHc8s8P7mxal1G4BtCT/fJM5XywEHWAcHkzW91iTK+ApAdqt6AHj35ogil9maFlUNKcXz2aW27hdbDtC0fautvWd9RIITHPq00rdvaHjRcc2msv8LddhBkStP8FrB39RPu9M+ikBkTwdQTSGcIBDYJgt3la2KMwmU1F81cq17wb21lPriBwr626lBiir/WdrBsoAsANeZfyzpAm8K4ssI3eu9eklxpEKdAdNRJbpQ==").unwrap();
958        assert_eq!(
959            Some("@revoked [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090".into()),
960            key.options
961        );
962        assert_eq!("@revoked [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAopjUBQqif5ILeoMHjJ9wGlGs2eNHEv3+OAiiEDHCapNm3guNa+T/ZMtedaC/0P8bLBCXMiyNQU04N/IRyN3Mp/SGhtGJl1PDENXzPB9aoxsB2HHc8s8P7mxal1G4BtCT/fJM5XywEHWAcHkzW91iTK+ApAdqt6AHj35ogil9maFlUNKcXz2aW27hdbDtC0fautvWd9RIITHPq00rdvaHjRcc2msv8LddhBkStP8FrB39RPu9M+ikBkTwdQTSGcIBDYJgt3la2KMwmU1F81cq17wb21lPriBwr626lBiir/WdrBsoAsANeZfyzpAm8K4ssI3eu9eklxpEKdAdNRJbpQ==", key.to_string().trim());
963        let key = PublicKey::parse("@cert-authority [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAopjUBQqif5ILeoMHjJ9wGlGs2eNHEv3+OAiiEDHCapNm3guNa+T/ZMtedaC/0P8bLBCXMiyNQU04N/IRyN3Mp/SGhtGJl1PDENXzPB9aoxsB2HHc8s8P7mxal1G4BtCT/fJM5XywEHWAcHkzW91iTK+ApAdqt6AHj35ogil9maFlUNKcXz2aW27hdbDtC0fautvWd9RIITHPq00rdvaHjRcc2msv8LddhBkStP8FrB39RPu9M+ikBkTwdQTSGcIBDYJgt3la2KMwmU1F81cq17wb21lPriBwr626lBiir/WdrBsoAsANeZfyzpAm8K4ssI3eu9eklxpEKdAdNRJbpQ==").unwrap();
964        assert_eq!(
965            Some("@cert-authority [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090".into()),
966            key.options
967        );
968        assert_eq!("@cert-authority [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAopjUBQqif5ILeoMHjJ9wGlGs2eNHEv3+OAiiEDHCapNm3guNa+T/ZMtedaC/0P8bLBCXMiyNQU04N/IRyN3Mp/SGhtGJl1PDENXzPB9aoxsB2HHc8s8P7mxal1G4BtCT/fJM5XywEHWAcHkzW91iTK+ApAdqt6AHj35ogil9maFlUNKcXz2aW27hdbDtC0fautvWd9RIITHPq00rdvaHjRcc2msv8LddhBkStP8FrB39RPu9M+ikBkTwdQTSGcIBDYJgt3la2KMwmU1F81cq17wb21lPriBwr626lBiir/WdrBsoAsANeZfyzpAm8K4ssI3eu9eklxpEKdAdNRJbpQ==", key.to_string().trim());
969    }
970
971    #[test]
972    fn read_keys() {
973        let authorized_keys = "# authorized keys
974
975command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril
976agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril
977
978
979
980
981ssh-dss AAAAB3NzaC1kc3MAAACBAIkd9CkqldM2St8f53rfJT7kPgiA8leZaN7hdZd48hYJyKzVLoPdBMaGFuOwGjv0Im3JWqWAewANe0xeLceQL0rSFbM/mZV+1gc1nm1WmtVw4KJIlLXl3gS7NYfQ9Ith4wFnZd/xhRz9Q+MBsA1DgXew1zz4dLYI46KmFivJ7XDzAAAAFQC8z4VIhI4HlHTvB7FdwAfqWsvcOwAAAIBEqPIkW3HHDTSEhUhhV2AlIPNwI/bqaCXy2zYQ6iTT3oUh+N4xlRaBSvW+h2NC97U8cxd7Y0dXIbQKPzwNzRX1KA1F9WAuNzrx9KkpCg2TpqXShhp+Sseb+l6uJjthIYM6/0dvr9cBDMeExabPPgBo3Eii2NLbFSqIe86qav8hZAAAAIBk5AetZrG8varnzv1khkKh6Xq/nX9r1UgIOCQos2XOi2ErjlB9swYCzReo1RT7dalITVi7K9BtvJxbutQEOvN7JjJnPJs+M3OqRMMF+anXPdCWUIBxZUwctbkAD5joEjGDrNXHQEw9XixZ9p3wudbISnPFgZhS1sbS9Rlw5QogKg==
982";
983        let key1 = "command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril";
984        let key2 = "agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril";
985        let key3 = "ssh-dss AAAAB3NzaC1kc3MAAACBAIkd9CkqldM2St8f53rfJT7kPgiA8leZaN7hdZd48hYJyKzVLoPdBMaGFuOwGjv0Im3JWqWAewANe0xeLceQL0rSFbM/mZV+1gc1nm1WmtVw4KJIlLXl3gS7NYfQ9Ith4wFnZd/xhRz9Q+MBsA1DgXew1zz4dLYI46KmFivJ7XDzAAAAFQC8z4VIhI4HlHTvB7FdwAfqWsvcOwAAAIBEqPIkW3HHDTSEhUhhV2AlIPNwI/bqaCXy2zYQ6iTT3oUh+N4xlRaBSvW+h2NC97U8cxd7Y0dXIbQKPzwNzRX1KA1F9WAuNzrx9KkpCg2TpqXShhp+Sseb+l6uJjthIYM6/0dvr9cBDMeExabPPgBo3Eii2NLbFSqIe86qav8hZAAAAIBk5AetZrG8varnzv1khkKh6Xq/nX9r1UgIOCQos2XOi2ErjlB9swYCzReo1RT7dalITVi7K9BtvJxbutQEOvN7JjJnPJs+M3OqRMMF+anXPdCWUIBxZUwctbkAD5joEjGDrNXHQEw9XixZ9p3wudbISnPFgZhS1sbS9Rlw5QogKg== ";
986        let keys = PublicKey::read_keys(authorized_keys.as_bytes()).unwrap();
987        assert_eq!(key1, keys[0].to_string());
988        assert_eq!(key2, keys[1].to_string());
989        assert_eq!(key3, keys[2].to_string());
990    }
991
992    #[test]
993    fn comment_should_be_none_when_absent() {
994        let key =
995            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/";
996        let key = PublicKey::parse(key).unwrap();
997        assert!(key.comment.is_none());
998    }
999
1000    #[test]
1001    fn comment_should_be_none_when_empty_string() {
1002        let key =
1003            "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/    ";
1004        let key = PublicKey::parse(key).unwrap();
1005        assert!(key.comment.is_none());
1006    }
1007
1008    #[test]
1009    fn comment_should_preserve_special_characters() {
1010        let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ !@#$%^&*()_+-={}|[]\\:\";'<>?,./";
1011        let key = PublicKey::parse(key).unwrap();
1012        assert_eq!(key.comment.unwrap(), "!@#$%^&*()_+-={}|[]\\:\";'<>?,./");
1013    }
1014
1015    #[test]
1016    fn comment_should_preserve_multiple_spaces() {
1017        let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ comment with multiple   spaces";
1018        let key = PublicKey::parse(key).unwrap();
1019        assert_eq!(key.comment.unwrap(), "comment with multiple   spaces");
1020    }
1021
1022    #[test]
1023    fn comment_should_remove_leading_and_trailing_spaces_while_keeping_body_intact() {
1024        let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/   leading and trailing   spaces are trimmed   ";
1025        let key = PublicKey::parse(key).unwrap();
1026        assert_eq!(
1027            key.comment.unwrap(),
1028            "leading and trailing   spaces are trimmed"
1029        );
1030    }
1031
1032    #[test]
1033    fn comment_should_not_preserve_newlines() {
1034        let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ comment with\nnewlines";
1035        let key = PublicKey::parse(key);
1036        assert!(key.is_err());
1037    }
1038
1039    #[test]
1040    fn comment_should_preserve_mixed_whitespace() {
1041        let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ mixed white\t space";
1042        let key = PublicKey::parse(key).unwrap();
1043        assert_eq!(key.comment.unwrap(), "mixed white\t space");
1044    }
1045
1046    #[test]
1047    fn comment_should_preserve_unicode_characters() {
1048        let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ comment with unicode: 中文, русский, عربى";
1049        let key = PublicKey::parse(key).unwrap();
1050        assert_eq!(
1051            key.comment.unwrap(),
1052            "comment with unicode: 中文, русский, عربى"
1053        );
1054    }
1055}