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