1use std::fmt;
49use std::net::IpAddr;
50use std::str::FromStr;
51
52use crate::Error;
53
54use super::key_cert::KeyCertificate;
55
56#[derive(Debug, Clone, PartialEq)]
76pub struct SharedRandomnessCommitment {
77 pub version: u32,
79 pub algorithm: String,
81 pub identity: String,
83 pub commit: String,
85 pub reveal: Option<String>,
87}
88
89#[derive(Debug, Clone, PartialEq)]
142pub struct DirectoryAuthority {
143 pub nickname: String,
146 pub v3ident: String,
149 pub hostname: String,
151 pub address: IpAddr,
153 pub dir_port: Option<u16>,
155 pub or_port: u16,
157 pub is_legacy: bool,
159 pub contact: Option<String>,
161 pub vote_digest: Option<String>,
163 pub legacy_dir_key: Option<String>,
165 pub key_certificate: Option<KeyCertificate>,
167 pub is_shared_randomness_participate: bool,
169 pub shared_randomness_commitments: Vec<SharedRandomnessCommitment>,
171 pub shared_randomness_previous_reveal_count: Option<u32>,
173 pub shared_randomness_previous_value: Option<String>,
175 pub shared_randomness_current_reveal_count: Option<u32>,
177 pub shared_randomness_current_value: Option<String>,
179 pub(crate) raw_content: Vec<u8>,
181 pub(crate) unrecognized_lines: Vec<String>,
183}
184
185impl Default for DirectoryAuthority {
186 fn default() -> Self {
187 Self {
188 nickname: String::new(),
189 v3ident: String::new(),
190 hostname: String::new(),
191 address: IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)),
192 dir_port: None,
193 or_port: 0,
194 is_legacy: false,
195 contact: None,
196 vote_digest: None,
197 legacy_dir_key: None,
198 key_certificate: None,
199 is_shared_randomness_participate: false,
200 shared_randomness_commitments: Vec::new(),
201 shared_randomness_previous_reveal_count: None,
202 shared_randomness_previous_value: None,
203 shared_randomness_current_reveal_count: None,
204 shared_randomness_current_value: None,
205 raw_content: Vec::new(),
206 unrecognized_lines: Vec::new(),
207 }
208 }
209}
210
211impl DirectoryAuthority {
212 pub fn parse(content: &str, is_vote: bool) -> Result<Self, Error> {
247 Self::parse_with_validation(content, true, is_vote)
248 }
249
250 pub fn parse_with_validation(
267 content: &str,
268 validate: bool,
269 is_vote: bool,
270 ) -> Result<Self, Error> {
271 let raw_content = content.as_bytes().to_vec();
272
273 let (authority_content, key_cert) =
274 if let Some(key_div) = content.find("\ndir-key-certificate-version") {
275 let cert_content = &content[key_div + 1..];
276 let cert = if validate {
277 Some(KeyCertificate::parse(cert_content)?)
278 } else {
279 KeyCertificate::parse_with_validation(cert_content, false).ok()
280 };
281 (&content[..key_div + 1], cert)
282 } else {
283 (content, None)
284 };
285
286 let lines: Vec<&str> = authority_content.lines().collect();
287
288 let mut nickname: Option<String> = None;
289 let mut v3ident: Option<String> = None;
290 let mut hostname: Option<String> = None;
291 let mut address: Option<IpAddr> = None;
292 let mut dir_port: Option<u16> = None;
293 let mut or_port: Option<u16> = None;
294 let mut is_legacy = false;
295 let mut contact: Option<String> = None;
296 let mut vote_digest: Option<String> = None;
297 let mut legacy_dir_key: Option<String> = None;
298 let mut is_shared_randomness_participate = false;
299 let mut shared_randomness_commitments: Vec<SharedRandomnessCommitment> = Vec::new();
300 let mut shared_randomness_previous_reveal_count: Option<u32> = None;
301 let mut shared_randomness_previous_value: Option<String> = None;
302 let mut shared_randomness_current_reveal_count: Option<u32> = None;
303 let mut shared_randomness_current_value: Option<String> = None;
304 let mut unrecognized_lines: Vec<String> = Vec::new();
305 let mut first_keyword: Option<&str> = None;
306
307 for line in &lines {
308 let line = line.trim();
309 if line.is_empty() {
310 continue;
311 }
312
313 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
314 (&line[..space_pos], line[space_pos + 1..].trim())
315 } else {
316 (line, "")
317 };
318
319 if first_keyword.is_none() {
320 first_keyword = Some(keyword);
321 }
322
323 match keyword {
324 "dir-source" => {
325 let parts: Vec<&str> = value.split_whitespace().collect();
326 if parts.len() >= 6 {
327 let nick = parts[0].to_string();
328 is_legacy = nick.ends_with("-legacy");
329
330 let nick_to_validate = nick.trim_end_matches("-legacy");
331 if validate && !is_valid_nickname(nick_to_validate) {
332 return Err(Error::Parse {
333 location: "dir-source".to_string(),
334 reason: format!("Authority's nickname is invalid: {}", nick),
335 });
336 }
337
338 if validate && !is_valid_fingerprint(parts[1]) {
339 return Err(Error::Parse {
340 location: "dir-source".to_string(),
341 reason: format!("Authority's v3ident is invalid: {}", parts[1]),
342 });
343 }
344
345 if validate && parts[2].is_empty() {
346 return Err(Error::Parse {
347 location: "dir-source".to_string(),
348 reason: "Authority's hostname can't be blank".to_string(),
349 });
350 }
351
352 let addr: Result<IpAddr, _> = parts[3].parse();
353 if validate && addr.is_err() {
354 return Err(Error::Parse {
355 location: "dir-source".to_string(),
356 reason: format!(
357 "Authority's address isn't a valid IPv4 address: {}",
358 parts[3]
359 ),
360 });
361 }
362
363 let dport: Result<u16, _> = parts[4].parse();
364 if validate && dport.is_err() {
365 return Err(Error::Parse {
366 location: "dir-source".to_string(),
367 reason: format!("Authority's DirPort is invalid: {}", parts[4]),
368 });
369 }
370
371 let oport: Result<u16, _> = parts[5].parse();
372 if validate && oport.is_err() {
373 return Err(Error::Parse {
374 location: "dir-source".to_string(),
375 reason: format!("Authority's ORPort is invalid: {}", parts[5]),
376 });
377 }
378
379 nickname = Some(nick);
380 v3ident = Some(parts[1].to_string());
381 hostname = Some(parts[2].to_string());
382 address = addr.ok();
383 dir_port = dport.ok().and_then(|p| if p == 0 { None } else { Some(p) });
384 or_port = oport.ok();
385 } else if validate {
386 return Err(Error::Parse {
387 location: "dir-source".to_string(),
388 reason: format!(
389 "Authority entry's 'dir-source' line must have six values: dir-source {}",
390 value
391 ),
392 });
393 }
394 }
395 "contact" => {
396 contact = Some(value.to_string());
397 }
398 "vote-digest" => {
399 if validate && !is_valid_fingerprint(value) {
400 return Err(Error::Parse {
401 location: "vote-digest".to_string(),
402 reason: format!("Invalid vote-digest: {}", value),
403 });
404 }
405 vote_digest = if is_valid_fingerprint(value) {
406 Some(value.to_string())
407 } else {
408 None
409 };
410 }
411 "legacy-dir-key" => {
412 if validate && !is_valid_fingerprint(value) {
413 return Err(Error::Parse {
414 location: "legacy-dir-key".to_string(),
415 reason: format!("Invalid legacy-dir-key: {}", value),
416 });
417 }
418 legacy_dir_key = if is_valid_fingerprint(value) {
419 Some(value.to_string())
420 } else {
421 None
422 };
423 }
424 "shared-rand-participate" => {
425 is_shared_randomness_participate = true;
426 }
427 "shared-rand-commit" => {
428 if let Some(commitment) = parse_shared_rand_commit(value) {
429 shared_randomness_commitments.push(commitment);
430 }
431 }
432 "shared-rand-previous-value" => {
433 let parts: Vec<&str> = value.split_whitespace().collect();
434 if parts.len() >= 2 {
435 shared_randomness_previous_reveal_count = parts[0].parse().ok();
436 shared_randomness_previous_value = Some(parts[1].to_string());
437 }
438 }
439 "shared-rand-current-value" => {
440 let parts: Vec<&str> = value.split_whitespace().collect();
441 if parts.len() >= 2 {
442 shared_randomness_current_reveal_count = parts[0].parse().ok();
443 shared_randomness_current_value = Some(parts[1].to_string());
444 }
445 }
446 _ => {
447 if !line.is_empty() && !line.starts_with("-----") {
448 unrecognized_lines.push(line.to_string());
449 }
450 }
451 }
452 }
453
454 if validate {
455 if first_keyword != Some("dir-source") {
456 return Err(Error::Parse {
457 location: "DirectoryAuthority".to_string(),
458 reason: "Authority entries are expected to start with a 'dir-source' line"
459 .to_string(),
460 });
461 }
462
463 if nickname.is_none() {
464 return Err(Error::Parse {
465 location: "DirectoryAuthority".to_string(),
466 reason: "Authority entries must have a 'dir-source' line".to_string(),
467 });
468 }
469
470 if !is_legacy && contact.is_none() {
471 return Err(Error::Parse {
472 location: "DirectoryAuthority".to_string(),
473 reason: "Authority entries must have a 'contact' line".to_string(),
474 });
475 }
476
477 if is_vote {
478 if key_cert.is_none() {
479 return Err(Error::Parse {
480 location: "DirectoryAuthority".to_string(),
481 reason: "Authority votes must have a key certificate".to_string(),
482 });
483 }
484 if vote_digest.is_some() {
485 return Err(Error::Parse {
486 location: "DirectoryAuthority".to_string(),
487 reason: "Authority votes shouldn't have a 'vote-digest' line".to_string(),
488 });
489 }
490 } else {
491 if key_cert.is_some() {
492 return Err(Error::Parse {
493 location: "DirectoryAuthority".to_string(),
494 reason: "Authority consensus entries shouldn't have a key certificate"
495 .to_string(),
496 });
497 }
498 if !is_legacy && vote_digest.is_none() {
499 return Err(Error::Parse {
500 location: "DirectoryAuthority".to_string(),
501 reason: "Authority entries must have a 'vote-digest' line".to_string(),
502 });
503 }
504 if legacy_dir_key.is_some() {
505 return Err(Error::Parse {
506 location: "DirectoryAuthority".to_string(),
507 reason:
508 "Authority consensus entries shouldn't have a 'legacy-dir-key' line"
509 .to_string(),
510 });
511 }
512 }
513 }
514
515 Ok(DirectoryAuthority {
516 nickname: nickname.unwrap_or_default(),
517 v3ident: v3ident.unwrap_or_default(),
518 hostname: hostname.unwrap_or_default(),
519 address: address.unwrap_or_else(|| "0.0.0.0".parse().unwrap()),
520 dir_port,
521 or_port: or_port.unwrap_or(0),
522 is_legacy,
523 contact,
524 vote_digest,
525 legacy_dir_key,
526 key_certificate: key_cert,
527 is_shared_randomness_participate,
528 shared_randomness_commitments,
529 shared_randomness_previous_reveal_count,
530 shared_randomness_previous_value,
531 shared_randomness_current_reveal_count,
532 shared_randomness_current_value,
533 raw_content,
534 unrecognized_lines,
535 })
536 }
537
538 pub fn raw_content(&self) -> &[u8] {
543 &self.raw_content
544 }
545
546 pub fn unrecognized_lines(&self) -> &[String] {
568 &self.unrecognized_lines
569 }
570
571 pub fn to_descriptor_string(&self) -> String {
581 let mut result = String::new();
582
583 let dir_port_str = self
584 .dir_port
585 .map(|p| p.to_string())
586 .unwrap_or_else(|| "0".to_string());
587 result.push_str(&format!(
588 "dir-source {} {} {} {} {} {}\n",
589 self.nickname, self.v3ident, self.hostname, self.address, dir_port_str, self.or_port
590 ));
591
592 if let Some(ref contact) = self.contact {
593 result.push_str(&format!("contact {}\n", contact));
594 }
595
596 if let Some(ref legacy_key) = self.legacy_dir_key {
597 result.push_str(&format!("legacy-dir-key {}\n", legacy_key));
598 }
599
600 if self.is_shared_randomness_participate {
601 result.push_str("shared-rand-participate\n");
602 }
603
604 for commitment in &self.shared_randomness_commitments {
605 if let Some(ref reveal) = commitment.reveal {
606 result.push_str(&format!(
607 "shared-rand-commit {} {} {} {} {}\n",
608 commitment.version,
609 commitment.algorithm,
610 commitment.identity,
611 commitment.commit,
612 reveal
613 ));
614 } else {
615 result.push_str(&format!(
616 "shared-rand-commit {} {} {} {}\n",
617 commitment.version,
618 commitment.algorithm,
619 commitment.identity,
620 commitment.commit
621 ));
622 }
623 }
624
625 if let (Some(count), Some(ref value)) = (
626 self.shared_randomness_previous_reveal_count,
627 &self.shared_randomness_previous_value,
628 ) {
629 result.push_str(&format!("shared-rand-previous-value {} {}\n", count, value));
630 }
631
632 if let (Some(count), Some(ref value)) = (
633 self.shared_randomness_current_reveal_count,
634 &self.shared_randomness_current_value,
635 ) {
636 result.push_str(&format!("shared-rand-current-value {} {}\n", count, value));
637 }
638
639 if let Some(ref digest) = self.vote_digest {
640 result.push_str(&format!("vote-digest {}\n", digest));
641 }
642
643 if let Some(ref cert) = self.key_certificate {
644 result.push_str(&cert.to_descriptor_string());
645 }
646
647 result
648 }
649}
650
651impl FromStr for DirectoryAuthority {
652 type Err = Error;
653
654 fn from_str(s: &str) -> Result<Self, Self::Err> {
655 Self::parse(s, false)
656 }
657}
658
659impl fmt::Display for DirectoryAuthority {
660 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661 write!(f, "{}", self.to_descriptor_string())
662 }
663}
664
665fn is_valid_fingerprint(s: &str) -> bool {
670 s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
671}
672
673fn is_valid_nickname(s: &str) -> bool {
678 !s.is_empty()
679 && s.len() <= 19
680 && s.chars()
681 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
682}
683
684fn parse_shared_rand_commit(value: &str) -> Option<SharedRandomnessCommitment> {
688 let parts: Vec<&str> = value.split_whitespace().collect();
689 if parts.len() >= 4 {
690 Some(SharedRandomnessCommitment {
691 version: parts[0].parse().ok()?,
692 algorithm: parts[1].to_string(),
693 identity: parts[2].to_string(),
694 commit: parts[3].to_string(),
695 reveal: parts.get(4).map(|s| s.to_string()),
696 })
697 } else {
698 None
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 const DIR_SOURCE_LINE: &str =
707 "turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090";
708
709 const MINIMAL_CONSENSUS_AUTHORITY: &str = r#"dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090
710contact Mike Perry <email>
711vote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956
712"#;
713
714 const LEGACY_AUTHORITY: &str =
715 "dir-source gabelmoo-legacy 81349FC1F2DBA2C2C11B45CB9706637D480AB913 131.188.40.189 131.188.40.189 80 443";
716
717 const MINIMAL_KEY_CERT: &str = r#"dir-key-certificate-version 3
718fingerprint BCB380A633592C218757BEE11E630511A485658A
719dir-key-published 2017-05-25 04:45:52
720dir-key-expires 2018-05-25 04:45:52
721dir-identity-key
722-----BEGIN RSA PUBLIC KEY-----
723MIIBCgKCAQEA
724-----END RSA PUBLIC KEY-----
725dir-signing-key
726-----BEGIN RSA PUBLIC KEY-----
727MIIBCgKCAQEA
728-----END RSA PUBLIC KEY-----
729dir-key-certification
730-----BEGIN SIGNATURE-----
731AAAA
732-----END SIGNATURE-----
733"#;
734
735 #[test]
736 fn test_minimal_consensus_authority() {
737 let authority = DirectoryAuthority::parse(MINIMAL_CONSENSUS_AUTHORITY, false).unwrap();
738
739 assert_eq!("Unnamed", authority.nickname);
740 assert_eq!(40, authority.v3ident.len());
741 assert_eq!("no.place.com", authority.hostname);
742 assert_eq!(Some(9030), authority.dir_port);
743 assert_eq!(9090, authority.or_port);
744 assert!(!authority.is_legacy);
745 assert_eq!(Some("Mike Perry <email>".to_string()), authority.contact);
746 assert_eq!(40, authority.vote_digest.as_ref().unwrap().len());
747 assert_eq!(None, authority.legacy_dir_key);
748 assert_eq!(None, authority.key_certificate);
749 assert!(authority.unrecognized_lines().is_empty());
750 }
751
752 #[test]
753 fn test_minimal_vote_authority() {
754 let content = format!(
755 "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\n{}",
756 MINIMAL_KEY_CERT
757 );
758 let authority = DirectoryAuthority::parse(&content, true).unwrap();
759
760 assert_eq!("Unnamed", authority.nickname);
761 assert_eq!(40, authority.v3ident.len());
762 assert_eq!("no.place.com", authority.hostname);
763 assert_eq!(Some(9030), authority.dir_port);
764 assert_eq!(9090, authority.or_port);
765 assert!(!authority.is_legacy);
766 assert_eq!(Some("Mike Perry <email>".to_string()), authority.contact);
767 assert_eq!(None, authority.vote_digest);
768 assert_eq!(None, authority.legacy_dir_key);
769 assert!(authority.key_certificate.is_some());
770 assert!(authority.unrecognized_lines().is_empty());
771 }
772
773 #[test]
774 fn test_unrecognized_line() {
775 let content = "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\npepperjack is oh so tasty!\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n".to_string();
776 let authority = DirectoryAuthority::parse(&content, false).unwrap();
777 assert_eq!(
778 vec!["pepperjack is oh so tasty!"],
779 authority.unrecognized_lines()
780 );
781 }
782
783 #[test]
784 fn test_legacy_authority() {
785 let authority = DirectoryAuthority::parse(LEGACY_AUTHORITY, false).unwrap();
786
787 assert_eq!("gabelmoo-legacy", authority.nickname);
788 assert_eq!(
789 "81349FC1F2DBA2C2C11B45CB9706637D480AB913",
790 authority.v3ident
791 );
792 assert_eq!("131.188.40.189", authority.hostname);
793 assert_eq!(
794 "131.188.40.189".parse::<IpAddr>().unwrap(),
795 authority.address
796 );
797 assert_eq!(Some(80), authority.dir_port);
798 assert_eq!(443, authority.or_port);
799 assert!(authority.is_legacy);
800 assert_eq!(None, authority.contact);
801 assert_eq!(None, authority.vote_digest);
802 assert_eq!(None, authority.legacy_dir_key);
803 assert_eq!(None, authority.key_certificate);
804 assert!(authority.unrecognized_lines().is_empty());
805 }
806
807 #[test]
808 fn test_first_line_validation() {
809 let content = format!("ho-hum 567\n{}", MINIMAL_CONSENSUS_AUTHORITY);
810 let result = DirectoryAuthority::parse(&content, false);
811 assert!(result.is_err());
812
813 let authority = DirectoryAuthority::parse_with_validation(&content, false, false).unwrap();
814 assert_eq!(vec!["ho-hum 567"], authority.unrecognized_lines());
815 }
816
817 #[test]
818 fn test_missing_dir_source() {
819 let content =
820 "contact Mike Perry <email>\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n";
821 let result = DirectoryAuthority::parse(content, false);
822 assert!(result.is_err());
823 }
824
825 #[test]
826 fn test_missing_contact() {
827 let content = "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n";
828 let result = DirectoryAuthority::parse(content, false);
829 assert!(result.is_err());
830 }
831
832 #[test]
833 fn test_blank_lines() {
834 let content = format!(
835 "dir-source {} \n\n\ncontact Mike Perry <email>\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n",
836 DIR_SOURCE_LINE.replace("dir-source ", "")
837 );
838 let authority = DirectoryAuthority::parse(&content, false).unwrap();
839 assert_eq!(Some("Mike Perry <email>".to_string()), authority.contact);
840 }
841
842 #[test]
843 fn test_missing_dir_source_field() {
844 let content = "dir-source 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact test\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n";
845 let result = DirectoryAuthority::parse(content, false);
846 assert!(result.is_err());
847 }
848
849 #[test]
850 fn test_malformed_fingerprint() {
851 let test_values = ["", "zzzzz", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"];
852
853 for value in &test_values {
854 let content = format!(
855 "dir-source turtles {} no.place.com 76.73.17.194 9030 9090\ncontact test\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n",
856 value
857 );
858 let result = DirectoryAuthority::parse(&content, false);
859 assert!(result.is_err(), "Expected error for fingerprint: {}", value);
860
861 let authority =
862 DirectoryAuthority::parse_with_validation(&content, false, false).unwrap();
863 assert!(authority.v3ident.is_empty() || authority.v3ident == *value);
864 }
865 }
866
867 #[test]
868 fn test_malformed_address() {
869 let test_values = [
870 "",
871 "71.35.150.",
872 "71.35..29",
873 "71.35.150",
874 "71.35.150.256",
875 "[fd9f:2e19:3bcf::02:9970]",
876 ];
877
878 for value in &test_values {
879 let content = format!(
880 "dir-source turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com {} 9030 9090\ncontact test\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n",
881 value
882 );
883 let result = DirectoryAuthority::parse(&content, false);
884 assert!(result.is_err(), "Expected error for address: {}", value);
885 }
886 }
887
888 #[test]
889 fn test_malformed_port() {
890 let test_values = ["", "-1", "399482", "blarg"];
891
892 for value in &test_values {
893 let content = format!(
894 "dir-source turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 {}\ncontact test\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n",
895 value
896 );
897 let result = DirectoryAuthority::parse(&content, false);
898 assert!(result.is_err(), "Expected error for or_port: {}", value);
899
900 let content = format!(
901 "dir-source turtles 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 {} 9090\ncontact test\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n",
902 value
903 );
904 let result = DirectoryAuthority::parse(&content, false);
905 assert!(result.is_err(), "Expected error for dir_port: {}", value);
906 }
907 }
908
909 #[test]
910 fn test_legacy_dir_key() {
911 let test_value = "65968CCB6BECB5AA88459C5A072624C6995B6B72";
912 let content = format!(
913 "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\nlegacy-dir-key {}\n{}",
914 test_value, MINIMAL_KEY_CERT
915 );
916 let authority = DirectoryAuthority::parse(&content, true).unwrap();
917 assert_eq!(Some(test_value.to_string()), authority.legacy_dir_key);
918
919 let content = format!(
920 "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\nlegacy-dir-key {}\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n",
921 test_value
922 );
923 let result = DirectoryAuthority::parse(&content, false);
924 assert!(result.is_err());
925 }
926
927 #[test]
928 fn test_invalid_legacy_dir_key() {
929 let test_values = ["", "zzzzz", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"];
930
931 for value in &test_values {
932 let content = format!(
933 "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\nlegacy-dir-key {}\n{}",
934 value, MINIMAL_KEY_CERT
935 );
936 let result = DirectoryAuthority::parse(&content, true);
937 assert!(
938 result.is_err(),
939 "Expected error for legacy-dir-key: {}",
940 value
941 );
942 }
943 }
944
945 #[test]
946 fn test_key_certificate_in_consensus() {
947 let content = format!(
948 "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n{}",
949 MINIMAL_KEY_CERT
950 );
951 let result = DirectoryAuthority::parse(&content, false);
952 assert!(result.is_err());
953 }
954
955 #[test]
956 fn test_missing_key_certificate_in_vote() {
957 let content = "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\n";
958 let result = DirectoryAuthority::parse(content, true);
959 assert!(result.is_err());
960 }
961
962 #[test]
963 fn test_vote_digest_in_vote() {
964 let content = format!(
965 "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090\ncontact Mike Perry <email>\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n{}",
966 MINIMAL_KEY_CERT
967 );
968 let result = DirectoryAuthority::parse(&content, true);
969 assert!(result.is_err());
970 }
971
972 #[test]
973 fn test_dir_port_zero() {
974 let content = "dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 0 9090\ncontact Mike Perry <email>\nvote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956\n";
975 let authority = DirectoryAuthority::parse(content, false).unwrap();
976 assert_eq!(None, authority.dir_port);
977 }
978
979 #[test]
980 fn test_shared_randomness() {
981 let content = r#"dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090
982contact Mike Perry <email>
983shared-rand-participate
984shared-rand-commit 1 sha3-256 27B6B5996C426270A5C95488AA5BCEB6BCC86956 AAAA
985shared-rand-previous-value 5 BBBB
986shared-rand-current-value 3 CCCC
987vote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956
988"#;
989 let authority = DirectoryAuthority::parse(content, false).unwrap();
990
991 assert!(authority.is_shared_randomness_participate);
992 assert_eq!(1, authority.shared_randomness_commitments.len());
993 let commitment = &authority.shared_randomness_commitments[0];
994 assert_eq!(1, commitment.version);
995 assert_eq!("sha3-256", commitment.algorithm);
996 assert_eq!(
997 "27B6B5996C426270A5C95488AA5BCEB6BCC86956",
998 commitment.identity
999 );
1000 assert_eq!("AAAA", commitment.commit);
1001 assert_eq!(None, commitment.reveal);
1002
1003 assert_eq!(Some(5), authority.shared_randomness_previous_reveal_count);
1004 assert_eq!(
1005 Some("BBBB".to_string()),
1006 authority.shared_randomness_previous_value
1007 );
1008 assert_eq!(Some(3), authority.shared_randomness_current_reveal_count);
1009 assert_eq!(
1010 Some("CCCC".to_string()),
1011 authority.shared_randomness_current_value
1012 );
1013 }
1014
1015 #[test]
1016 fn test_shared_rand_commit_with_reveal() {
1017 let content = r#"dir-source Unnamed 27B6B5996C426270A5C95488AA5BCEB6BCC86956 no.place.com 76.73.17.194 9030 9090
1018contact Mike Perry <email>
1019shared-rand-commit 1 sha3-256 27B6B5996C426270A5C95488AA5BCEB6BCC86956 AAAA REVEAL
1020vote-digest 27B6B5996C426270A5C95488AA5BCEB6BCC86956
1021"#;
1022 let authority = DirectoryAuthority::parse(content, false).unwrap();
1023
1024 assert_eq!(1, authority.shared_randomness_commitments.len());
1025 let commitment = &authority.shared_randomness_commitments[0];
1026 assert_eq!(Some("REVEAL".to_string()), commitment.reveal);
1027 }
1028
1029 #[test]
1030 fn test_to_descriptor_string() {
1031 let authority = DirectoryAuthority::parse(MINIMAL_CONSENSUS_AUTHORITY, false).unwrap();
1032 let output = authority.to_descriptor_string();
1033
1034 assert!(output.contains("dir-source Unnamed"));
1035 assert!(output.contains("27B6B5996C426270A5C95488AA5BCEB6BCC86956"));
1036 assert!(output.contains("no.place.com"));
1037 assert!(output.contains("76.73.17.194"));
1038 assert!(output.contains("9030"));
1039 assert!(output.contains("9090"));
1040 assert!(output.contains("contact Mike Perry <email>"));
1041 assert!(output.contains("vote-digest"));
1042 }
1043
1044 #[test]
1045 fn test_from_str() {
1046 let authority: DirectoryAuthority = MINIMAL_CONSENSUS_AUTHORITY.parse().unwrap();
1047 assert_eq!("Unnamed", authority.nickname);
1048 }
1049
1050 #[test]
1051 fn test_display() {
1052 let authority = DirectoryAuthority::parse(MINIMAL_CONSENSUS_AUTHORITY, false).unwrap();
1053 let display = format!("{}", authority);
1054 assert!(display.contains("dir-source Unnamed"));
1055 }
1056}