1use crate::control::{
22 ChallengeResponse, MAX_REFUSAL_REASON, RefusalKind, RequestId, TransportResponse,
23};
24use crate::error::ProtocolError;
25use crate::framing::PREFIX_LEN;
26
27pub const TAG_LEN: usize = 1;
30
31pub const REQUEST_ID_LEN: usize = 8;
34
35pub const CLIENT_PROTOCOL_VERSION: u16 = 1;
41
42pub const TAG_APP: u8 = 0x09;
51
52pub const TAG_RESPONSE_HEARTBEAT: u8 = 0x01;
53pub const TAG_BATCH_END: u8 = 0x02;
56pub const TAG_SERVER_BUSY: u8 = 0x04;
58pub const TAG_CHALLENGE: u8 = 0x05;
59pub const TAG_CHALLENGE_RESPONSE: u8 = 0x06;
60pub const TAG_AUTH_FAILED: u8 = 0x07;
61pub const TAG_SERVER_READY: u8 = 0x08;
62pub const TAG_BATCH_END_DEGRADED: u8 = 0x0A;
68pub const TAG_REQUEST: u8 = 0x0B;
71pub const TAG_REFUSED: u8 = 0x0C;
74
75const CONTROL_TAGS: [u8; 10] = [
78 TAG_RESPONSE_HEARTBEAT,
79 TAG_BATCH_END,
80 TAG_SERVER_BUSY,
81 TAG_CHALLENGE,
82 TAG_CHALLENGE_RESPONSE,
83 TAG_AUTH_FAILED,
84 TAG_SERVER_READY,
85 TAG_BATCH_END_DEGRADED,
86 TAG_REQUEST,
87 TAG_REFUSED,
88];
89
90pub const RETIRED_TAGS: [u8; 1] = [0x03];
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub(crate) enum TagRule {
100 AppTagOutOfRange,
102 OutOfRange(u8),
104 Duplicate(u8),
106 IsAppTag(u8),
108}
109
110pub(crate) const fn tag_rule_violation(control: &[u8], retired: &[u8], app: u8) -> Option<TagRule> {
117 if app == 0x00 || app >= 0x10 {
118 return Some(TagRule::AppTagOutOfRange);
119 }
120 let total = control.len() + retired.len();
121 let mut i = 0;
122 while i < total {
123 let tag = tag_at(control, retired, i);
124 if tag == 0x00 || tag >= 0x10 {
125 return Some(TagRule::OutOfRange(tag));
126 }
127 if tag == app {
128 return Some(TagRule::IsAppTag(tag));
129 }
130 let mut j = i + 1;
131 while j < total {
132 if tag_at(control, retired, j) == tag {
133 return Some(TagRule::Duplicate(tag));
134 }
135 j += 1;
136 }
137 i += 1;
138 }
139 None
140}
141
142const fn tag_at(control: &[u8], retired: &[u8], i: usize) -> u8 {
144 if i < control.len() {
145 control[i]
146 } else {
147 retired[i - control.len()]
148 }
149}
150
151const _: () = assert!(
152 tag_rule_violation(&CONTROL_TAGS, &RETIRED_TAGS, TAG_APP).is_none(),
153 "the protocol's tags break a rule of the tag space: see `tag_rule_violation`"
154);
155
156pub const BATCH_END_FRAME_LEN: usize = PREFIX_LEN + TAG_LEN + REQUEST_ID_LEN;
161
162pub const MAX_CONTROL_FRAME_LEN: usize =
165 PREFIX_LEN + TAG_LEN + REQUEST_ID_LEN + 2 + MAX_REFUSAL_REASON;
166
167const ID_OFFSET: usize = TAG_LEN;
170
171#[inline]
176pub fn write_terminal_id(frame: &mut [u8; BATCH_END_FRAME_LEN], id: RequestId) {
177 frame[PREFIX_LEN + ID_OFFSET..].copy_from_slice(&id.0.to_le_bytes());
178}
179
180#[inline]
186pub fn terminal_request_id(payload: &[u8]) -> Option<RequestId> {
187 match payload.first()? {
188 &TAG_BATCH_END | &TAG_BATCH_END_DEGRADED | &TAG_SERVER_BUSY | &TAG_REFUSED => {
189 read_id(payload)
190 }
191 _ => None,
192 }
193}
194
195#[inline]
198pub(crate) fn read_id(payload: &[u8]) -> Option<RequestId> {
199 let id = payload.get(ID_OFFSET..ID_OFFSET + REQUEST_ID_LEN)?;
200 Some(RequestId(u64::from_le_bytes(
201 id.try_into().expect("a slice of the ID's length"),
202 )))
203}
204
205fn truncate_reason(reason: &str) -> &str {
209 if reason.len() <= MAX_REFUSAL_REASON {
210 return reason;
211 }
212 let mut end = MAX_REFUSAL_REASON;
213 while !reason.is_char_boundary(end) {
216 end -= 1;
217 }
218 &reason[..end]
219}
220
221pub fn encode_transport_response(
229 response: &TransportResponse,
230 buf: &mut [u8],
231) -> Result<usize, ProtocolError> {
232 let reason = match response {
234 TransportResponse::Refused { reason, .. } => truncate_reason(reason),
235 _ => "",
236 };
237 let body_len = match response {
238 TransportResponse::Heartbeat
239 | TransportResponse::AuthFailed
240 | TransportResponse::ServerReady => 0,
241 TransportResponse::BatchEnd { .. }
242 | TransportResponse::BatchEndDegraded { .. }
243 | TransportResponse::ServerBusy { .. } => REQUEST_ID_LEN,
244 TransportResponse::Refused { .. } => REQUEST_ID_LEN + 2 + reason.len(),
245 TransportResponse::Challenge { .. } => CHALLENGE_VERSION_LEN + CHALLENGE_NONCE_LEN,
246 };
247 let needed = PREFIX_LEN + TAG_LEN + body_len;
248 let Some(frame) = buf.get_mut(..needed) else {
249 return Err(ProtocolError::Truncated);
250 };
251 let (tag, id) = match *response {
252 TransportResponse::Heartbeat => (TAG_RESPONSE_HEARTBEAT, None),
253 TransportResponse::BatchEnd { id } => (TAG_BATCH_END, Some(id)),
254 TransportResponse::BatchEndDegraded { id } => (TAG_BATCH_END_DEGRADED, Some(id)),
255 TransportResponse::ServerBusy { id } => (TAG_SERVER_BUSY, Some(id)),
256 TransportResponse::Refused { id, .. } => (TAG_REFUSED, Some(id)),
257 TransportResponse::Challenge { .. } => (TAG_CHALLENGE, None),
258 TransportResponse::AuthFailed => (TAG_AUTH_FAILED, None),
259 TransportResponse::ServerReady => (TAG_SERVER_READY, None),
260 };
261 let payload_len = (TAG_LEN + body_len) as u32;
263 frame[..PREFIX_LEN].copy_from_slice(&payload_len.to_le_bytes());
264 frame[PREFIX_LEN] = tag;
265 let mut pos = PREFIX_LEN + TAG_LEN;
266 if let Some(id) = id {
267 frame[pos..pos + REQUEST_ID_LEN].copy_from_slice(&id.0.to_le_bytes());
268 pos += REQUEST_ID_LEN;
269 }
270 match *response {
271 TransportResponse::Refused { kind, .. } => {
272 frame[pos] = kind.to_byte();
273 frame[pos + 1] = reason.len() as u8;
275 frame[pos + 2..].copy_from_slice(reason.as_bytes());
276 }
277 TransportResponse::Challenge { nonce } => {
278 frame[pos..pos + CHALLENGE_VERSION_LEN]
279 .copy_from_slice(&CLIENT_PROTOCOL_VERSION.to_le_bytes());
280 frame[pos + CHALLENGE_VERSION_LEN..].copy_from_slice(&nonce);
281 }
282 TransportResponse::Heartbeat
283 | TransportResponse::BatchEnd { .. }
284 | TransportResponse::BatchEndDegraded { .. }
285 | TransportResponse::ServerBusy { .. }
286 | TransportResponse::AuthFailed
287 | TransportResponse::ServerReady => {}
288 }
289 Ok(needed)
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub struct Refused<'a> {
296 pub id: RequestId,
297 pub kind: RefusalKind,
298 pub reason: &'a str,
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum BadRefusal {
304 Truncated,
306 UnknownKind(u8),
308 ReasonLength { declared: usize, available: usize },
311 ReasonNotUtf8,
313}
314
315impl std::fmt::Display for BadRefusal {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 match self {
318 Self::Truncated => f.write_str("a refusal too short for its fields"),
319 Self::UnknownKind(kind) => write!(f, "a refusal of unknown kind {kind:#04x}"),
320 Self::ReasonLength {
321 declared,
322 available,
323 } => write!(
324 f,
325 "a refusal declaring a {declared}-byte reason with {available} bytes after it"
326 ),
327 Self::ReasonNotUtf8 => f.write_str("a refusal whose reason is not UTF-8"),
328 }
329 }
330}
331
332pub fn decode_refused(payload: &[u8]) -> Result<Refused<'_>, BadRefusal> {
339 let id = read_id(payload).ok_or(BadRefusal::Truncated)?;
340 let fields = ID_OFFSET + REQUEST_ID_LEN;
341 let [kind, len, reason @ ..] = payload.get(fields..).ok_or(BadRefusal::Truncated)? else {
342 return Err(BadRefusal::Truncated);
343 };
344 let kind = RefusalKind::from_byte(*kind).ok_or(BadRefusal::UnknownKind(*kind))?;
345 let declared = usize::from(*len);
346 if reason.len() != declared {
347 return Err(BadRefusal::ReasonLength {
348 declared,
349 available: reason.len(),
350 });
351 }
352 let reason = std::str::from_utf8(reason).map_err(|_| BadRefusal::ReasonNotUtf8)?;
353 Ok(Refused { id, kind, reason })
354}
355
356const CHALLENGE_VERSION_LEN: usize = 2;
362
363pub const CHALLENGE_NONCE_LEN: usize = 32;
365
366pub const CHALLENGE_LEN: usize = TAG_LEN + CHALLENGE_VERSION_LEN + CHALLENGE_NONCE_LEN;
369
370pub const CHALLENGE_V0_LEN: usize = TAG_LEN + CHALLENGE_NONCE_LEN;
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum BadChallenge {
377 NotAChallenge { len: usize, tag: Option<u8> },
380 Version { node: u16 },
384}
385
386pub fn decode_challenge(payload: &[u8]) -> Result<[u8; CHALLENGE_NONCE_LEN], BadChallenge> {
391 let not_a_challenge = BadChallenge::NotAChallenge {
392 len: payload.len(),
393 tag: payload.first().copied(),
394 };
395 let Some((&TAG_CHALLENGE, rest)) = payload.split_first() else {
396 return Err(not_a_challenge);
397 };
398 match payload.len() {
399 CHALLENGE_V0_LEN => Err(BadChallenge::Version { node: 0 }),
400 CHALLENGE_LEN => {
401 let (version, nonce) = rest.split_at(CHALLENGE_VERSION_LEN);
402 let version = u16::from_le_bytes(version.try_into().expect("two bytes"));
403 if version != CLIENT_PROTOCOL_VERSION {
404 return Err(BadChallenge::Version { node: version });
405 }
406 Ok(nonce.try_into().expect("the nonce's length"))
407 }
408 _ => Err(not_a_challenge),
409 }
410}
411
412pub const CHALLENGE_RESPONSE_LEN: usize = TAG_LEN + 64 + 32;
415
416pub fn encode_challenge_response(
424 response: &ChallengeResponse,
425 buf: &mut [u8],
426) -> Result<usize, ProtocolError> {
427 if buf.len() < CHALLENGE_RESPONSE_LEN {
428 return Err(ProtocolError::Truncated);
429 }
430 buf[0] = TAG_CHALLENGE_RESPONSE;
431 buf[1..65].copy_from_slice(&response.signature);
432 buf[65..97].copy_from_slice(&response.public_key);
433 Ok(CHALLENGE_RESPONSE_LEN)
434}
435
436pub fn decode_challenge_response(buf: &[u8]) -> Result<ChallengeResponse, ProtocolError> {
443 if buf.len() < CHALLENGE_RESPONSE_LEN {
444 return Err(ProtocolError::Truncated);
445 }
446
447 let tag = buf[0];
448 if tag != TAG_CHALLENGE_RESPONSE {
449 return Err(ProtocolError::UnknownTag(tag));
450 }
451
452 let mut signature = [0u8; 64];
453 signature.copy_from_slice(&buf[1..65]);
454 let mut public_key = [0u8; 32];
455 public_key.copy_from_slice(&buf[65..97]);
456
457 Ok(ChallengeResponse {
458 signature,
459 public_key,
460 })
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use proptest::prelude::*;
467
468 fn every_response(id: RequestId) -> [TransportResponse; 8] {
470 [
471 TransportResponse::Heartbeat,
472 TransportResponse::BatchEnd { id },
473 TransportResponse::BatchEndDegraded { id },
474 TransportResponse::ServerBusy { id },
475 TransportResponse::Refused {
476 id,
477 kind: RefusalKind::Malformed,
478 reason: "short",
479 },
480 TransportResponse::Challenge { nonce: [0x5A; 32] },
481 TransportResponse::AuthFailed,
482 TransportResponse::ServerReady,
483 ]
484 }
485
486 fn encoded(response: &TransportResponse) -> Vec<u8> {
488 let mut buf = [0u8; MAX_CONTROL_FRAME_LEN];
489 let n = encode_transport_response(response, &mut buf).unwrap();
490 buf[..n].to_vec()
491 }
492
493 #[test]
494 fn tag_only_variants_are_five_bytes() {
495 for variant in [
496 TransportResponse::Heartbeat,
497 TransportResponse::AuthFailed,
498 TransportResponse::ServerReady,
499 ] {
500 let frame = encoded(&variant);
501 assert_eq!(frame.len(), PREFIX_LEN + TAG_LEN, "{variant:?}");
502 assert_eq!(u32::from_le_bytes(frame[..4].try_into().unwrap()), 1);
503 assert_eq!(terminal_request_id(&frame[PREFIX_LEN..]), None);
504 }
505 }
506
507 #[test]
510 fn each_terminal_encodes_with_its_id_and_decodes_back() {
511 let id = RequestId(0x0102_0304_0506_0708);
512 for (response, tag) in [
513 (TransportResponse::BatchEnd { id }, TAG_BATCH_END),
514 (
515 TransportResponse::BatchEndDegraded { id },
516 TAG_BATCH_END_DEGRADED,
517 ),
518 (TransportResponse::ServerBusy { id }, TAG_SERVER_BUSY),
519 ] {
520 let frame = encoded(&response);
521 assert_eq!(frame.len(), BATCH_END_FRAME_LEN, "{response:?}");
522 assert_eq!(
523 u32::from_le_bytes(frame[..4].try_into().unwrap()) as usize,
524 TAG_LEN + REQUEST_ID_LEN
525 );
526 assert_eq!(frame[PREFIX_LEN], tag);
527 assert_eq!(&frame[PREFIX_LEN + 1..], &id.0.to_le_bytes());
528 assert_eq!(terminal_request_id(&frame[PREFIX_LEN..]), Some(id));
529 }
530 let refused = encoded(&TransportResponse::Refused {
531 id,
532 kind: RefusalKind::PermissionDenied,
533 reason: "no",
534 });
535 assert_eq!(terminal_request_id(&refused[PREFIX_LEN..]), Some(id));
536 let zero = encoded(&TransportResponse::BatchEnd { id: RequestId(0) });
538 assert_eq!(terminal_request_id(&zero[PREFIX_LEN..]), Some(RequestId(0)));
539 }
540
541 #[test]
542 fn terminal_request_id_is_none_for_other_tags_and_short_heads() {
543 assert_eq!(terminal_request_id(&[]), None);
544 for tag in 0..=u8::MAX {
545 let head = [&[tag][..], &[7; REQUEST_ID_LEN]].concat();
546 let terminal = matches!(
547 tag,
548 TAG_BATCH_END | TAG_BATCH_END_DEGRADED | TAG_SERVER_BUSY | TAG_REFUSED
549 );
550 assert_eq!(
551 terminal_request_id(&head).is_some(),
552 terminal,
553 "tag {tag:#04x}"
554 );
555 for cut in 0..head.len() {
557 assert_eq!(terminal_request_id(&head[..cut]), None, "tag {tag:#04x}");
558 }
559 }
560 }
561
562 #[test]
566 fn every_encoded_tag_is_a_live_control_tag() {
567 for variant in &every_response(RequestId(1)) {
568 let frame = encoded(variant);
569 assert_ne!(frame[4], TAG_APP, "{variant:?}");
570 assert!(CONTROL_TAGS.contains(&frame[4]), "{variant:?}");
571 assert!(!RETIRED_TAGS.contains(&frame[4]), "{variant:?}");
572 }
573 }
574
575 #[test]
576 fn the_tag_rules_hold_and_are_checked() {
577 assert_eq!(
578 tag_rule_violation(&CONTROL_TAGS, &RETIRED_TAGS, TAG_APP),
579 None
580 );
581 assert_eq!(tag_rule_violation(&[1, 2, 3], &[4], 9), None);
582 assert_eq!(
583 tag_rule_violation(&[1, 2, 1], &[], 9),
584 Some(TagRule::Duplicate(1))
585 );
586 assert_eq!(
588 tag_rule_violation(&[1, 3], &[3], 9),
589 Some(TagRule::Duplicate(3))
590 );
591 assert_eq!(
592 tag_rule_violation(&[1, 0x00], &[], 9),
593 Some(TagRule::OutOfRange(0x00))
594 );
595 assert_eq!(
596 tag_rule_violation(&[1], &[0x10], 9),
597 Some(TagRule::OutOfRange(0x10))
598 );
599 assert_eq!(
600 tag_rule_violation(&[1, 9], &[], 9),
601 Some(TagRule::IsAppTag(9))
602 );
603 assert_eq!(
604 tag_rule_violation(&[1], &[], 0x00),
605 Some(TagRule::AppTagOutOfRange)
606 );
607 assert_eq!(
608 tag_rule_violation(&[1], &[], 0x10),
609 Some(TagRule::AppTagOutOfRange)
610 );
611 }
612
613 #[test]
614 fn every_refusal_kind_round_trips_and_no_other_byte_is_one() {
615 for kind in [RefusalKind::PermissionDenied, RefusalKind::Malformed] {
616 assert_eq!(RefusalKind::from_byte(kind.to_byte()), Some(kind));
617 }
618 for byte in 0..=u8::MAX {
619 match RefusalKind::from_byte(byte) {
620 Some(kind) => assert_eq!(kind.to_byte(), byte),
621 None => assert!(!(1..=2).contains(&byte), "{byte}"),
622 }
623 }
624 assert_eq!(RefusalKind::from_byte(0), None, "a zeroed byte is no kind");
625 }
626
627 fn refused_round_trip(reason: &'static str) -> (Vec<u8>, String) {
629 let id = RequestId(42);
630 let frame = encoded(&TransportResponse::Refused {
631 id,
632 kind: RefusalKind::Malformed,
633 reason,
634 });
635 let refused = decode_refused(&frame[PREFIX_LEN..]).unwrap();
636 assert_eq!(refused.id, id);
637 assert_eq!(refused.kind, RefusalKind::Malformed);
638 let reason = refused.reason.to_string();
639 (frame, reason)
640 }
641
642 #[test]
643 fn a_refusal_carries_its_kind_and_reason() {
644 let (frame, reason) = refused_round_trip("auditor role may not notarize");
645 assert_eq!(reason, "auditor role may not notarize");
646 assert_eq!(frame[PREFIX_LEN], TAG_REFUSED);
647 assert_eq!(
648 frame.len(),
649 PREFIX_LEN + TAG_LEN + REQUEST_ID_LEN + 2 + reason.len()
650 );
651 let (_, empty) = refused_round_trip("");
652 assert_eq!(empty, "");
653 }
654
655 #[test]
659 fn a_refusal_reason_is_truncated_on_a_character_boundary() {
660 let at: &'static str = "a".repeat(MAX_REFUSAL_REASON).leak();
662 let (frame, reason) = refused_round_trip(at);
663 assert_eq!(reason, at);
664 assert_eq!(frame.len(), MAX_CONTROL_FRAME_LEN);
665 assert_eq!(MAX_CONTROL_FRAME_LEN, 270);
666 assert_eq!(frame.len() - PREFIX_LEN, 266, "with the tag");
667
668 let past: &'static str = "b".repeat(MAX_REFUSAL_REASON + 1).leak();
670 assert_eq!(refused_round_trip(past).1, &past[..MAX_REFUSAL_REASON]);
671
672 let far: &'static str = "c".repeat(4 * MAX_REFUSAL_REASON).leak();
674 assert_eq!(refused_round_trip(far).1.len(), MAX_REFUSAL_REASON);
675
676 for lead in MAX_REFUSAL_REASON - 3..=MAX_REFUSAL_REASON {
678 let s: &'static str = format!("{}€tail", "d".repeat(lead)).leak();
679 let reason = refused_round_trip(s).1;
680 assert!(reason.len() <= MAX_REFUSAL_REASON, "lead {lead}");
681 assert!(s.starts_with(&reason), "lead {lead}");
682 let euro_fits = lead + '€'.len_utf8() <= MAX_REFUSAL_REASON;
683 assert_eq!(reason.ends_with('€'), euro_fits, "lead {lead}");
684 if !euro_fits {
685 assert_eq!(reason.len(), lead, "lead {lead}");
686 }
687 }
688 }
689
690 #[test]
691 fn a_malformed_refusal_is_refused() {
692 let good = encoded(&TransportResponse::Refused {
693 id: RequestId(1),
694 kind: RefusalKind::PermissionDenied,
695 reason: "abc",
696 });
697 let payload = &good[PREFIX_LEN..];
698 assert!(decode_refused(payload).is_ok());
699 for cut in 0..TAG_LEN + REQUEST_ID_LEN + 2 {
701 assert_eq!(decode_refused(&payload[..cut]), Err(BadRefusal::Truncated));
702 }
703 assert_eq!(
705 decode_refused(&payload[..payload.len() - 1]),
706 Err(BadRefusal::ReasonLength {
707 declared: 3,
708 available: 2
709 })
710 );
711 let long = [payload, b"x"].concat();
712 assert!(matches!(
713 decode_refused(&long),
714 Err(BadRefusal::ReasonLength {
715 declared: 3,
716 available: 4
717 })
718 ));
719 let kind_at = TAG_LEN + REQUEST_ID_LEN;
721 for kind in [0x00, 0x03, 0xFF] {
722 let mut bad = payload.to_vec();
723 bad[kind_at] = kind;
724 assert_eq!(decode_refused(&bad), Err(BadRefusal::UnknownKind(kind)));
725 }
726 let mut bad = payload.to_vec();
728 bad[kind_at + 2] = 0xFF;
729 assert_eq!(decode_refused(&bad), Err(BadRefusal::ReasonNotUtf8));
730 for err in [
732 BadRefusal::Truncated,
733 BadRefusal::UnknownKind(3),
734 BadRefusal::ReasonLength {
735 declared: 3,
736 available: 2,
737 },
738 BadRefusal::ReasonNotUtf8,
739 ] {
740 assert!(err.to_string().contains("refusal"), "{err}");
741 }
742 }
743
744 #[test]
745 fn encode_refuses_a_buffer_too_small_for_the_variant() {
746 for variant in every_response(RequestId(9)) {
747 let len = encoded(&variant).len();
748 let mut buf = vec![0u8; len - 1];
749 assert!(
750 matches!(
751 encode_transport_response(&variant, &mut buf),
752 Err(ProtocolError::Truncated)
753 ),
754 "{variant:?}"
755 );
756 let mut buf = vec![0u8; len];
757 assert_eq!(encode_transport_response(&variant, &mut buf).unwrap(), len);
758 }
759 }
760
761 #[test]
762 fn the_challenge_carries_the_version_ahead_of_the_nonce() {
763 let nonce = [0xAB; 32];
764 let frame = encoded(&TransportResponse::Challenge { nonce });
765 assert_eq!(frame.len(), PREFIX_LEN + CHALLENGE_LEN);
766 assert_eq!(CHALLENGE_LEN, 35);
767 assert_eq!(
768 u32::from_le_bytes(frame[..4].try_into().unwrap()) as usize,
769 CHALLENGE_LEN
770 );
771 assert_eq!(frame[4], TAG_CHALLENGE);
772 assert_eq!(&frame[5..7], &CLIENT_PROTOCOL_VERSION.to_le_bytes());
773 assert_eq!(&frame[7..], &nonce);
774 assert_eq!(decode_challenge(&frame[PREFIX_LEN..]), Ok(nonce));
775 }
776
777 #[test]
778 fn a_challenge_from_another_protocol_names_its_version() {
779 let v0 = [&[TAG_CHALLENGE][..], &[0x11; 32]].concat();
781 assert_eq!(v0.len(), CHALLENGE_V0_LEN);
782 assert_eq!(
783 decode_challenge(&v0),
784 Err(BadChallenge::Version { node: 0 })
785 );
786 for version in [0, 2, u16::MAX] {
788 let other = [&[TAG_CHALLENGE][..], &version.to_le_bytes(), &[0x11; 32]].concat();
789 assert_eq!(
790 decode_challenge(&other),
791 Err(BadChallenge::Version { node: version })
792 );
793 }
794 for wrong in [
796 vec![TAG_SERVER_READY],
797 vec![TAG_CHALLENGE; 34],
798 vec![TAG_CHALLENGE; 36],
799 [&[TAG_BATCH_END][..], &[0; 34]].concat(),
800 ] {
801 assert_eq!(
802 decode_challenge(&wrong),
803 Err(BadChallenge::NotAChallenge {
804 len: wrong.len(),
805 tag: wrong.first().copied()
806 })
807 );
808 }
809 assert_eq!(
810 decode_challenge(&[]),
811 Err(BadChallenge::NotAChallenge { len: 0, tag: None })
812 );
813 }
814
815 #[test]
816 fn decode_valid_challenge_response() {
817 let sig = [0x11; 64];
818 let pubkey = [0x22; 32];
819
820 let mut buf = [0u8; 97];
822 buf[0] = TAG_CHALLENGE_RESPONSE;
823 buf[1..65].copy_from_slice(&sig);
824 buf[65..97].copy_from_slice(&pubkey);
825
826 let cr = decode_challenge_response(&buf).unwrap();
827 assert_eq!(cr.signature, sig);
828 assert_eq!(cr.public_key, pubkey);
829 }
830
831 #[test]
832 fn challenge_response_round_trips() {
833 let response = ChallengeResponse {
834 signature: [0x33; 64],
835 public_key: [0x44; 32],
836 };
837 let mut buf = [0u8; CHALLENGE_RESPONSE_LEN];
838 let written = encode_challenge_response(&response, &mut buf).unwrap();
839 assert_eq!(written, CHALLENGE_RESPONSE_LEN);
840 assert_eq!(decode_challenge_response(&buf).unwrap(), response);
841
842 let mut short = [0u8; CHALLENGE_RESPONSE_LEN - 1];
843 assert!(matches!(
844 encode_challenge_response(&response, &mut short),
845 Err(ProtocolError::Truncated)
846 ));
847 }
848
849 #[test]
850 fn decode_truncated() {
851 let buf = [0u8; 50]; assert!(matches!(
853 decode_challenge_response(&buf),
854 Err(ProtocolError::Truncated)
855 ));
856 }
857
858 #[test]
859 fn decode_wrong_tag() {
860 let mut buf = [0u8; CHALLENGE_RESPONSE_LEN];
861 buf[0] = 99; assert!(matches!(
863 decode_challenge_response(&buf),
864 Err(ProtocolError::UnknownTag(99))
865 ));
866 }
867
868 proptest! {
869 #[test]
873 fn the_template_writer_matches_the_encoder(id in any::<u64>(), stale in any::<u64>()) {
874 let id = RequestId(id);
875 for (make, name) in [
876 (
877 (|id| TransportResponse::BatchEnd { id }) as fn(RequestId) -> TransportResponse,
878 "BatchEnd",
879 ),
880 (|id| TransportResponse::BatchEndDegraded { id }, "BatchEndDegraded"),
881 ] {
882 let mut template = [0u8; BATCH_END_FRAME_LEN];
883 let n = encode_transport_response(&make(RequestId(stale)), &mut template).unwrap();
884 prop_assert_eq!(n, BATCH_END_FRAME_LEN, "{}", name);
885 write_terminal_id(&mut template, id);
886 prop_assert_eq!(template.to_vec(), encoded(&make(id)), "{}", name);
887 }
888 }
889 }
890}