1use chrono::{DateTime, NaiveDateTime, Utc};
90use std::fmt;
91use std::net::IpAddr;
92use std::str::FromStr;
93
94use crate::Error;
95
96#[derive(Debug, Clone, PartialEq)]
160pub struct KeyCertificate {
161 pub version: Option<u32>,
167
168 pub address: Option<IpAddr>,
173
174 pub dir_port: Option<u16>,
179
180 pub fingerprint: Option<String>,
186
187 pub identity_key: Option<String>,
193
194 pub published: Option<DateTime<Utc>>,
199
200 pub expires: Option<DateTime<Utc>>,
206
207 pub signing_key: Option<String>,
213
214 pub crosscert: Option<String>,
220
221 pub certification: Option<String>,
228
229 raw_content: Vec<u8>,
231
232 unrecognized_lines: Vec<String>,
237}
238
239impl KeyCertificate {
240 pub fn parse(content: &str) -> Result<Self, Error> {
290 Self::parse_with_validation(content, true)
291 }
292
293 pub fn parse_with_validation(content: &str, validate: bool) -> Result<Self, Error> {
331 let raw_content = content.as_bytes().to_vec();
332 let lines: Vec<&str> = content.lines().collect();
333
334 let mut version: Option<u32> = None;
335 let mut address: Option<IpAddr> = None;
336 let mut dir_port: Option<u16> = None;
337 let mut fingerprint: Option<String> = None;
338 let mut identity_key: Option<String> = None;
339 let mut published: Option<DateTime<Utc>> = None;
340 let mut expires: Option<DateTime<Utc>> = None;
341 let mut signing_key: Option<String> = None;
342 let mut crosscert: Option<String> = None;
343 let mut certification: Option<String> = None;
344 let mut unrecognized_lines: Vec<String> = Vec::new();
345
346 let mut idx = 0;
347 let mut first_keyword: Option<&str> = None;
348 let mut last_keyword: Option<&str> = None;
349
350 while idx < lines.len() {
351 let line = lines[idx];
352
353 if line.trim().is_empty() {
354 idx += 1;
355 continue;
356 }
357
358 if line.starts_with("@type ") {
359 idx += 1;
360 continue;
361 }
362
363 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
364 (&line[..space_pos], line[space_pos + 1..].trim())
365 } else {
366 (line, "")
367 };
368
369 if first_keyword.is_none() {
370 first_keyword = Some(keyword);
371 }
372 last_keyword = Some(keyword);
373
374 match keyword {
375 "dir-key-certificate-version" => {
376 version = value.parse().ok();
377 if validate && version.is_none() {
378 return Err(Error::Parse {
379 location: "dir-key-certificate-version".to_string(),
380 reason: format!("invalid version: {}", value),
381 });
382 }
383 }
384 "dir-address" => {
385 if let Some((addr_str, port_str)) = value.split_once(':') {
386 if let Ok(addr) = addr_str.parse::<IpAddr>() {
387 if let Ok(port) = port_str.parse::<u16>() {
388 address = Some(addr);
389 dir_port = Some(port);
390 } else if validate {
391 return Err(Error::Parse {
392 location: "dir-address".to_string(),
393 reason: format!("invalid port: {}", port_str),
394 });
395 }
396 } else if validate {
397 return Err(Error::Parse {
398 location: "dir-address".to_string(),
399 reason: format!("invalid address: {}", addr_str),
400 });
401 }
402 } else if validate && !value.is_empty() {
403 return Err(Error::Parse {
404 location: "dir-address".to_string(),
405 reason: format!("invalid dir-address format: {}", value),
406 });
407 }
408 }
409 "fingerprint" => {
410 if is_valid_fingerprint(value) {
411 fingerprint = Some(value.to_string());
412 } else if validate {
413 return Err(Error::Parse {
414 location: "fingerprint".to_string(),
415 reason: format!("invalid fingerprint: {}", value),
416 });
417 }
418 }
419 "dir-key-published" => {
420 published = parse_datetime(value);
421 if validate && published.is_none() {
422 return Err(Error::Parse {
423 location: "dir-key-published".to_string(),
424 reason: format!("invalid datetime: {}", value),
425 });
426 }
427 }
428 "dir-key-expires" => {
429 expires = parse_datetime(value);
430 if validate && expires.is_none() {
431 return Err(Error::Parse {
432 location: "dir-key-expires".to_string(),
433 reason: format!("invalid datetime: {}", value),
434 });
435 }
436 }
437 "dir-identity-key" => {
438 let (block, end_idx) =
439 extract_key_block(&lines, idx + 1, "RSA PUBLIC KEY", validate)?;
440 identity_key = block;
441 idx = end_idx;
442 }
443 "dir-signing-key" => {
444 let (block, end_idx) =
445 extract_key_block(&lines, idx + 1, "RSA PUBLIC KEY", validate)?;
446 signing_key = block;
447 idx = end_idx;
448 }
449 "dir-key-crosscert" => {
450 let (block, end_idx) =
451 extract_key_block(&lines, idx + 1, "ID SIGNATURE", validate)?;
452 crosscert = block;
453 idx = end_idx;
454 }
455 "dir-key-certification" => {
456 let (block, end_idx) =
457 extract_key_block(&lines, idx + 1, "SIGNATURE", validate)?;
458 certification = block;
459 idx = end_idx;
460 }
461 _ => {
462 if !line.is_empty() && !line.starts_with("-----") {
463 unrecognized_lines.push(line.to_string());
464 }
465 }
466 }
467 idx += 1;
468 }
469
470 if validate {
471 if first_keyword != Some("dir-key-certificate-version") {
472 return Err(Error::Parse {
473 location: "KeyCertificate".to_string(),
474 reason: "Key certificates must start with a 'dir-key-certificate-version' line"
475 .to_string(),
476 });
477 }
478
479 if last_keyword != Some("dir-key-certification") {
480 return Err(Error::Parse {
481 location: "KeyCertificate".to_string(),
482 reason: "Key certificates must end with a 'dir-key-certification' line"
483 .to_string(),
484 });
485 }
486
487 if version.is_none() {
488 return Err(Error::Parse {
489 location: "KeyCertificate".to_string(),
490 reason: "Key certificates must have a 'dir-key-certificate-version' line"
491 .to_string(),
492 });
493 }
494 if fingerprint.is_none() {
495 return Err(Error::Parse {
496 location: "KeyCertificate".to_string(),
497 reason: "Key certificates must have a 'fingerprint' line".to_string(),
498 });
499 }
500 if published.is_none() {
501 return Err(Error::Parse {
502 location: "KeyCertificate".to_string(),
503 reason: "Key certificates must have a 'dir-key-published' line".to_string(),
504 });
505 }
506 if expires.is_none() {
507 return Err(Error::Parse {
508 location: "KeyCertificate".to_string(),
509 reason: "Key certificates must have a 'dir-key-expires' line".to_string(),
510 });
511 }
512 if identity_key.is_none() {
513 return Err(Error::Parse {
514 location: "KeyCertificate".to_string(),
515 reason: "Key certificates must have a 'dir-identity-key' line".to_string(),
516 });
517 }
518 if signing_key.is_none() {
519 return Err(Error::Parse {
520 location: "KeyCertificate".to_string(),
521 reason: "Key certificates must have a 'dir-signing-key' line".to_string(),
522 });
523 }
524 if certification.is_none() {
525 return Err(Error::Parse {
526 location: "KeyCertificate".to_string(),
527 reason: "Key certificates must have a 'dir-key-certification' line".to_string(),
528 });
529 }
530 }
531
532 Ok(KeyCertificate {
533 version,
534 address,
535 dir_port,
536 fingerprint,
537 identity_key,
538 published,
539 expires,
540 signing_key,
541 crosscert,
542 certification,
543 raw_content,
544 unrecognized_lines,
545 })
546 }
547
548 pub fn raw_content(&self) -> &[u8] {
560 &self.raw_content
561 }
562
563 pub fn unrecognized_lines(&self) -> &[String] {
574 &self.unrecognized_lines
575 }
576
577 pub fn is_expired(&self) -> bool {
617 match self.expires {
618 Some(exp) => Utc::now() > exp,
619 None => false,
620 }
621 }
622
623 pub fn to_descriptor_string(&self) -> String {
669 let mut result = String::new();
670
671 if let Some(v) = self.version {
672 result.push_str(&format!("dir-key-certificate-version {}\n", v));
673 }
674
675 if let (Some(addr), Some(port)) = (&self.address, self.dir_port) {
676 result.push_str(&format!("dir-address {}:{}\n", addr, port));
677 }
678
679 if let Some(ref fp) = self.fingerprint {
680 result.push_str(&format!("fingerprint {}\n", fp));
681 }
682
683 if let Some(ref dt) = self.published {
684 result.push_str(&format!(
685 "dir-key-published {}\n",
686 dt.format("%Y-%m-%d %H:%M:%S")
687 ));
688 }
689
690 if let Some(ref dt) = self.expires {
691 result.push_str(&format!(
692 "dir-key-expires {}\n",
693 dt.format("%Y-%m-%d %H:%M:%S")
694 ));
695 }
696
697 if let Some(ref key) = self.identity_key {
698 result.push_str("dir-identity-key\n");
699 result.push_str(key);
700 result.push('\n');
701 }
702
703 if let Some(ref key) = self.signing_key {
704 result.push_str("dir-signing-key\n");
705 result.push_str(key);
706 result.push('\n');
707 }
708
709 if let Some(ref sig) = self.crosscert {
710 result.push_str("dir-key-crosscert\n");
711 result.push_str(sig);
712 result.push('\n');
713 }
714
715 if let Some(ref sig) = self.certification {
716 result.push_str("dir-key-certification\n");
717 result.push_str(sig);
718 result.push('\n');
719 }
720
721 result
722 }
723}
724
725impl FromStr for KeyCertificate {
726 type Err = Error;
727
728 fn from_str(s: &str) -> Result<Self, Self::Err> {
729 Self::parse(s)
730 }
731}
732
733impl fmt::Display for KeyCertificate {
734 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
735 write!(f, "{}", self.to_descriptor_string())
736 }
737}
738
739fn is_valid_fingerprint(s: &str) -> bool {
744 s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
745}
746
747fn parse_datetime(s: &str) -> Option<DateTime<Utc>> {
751 let s = s.trim();
752 NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
753 .ok()
754 .map(|dt| dt.and_utc())
755}
756
757fn extract_key_block(
762 lines: &[&str],
763 start_idx: usize,
764 expected_type: &str,
765 validate: bool,
766) -> Result<(Option<String>, usize), Error> {
767 let mut block = String::new();
768 let mut idx = start_idx;
769 let begin_marker = format!("-----BEGIN {}-----", expected_type);
770 let end_marker = format!("-----END {}-----", expected_type);
771 let mut found_begin = false;
772 let mut found_end = false;
773
774 while idx < lines.len() {
775 let line = lines[idx];
776 block.push_str(line);
777 block.push('\n');
778
779 if line.contains(&begin_marker) {
780 found_begin = true;
781 }
782
783 if line.contains(&end_marker) {
784 found_end = true;
785 break;
786 }
787
788 if line.starts_with("-----END ") && !line.contains(&end_marker) {
789 if validate {
790 return Err(Error::Parse {
791 location: "key_block".to_string(),
792 reason: format!("Expected {} block but found: {}", expected_type, line),
793 });
794 }
795 return Ok((None, idx));
796 }
797
798 idx += 1;
799 }
800
801 if validate && (!found_begin || !found_end) {
802 return Err(Error::Parse {
803 location: "key_block".to_string(),
804 reason: format!("Incomplete {} block", expected_type),
805 });
806 }
807
808 if found_begin && found_end {
809 Ok((Some(block.trim_end().to_string()), idx))
810 } else {
811 Ok((None, idx))
812 }
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818 use chrono::TimeZone;
819
820 const SAMPLE_CERT: &str = r#"dir-key-certificate-version 3
821dir-address 127.0.0.1:7000
822fingerprint BCB380A633592C218757BEE11E630511A485658A
823dir-key-published 2017-05-25 04:45:52
824dir-key-expires 2018-05-25 04:45:52
825dir-identity-key
826-----BEGIN RSA PUBLIC KEY-----
827MIIBigKCAYEAxfTHG1b3Sxe8n3JQ/nIk4+1/chj7+jAyLLK+WrEBiP1vnDxTXMuo
828x26ntWEjOaxjtKB12k5wMQW94/KvE754Gn98uQRFBHqLkrS4hUnn4/MqiBQVd2y3
829UtE6KDSRhJZ5LfFH+dCKwu5+695PyJp/pfCUSOyPj0HQbFOnAOqdHPok8dtdfsy0
830LaI7ycpzqAalzgrlwFP5KwwLtL+VapUGN4QOZlIXgL4W5e7OAG42lZhHt0b7/zdt
831oIegZM1y8tK2l75ijqsvbetddQcFlnVaYzNwlQAUIZuxJOGfnPfTo+WrjCgrK2ur
832ed5NiQMrEbZn5uCUscs+xLlKl4uKW0XXo1EIL45yBrVbmlP6V3/9diTHk64W9+m8
8332G4ToDyH8J7LvnYPsmD0cCaQEceebxYVlmmwgqdORH/ixbeGF7JalTwtWBQYo2r0
834VZAqjRwxR9dri6m1MIpzmzWmrbXghZ1IzJEL1rpB0okA/bE8AUGRx61eKnbI415O
835PmO06JMpvkxxAgMBAAE=
836-----END RSA PUBLIC KEY-----
837dir-signing-key
838-----BEGIN RSA PUBLIC KEY-----
839MIIBCgKCAQEAvzugxJl1gc7BgXarBO5IWejNZC30U1xVjZ/myQTzxtiKkPU0agQh
840sPqn4vVsaW6ZnWjJ2pSOq0/jg8WgFyGHGQ9cG8tv2TlpObeb/tI7iANxWx+MXJAh
841/CnFDBQ1ifKntJrs2IcRKMivfobqaHAL3Pz93noLWOTQunWjZ8D6kovYvUXe+yUQ
842tZEROrmXJx7ZIIJF6BNKYBTc+iEkYtkWlJVs0my7yP/bbS075QyBsr6CfT+O2yU4
843mgIg43QuqcFRbjyUvGI/gap06QNlB6yj8pqeE5rWo++5EpEvMK76fK6ymYuTN2SN
844Oil+Fo7qgG8UP/fv0GelSz6Tk7pBoeHJlQIDAQAB
845-----END RSA PUBLIC KEY-----
846dir-key-crosscert
847-----BEGIN ID SIGNATURE-----
848Oz+rvXDzlxLgQSb3nS5/4hrHVWgGCy0OnuNmFsyw8bi2eBst5Yj79dQ+D25giZke
84981FRGIFU4eS6dshB+pJ+z0hc9ozlRTYh/qevY6l6o0amvuhHyk/cQXrh8oYU9Ihe
850XQ1yVItvxC24HENsoGIGbr5uxc85FOcNs+R9qTLYA/56TjvAU4WUje3nTZE1awml
851lj/Y6DM7ruMF6UoYJZPTklukZ+XHZg4Z2eE55e/oIaD7bfU/lFWU/alMyTV/J5oT
852sxaD2XBLBScYiKypUmgrZ50W4ZqsXaYk76ClrudZnDbce+FuugVxok+jKYGjMu75
8532es2ucuik7iuO7QPdPIXfg==
854-----END ID SIGNATURE-----
855dir-key-certification
856-----BEGIN SIGNATURE-----
857I86FTQ5ZyCZUzm19HVAQWByrrRgUmddoRBfNiCj0iTGN3kdIq9OfuNLhWAqz71xP
8588Nn0Vun8Uj3/vBq/odIFpnngL3mKI6OEKcNDr0D5hEV9Yjrxe8msMoaUZT+LHzUW
8591q3pzxfMx6EmlSilMhuzSsa4YEbXMZzMqASKANSJHo2fzUkzQOpPw2SlWSTIgyqw
860wAOB6QOvFfP3c0NTwxXrYE/iT+r90wZBuzS+v7r9B94alNAkE1KZQKnq2QTTIznP
861iF9LWMsZcMHCjoTxszK4jF4MRMN/S4Xl8yQo0/z6FoqBz4RIXzFtJoG/rbXdKfkE
862nJK9iEhaZbS1IN0o+uIGtvOm2rQSu9gS8merurr5GDSK3szjesPVJuF00mCNgOx4
863hAYPN9N8HAL4zGE/l1UM7BGg3L84A0RMpDxnpXePd9mlHLhl4UV2lrkkf8S9Z6fX
864PPc3r7zKlL/jEGHwz+C7kE88HIvkVnKLLn
865-----END SIGNATURE-----
866"#;
867
868 const MINIMAL_CERT: &str = r#"dir-key-certificate-version 3
869fingerprint BCB380A633592C218757BEE11E630511A485658A
870dir-key-published 2017-05-25 04:45:52
871dir-key-expires 2018-05-25 04:45:52
872dir-identity-key
873-----BEGIN RSA PUBLIC KEY-----
874MIIBCgKCAQEA
875-----END RSA PUBLIC KEY-----
876dir-signing-key
877-----BEGIN RSA PUBLIC KEY-----
878MIIBCgKCAQEA
879-----END RSA PUBLIC KEY-----
880dir-key-certification
881-----BEGIN SIGNATURE-----
882AAAA
883-----END SIGNATURE-----
884"#;
885
886 #[test]
887 fn test_parse_full_certificate() {
888 let cert = KeyCertificate::parse(SAMPLE_CERT).unwrap();
889
890 assert_eq!(Some(3), cert.version);
891 assert_eq!(Some("127.0.0.1".parse().unwrap()), cert.address);
892 assert_eq!(Some(7000), cert.dir_port);
893 assert_eq!(
894 Some("BCB380A633592C218757BEE11E630511A485658A".to_string()),
895 cert.fingerprint
896 );
897 assert!(cert.identity_key.is_some());
898 assert!(cert
899 .identity_key
900 .as_ref()
901 .unwrap()
902 .contains("RSA PUBLIC KEY"));
903 assert_eq!(
904 Some(Utc.with_ymd_and_hms(2017, 5, 25, 4, 45, 52).unwrap()),
905 cert.published
906 );
907 assert_eq!(
908 Some(Utc.with_ymd_and_hms(2018, 5, 25, 4, 45, 52).unwrap()),
909 cert.expires
910 );
911 assert!(cert.signing_key.is_some());
912 assert!(cert.crosscert.is_some());
913 assert!(cert.crosscert.as_ref().unwrap().contains("ID SIGNATURE"));
914 assert!(cert.certification.is_some());
915 assert!(cert.certification.as_ref().unwrap().contains("SIGNATURE"));
916 assert!(cert.unrecognized_lines().is_empty());
917 }
918
919 #[test]
920 fn test_parse_minimal_certificate() {
921 let cert = KeyCertificate::parse(MINIMAL_CERT).unwrap();
922
923 assert_eq!(Some(3), cert.version);
924 assert_eq!(None, cert.address);
925 assert_eq!(None, cert.dir_port);
926 assert_eq!(
927 Some("BCB380A633592C218757BEE11E630511A485658A".to_string()),
928 cert.fingerprint
929 );
930 assert!(cert.identity_key.is_some());
931 assert!(cert.signing_key.is_some());
932 assert_eq!(None, cert.crosscert);
933 assert!(cert.certification.is_some());
934 }
935
936 #[test]
937 fn test_missing_version() {
938 let content = r#"fingerprint BCB380A633592C218757BEE11E630511A485658A
939dir-key-published 2017-05-25 04:45:52
940dir-key-expires 2018-05-25 04:45:52
941dir-identity-key
942-----BEGIN RSA PUBLIC KEY-----
943MIIBCgKCAQEA
944-----END RSA PUBLIC KEY-----
945dir-signing-key
946-----BEGIN RSA PUBLIC KEY-----
947MIIBCgKCAQEA
948-----END RSA PUBLIC KEY-----
949dir-key-certification
950-----BEGIN SIGNATURE-----
951AAAA
952-----END SIGNATURE-----
953"#;
954 let result = KeyCertificate::parse(content);
955 assert!(result.is_err());
956 assert!(result
957 .unwrap_err()
958 .to_string()
959 .contains("dir-key-certificate-version"));
960 }
961
962 #[test]
963 fn test_missing_fingerprint() {
964 let content = r#"dir-key-certificate-version 3
965dir-key-published 2017-05-25 04:45:52
966dir-key-expires 2018-05-25 04:45:52
967dir-identity-key
968-----BEGIN RSA PUBLIC KEY-----
969MIIBCgKCAQEA
970-----END RSA PUBLIC KEY-----
971dir-signing-key
972-----BEGIN RSA PUBLIC KEY-----
973MIIBCgKCAQEA
974-----END RSA PUBLIC KEY-----
975dir-key-certification
976-----BEGIN SIGNATURE-----
977AAAA
978-----END SIGNATURE-----
979"#;
980 let result = KeyCertificate::parse(content);
981 assert!(result.is_err());
982 assert!(result.unwrap_err().to_string().contains("fingerprint"));
983 }
984
985 #[test]
986 fn test_invalid_fingerprint() {
987 let content = r#"dir-key-certificate-version 3
988fingerprint INVALID
989dir-key-published 2017-05-25 04:45:52
990dir-key-expires 2018-05-25 04:45:52
991dir-identity-key
992-----BEGIN RSA PUBLIC KEY-----
993MIIBCgKCAQEA
994-----END RSA PUBLIC KEY-----
995dir-signing-key
996-----BEGIN RSA PUBLIC KEY-----
997MIIBCgKCAQEA
998-----END RSA PUBLIC KEY-----
999dir-key-certification
1000-----BEGIN SIGNATURE-----
1001AAAA
1002-----END SIGNATURE-----
1003"#;
1004 let result = KeyCertificate::parse(content);
1005 assert!(result.is_err());
1006 assert!(result.unwrap_err().to_string().contains("fingerprint"));
1007 }
1008
1009 #[test]
1010 fn test_invalid_datetime() {
1011 let content = r#"dir-key-certificate-version 3
1012fingerprint BCB380A633592C218757BEE11E630511A485658A
1013dir-key-published invalid-date
1014dir-key-expires 2018-05-25 04:45:52
1015dir-identity-key
1016-----BEGIN RSA PUBLIC KEY-----
1017MIIBCgKCAQEA
1018-----END RSA PUBLIC KEY-----
1019dir-signing-key
1020-----BEGIN RSA PUBLIC KEY-----
1021MIIBCgKCAQEA
1022-----END RSA PUBLIC KEY-----
1023dir-key-certification
1024-----BEGIN SIGNATURE-----
1025AAAA
1026-----END SIGNATURE-----
1027"#;
1028 let result = KeyCertificate::parse(content);
1029 assert!(result.is_err());
1030 assert!(result.unwrap_err().to_string().contains("datetime"));
1031 }
1032
1033 #[test]
1034 fn test_unrecognized_lines() {
1035 let content = r#"dir-key-certificate-version 3
1036fingerprint BCB380A633592C218757BEE11E630511A485658A
1037pepperjack is oh so tasty!
1038dir-key-published 2017-05-25 04:45:52
1039dir-key-expires 2018-05-25 04:45:52
1040dir-identity-key
1041-----BEGIN RSA PUBLIC KEY-----
1042MIIBCgKCAQEA
1043-----END RSA PUBLIC KEY-----
1044dir-signing-key
1045-----BEGIN RSA PUBLIC KEY-----
1046MIIBCgKCAQEA
1047-----END RSA PUBLIC KEY-----
1048dir-key-certification
1049-----BEGIN SIGNATURE-----
1050AAAA
1051-----END SIGNATURE-----
1052"#;
1053 let cert = KeyCertificate::parse(content).unwrap();
1054 assert_eq!(
1055 vec!["pepperjack is oh so tasty!"],
1056 cert.unrecognized_lines()
1057 );
1058 }
1059
1060 #[test]
1061 fn test_parse_without_validation() {
1062 let content = r#"dir-key-certificate-version 3
1063fingerprint BCB380A633592C218757BEE11E630511A485658A
1064"#;
1065 let cert = KeyCertificate::parse_with_validation(content, false).unwrap();
1066 assert_eq!(Some(3), cert.version);
1067 assert_eq!(
1068 Some("BCB380A633592C218757BEE11E630511A485658A".to_string()),
1069 cert.fingerprint
1070 );
1071 assert_eq!(None, cert.identity_key);
1072 }
1073
1074 #[test]
1075 fn test_is_expired() {
1076 let cert = KeyCertificate::parse(SAMPLE_CERT).unwrap();
1077 assert!(cert.is_expired());
1078 }
1079
1080 #[test]
1081 fn test_to_descriptor_string() {
1082 let cert = KeyCertificate::parse(SAMPLE_CERT).unwrap();
1083 let output = cert.to_descriptor_string();
1084
1085 assert!(output.contains("dir-key-certificate-version 3"));
1086 assert!(output.contains("dir-address 127.0.0.1:7000"));
1087 assert!(output.contains("fingerprint BCB380A633592C218757BEE11E630511A485658A"));
1088 assert!(output.contains("dir-key-published 2017-05-25 04:45:52"));
1089 assert!(output.contains("dir-key-expires 2018-05-25 04:45:52"));
1090 assert!(output.contains("dir-identity-key"));
1091 assert!(output.contains("dir-signing-key"));
1092 assert!(output.contains("dir-key-crosscert"));
1093 assert!(output.contains("dir-key-certification"));
1094 }
1095
1096 #[test]
1097 fn test_from_str() {
1098 let cert: KeyCertificate = MINIMAL_CERT.parse().unwrap();
1099 assert_eq!(Some(3), cert.version);
1100 }
1101
1102 #[test]
1103 fn test_display() {
1104 let cert = KeyCertificate::parse(MINIMAL_CERT).unwrap();
1105 let display = format!("{}", cert);
1106 assert!(display.contains("dir-key-certificate-version 3"));
1107 }
1108
1109 #[test]
1110 fn test_type_annotation() {
1111 let content = r#"@type dir-key-certificate-3 1.0
1112dir-key-certificate-version 3
1113fingerprint BCB380A633592C218757BEE11E630511A485658A
1114dir-key-published 2017-05-25 04:45:52
1115dir-key-expires 2018-05-25 04:45:52
1116dir-identity-key
1117-----BEGIN RSA PUBLIC KEY-----
1118MIIBCgKCAQEA
1119-----END RSA PUBLIC KEY-----
1120dir-signing-key
1121-----BEGIN RSA PUBLIC KEY-----
1122MIIBCgKCAQEA
1123-----END RSA PUBLIC KEY-----
1124dir-key-certification
1125-----BEGIN SIGNATURE-----
1126AAAA
1127-----END SIGNATURE-----
1128"#;
1129 let cert = KeyCertificate::parse(content).unwrap();
1130 assert_eq!(Some(3), cert.version);
1131 }
1132
1133 #[test]
1134 fn test_blank_lines() {
1135 let content = r#"dir-key-certificate-version 3
1136fingerprint BCB380A633592C218757BEE11E630511A485658A
1137
1138dir-key-published 2017-05-25 04:45:52
1139
1140dir-key-expires 2018-05-25 04:45:52
1141dir-identity-key
1142-----BEGIN RSA PUBLIC KEY-----
1143MIIBCgKCAQEA
1144-----END RSA PUBLIC KEY-----
1145dir-signing-key
1146-----BEGIN RSA PUBLIC KEY-----
1147MIIBCgKCAQEA
1148-----END RSA PUBLIC KEY-----
1149dir-key-certification
1150-----BEGIN SIGNATURE-----
1151AAAA
1152-----END SIGNATURE-----
1153"#;
1154 let cert = KeyCertificate::parse(content).unwrap();
1155 assert_eq!(Some(3), cert.version);
1156 }
1157}