Skip to main content

darkbio_wire/transport/
client.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3
4use crate::LogId;
5use crate::transport::DEFAULT_HANDSHAKE_TIMEOUT;
6use crate::transport::framing::FrameReader;
7use crate::transport::handshake;
8use crate::transport::io::check_deadline;
9use crate::transport::outbound::{Outbound, Side};
10use crate::transport::sealing;
11use crate::transport::sender::Sender;
12use crate::transport::server::Attestation;
13use crate::transport::{
14    CRYPTO_DOMAIN_WIRE, CRYPTO_DOMAIN_WIRE_ARK_TO_HOST, CRYPTO_DOMAIN_WIRE_HOST_TO_ARK, Closer,
15    Error, Read, Stream, Write,
16};
17use darkbio_crypto::{cbor, cose, xdsa, xhpke};
18use darkbio_trust as trust;
19use std::sync::{Arc, Mutex};
20use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
21use tracing::{debug, info, trace, warn};
22
23/// Trust policy for the device attestation presented during a handshake.
24/// The caller decides which roots to trust and whether to allow self-signed
25/// attestations or recovery overrides. Transport enforces that decision.
26pub trait Verifier {
27    /// Session info extracted from an accepted attestation.
28    type Info;
29
30    /// Verifies the device attestation, returning the server's identity key along
31    /// with any info extracted from the attestation. Transport checks the
32    /// handshake signature against that key. Rejecting the attestation aborts
33    /// the handshake.
34    fn verify(&self, attestation: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String>;
35}
36
37/// Authenticates the handshake against this pinned identity key. The presented
38/// attestation is returned unchanged, without checking who issued it.
39impl Verifier for xdsa::PublicKey {
40    type Info = Attestation;
41
42    fn verify(&self, attestation: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String> {
43        Ok((self.clone(), attestation.clone()))
44    }
45}
46
47/// Roots trusted to attest Arks. Hardware and emulator roots are checked
48/// separately, and attestations must be valid at the current time. Self-signed
49/// attestations from devices that have not been onboarded are rejected.
50pub struct Roots<'a> {
51    pub hardware: &'a [xdsa::PublicKey], // Roots attesting hardware Arks
52    pub emulator: &'a [xdsa::PublicKey], // Roots attesting emulated Arks
53}
54
55impl Verifier for Roots<'_> {
56    type Info = trust::device::Device;
57
58    fn verify(&self, attestation: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String> {
59        let now = SystemTime::now()
60            .duration_since(UNIX_EPOCH)
61            .map_err(|err| err.to_string())?
62            .as_secs();
63
64        let device = trust::device::verify(
65            attestation.as_bytes(),
66            self.hardware,
67            self.emulator,
68            Some(now),
69        )
70        .map_err(|err| err.to_string())?;
71        Ok((device.signer.clone(), device))
72    }
73}
74/// Client side of the wire, exchanging encrypted messages over a byte stream.
75/// [`Client::connect`] sends a reset and runs the handshake. It returns a
76/// [`Sender`] for outbound messages. [`Client::recv`] decrypts inbound messages.
77///
78/// An empty frame from the server means it has no session with the client anymore.
79/// The client ends its session and returns [`Error::SessionReset`]. The caller
80/// can then reconnect.
81///
82/// Transport checks the shape of the device attestation. A [`Verifier`] decides
83/// whether to trust the server presenting it.
84pub struct Client<R: Read, W: Write> {
85    handshake_timeout: Duration, // Budget for each new handshake attempt
86
87    reader: FrameReader<R>, // COBS framed transport for ingress data
88    receiver: Option<xhpke::Receiver>, // Receive context used exclusively by this client
89    sealer: Option<Arc<Mutex<xhpke::Sender>>>, // Send context shared with active sends
90    outbound: Arc<Outbound<W>>, // Outgoing transport, shared with the senders
91    log_id: LogId,          // Label of the current session in log lines, unset before the first
92}
93
94impl<R: Read, W: Write> Client<R, W> {
95    /// Creates a client owning the byte stream and its shutdown operation, without
96    /// an encrypted session. Call [`Client::connect`] to establish one.
97    /// Output uses the stream's configured write timeout; its adapter must
98    /// enforce deadlines and the shutdown cancellation contract.
99    pub fn new(stream: Stream<R, W>) -> Self {
100        let (reader, writer, close, timeout) = stream.into_parts();
101
102        let outbound = Arc::new(Outbound::new(writer, Side::Client, close.clone(), timeout));
103
104        Self {
105            handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
106            reader: FrameReader::new(reader, close),
107            receiver: None,
108            sealer: None,
109            outbound,
110            log_id: LogId::default(),
111        }
112    }
113
114    /// Sets the budget for each subsequent handshake, starting when connect is
115    /// called. Defaults to [`DEFAULT_HANDSHAKE_TIMEOUT`]. Output and peer replies
116    /// share one deadline; progress and stale frames do not refresh it. Each
117    /// outgoing frame is also limited by the stream's write timeout. Waiting for
118    /// locks and verifier callbacks can extend the call beyond the deadline.
119    ///
120    /// Zero expires attempts immediately. A duration too large to add to an
121    /// [`Instant`] panics when the next handshake's deadline is constructed.
122    pub fn set_handshake_timeout(mut self, timeout: Duration) -> Self {
123        self.handshake_timeout = timeout;
124        self
125    }
126
127    /// A handle that permanently closes the stream from another thread.
128    pub fn closer(&self) -> Closer {
129        self.outbound.closer()
130    }
131
132    /// Permanently closes the stream and waits for adapter shutdown. Senders
133    /// observe closure through write failure; buffered messages remain readable.
134    /// See [`Closer::close`].
135    pub fn close(&self) {
136        self.outbound.close();
137    }
138
139    /// Establishes an encrypted session over the supplied stream, ending any
140    /// previous session first. Sends a reset and drives the handshake:
141    ///
142    ///   1. Client -> Server: HostHello { host_signer, host_crypto }           (plain CBOR)
143    ///   2. Server -> Client: ArkHello  { ark_attest, ark_crypto, a2h_encap }  (cose::seal)
144    ///   3. Client -> Server: HostAck   { h2a_encap }                          (cose::seal)
145    ///
146    /// The verifier receives the server's device attestation. Its accepted info
147    /// is returned alongside the new sender. That sender belongs to this session
148    /// and cannot send into a replacement established by a later handshake.
149    ///
150    /// Sends reset and hello before draining old input. The adapter must allow
151    /// that output to finish without concurrent client reads for this attempt
152    /// to progress. Backpressure can instead fail an outgoing frame on timeout.
153    ///
154    /// The handshake uses one configured deadline, shared by output and peer
155    /// waits. Existing writer-lock cleanup may extend the call. If connecting
156    /// fails, the client has no session and previously issued senders are invalid.
157    pub fn connect<V: Verifier>(&mut self, verifier: &V) -> Result<(Sender<W>, V::Info), Error> {
158        // Compute the deadline by which the handshake must finish
159        let deadline = Instant::now() + self.handshake_timeout;
160
161        // Generate ephemeral client keys for this session
162        let host_xdsa_sk = xdsa::SecretKey::generate();
163        let host_xhpke_sk = xhpke::SecretKey::generate();
164
165        self.handshake(verifier, host_xdsa_sk, host_xhpke_sk, None, deadline)
166    }
167
168    /// Ends any previous session, sends a reset and drives the handshake with the
169    /// given ephemeral keys. Returns the new session's sender and verified info.
170    /// The optional signing time makes the exchange deterministic for test vectors.
171    fn handshake<V: Verifier>(
172        &mut self,
173        verifier: &V,
174        host_xdsa_sk: xdsa::SecretKey,
175        host_xhpke_sk: xhpke::SecretKey,
176        timestamp: Option<i64>,
177        deadline: Instant,
178    ) -> Result<(Sender<W>, V::Info), Error> {
179        let host_xdsa_pk = host_xdsa_sk.public_key();
180        let host_xhpke_pk = host_xhpke_sk.public_key();
181        debug!("starting wire handshake");
182
183        // Message 1: Send HostHello (plain CBOR, COBS-framed)
184        let hello = cbor::encode(&handshake::HostHello {
185            host_signer: host_xdsa_pk.clone(),
186            host_crypto: host_xhpke_pk.clone(),
187        })
188        .map_err(|err| {
189            self.end_session();
190            Error::HandshakeFailed(format!("failed to encode client hello: {}", err))
191        })?;
192
193        // Retire the old binding and serialize reset/hello after admitted sends.
194        // The client starts reading only once this output has finished.
195        self.receiver = None;
196        self.sealer = None;
197        self.outbound.send_reset(deadline)?;
198        self.outbound.send_packet(&hello, Some(deadline))?;
199
200        // Message 2: Skip old replies, notifications and partial-frame leftovers
201        // until ArkHello names this attempt's fresh key. All draining shares the
202        // same deadline; authentication follows below.
203        let recipient = host_xhpke_pk.fingerprint();
204        let packet = loop {
205            let packet = match self.reader.next_packet(Some(deadline)) {
206                Ok(Some(packet)) => packet,
207                Ok(None) | Err(Error::FrameDecodingFailed(_) | Error::FrameTooLarge(_)) => &[],
208                Err(err) => return Err(err),
209            };
210            if cose::recipient(packet).is_ok_and(|fp| fp == recipient) {
211                break packet;
212            }
213            debug!("skipping stale frame during handshake");
214        };
215        let auth = handshake::ArkHelloAuth {
216            host_signer: host_xdsa_pk.clone(),
217            host_crypto: host_xhpke_pk.clone(),
218        };
219
220        // Step 2a: Decrypt the outer COSE_Encrypt0 layer
221        let sign1 =
222            cose::decrypt(packet, &auth, &host_xhpke_sk, CRYPTO_DOMAIN_WIRE).map_err(|err| {
223                Error::HandshakeFailed(format!("failed to decrypt server hello: {}", err))
224            })?;
225
226        // Step 2b: Peek at the unverified payload to discover the server's identity
227        let unverified: handshake::ArkHello = cose::peek(&sign1).map_err(|err| {
228            Error::HandshakeFailed(format!("invalid server hello payload: {}", err))
229        })?;
230
231        // Step 2c: Hand the attestation to the verifier to obtain the server's
232        // identity key and the caller's session info
233        let attestation = Attestation::new(unverified.ark_attest)?;
234        let (ark_identity, info) = verifier
235            .verify(&attestation)
236            .map_err(Error::HandshakeFailed)?;
237
238        // Step 2d: Verify the COSE_Sign1 signature with the discovered identity
239        let ark_hello: handshake::ArkHello =
240            cose::verify(&sign1, &auth, &ark_identity, CRYPTO_DOMAIN_WIRE, None).map_err(
241                |err| Error::HandshakeFailed(format!("server hello signature invalid: {}", err)),
242            )?;
243
244        // Set up the server->Client receiver context
245        let enc_a2h: [u8; xhpke::ENCAP_KEY_SIZE] = ark_hello
246            .a2h_encap
247            .try_into()
248            .map_err(|_| Error::HandshakeFailed("invalid a2h_encap size".into()))?;
249
250        let receiver = host_xhpke_sk
251            .new_receiver(&enc_a2h, CRYPTO_DOMAIN_WIRE_ARK_TO_HOST)
252            .map_err(|err| {
253                Error::HandshakeFailed(format!("client receiver setup failed: {}", err))
254            })?;
255
256        // Set up the Client->server sender context
257        let ark_xhpke_pk = ark_hello.ark_crypto;
258        let (sender, enc_h2a) = ark_xhpke_pk
259            .new_sender(CRYPTO_DOMAIN_WIRE_HOST_TO_ARK)
260            .map_err(|err| {
261                Error::HandshakeFailed(format!("client sender setup failed: {}", err))
262            })?;
263
264        // Message 3: Send HostAck (COSE seal'd, COBS-framed)
265        let ack = handshake::HostAck {
266            h2a_encap: enc_h2a.to_vec(),
267        };
268        let auth = handshake::HostAckAuth {
269            ark_signer: ark_identity,
270            ark_crypto: ark_xhpke_pk.clone(),
271        };
272        let ack = match timestamp {
273            Some(timestamp) => cose::seal_at(
274                &ack,
275                &auth,
276                &host_xdsa_sk,
277                &ark_xhpke_pk,
278                CRYPTO_DOMAIN_WIRE,
279                timestamp,
280            ),
281            None => cose::seal(
282                &ack,
283                &auth,
284                &host_xdsa_sk,
285                &ark_xhpke_pk,
286                CRYPTO_DOMAIN_WIRE,
287            ),
288        }
289        .map_err(|err| Error::HandshakeFailed(format!("failed to seal client ack: {}", err)))?;
290
291        self.outbound.send_packet(&ack, Some(deadline))?;
292        check_deadline(deadline).map_err(Error::RecvFailed)?;
293
294        // Session established, the ack ahead of anything sealed into it
295        let sender = self.new_session(sender, receiver);
296        info!(
297            "wire session {} established with ark {}",
298            self.log_id,
299            hex(&auth.ark_signer.fingerprint())
300        );
301        Ok((sender, info))
302    }
303
304    /// Reads and decrypts the next ark-to-host message. Invalid or oversized
305    /// frames end the session because its encryption sequence may be lost.
306    /// Decryption failures also end the session.
307    /// An empty frame means the server dropped the session and ends it here too.
308    /// Adapter read failures and EOF also end the session. Idle read timeouts are
309    /// retried internally. Call [`Self::connect`] to establish a new session after failure.
310    /// Without a receive context, returns an error without reading the stream.
311    ///
312    /// After decryption, message acceptance is ordered with session ending
313    /// without waiting for the writer. A concurrent send failure can cause a
314    /// decrypted message to be discarded before acceptance. An accepted message
315    /// may reach this caller after another thread ends the session. Returning
316    /// a receive error does wait for outgoing writes to finish.
317    pub fn recv(&mut self) -> Result<Vec<u8>, Error> {
318        let receiver = self
319            .receiver
320            .as_mut()
321            .ok_or_else(|| Error::EncryptionFailed("no active session".into()))?;
322
323        // Retrieve the next COBS encoded packet. A skipped frame may have
324        // carried a sealed message, so the session cannot continue past it.
325        // An empty frame is the server telling us it has no session with us.
326        let packet = match self.reader.next_packet(None) {
327            Err(err) => {
328                if matches!(err, Error::FrameDecodingFailed(_) | Error::FrameTooLarge(_)) {
329                    warn!("ending session {}: {}", self.log_id, err);
330                }
331                self.end_session();
332                return Err(err);
333            }
334            Ok(None) => {
335                info!("wire session {} reset by ark", self.log_id);
336                self.end_session();
337                return Err(Error::SessionReset);
338            }
339            Ok(Some(packet)) => packet,
340        };
341        let sealer = self
342            .sealer
343            .as_ref()
344            .expect("receiver has a sending context");
345        let opened = sealing::open(receiver, packet);
346        let undecryptable = opened.is_err();
347        let message = match self.outbound.finish_receive(sealer, opened) {
348            Err(err) => {
349                if undecryptable {
350                    warn!("ending session {}: {}", self.log_id, err);
351                } else {
352                    debug!(
353                        "discarding message read after session {} ended",
354                        self.log_id
355                    );
356                }
357                self.end_session();
358                return Err(err);
359            }
360            Ok(message) => message,
361        };
362        trace!("received ark-to-host message ({} bytes)", packet.len());
363        Ok(message)
364    }
365
366    /// Stores the negotiated contexts and returns a sender for the new session.
367    /// The sending context's allocation identifies the session. The client owns
368    /// both contexts and shares the sending context with active sends. Idle
369    /// senders hold weak references and keep neither context nor stream alive.
370    ///
371    /// Takes the writer lock, then the binding lock. An old write that already
372    /// holds the writer lock may finish first. Once the binding is replaced,
373    /// old sends cannot write and old received messages cannot be accepted.
374    /// This method performs no handshake, crypto or stream I/O.
375    fn new_session(&mut self, sender: xhpke::Sender, receiver: xhpke::Receiver) -> Sender<W> {
376        let sealer = Arc::new(Mutex::new(sender));
377        let sender = self.outbound.bind(&sealer);
378        self.log_id = sender.log_id();
379
380        self.receiver = Some(receiver);
381        self.sealer = Some(sealer);
382
383        sender
384    }
385
386    /// Ends the current binding before releasing the client's crypto contexts.
387    /// Waits for the writer. After this returns, no write or flush for that
388    /// session is running or can start. A send that gets the writer first may
389    /// finish. A send still sealing after removal cannot write its packet.
390    /// This takes no encryption lock and does not wait for crypto work.
391    ///
392    /// This does not close the stream or send a notification. An active write
393    /// may delay ending until its frame deadline. Another thread can use the
394    /// Closer to cancel I/O without taking the writer lock.
395    fn end_session(&mut self) {
396        if let Some(sealer) = self.sealer.as_ref() {
397            self.outbound.end(sealer);
398        }
399        self.receiver = None;
400        self.sealer = None;
401    }
402
403    /// Runs a test handshake with fixed keys and signing time for vector replay.
404    /// Not part of the normal transport API.
405    #[doc(hidden)]
406    #[inline]
407    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
408    #[cfg_attr(coverage_nightly, coverage(off))]
409    pub fn handshake_with_keys<V: Verifier>(
410        &mut self,
411        verifier: &V,
412        host_xdsa_sk: xdsa::SecretKey,
413        host_xhpke_sk: xhpke::SecretKey,
414        timestamp: i64,
415    ) -> Result<(Sender<W>, V::Info), Error> {
416        self.handshake(
417            verifier,
418            host_xdsa_sk,
419            host_xhpke_sk,
420            Some(timestamp),
421            Instant::now() + self.handshake_timeout,
422        )
423    }
424
425    /// Reads a framed packet without decryption for tests and benchmarks.
426    #[doc(hidden)]
427    #[inline]
428    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
429    #[cfg_attr(coverage_nightly, coverage(off))]
430    pub fn next_packet_blob(&mut self) -> Result<Option<&[u8]>, Error> {
431        self.reader.next_packet(None)
432    }
433
434    /// Writes a packet without encryption for tests and benchmarks.
435    #[doc(hidden)]
436    #[inline]
437    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
438    #[cfg_attr(coverage_nightly, coverage(off))]
439    pub fn send_packet_blob(&mut self, packet: &[u8]) -> Result<(), Error> {
440        self.outbound.send_packet(packet, None)
441    }
442
443    /// Reads an encoded frame without its delimiter for tests and benchmarks.
444    #[doc(hidden)]
445    #[inline]
446    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
447    #[cfg_attr(coverage_nightly, coverage(off))]
448    pub fn next_frame_blob(&mut self) -> Result<&[u8], Error> {
449        self.reader.next_frame_blob()
450    }
451
452    /// Writes an already encoded frame with a delimiter for tests and benchmarks.
453    #[doc(hidden)]
454    #[inline]
455    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
456    #[cfg_attr(coverage_nightly, coverage(off))]
457    pub fn send_frame_blob(&mut self, frame: &[u8]) -> Result<(), Error> {
458        self.outbound.send_frame_blob(frame)
459    }
460}
461
462impl<R: Read, W: Write> Drop for Client<R, W> {
463    /// Closes the stream to cancel blocked I/O, then ends the binding before
464    /// releasing the contexts. Shutdown must precede waiting for the writer.
465    /// Idle senders hold weak references and cannot extend the stream's lifetime.
466    fn drop(&mut self) {
467        self.outbound.close();
468        self.end_session();
469    }
470}
471
472/// Hex encodes a fingerprint for the session log line.
473fn hex(fingerprint: &xdsa::Fingerprint) -> String {
474    fingerprint
475        .to_bytes()
476        .iter()
477        .map(|byte| format!("{byte:02x}"))
478        .collect()
479}
480
481#[cfg(test)]
482#[cfg_attr(coverage_nightly, coverage(off))]
483mod tests {
484    use super::*;
485    use crate::transport::DEFAULT_WRITE_TIMEOUT;
486    use crate::transport::framing::FrameWriter;
487    use crate::transport::mock::{payload, self_attestation};
488    use crate::transport::server::Server;
489    use crate::transport::testing::Memory;
490    use crate::{memory, testing};
491    use std::io::{self, Read as _};
492    use std::sync::mpsc;
493    use std::thread;
494    use std::time::{Duration, Instant};
495
496    /// A pair of contexts standing in for an established session.
497    fn contexts() -> (xhpke::Sender, xhpke::Receiver) {
498        let secret = xhpke::SecretKey::generate();
499        let (sender, encap) = secret.public_key().new_sender(b"test").unwrap();
500        let receiver = secret.new_receiver(&encap, b"test").unwrap();
501        (sender, receiver)
502    }
503
504    /// Writer accepting bytes immediately but holding its first flush until
505    /// the test releases it. This distinguishes finished writes from a fully
506    /// completed send, which must also wait for its flush.
507    struct BlockedFlush {
508        entered: Option<mpsc::Sender<()>>,
509        release: mpsc::Receiver<()>,
510        deadline: Option<Instant>,
511    }
512
513    impl Write for BlockedFlush {
514        fn set_write_deadline(&mut self, deadline: Instant) -> io::Result<()> {
515            self.deadline = Some(deadline);
516            Ok(())
517        }
518    }
519
520    impl io::Write for BlockedFlush {
521        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
522            testing::remaining(self.deadline.expect("write deadline installed"))?;
523            Ok(bytes.len())
524        }
525
526        fn flush(&mut self) -> io::Result<()> {
527            let deadline = self.deadline.expect("write deadline installed");
528            testing::remaining(deadline)?;
529            if let Some(entered) = self.entered.take() {
530                entered.send(()).unwrap();
531                self.release
532                    .recv_timeout(testing::remaining(deadline)?)
533                    .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))?;
534            }
535            Ok(())
536        }
537    }
538
539    // Tests that ending waits through an active flush. After it returns the
540    // old sender is refused, and fresh contexts can use the still-open stream.
541    #[test]
542    fn test_end_waits_for_flush() {
543        testing::init_tracing();
544
545        let (entered_tx, entered) = mpsc::channel();
546        let (release, release_rx) = mpsc::channel();
547        let mut client = Client::new(Stream::new(
548            Memory::new(io::empty()),
549            BlockedFlush {
550                entered: Some(entered_tx),
551                release: release_rx,
552                deadline: None,
553            },
554            || {},
555        ));
556        let (crypto, receiver) = contexts();
557        let sender = client.new_session(crypto, receiver);
558        let sending = {
559            let sender = sender.clone();
560            thread::spawn(move || sender.send(&payload(1)))
561        };
562        entered.recv_timeout(Duration::from_secs(5)).unwrap();
563
564        let (started_tx, started) = mpsc::channel();
565        let (ended_tx, ended) = mpsc::channel();
566        let ending = thread::spawn(move || {
567            started_tx.send(()).unwrap();
568            client.end_session();
569            ended_tx.send(()).unwrap();
570            client
571        });
572        started.recv_timeout(Duration::from_secs(5)).unwrap();
573        let early = ended.recv_timeout(Duration::from_millis(50));
574        release.send(()).unwrap();
575        sending.join().unwrap().unwrap();
576        let mut client = ending.join().unwrap();
577        assert!(matches!(early, Err(mpsc::RecvTimeoutError::Timeout)));
578        ended.recv_timeout(Duration::from_secs(5)).unwrap();
579        assert!(matches!(
580            sender.send(&payload(2)),
581            Err(Error::EncryptionFailed(_))
582        ));
583
584        let (crypto, receiver) = contexts();
585        let fresh = client.new_session(crypto, receiver);
586        fresh.send(&payload(3)).unwrap();
587        assert!(matches!(
588            sender.send(&payload(4)),
589            Err(Error::EncryptionFailed(_))
590        ));
591    }
592
593    // Tests that a real receive reads, decrypts and returns a message while an
594    // outgoing flush is blocked. Waiting for the writer on the success path
595    // would time out before the test releases that flush.
596    #[test]
597    fn test_recv_during_blocked_flush() {
598        testing::init_tracing();
599
600        let (mut peer, receiver) = contexts();
601        let packet = sealing::seal(&mut peer, &payload(1)).unwrap();
602        let mut bytes = Vec::new();
603        FrameWriter::new(Memory::new(&mut bytes), Closer::new(|| {}))
604            .send_packet(&packet, Instant::now() + DEFAULT_WRITE_TIMEOUT)
605            .unwrap();
606        let (entered_tx, entered) = mpsc::channel();
607        let (release, release_rx) = mpsc::channel();
608        let mut client = Client::new(Stream::new(
609            Memory::new(io::Cursor::new(bytes)),
610            BlockedFlush {
611                entered: Some(entered_tx),
612                release: release_rx,
613                deadline: None,
614            },
615            || {},
616        ));
617        let sender = client.new_session(contexts().0, receiver);
618        let sending = thread::spawn(move || sender.send(&payload(2)));
619        entered.recv_timeout(Duration::from_secs(5)).unwrap();
620
621        let (received_tx, received) = mpsc::channel();
622        let receiving = thread::spawn(move || {
623            received_tx.send(client.recv()).unwrap();
624            client
625        });
626        let result = received.recv_timeout(Duration::from_secs(5));
627        // Release the writer even on a timeout, so a failing test can unwind.
628        release.send(()).unwrap();
629        sending.join().unwrap().unwrap();
630        let _client = receiving.join().unwrap();
631        assert_eq!(result.unwrap().unwrap(), payload(1));
632    }
633
634    // Tests that releasing an old session's contexts cannot invalidate its
635    // replacement, which can still receive and send. Dropping the client ends
636    // the replacement even while active operations retain its sending context
637    // and outbound transport, modeled here by retaining those references.
638    #[test]
639    fn test_owner_drop() {
640        testing::init_tracing();
641
642        let (mut peer, receiver) = contexts();
643        let packet = sealing::seal(&mut peer, &payload(2)).unwrap();
644        let mut bytes = Vec::new();
645        FrameWriter::new(Memory::new(&mut bytes), Closer::new(|| {}))
646            .send_packet(&packet, Instant::now() + DEFAULT_WRITE_TIMEOUT)
647            .unwrap();
648        let mut client = Client::new(Stream::new(
649            Memory::new(&bytes[..]),
650            Memory::new(Vec::new()),
651            || {},
652        ));
653        let (crypto, old_receiver) = contexts();
654        let stale = client.new_session(crypto, old_receiver);
655        let old_sealer = client.sealer.as_ref().unwrap().clone();
656
657        let fresh = client.new_session(contexts().0, receiver);
658        client.outbound.end(&old_sealer);
659        drop(old_sealer);
660        assert!(matches!(
661            stale.send(&payload(1)),
662            Err(Error::EncryptionFailed(_))
663        ));
664        assert_eq!(client.recv().unwrap(), payload(2));
665        fresh.send(&payload(3)).unwrap();
666
667        let outbound = client.outbound.clone();
668        let sealer = client.sealer.as_ref().unwrap().clone();
669        drop(client);
670        assert!(outbound.finish_receive(&sealer, Ok(Vec::new())).is_err());
671        assert!(matches!(
672            fresh.send(&payload(4)),
673            Err(Error::EncryptionFailed(_))
674        ));
675    }
676
677    // Tests sending from other threads while the client blocks in a read.
678    // The server must receive every message in encryption order to decrypt
679    // and echo it successfully.
680    #[test]
681    fn test_senders() {
682        testing::init_tracing();
683
684        // Echo every request over a bounded in-memory stream, then hang up
685        let (host, ark_stream) = memory::duplex(64 * 1024);
686
687        let signer = xdsa::SecretKey::generate();
688        let identity = signer.public_key();
689        let attestation = self_attestation(&signer);
690        let ark = thread::spawn(move || {
691            let mut server = Server::new(ark_stream, signer, attestation);
692            let mut sender = None;
693            for _ in 0..100 {
694                let req = testing::served(&mut server, &mut sender).unwrap();
695                sender.as_ref().unwrap().send(&req).unwrap();
696            }
697        });
698        let mut client = Client::new(host);
699        let (sender, _) = client.connect(&identity).unwrap();
700
701        // Send from a few threads at once while reading the echoes on this one
702        let senders: Vec<_> = (0..4)
703            .map(|thread| {
704                let sender = sender.clone();
705                thread::spawn(move || {
706                    for i in 0..25 {
707                        sender.send(&payload(thread * 100 + i)).unwrap();
708                    }
709                })
710            })
711            .collect();
712        let mut echoes: Vec<Vec<u8>> = (0..100).map(|_| client.recv().unwrap()).collect();
713        for sender in senders {
714            sender.join().unwrap();
715        }
716        ark.join().unwrap();
717
718        echoes.sort_unstable();
719        let mut expected: Vec<Vec<u8>> = (0..4)
720            .flat_map(|thread| (0..25).map(move |i| payload(thread * 100 + i)))
721            .collect();
722        expected.sort_unstable();
723        assert_eq!(echoes, expected);
724    }
725
726    // Tests that dropping the client ends the session for its senders and
727    // releases the transport writer even while sender handles remain.
728    #[test]
729    fn test_sender_outlives_client() {
730        testing::init_tracing();
731
732        let (mut reader, writer) = testing::pipe();
733        let (sender, receiver) = contexts();
734        let mut client = Client::new(Stream::new(Memory::new(io::empty()), writer, || {}));
735        let sender = client.new_session(sender, receiver);
736        sender.send(&payload(1)).unwrap();
737        drop(client);
738
739        let result = sender.send(&payload(2));
740        assert!(matches!(result, Err(Error::Terminated)), "{result:?}");
741        // The read only returns once the writer is gone
742        let mut bytes = Vec::new();
743        reader
744            .set_read_deadline(Some(Instant::now() + DEFAULT_WRITE_TIMEOUT))
745            .unwrap();
746        reader.read_to_end(&mut bytes).unwrap();
747        assert!(!bytes.is_empty());
748    }
749}