1use anyhow::{bail, ensure, Result};
6use openssl::{
7 memcmp,
8 pkey::{Id, PKey},
9 sign::Signer,
10 symm::{Cipher, Crypter, Mode},
11};
12use std::collections::BTreeMap;
13use zeroize::Zeroizing;
14
15const HEADER: usize = 19;
16const MAX_PACKET: usize = 1519;
17const MAX_CONF: usize = 1024;
18const WINDOW: u16 = 64;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct MediaEpoch {
22 pub account_generation: u64,
23 pub logout_epoch: u64,
24 pub call_generation: u64,
25 pub key_generation: u64,
26}
27impl MediaEpoch {
28 fn validate(self) -> Result<()> {
29 ensure!(
30 self.account_generation != 0 && self.call_generation != 0 && self.key_generation != 0,
31 "Inactive media generation"
32 );
33 Ok(())
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct MediaContext {
39 pub room_id: u64,
40 pub own_member: u16,
41 pub peer_member: u16,
42 pub connection_id: u16,
43}
44
45pub enum TransmitScope {
50 VerifiedGeneration {
51 key_generation: u64,
52 },
53 IsolatedCompatibilityTest {
54 intent_id: [u8; 32],
55 deadline_ms: u64,
56 allow_known_audio_overlap: bool,
57 },
58}
59
60pub struct OpenedPayload {
64 pub packed_kind: u8,
65 pub sequence: u16,
66 payload: Zeroizing<Vec<u8>>,
67 epoch: MediaEpoch,
68 context: MediaContext,
69}
70impl OpenedPayload {
71 pub fn payload(&self) -> &[u8] {
72 &self.payload
73 }
74}
75
76pub struct Live96 {
80 key: Zeroizing<[u8; 32]>,
81 base: Zeroizing<u64>,
82 context: MediaContext,
83 epoch: MediaEpoch,
84 tx_scope: Option<TransmitScope>,
85 next_audio: u32,
86 next_control: u32,
87 clear_sequence: u32,
88 rx_highest: BTreeMap<u8, u16>,
89 rx_seen: BTreeMap<u8, Box<[u64; 1024]>>,
90 rx: BTreeMap<[u8; 8], [u8; 32]>,
91 audio_overlaps: u64,
92 closed: bool,
93}
94impl Live96 {
95 pub fn new(
96 key: [u8; 32],
97 nonce_base: u64,
98 context: MediaContext,
99 epoch: MediaEpoch,
100 ) -> Result<Self> {
101 epoch.validate()?;
102 ensure!(
103 context.room_id != 0 && nonce_base != 0 && context.own_member != context.peer_member,
104 "Invalid media room/member context"
105 );
106 Ok(Self {
107 key: Zeroizing::new(key),
108 base: Zeroizing::new(nonce_base),
109 context,
110 epoch,
111 tx_scope: None,
112 next_audio: 0,
113 next_control: 0,
114 clear_sequence: 0,
115 rx_highest: BTreeMap::new(),
116 rx_seen: BTreeMap::new(),
117 rx: BTreeMap::new(),
118 audio_overlaps: 0,
119 closed: false,
120 })
121 }
122 fn check(&mut self, current: MediaEpoch) -> Result<()> {
123 if self.closed || self.epoch != current {
124 self.close();
125 bail!("Media generation changed or closed");
126 }
127 Ok(())
128 }
129 pub fn authorize_transmit(&mut self, current: MediaEpoch, scope: TransmitScope) -> Result<()> {
130 self.check(current)?;
131 ensure!(
132 self.tx_scope.is_none(),
133 "Media transmit scope already installed"
134 );
135 match &scope {
136 TransmitScope::VerifiedGeneration { key_generation } => ensure!(
137 *key_generation == current.key_generation,
138 "Media key generation mismatch"
139 ),
140 TransmitScope::IsolatedCompatibilityTest {
141 intent_id,
142 deadline_ms,
143 ..
144 } => ensure!(
145 *intent_id != [0; 32] && *deadline_ms != 0,
146 "Missing isolated media test scope"
147 ),
148 };
149 self.tx_scope = Some(scope);
150 Ok(())
151 }
152 fn tx_check(&mut self, current: MediaEpoch, now_ms: u64) -> Result<()> {
153 self.check(current)?;
154 match &self.tx_scope {
155 None => bail!("Media transmission is not authorized"),
156 Some(TransmitScope::VerifiedGeneration { .. }) => Ok(()),
157 Some(TransmitScope::IsolatedCompatibilityTest { deadline_ms, .. }) => {
158 if now_ms >= *deadline_ms {
159 self.close();
160 bail!("Isolated media test expired");
161 }
162 Ok(())
163 }
164 }
165 }
166 fn nonce(&self, kind: u8, sequence: u16) -> [u8; 8] {
167 self.base
168 .wrapping_add(u64::from(kind) << 32)
169 .wrapping_add(u64::from(sequence))
170 .to_le_bytes()
171 }
172 fn diagnostic_audio_overlap(&mut self, kind: u8) -> Result<()> {
173 if kind == 0x10
174 && matches!(
175 self.tx_scope,
176 Some(TransmitScope::IsolatedCompatibilityTest {
177 allow_known_audio_overlap: true,
178 ..
179 })
180 )
181 {
182 self.audio_overlaps = self
183 .audio_overlaps
184 .checked_add(1)
185 .ok_or_else(|| anyhow::anyhow!("Media diagnostic count exhausted"))?;
186 Ok(())
187 } else {
188 self.close();
189 bail!("Known media cross-direction nonce reuse")
190 }
191 }
192 pub fn known_audio_overlaps(&self) -> u64 {
193 self.audio_overlaps
194 }
195 pub fn seal_audio(
196 &mut self,
197 current: MediaEpoch,
198 now_ms: u64,
199 conf_payload: &[u8],
200 ) -> Result<Vec<u8>> {
201 parse_ordinary95(conf_payload)?;
202 self.seal(current, now_ms, 0x10, conf_payload)
203 }
204 fn seal(
205 &mut self,
206 current: MediaEpoch,
207 now_ms: u64,
208 kind: u8,
209 payload: &[u8],
210 ) -> Result<Vec<u8>> {
211 self.tx_check(current, now_ms)?;
212 ensure!(
213 !payload.is_empty() && payload.len() <= 1482,
214 "Invalid media payload extent"
215 );
216 let next = match kind {
217 0x10 => &mut self.next_audio,
218 0x70 => &mut self.next_control,
219 _ => bail!("Unsupported media sender family"),
220 };
221 ensure!(
222 *next <= u32::from(u16::MAX) && self.clear_sequence < u32::MAX,
223 "Media sequence exhausted; verified rekey required"
224 );
225 let sequence = *next as u16;
226 *next += 1;
227 let nonce = self.nonce(kind, sequence);
228 if self
229 .rx_seen
230 .get(&kind)
231 .is_some_and(|seen| seen[usize::from(sequence) / 64] & (1u64 << (sequence % 64)) != 0)
232 {
233 self.diagnostic_audio_overlap(kind)?;
234 }
235 let mut protected = Zeroizing::new(Vec::with_capacity(payload.len() + 2));
236 protected.extend(self.context.own_member.to_le_bytes());
237 protected.extend(payload);
238 let cipher = seal_original(&self.key, nonce, &protected)?;
239 let mut packet = Vec::with_capacity(HEADER + cipher.len());
240 packet.extend([0x96, 19]);
241 packet.extend(self.context.room_id.to_le_bytes());
242 packet.extend(self.context.own_member.to_le_bytes());
243 packet.extend((self.clear_sequence as u16).to_le_bytes());
244 packet.push(kind);
245 packet.extend(sequence.to_le_bytes());
246 packet.extend([0, self.context.connection_id as u8]);
247 packet.extend(cipher);
248 self.clear_sequence += 1;
249 Ok(packet)
250 }
251 pub fn open(&mut self, current: MediaEpoch, packet: &[u8]) -> Result<Option<OpenedPayload>> {
252 self.check(current)?;
253 ensure!(
254 (38..=MAX_PACKET).contains(&packet.len()) && packet[..2] == [0x96, 19],
255 "Unsupported Live96 packet shape"
256 );
257 ensure!(
258 u64::from_le_bytes(packet[2..10].try_into()?) == self.context.room_id
259 && u16::from_le_bytes(packet[10..12].try_into()?) == self.context.peer_member,
260 "Media room/member mismatch"
261 );
262 let kind = packet[14];
263 ensure!(
264 matches!(kind, 0x10 | 0x20 | 0x70) && packet[17] == 0,
265 "Unsupported Live96 packet family or extension"
266 );
267 let sequence = u16::from_le_bytes(packet[15..17].try_into()?);
268 let nonce = self.nonce(kind, sequence);
269 let clear = open_original(&self.key, nonce, &packet[HEADER..])?;
270 let digest: [u8; 32] = openssl::sha::sha256(&packet[HEADER..]);
271 if let Some(previous) = self.rx.get(&nonce) {
272 if *previous != digest {
273 self.close();
274 bail!("Authenticated media nonce conflict");
275 }
276 return Ok(None);
277 }
278 let own_next = if kind == 0x10 {
279 self.next_audio
280 } else if kind == 0x70 {
281 self.next_control
282 } else {
283 0
284 };
285 if u32::from(sequence) < own_next {
286 self.diagnostic_audio_overlap(kind)?;
287 }
288 let seen = self
292 .rx_seen
293 .entry(kind)
294 .or_insert_with(|| Box::new([0; 1024]));
295 seen[usize::from(sequence) / 64] |= 1u64 << (sequence % 64);
296 if self
297 .rx_highest
298 .get(&kind)
299 .is_some_and(|high| sequence < *high && *high - sequence >= WINDOW)
300 {
301 return Ok(None);
302 }
303 let high = self.rx_highest.entry(kind).or_insert(sequence);
304 *high = (*high).max(sequence);
305 let minimum = high.saturating_sub(WINDOW - 1);
306 let base = *self.base;
307 self.rx.retain(|nonce, _| {
308 let combined = u64::from_le_bytes(*nonce).wrapping_sub(base);
309 let k = (combined >> 32) as u8;
310 let s = combined as u16;
311 k != kind || s >= minimum
312 });
313 self.rx.insert(nonce, digest);
314 ensure!(
315 clear.len() > 2
316 && clear.len() <= 1484
317 && u16::from_le_bytes(clear[..2].try_into()?) == self.context.peer_member,
318 "Protected media member mismatch or extent"
319 );
320 Ok(Some(OpenedPayload {
322 packed_kind: kind,
323 sequence,
324 payload: Zeroizing::new(clear[2..].to_vec()),
325 epoch: current,
326 context: self.context,
327 }))
328 }
329 pub fn reply_to_probe(
333 &mut self,
334 current: MediaEpoch,
335 now_ms: u64,
336 opened: &OpenedPayload,
337 ) -> Result<Option<Vec<u8>>> {
338 self.check(current)?;
339 ensure!(
340 opened.epoch == current && opened.context == self.context,
341 "Stale authenticated control payload"
342 );
343 if opened.packed_kind != 0x70 {
344 return Ok(None);
345 }
346 match opened.payload() {
347 [8, 1, 18, 2, 8, 1] => {
348 if self.next_control <= u32::from(u16::MAX)
351 && self.rx_seen.get(&0x70).is_some_and(|seen| {
352 let sequence = self.next_control as usize;
353 seen[sequence / 64] & (1u64 << (sequence % 64)) != 0
354 })
355 {
356 return Ok(None);
357 }
358 Ok(Some(self.seal(
359 current,
360 now_ms,
361 0x70,
362 &[8, 1, 18, 2, 8, 2],
363 )?))
364 }
365 [8, 1, 18, 2, 8, 0 | 2] => Ok(None),
366 _ => Ok(None),
369 }
370 }
371 pub fn close(&mut self) {
372 self.key = Zeroizing::new([0; 32]);
373 self.base = Zeroizing::new(0);
374 self.tx_scope = None;
375 self.closed = true;
376 self.rx.clear();
377 self.rx_highest.clear();
378 self.rx_seen.clear();
379 }
380}
381
382pub fn ordinary95_single(
384 metadata: u8,
385 sequence: u16,
386 timestamp: u32,
387 frame: &[u8],
388) -> Result<Vec<u8>> {
389 ensure!(
390 !frame.is_empty() && frame.len() <= 1023 && 15 + frame.len() <= MAX_CONF,
391 "Invalid ordinary95 primary extent"
392 );
393 let mut out = Vec::with_capacity(15 + frame.len());
394 out.extend([1, metadata, 0, 0, 0x95, 0x74]);
395 out.extend(sequence.to_be_bytes());
396 out.extend(timestamp.to_be_bytes());
397 out.push(1);
398 out.extend((frame.len() as i16).to_le_bytes());
399 out.extend(frame);
400 Ok(out)
401}
402
403pub fn ordinary95_pair(
406 metadata: u8,
407 last_sequence: u16,
408 last_timestamp: u32,
409 first: &[u8],
410 second: &[u8],
411) -> Result<Vec<u8>> {
412 ensure!(
413 !first.is_empty()
414 && !second.is_empty()
415 && first.len() <= 1023
416 && second.len() <= 1023
417 && 17 + first.len() + second.len() <= MAX_CONF,
418 "Invalid ordinary95 primary extent"
419 );
420 let mut out = Vec::with_capacity(17 + first.len() + second.len());
421 out.extend([1, metadata, 0, 0, 0x95, 0x74]);
422 out.extend(last_sequence.to_be_bytes());
423 out.extend(last_timestamp.to_be_bytes());
424 out.push(2);
425 out.extend((first.len() as i16).to_le_bytes());
426 out.extend((second.len() as i16).to_le_bytes());
427 out.extend(first);
428 out.extend(second);
429 Ok(out)
430}
431
432pub struct OrdinaryAudio<'a> {
435 pub metadata: [u8; 4],
436 pub last_sequence: u16,
437 pub last_timestamp: u32,
438 pub primaries: Vec<&'a [u8]>,
439}
440pub fn parse_ordinary95(payload: &[u8]) -> Result<OrdinaryAudio<'_>> {
441 ensure!(
442 (16..=MAX_CONF).contains(&payload.len())
443 && payload[0] == 1
444 && payload[4..6] == [0x95, 0x74],
445 "Unsupported ordinary95 audio shape"
446 );
447 let count = usize::from(payload[12]);
448 ensure!(
449 (1..=3).contains(&count) && 13 + 2 * count <= payload.len(),
450 "Invalid ordinary95 primary count"
451 );
452 let mut cursor = 13 + 2 * count;
453 let mut primaries = Vec::with_capacity(count);
454 for n in 0..count {
455 let length = i16::from_le_bytes(payload[13 + 2 * n..15 + 2 * n].try_into()?);
456 ensure!(
457 (1..=1023).contains(&length),
458 "Invalid ordinary95 primary length"
459 );
460 let length = length as usize;
461 ensure!(
462 length <= payload.len() - cursor,
463 "Truncated ordinary95 primary"
464 );
465 primaries.push(&payload[cursor..cursor + length]);
466 cursor += length;
467 }
468 ensure!(cursor == payload.len(), "Trailing ordinary95 bytes");
469 Ok(OrdinaryAudio {
470 metadata: payload[..4].try_into()?,
471 last_sequence: u16::from_be_bytes(payload[6..8].try_into()?),
472 last_timestamp: u32::from_be_bytes(payload[8..12].try_into()?),
473 primaries,
474 })
475}
476fn xor(key: &[u8; 32], nonce: [u8; 8], counter: u64, input: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
477 let mut iv = Zeroizing::new([0; 16]);
478 iv[..8].copy_from_slice(&counter.to_le_bytes());
479 iv[8..].copy_from_slice(&nonce);
480 let cipher = Cipher::chacha20();
481 let mut crypter = Crypter::new(cipher, Mode::Encrypt, key, Some(&*iv))?;
482 crypter.pad(false);
483 let mut out = Zeroizing::new(vec![0; input.len() + cipher.block_size()]);
484 let n = crypter.update(input, &mut out)?;
485 let end = crypter.finalize(&mut out[n..])?;
486 out.truncate(n + end);
487 ensure!(out.len() == input.len(), "Unexpected call cipher size");
488 Ok(out)
489}
490fn tag(key: &[u8; 32], nonce: [u8; 8], cipher: &[u8]) -> Result<[u8; 16]> {
491 let one = xor(key, nonce, 0, &[0; 32])?;
492 let key = PKey::private_key_from_raw_bytes(&one, Id::POLY1305)?;
493 let mut signer = Signer::new_without_digest(&key)?;
494 signer.update(&0u64.to_le_bytes())?;
495 signer.update(cipher)?;
496 signer.update(&(cipher.len() as u64).to_le_bytes())?;
497 let mut tag = [0; 16];
498 ensure!(signer.sign(&mut tag)? == 16, "Unexpected call tag size");
499 Ok(tag)
500}
501fn seal_original(key: &[u8; 32], nonce: [u8; 8], clear: &[u8]) -> Result<Vec<u8>> {
502 ensure!(clear.len() <= 1484, "Call cipher input too large");
503 let cipher = xor(key, nonce, 1, clear)?;
504 let tag = tag(key, nonce, &cipher)?;
505 let mut out = Vec::with_capacity(cipher.len() + 16);
506 out.extend(&*cipher);
507 out.extend(tag);
508 Ok(out)
509}
510fn open_original(key: &[u8; 32], nonce: [u8; 8], sealed: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
511 ensure!(
512 (16..=1500).contains(&sealed.len()),
513 "Invalid call ciphertext extent"
514 );
515 let (cipher, expected) = sealed.split_at(sealed.len() - 16);
516 let actual = tag(key, nonce, cipher)?;
517 ensure!(
518 memcmp::eq(&actual, expected),
519 "Call media authentication failed"
520 );
521 xor(key, nonce, 1, cipher)
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 fn epoch() -> MediaEpoch {
528 MediaEpoch {
529 account_generation: 1,
530 logout_epoch: 0,
531 call_generation: 1,
532 key_generation: 1,
533 }
534 }
535 fn context() -> MediaContext {
536 MediaContext {
537 room_id: 9,
538 own_member: 0,
539 peer_member: 1,
540 connection_id: 2,
541 }
542 }
543 fn receiver() -> Live96 {
544 Live96::new([7; 32], 11, context(), epoch()).unwrap()
545 }
546 fn peer() -> Live96 {
547 let mut c = context();
548 std::mem::swap(&mut c.own_member, &mut c.peer_member);
549 let mut sender = Live96::new([7; 32], 11, c, epoch()).unwrap();
550 sender
551 .authorize_transmit(
552 epoch(),
553 TransmitScope::VerifiedGeneration { key_generation: 1 },
554 )
555 .unwrap();
556 sender
557 }
558 #[test]
559 fn original_aead_matches_independent_literal_vectors() {
560 let vectors = [
563 (
564 "43b718d5a398bf36a5f3904a469febac12f7b5b22efa1ae5cc5deb54e75f5574",
565 "f06be3d7749e299c",
566 "",
567 "a02210edb5c4fd736d7366ae81089103",
568 ),
569 (
570 "9456a10f0c4b0644e9524fedcc329f052c443f55df5d911f4c172e07121e3740",
571 "beb908121cb81960",
572 "67736a080137aef7b92f53145b6ec56622",
573 "4576fc20a665cd99f668062c319677cd82a2bbd02898dbd27db411e002570cf3d5",
574 ),
575 (
576 "4b0fdc9081934247c7e945a10ca2ef52989dad5a2477fe041a6780e06d9254ed",
577 "948fb8afa967f654",
578 "1169e988a0972d513ca73734aad2e21f053d292b78d3eb9d3a6486da320d3e9bc67871bb80fdceb0f18b197aa8862b69b2b83993bab8bff256ff7c87ff9a486beb",
579 "ff673095b4835ccdbbd73719c1351799309fca1063534ccd3fb8a5a5b4492b282ef72fa53cb0af85ed86900773e081dcd78ae54215970a122fef411d62561ad3dd67a359f949c8d666c180287b67f297d3",
580 ),
581 ];
582 for (key_hex, nonce_hex, plaintext_hex, expected_hex) in vectors {
583 let key = hex::decode(key_hex).unwrap().try_into().unwrap();
584 let nonce = hex::decode(nonce_hex).unwrap().try_into().unwrap();
585 let plaintext = hex::decode(plaintext_hex).unwrap();
586 let expected = hex::decode(expected_hex).unwrap();
587 assert_eq!(seal_original(&key, nonce, &plaintext).unwrap(), expected);
588 assert_eq!(&*open_original(&key, nonce, &expected).unwrap(), &plaintext);
589 }
590 }
591 #[test]
592 fn literal_primary_layout_and_extent() {
593 let single = ordinary95_single(3, 1, 320, &[1, 2]).unwrap();
594 assert_eq!(
595 single,
596 [1, 3, 0, 0, 0x95, 0x74, 0, 1, 0, 0, 1, 64, 1, 2, 0, 1, 2]
597 );
598 assert_eq!(
599 parse_ordinary95(&single).unwrap().primaries,
600 vec![&[1, 2][..]]
601 );
602 let raw = ordinary95_pair(3, 1, 320, &[1, 2], &[3]).unwrap();
603 assert_eq!(
604 &raw[..17],
605 &[1, 3, 0, 0, 0x95, 0x74, 0, 1, 0, 0, 1, 64, 2, 2, 0, 1, 0]
606 );
607 let decoded = parse_ordinary95(&raw).unwrap();
608 assert_eq!(decoded.primaries, vec![&[1, 2][..], &[3][..]]);
609 let mut trailing = raw;
610 trailing.push(0);
611 assert!(parse_ordinary95(&trailing).is_err());
612 }
613 #[test]
614 fn unauthorized_sender_fails_and_authenticated_receiver_rejects_replay() {
615 let raw = ordinary95_pair(0, 1, 320, &[1], &[2]).unwrap();
616 let mut receiver = receiver();
617 assert!(receiver.seal_audio(epoch(), 0, &raw).is_err());
618 let mut peer = peer();
619 let packet = peer.seal_audio(epoch(), 0, &raw).unwrap();
620 assert_eq!(&packet[..2], &[0x96, 19]);
621 let clear = receiver.open(epoch(), &packet).unwrap().unwrap();
622 assert_eq!(clear.payload(), raw);
623 assert!(receiver.open(epoch(), &packet).unwrap().is_none());
624 let mut corrupt = packet;
625 *corrupt.last_mut().unwrap() ^= 1;
626 assert!(receiver.open(epoch(), &corrupt).is_err());
627 }
628 #[test]
629 fn clear_header_member_cannot_replace_protected_member() {
630 let raw = ordinary95_pair(0, 1, 320, &[1], &[2]).unwrap();
631 let mut peer = peer();
632 let mut packet = peer.seal_audio(epoch(), 0, &raw).unwrap();
633 packet[10..12].copy_from_slice(&2u16.to_le_bytes());
634 assert!(receiver().open(epoch(), &packet).is_err());
635 }
636 #[test]
637 fn known_overlap_fails_closed_outside_explicit_test_scope() {
638 let raw = ordinary95_pair(0, 1, 320, &[1], &[2]).unwrap();
639 let mut peer = peer();
640 let packet = peer.seal_audio(epoch(), 0, &raw).unwrap();
641 let mut receiver = receiver();
642 receiver.open(epoch(), &packet).unwrap();
643 receiver
644 .authorize_transmit(
645 epoch(),
646 TransmitScope::VerifiedGeneration { key_generation: 1 },
647 )
648 .unwrap();
649 assert!(receiver.seal_audio(epoch(), 0, &raw).is_err());
650 let mut diagnostic = self::receiver();
651 diagnostic
652 .authorize_transmit(
653 epoch(),
654 TransmitScope::IsolatedCompatibilityTest {
655 intent_id: [1; 32],
656 deadline_ms: 100,
657 allow_known_audio_overlap: true,
658 },
659 )
660 .unwrap();
661 diagnostic.open(epoch(), &packet).unwrap();
662 assert!(diagnostic.seal_audio(epoch(), 1, &raw).is_ok());
663 assert_eq!(diagnostic.known_audio_overlaps(), 1);
664 assert!(diagnostic.seal_audio(epoch(), 100, &raw).is_err());
665 }
666 #[test]
667 fn authenticated_controls_preserve_nonce_fence_and_ignore_unhandled_messages() {
668 let mut other_peer = peer();
669 let other = other_peer.seal(epoch(), 0, 0x70, &[8, 7]).unwrap();
670 let mut other_receiver = receiver();
671 let opened = other_receiver.open(epoch(), &other).unwrap().unwrap();
672 assert!(other_receiver
673 .reply_to_probe(epoch(), 0, &opened)
674 .unwrap()
675 .is_none());
676 assert!(other_receiver.check(epoch()).is_ok());
677
678 let mut peer = peer();
679 let request = peer.seal(epoch(), 0, 0x70, &[8, 1, 18, 2, 8, 1]).unwrap();
680 let mut receiver = receiver();
681 let opened = receiver.open(epoch(), &request).unwrap().unwrap();
682 receiver
683 .authorize_transmit(
684 epoch(),
685 TransmitScope::VerifiedGeneration { key_generation: 1 },
686 )
687 .unwrap();
688 assert!(receiver
689 .reply_to_probe(epoch(), 0, &opened)
690 .unwrap()
691 .is_none());
692 assert!(receiver.check(epoch()).is_ok());
693 assert!(receiver
695 .seal(epoch(), 0, 0x70, &[8, 1, 18, 2, 8, 2])
696 .is_err());
697 }
698}