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 mut parts = key.split_whitespace();
268 let keytype = parts.next().ok_or(OpenSSHKeyError::InvalidFormat)?;
269 let data = parts.next().ok_or(OpenSSHKeyError::InvalidFormat)?;
270 let comment = parts.next().and_then(|c| {
273 if c.is_empty() {
274 None
275 } else {
276 Some(c.to_string())
277 }
278 });
279
280 let buf = BASE64
281 .decode(data)
282 .map_err(|e| OpenSSHKeyError::InvalidBase64 {
283 detail: format!("{}", e),
284 })?;
285 let mut reader = Reader::new(&buf);
286 let data_keytype = reader.read_string()?;
287 if keytype != data_keytype {
288 return Err(OpenSSHKeyError::InvalidFormat);
289 }
290
291 let data = match keytype {
292 SSH_RSA => {
293 let e = reader.read_mpint()?;
297 let n = reader.read_mpint()?;
298 Data::Rsa {
299 exponent: e.into(),
300 modulus: n.into(),
301 }
302 }
303 SSH_DSA => {
304 let p = reader.read_mpint()?;
314 let q = reader.read_mpint()?;
315 let g = reader.read_mpint()?;
316 let pub_key = reader.read_mpint()?;
317 Data::Dsa {
318 p: p.into(),
319 q: q.into(),
320 g: g.into(),
321 pub_key: pub_key.into(),
322 }
323 }
324 SSH_ED25519 => {
325 let key = reader.read_bytes()?;
333 Data::Ed25519 { key: key.into() }
334 }
335 SSH_ED25519_SK => {
336 let key = reader.read_bytes()?;
338 let application = reader.read_bytes()?;
339 Data::Ed25519Sk {
340 key: key.into(),
341 application: application.into(),
342 }
343 }
344 SSH_ECDSA_256 | SSH_ECDSA_384 | SSH_ECDSA_521 => {
345 let curve = reader.read_string()?;
362 let key = reader.read_bytes()?;
363 Data::Ecdsa {
364 curve: Curve::get(curve)?,
365 key: key.into(),
366 }
367 }
368 SSH_ECDSA_SK => {
369 let curve = reader.read_string()?;
371 let key = reader.read_bytes()?;
372 let application = reader.read_bytes()?;
373 Data::EcdsaSk {
374 curve: Curve::get(curve)?,
375 key: key.into(),
376 application: application.into(),
377 }
378 }
379 _ => {
380 return Err(OpenSSHKeyError::UnsupportedKeyType {
381 keytype: keytype.to_string(),
382 })
383 }
384 };
385
386 Ok(PublicKey {
387 options: None,
388 data,
389 comment,
390 })
391 }
392
393 pub fn read_keys<R>(r: R) -> Result<Vec<Self>>
397 where
398 R: Read,
399 {
400 let keybuf = BufReader::new(r);
401 let mut keys = vec![];
403 for key in keybuf.lines() {
404 let key = key?;
405 if !key.is_empty() && !(key.trim().starts_with('#')) {
407 keys.push(PublicKey::parse(&key)?);
408 }
409 }
410 Ok(keys)
411 }
412
413 pub fn from_rsa(e: Vec<u8>, n: Vec<u8>) -> Self {
415 PublicKey {
416 options: None,
417 data: Data::Rsa {
418 exponent: e,
419 modulus: n,
420 },
421 comment: None,
422 }
423 }
424
425 pub fn from_dsa(p: Vec<u8>, q: Vec<u8>, g: Vec<u8>, pkey: Vec<u8>) -> Self {
427 PublicKey {
428 options: None,
429 data: Data::Dsa {
430 p,
431 q,
432 g,
433 pub_key: pkey,
434 },
435 comment: None,
436 }
437 }
438
439 pub fn keytype(&self) -> &'static str {
442 match self.data {
443 Data::Rsa { .. } => SSH_RSA,
444 Data::Dsa { .. } => SSH_DSA,
445 Data::Ed25519 { .. } => SSH_ED25519,
446 Data::Ed25519Sk { .. } => SSH_ED25519_SK,
447 Data::Ecdsa { ref curve, .. } => match *curve {
448 Curve::Nistp256 => SSH_ECDSA_256,
449 Curve::Nistp384 => SSH_ECDSA_384,
450 Curve::Nistp521 => SSH_ECDSA_521,
451 },
452 Data::EcdsaSk { .. } => SSH_ECDSA_SK,
453 }
454 }
455
456 pub fn data(&self) -> Vec<u8> {
462 let mut writer = Writer::new();
463 writer.write_string(self.keytype());
464 match self.data {
465 Data::Rsa {
466 ref exponent,
467 ref modulus,
468 } => {
469 writer.write_mpint(exponent.clone());
473 writer.write_mpint(modulus.clone());
474 }
475 Data::Dsa {
476 ref p,
477 ref q,
478 ref g,
479 ref pub_key,
480 } => {
481 writer.write_mpint(p.clone());
482 writer.write_mpint(q.clone());
483 writer.write_mpint(g.clone());
484 writer.write_mpint(pub_key.clone());
485 }
486 Data::Ed25519 { ref key } => {
487 writer.write_bytes(key.clone());
488 }
489 Data::Ed25519Sk {
490 ref key,
491 ref application,
492 } => {
493 writer.write_bytes(key.clone());
494 writer.write_bytes(application.clone());
495 }
496 Data::Ecdsa { ref curve, ref key } => {
497 writer.write_string(curve.curvetype());
498 writer.write_bytes(key.clone());
499 }
500 Data::EcdsaSk {
501 ref curve,
502 ref key,
503 ref application,
504 } => {
505 writer.write_string(curve.curvetype());
506 writer.write_bytes(key.clone());
507 writer.write_bytes(application.clone());
508 }
509 }
510 writer.into_vec()
511 }
512
513 pub fn set_comment(&mut self, comment: &str) {
514 self.comment = Some(comment.to_string());
515 }
516
517 pub fn to_key_format(&self) -> String {
530 let key = format!(
531 "{} {} {}",
532 self.keytype(),
533 BASE64.encode(self.data()),
534 self.comment.clone().unwrap_or_default()
535 );
536 if let Some(ref options) = self.options {
537 format!("{} {}", options, key)
538 } else {
539 key
540 }
541 }
542
543 pub fn size(&self) -> usize {
550 match self.data {
551 Data::Rsa { ref modulus, .. } => modulus.len() * 8,
552 Data::Dsa { ref p, .. } => p.len() * 8,
553 Data::Ed25519 { .. } | Data::Ed25519Sk { .. } => 256, Data::Ecdsa { ref curve, .. } | Data::EcdsaSk { ref curve, .. } => match *curve {
555 Curve::Nistp256 => 256,
556 Curve::Nistp384 => 384,
557 Curve::Nistp521 => 521,
558 },
559 }
560 }
561
562 pub fn fingerprint(&self) -> String {
567 let data = self.data();
568 let mut hasher = Sha256::new();
569 hasher.update(&data);
570 let hashed = hasher.finalize();
571 let mut fingerprint = BASE64.encode(hashed);
572 if let Some(l) = fingerprint.find('=') {
576 fingerprint.truncate(l);
577 };
578 fingerprint
579 }
580
581 pub fn to_fingerprint_string(&self) -> String {
587 let keytype = match self.data {
588 Data::Rsa { .. } => "RSA",
589 Data::Dsa { .. } => "DSA",
590 Data::Ed25519 { .. } => "ED25519",
591 Data::Ed25519Sk { .. } => "ED25519_SK",
592 Data::Ecdsa { .. } => "ECDSA",
593 Data::EcdsaSk { .. } => "ECDSA_SK",
594 };
595
596 let comment = self
597 .comment
598 .clone()
599 .unwrap_or_else(|| "no comment".to_string());
600 format!(
601 "{} SHA256:{} {} ({})",
602 self.size(),
603 self.fingerprint(),
604 comment,
605 keytype
606 )
607 }
608
609 pub fn fingerprint_md5(&self) -> String {
613 let mut sh = Md5::default();
614 sh.update(&self.data());
615
616 let md5: Vec<String> = sh.finalize().iter().map(|n| format!("{:02x}", n)).collect();
617 md5.join(":")
618 }
619
620 pub fn to_fingerprint_md5_string(&self) -> String {
624 let keytype = match self.data {
625 Data::Rsa { .. } => "RSA",
626 Data::Dsa { .. } => "DSA",
627 Data::Ed25519 { .. } => "ED25519",
628 Data::Ed25519Sk { .. } => "ED25519_SK",
629 Data::Ecdsa { .. } => "ECDSA",
630 Data::EcdsaSk { .. } => "ECDSA_SK",
631 };
632
633 let comment = self
634 .comment
635 .clone()
636 .unwrap_or_else(|| "no comment".to_string());
637 format!(
638 "{} MD5:{} {} ({})",
639 self.size(),
640 self.fingerprint_md5(),
641 comment,
642 keytype
643 )
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 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";
652 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";
653 const TEST_DSA_KEY: &str = "ssh-dss AAAAB3NzaC1kc3MAAACBAIkd9CkqldM2St8f53rfJT7kPgiA8leZaN7hdZd48hYJyKzVLoPdBMaGFuOwGjv0Im3JWqWAewANe0xeLceQL0rSFbM/mZV+1gc1nm1WmtVw4KJIlLXl3gS7NYfQ9Ith4wFnZd/xhRz9Q+MBsA1DgXew1zz4dLYI46KmFivJ7XDzAAAAFQC8z4VIhI4HlHTvB7FdwAfqWsvcOwAAAIBEqPIkW3HHDTSEhUhhV2AlIPNwI/bqaCXy2zYQ6iTT3oUh+N4xlRaBSvW+h2NC97U8cxd7Y0dXIbQKPzwNzRX1KA1F9WAuNzrx9KkpCg2TpqXShhp+Sseb+l6uJjthIYM6/0dvr9cBDMeExabPPgBo3Eii2NLbFSqIe86qav8hZAAAAIBk5AetZrG8varnzv1khkKh6Xq/nX9r1UgIOCQos2XOi2ErjlB9swYCzReo1RT7dalITVi7K9BtvJxbutQEOvN7JjJnPJs+M3OqRMMF+anXPdCWUIBxZUwctbkAD5joEjGDrNXHQEw9XixZ9p3wudbISnPFgZhS1sbS9Rlw5QogKg== demos@siril";
654 const TEST_ED25519_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril";
655 const TEST_ED25519_SK_KEY: &str = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIEX/dQ0v4127bEo8eeG1EV0ApO2lWbSnN6RWusn/NjqIAAAABHNzaDo= demos@siril";
656 const TEST_ECDSA256_KEY: &str = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIhfLQrww4DlhYzbSWXoX3ctOQ0jVosvfHfW+QWVotksbPzM2YgkIikTpoHUfZrYpJKWx7WYs5aqeLkdCDdk+jk= demos@siril";
657 const TEST_ECDSA_SK_KEY: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBDZ+f5tSRhlB7EN39f93SscTN5PUvbD3UQsNrlE1ZdbwPMMRul2zlPiUvwAvnJitW0jlD/vwZOW2YN+q+iZ5c0MAAAAEc3NoOg== demos@siril";
658
659 #[test]
660 fn rsa_parse_to_string() {
661 let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
662 let out = key.to_string();
663 assert_eq!(TEST_RSA_KEY, out);
664 }
665
666 #[test]
667 fn rsa_size() {
668 let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
669 assert_eq!(2048, key.size());
670 }
671
672 #[test]
673 fn rsa_keytype() {
674 let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
675 assert_eq!("ssh-rsa", key.keytype());
676 }
677
678 #[test]
679 fn rsa_fingerprint() {
680 let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
681 assert_eq!(
682 "YTw/JyJmeAAle1/7zuZkPP0C73BQ+6XrFEt2/Wy++2o",
683 key.fingerprint()
684 );
685 }
686
687 #[test]
688 fn rsa_fingerprint_string() {
689 let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
690 assert_eq!(
691 "2048 SHA256:YTw/JyJmeAAle1/7zuZkPP0C73BQ+6XrFEt2/Wy++2o demos@siril (RSA)",
692 key.to_fingerprint_string()
693 );
694 }
695
696 #[test]
697 fn rsa_fingerprint_md5() {
698 let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
699 assert_eq!(
700 "e9:a1:5b:cd:a3:69:d2:d9:17:cb:09:3e:78:e1:0d:dd",
701 key.fingerprint_md5()
702 );
703 }
704
705 #[test]
706 fn rsa_fingerprint_md5_string() {
707 let key = PublicKey::parse(TEST_RSA_KEY).unwrap();
708 assert_eq!(
709 "2048 MD5:e9:a1:5b:cd:a3:69:d2:d9:17:cb:09:3e:78:e1:0d:dd demos@siril (RSA)",
710 key.to_fingerprint_md5_string()
711 );
712 }
713
714 #[test]
715 fn rsa_set_comment() {
716 let mut key = PublicKey::parse(TEST_RSA_KEY).unwrap();
717 key.set_comment("test");
718 let out = key.to_string();
719 assert_eq!(TEST_RSA_COMMENT_KEY, out);
720 }
721
722 #[test]
723 fn dsa_parse_to_string() {
724 let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
725 let out = key.to_string();
726 assert_eq!(TEST_DSA_KEY, out);
727 }
728
729 #[test]
730 fn dsa_size() {
731 let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
732 assert_eq!(1024, key.size());
733 }
734
735 #[test]
736 fn dsa_keytype() {
737 let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
738 assert_eq!("ssh-dss", key.keytype());
739 }
740
741 #[test]
742 fn dsa_fingerprint() {
743 let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
744 assert_eq!(
745 "/Pyxrjot1Hs5PN2Dpg/4pK2wxxtP9Igc3sDTAWIEXT4",
746 key.fingerprint()
747 );
748 }
749
750 #[test]
751 fn dsa_fingerprint_string() {
752 let key = PublicKey::parse(TEST_DSA_KEY).unwrap();
753 assert_eq!(
754 "1024 SHA256:/Pyxrjot1Hs5PN2Dpg/4pK2wxxtP9Igc3sDTAWIEXT4 demos@siril (DSA)",
755 key.to_fingerprint_string()
756 );
757 }
758
759 #[test]
760 fn ed25519_parse_to_string() {
761 let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
762 let out = key.to_string();
763 assert_eq!(TEST_ED25519_KEY, out);
764 }
765
766 #[test]
767 fn ed25519_size() {
768 let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
769 assert_eq!(256, key.size());
770 }
771
772 #[test]
773 fn ed25519_keytype() {
774 let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
775 assert_eq!("ssh-ed25519", key.keytype());
776 }
777
778 #[test]
779 fn ed25519_fingerprint() {
780 let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
781 assert_eq!(
782 "A/lHzXxsgbp11dcKKfSDyNQIdep7EQgZEoRYVDBfNdI",
783 key.fingerprint()
784 );
785 }
786
787 #[test]
788 fn ed25519_fingerprint_string() {
789 let key = PublicKey::parse(TEST_ED25519_KEY).unwrap();
790 assert_eq!(
791 "256 SHA256:A/lHzXxsgbp11dcKKfSDyNQIdep7EQgZEoRYVDBfNdI demos@siril (ED25519)",
792 key.to_fingerprint_string()
793 );
794 }
795
796 #[test]
797 fn ed25519_sk_parse_to_string() {
798 let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
799 let out = key.to_string();
800 assert_eq!(TEST_ED25519_SK_KEY, out);
801 }
802
803 #[test]
804 fn ed25519_sk_size() {
805 let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
806 assert_eq!(256, key.size());
807 }
808
809 #[test]
810 fn ed25519_sk_keytype() {
811 let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
812 assert_eq!("sk-ssh-ed25519@openssh.com", key.keytype());
813 }
814
815 #[test]
816 fn ed25519_sk_fingerprint() {
817 let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
818 assert_eq!(
819 "U8IKRkIHed6vFMTflwweA3HhIf2DWgZ8EFTm9fgwOUk",
820 key.fingerprint()
821 );
822 }
823
824 #[test]
825 fn ed25519_sk_fingerprint_string() {
826 let key = PublicKey::parse(TEST_ED25519_SK_KEY).unwrap();
827 assert_eq!(
828 "256 SHA256:U8IKRkIHed6vFMTflwweA3HhIf2DWgZ8EFTm9fgwOUk demos@siril (ED25519_SK)",
829 key.to_fingerprint_string()
830 );
831 }
832
833 #[test]
834 fn ecdsa256_parse_to_string() {
835 let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
836 let out = key.to_string();
837 assert_eq!(TEST_ECDSA256_KEY, out);
838 }
839
840 #[test]
841 fn ecdsa256_size() {
842 let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
843 assert_eq!(256, key.size());
844 }
845
846 #[test]
847 fn ecdsa256_keytype() {
848 let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
849 assert_eq!("ecdsa-sha2-nistp256", key.keytype());
850 }
851
852 #[test]
853 fn ecdsa256_fingerprint() {
854 let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
855 assert_eq!(
856 "BzS5YXMW/d2vFk8Oqh+nKmvKr8X/FTLBfJgDGLu5GAs",
857 key.fingerprint()
858 );
859 }
860
861 #[test]
862 fn ecdsa256_fingerprint_string() {
863 let key = PublicKey::parse(TEST_ECDSA256_KEY).unwrap();
864 assert_eq!(
865 "256 SHA256:BzS5YXMW/d2vFk8Oqh+nKmvKr8X/FTLBfJgDGLu5GAs demos@siril (ECDSA)",
866 key.to_fingerprint_string()
867 );
868 }
869
870 #[test]
871 fn ecdsa_sk_parse_to_string() {
872 let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
873 let out = key.to_string();
874 assert_eq!(TEST_ECDSA_SK_KEY, out);
875 }
876
877 #[test]
878 fn ecdsa_sk_size() {
879 let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
880 assert_eq!(256, key.size());
881 }
882
883 #[test]
884 fn ecdsa_sk_keytype() {
885 let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
886 assert_eq!("sk-ecdsa-sha2-nistp256@openssh.com", key.keytype());
887 }
888
889 #[test]
890 fn ecdsa_sk_fingerprint() {
891 let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
892 assert_eq!(
893 "N0sNKBgWKK8usPuPegtgzHQQA9vQ/dRhAEhwFDAnLA4",
894 key.fingerprint()
895 );
896 }
897
898 #[test]
899 fn ecdsa_sk_fingerprint_string() {
900 let key = PublicKey::parse(TEST_ECDSA_SK_KEY).unwrap();
901 assert_eq!(
902 "256 SHA256:N0sNKBgWKK8usPuPegtgzHQQA9vQ/dRhAEhwFDAnLA4 demos@siril (ECDSA_SK)",
903 key.to_fingerprint_string()
904 );
905 }
906
907 #[test]
908 fn option_parse() {
909 let key = PublicKey::parse("agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
910 assert_eq!(Some("agent-forwarding".into()), key.options);
911 assert_eq!("agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
912 let key = PublicKey::parse("from=\"*.sales.example.net,!pc.sales.example.net\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
913 assert_eq!(
914 Some("from=\"*.sales.example.net,!pc.sales.example.net\"".into()),
915 key.options
916 );
917 assert_eq!("from=\"*.sales.example.net,!pc.sales.example.net\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
918 let key = PublicKey::parse("permitopen=\"192.0.2.1:80\",permitopen=\"192.0.2.2:25\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
919 assert_eq!(
920 Some("permitopen=\"192.0.2.1:80\",permitopen=\"192.0.2.2:25\"".into()),
921 key.options
922 );
923 assert_eq!("permitopen=\"192.0.2.1:80\",permitopen=\"192.0.2.2:25\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
924 let key = PublicKey::parse("command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
925 assert_eq!(
926 Some("command=\"echo \\\"holy shell escaping batman\\\"\"".into()),
927 key.options
928 );
929 assert_eq!("command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
930 let key = PublicKey::parse("command=\"dump /home\",no-pty,no-port-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril").unwrap();
931 assert_eq!(
932 Some("command=\"dump /home\",no-pty,no-port-forwarding".into()),
933 key.options
934 );
935 assert_eq!("command=\"dump /home\",no-pty,no-port-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril", key.to_string());
936 }
937
938 #[test]
939 fn hostname_parse() {
940 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();
941 assert_eq!(
942 Some("ec2-52-53-211-129.us-west-1.compute.amazonaws.com,52.53.211.129".into()),
943 key.options
944 );
945 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());
946 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();
947 assert_eq!(
948 Some("[fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090".into()),
949 key.options
950 );
951 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());
952 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();
953 assert_eq!(
954 Some("@revoked [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090".into()),
955 key.options
956 );
957 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());
958 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();
959 assert_eq!(
960 Some("@cert-authority [fangorn.csh.rit.edu]:9090,[129.21.50.131]:9090".into()),
961 key.options
962 );
963 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());
964 }
965
966 #[test]
967 fn read_keys() {
968 let authorized_keys = "# authorized keys
969
970command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril
971agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril
972
973
974
975
976ssh-dss AAAAB3NzaC1kc3MAAACBAIkd9CkqldM2St8f53rfJT7kPgiA8leZaN7hdZd48hYJyKzVLoPdBMaGFuOwGjv0Im3JWqWAewANe0xeLceQL0rSFbM/mZV+1gc1nm1WmtVw4KJIlLXl3gS7NYfQ9Ith4wFnZd/xhRz9Q+MBsA1DgXew1zz4dLYI46KmFivJ7XDzAAAAFQC8z4VIhI4HlHTvB7FdwAfqWsvcOwAAAIBEqPIkW3HHDTSEhUhhV2AlIPNwI/bqaCXy2zYQ6iTT3oUh+N4xlRaBSvW+h2NC97U8cxd7Y0dXIbQKPzwNzRX1KA1F9WAuNzrx9KkpCg2TpqXShhp+Sseb+l6uJjthIYM6/0dvr9cBDMeExabPPgBo3Eii2NLbFSqIe86qav8hZAAAAIBk5AetZrG8varnzv1khkKh6Xq/nX9r1UgIOCQos2XOi2ErjlB9swYCzReo1RT7dalITVi7K9BtvJxbutQEOvN7JjJnPJs+M3OqRMMF+anXPdCWUIBxZUwctbkAD5joEjGDrNXHQEw9XixZ9p3wudbISnPFgZhS1sbS9Rlw5QogKg==
977";
978 let key1 = "command=\"echo \\\"holy shell escaping batman\\\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril";
979 let key2 = "agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhBr6++FQXB8kkgOMbdxBuyrHzuX5HkElswrN6DQoN/ demos@siril";
980 let key3 = "ssh-dss AAAAB3NzaC1kc3MAAACBAIkd9CkqldM2St8f53rfJT7kPgiA8leZaN7hdZd48hYJyKzVLoPdBMaGFuOwGjv0Im3JWqWAewANe0xeLceQL0rSFbM/mZV+1gc1nm1WmtVw4KJIlLXl3gS7NYfQ9Ith4wFnZd/xhRz9Q+MBsA1DgXew1zz4dLYI46KmFivJ7XDzAAAAFQC8z4VIhI4HlHTvB7FdwAfqWsvcOwAAAIBEqPIkW3HHDTSEhUhhV2AlIPNwI/bqaCXy2zYQ6iTT3oUh+N4xlRaBSvW+h2NC97U8cxd7Y0dXIbQKPzwNzRX1KA1F9WAuNzrx9KkpCg2TpqXShhp+Sseb+l6uJjthIYM6/0dvr9cBDMeExabPPgBo3Eii2NLbFSqIe86qav8hZAAAAIBk5AetZrG8varnzv1khkKh6Xq/nX9r1UgIOCQos2XOi2ErjlB9swYCzReo1RT7dalITVi7K9BtvJxbutQEOvN7JjJnPJs+M3OqRMMF+anXPdCWUIBxZUwctbkAD5joEjGDrNXHQEw9XixZ9p3wudbISnPFgZhS1sbS9Rlw5QogKg== ";
981 let keys = PublicKey::read_keys(authorized_keys.as_bytes()).unwrap();
982 assert_eq!(key1, keys[0].to_string());
983 assert_eq!(key2, keys[1].to_string());
984 assert_eq!(key3, keys[2].to_string());
985 }
986}