1mod 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 #[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#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
94pub enum Curve {
95 Nistp256,
96 Nistp384,
97 Nistp521,
98}
99
100impl Curve {
101 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 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#[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#[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
180impl 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 pub fn parse(key: &str) -> Result<Self> {
228 let key = key.trim();
230 PublicKey::try_key_parse(key).or_else(|e| {
232 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 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 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 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 let key = reader.read_bytes()?;
338 Data::Ed25519 { key: key.into() }
339 }
340 SSH_ED25519_SK => {
341 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 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 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 pub fn read_keys<R>(r: R) -> Result<Vec<Self>>
402 where
403 R: Read,
404 {
405 let keybuf = BufReader::new(r);
406 let mut keys = vec![];
408 for key in keybuf.lines() {
409 let key = key?;
410 if !key.is_empty() && !(key.trim().starts_with('#')) {
412 keys.push(PublicKey::parse(&key)?);
413 }
414 }
415 Ok(keys)
416 }
417
418 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 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 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 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 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 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 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, 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 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 if let Some(l) = fingerprint.find('=') {
581 fingerprint.truncate(l);
582 };
583 fingerprint
584 }
585
586 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 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 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}