Skip to main content

darkbio_wire/transport/
server.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2025 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::{
13    CRYPTO_DOMAIN_WIRE, CRYPTO_DOMAIN_WIRE_ARK_TO_HOST, CRYPTO_DOMAIN_WIRE_HOST_TO_ARK, Closer,
14    Error, Read, Stream, Write,
15};
16use darkbio_crypto::{cbor, cose, cwt, xdsa, xhpke};
17use darkbio_trust as trust;
18use std::sync::{Arc, Mutex};
19use std::time::{Duration, Instant};
20use tracing::{debug, info, trace, warn};
21
22/// Device attestation presented during the handshake. The CWT must contain
23/// hardware or emulator claims as defined by darkbio-trust. Construction checks
24/// that shape; the client's verifier decides whether to trust the attestation.
25#[derive(Clone)]
26pub struct Attestation(Vec<u8>);
27
28impl Attestation {
29    /// Wraps a CWT after checking that it decodes as a device attestation.
30    pub fn new(cwt: Vec<u8>) -> Result<Self, Error> {
31        if cwt::peek::<trust::device::HardwareClaims>(&cwt).is_err()
32            && cwt::peek::<trust::device::EmulatorClaims>(&cwt).is_err()
33        {
34            return Err(Error::InvalidAttestation);
35        }
36        Ok(Self(cwt))
37    }
38
39    /// CWT bytes of the attestation.
40    pub fn as_bytes(&self) -> &[u8] {
41        &self.0
42    }
43
44    /// Unwraps the attestation into its CWT bytes.
45    pub fn into_bytes(self) -> Vec<u8> {
46        self.0
47    }
48}
49
50/// Supplies the server's device attestation on every handshake. This lets the
51/// server pick up a new attestation after onboarding without recreating transport.
52pub trait Attester {
53    /// Returns the device attestation to present to the client (e.g. a root-signed
54    /// CWT read from disk, or a self-signed fallback for pre-onboarding devices).
55    /// The identity key it embeds must be the one signing the wire's handshake.
56    fn attest(&mut self) -> Attestation;
57}
58
59/// A fixed attestation, presented as is on every handshake.
60impl Attester for Attestation {
61    fn attest(&mut self) -> Attestation {
62        self.clone()
63    }
64}
65
66/// A decrypted message or encrypted session transition returned by [`Server::recv`].
67/// Events arrive in receive order and refer to sessions over the existing byte
68/// stream. Permanent stream closure is observed through I/O results.
69#[derive(Debug)]
70pub enum Event<W: Write> {
71    /// A handshake completed and established an encrypted session. The sender
72    /// belongs to that session and cannot send into a later replacement.
73    /// Concurrent send failure or stream closure may make it unusable before
74    /// the caller handles the event.
75    Connected(Sender<W>),
76
77    /// The previously opened session ended through a peer reset, invalid
78    /// incoming data or an observed send failure. After a peer reset, the next
79    /// receive call runs the handshake. A local [`Server::disconnect`] does not emit this
80    /// event. Permanent stream closure is reported through I/O results instead.
81    Disconnected,
82
83    /// A decrypted message from the client.
84    Message(Vec<u8>),
85}
86
87/// Server side of the wire, accepting encrypted sessions over a supplied byte
88/// stream. [`Server::recv`] handles client resets and handshakes. Each successful
89/// handshake returns a sender through [`Event::Connected`]. Later reads deliver
90/// decrypted messages or report that the session ended.
91/// [`Server::disconnect`] ends a session while leaving the stream available for
92/// another; [`Server::close`] permanently closes the stream.
93///
94/// On a local disconnect, a session failure, a failed handshake or data received
95/// outside a session, the server attempts an empty frame notification. A client
96/// receiving it drops its old session. Notifications are best effort and bounded
97/// by an output deadline. A failed write's own notification uses only its
98/// remaining budget and is skipped after timeout. Later incoming traffic can
99/// prompt a standalone notification.
100///
101/// An [`Attester`] supplies the device attestation. Transport forwards it to the
102/// client, whose verifier decides whether to trust it.
103pub struct Server<R: Read, W: Write, A: Attester> {
104    reader: FrameReader<R>,     // COBS framed transport for ingress data
105    outbound: Arc<Outbound<W>>, // Outgoing transport, shared with the senders
106
107    signer: xdsa::SecretKey, // Server's identity key, signing the ArkHello
108    attester: A,             // Source of the device attestation for handshakes
109
110    receiver: Option<xhpke::Receiver>, // Receive context used exclusively by this server
111    sealer: Option<Arc<Mutex<xhpke::Sender>>>, // Send context shared with active sends
112
113    handshake_timeout: Duration, // Budget for each new handshake attempt
114    handshake_deadline: Option<Instant>, // Deadline of the handshake requested by a reset
115    log_id: LogId, // Label of the current session in log lines, unset before the first
116
117    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
118    timestamp: Option<i64>, // Test signing time for ArkHello; otherwise use the clock
119}
120
121impl<R: Read, W: Write, A: Attester> Server<R, W, A> {
122    /// Creates a server owning the byte stream and its shutdown operation.
123    /// The signer is the server's identity key. It must match the key embedded
124    /// in the device attestation. Output uses the stream's configured write
125    /// timeout. The adapter must enforce deadlines and shutdown cancellation.
126    pub fn new(stream: Stream<R, W>, signer: xdsa::SecretKey, attester: A) -> Self {
127        let (reader, writer, close, timeout) = stream.into_parts();
128        let outbound = Arc::new(Outbound::new(writer, Side::Server, close.clone(), timeout));
129        Self {
130            reader: FrameReader::new(reader, close),
131            outbound,
132            signer,
133            attester,
134            receiver: None,
135            sealer: None,
136            handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
137            handshake_deadline: None,
138            log_id: LogId::default(),
139            #[cfg(any(test, feature = "bench", feature = "fuzz"))]
140            timestamp: None,
141        }
142    }
143
144    /// Sets the budget for each subsequent handshake, starting when a reset is
145    /// received. Defaults to [`DEFAULT_HANDSHAKE_TIMEOUT`]. Output and peer
146    /// replies share one deadline; progress and repeated resets within the attempt
147    /// do not refresh it. An already pending handshake keeps its deadline. Each
148    /// outgoing frame is also limited by the stream's write timeout. Waiting for
149    /// locks and attester callbacks can extend the call beyond the deadline.
150    /// Time between recv calls also consumes the budget.
151    ///
152    /// Zero expires attempts immediately. A duration too large to add to an
153    /// [`Instant`] panics when the next handshake's deadline is constructed.
154    pub fn set_handshake_timeout(mut self, timeout: Duration) -> Self {
155        self.handshake_timeout = timeout;
156        self
157    }
158
159    /// A handle that permanently closes the stream from another thread.
160    pub fn closer(&self) -> Closer {
161        self.outbound.closer()
162    }
163
164    /// Permanently closes the stream and waits for adapter shutdown. Senders
165    /// observe closure through write failure; buffered messages remain readable.
166    /// See [`Closer::close`].
167    pub fn close(&self) {
168        self.outbound.close();
169    }
170
171    /// Creates a test server with a fixed ArkHello signing time for vector replay.
172    /// Not part of the normal transport API.
173    #[doc(hidden)]
174    #[inline]
175    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
176    #[cfg_attr(coverage_nightly, coverage(off))]
177    pub fn new_at(
178        stream: Stream<R, W>,
179        signer: xdsa::SecretKey,
180        attester: A,
181        timestamp: i64,
182    ) -> Self {
183        let mut server = Self::new(stream, signer, attester);
184        server.timestamp = Some(timestamp);
185        server
186    }
187
188    /// Receives a decrypted message or a session transition. A client reset
189    /// starts a handshake, whose completion returns [`Event::Connected`] with
190    /// a sender before any messages from that session are delivered.
191    ///
192    /// A client reset or invalid incoming data ends the current session and
193    /// returns [`Event::Disconnected`]. Oversized frames count as invalid data.
194    /// After a reset, the next call runs the handshake under one configured
195    /// deadline starting at that reset. Repeated resets within that attempt do
196    /// not refresh it. Expiry returns `RecvFailed(TimedOut)` and a fresh reset
197    /// can start another attempt. A send failure also ends
198    /// the session, but does not wake a blocked read. It is reported once
199    /// receiving progresses. Sessions ended by a local disconnect are not
200    /// reported again.
201    ///
202    /// After decryption, message acceptance is ordered with session ending
203    /// without waiting for the writer. A concurrent send failure can cause a
204    /// decrypted message to be discarded before acceptance. An accepted message
205    /// may reach the caller after another thread ends the session. Reporting
206    /// a session's end waits for outgoing writes to finish.
207    ///
208    /// Junk outside a session and handshake protocol or authentication failures
209    /// are logged, answered with a best-effort empty frame and skipped. Handshake
210    /// write failures surface as errors; calling again waits for a new reset on
211    /// the same stream. Adapter read failures and EOF also surface as errors,
212    /// without removing the binding. The caller can retry a transient read error,
213    /// disconnect the session or close the stream. Outside a handshake, reads
214    /// wait for data or adapter shutdown without a session timeout.
215    ///
216    /// Outgoing frames and standalone empty notifications use the stream's
217    /// configured write timeout. A notification sent while handling a failed
218    /// write shares that frame's remaining budget and is skipped after timeout.
219    /// These output failures do not themselves close the byte stream.
220    pub fn recv(&mut self) -> Result<Event<W>, Error> {
221        // Continue until a message, session transition or I/O error is ready.
222        // Empty frames request a handshake on the next pass.
223        loop {
224            // If a reset just arrived, run the handshake
225            if let Some(deadline) = self.handshake_deadline.take() {
226                match self.handshake(deadline) {
227                    // Transport errors propagate immediately
228                    Err(Error::Terminated) => return Err(Error::Terminated),
229                    Err(Error::RecvFailed(err)) => return Err(Error::RecvFailed(err)),
230                    // Outbound already attempted notification within the failed
231                    // frame's budget; a fresh attempt here could block again.
232                    Err(Error::SendFailed(err)) => return Err(Error::SendFailed(err)),
233
234                    // Tell the client that the handshake did not establish a session
235                    Err(err) => {
236                        warn!("dropping wire handshake: {}", err);
237                        if let Err(err) = self.outbound.send_dropped(Some(deadline)) {
238                            warn!("failed to signal dropped handshake: {}", err);
239                        }
240                        // Do not swallow an attempt deadline exhausted during
241                        // authentication or its failure notification.
242                        check_deadline(deadline).map_err(Error::RecvFailed)?;
243                    }
244                    // Report the completed handshake before reading messages.
245                    // The caller can now send without waiting for a client request.
246                    Ok((sender, receiver)) => {
247                        let sender = self.new_session(sender, receiver);
248                        info!("wire session {} established", self.log_id);
249                        return Ok(Event::Connected(sender));
250                    }
251                }
252                continue;
253            }
254            // Retrieve the next COBS encoded packet
255            let packet = match self.reader.next_packet(None) {
256                // Transport errors propagate immediately
257                Err(Error::Terminated) => return Err(Error::Terminated),
258                Err(Error::RecvFailed(err)) => return Err(Error::RecvFailed(err)),
259
260                // A reset can terminate a partial frame and cause a framing error.
261                // The frame may also have carried a sealed message. End any active
262                // session because its encryption sequence can no longer be trusted.
263                Err(err) => {
264                    let ended = self.end_session();
265                    if ended {
266                        warn!("ending session {}: {}", self.log_id, err);
267                    } else {
268                        debug!("discarding invalid frame outside session: {}", err);
269                    }
270                    self.send_dropped();
271                    if ended {
272                        return Ok(Event::Disconnected);
273                    }
274                    continue;
275                }
276                // A reset ends any active session. Run the handshake on the next
277                // receive call if we return an event, or on the next loop pass.
278                Ok(None) => {
279                    self.handshake_deadline = Some(Instant::now() + self.handshake_timeout);
280                    if self.end_session() {
281                        info!("wire session {} reset by host", self.log_id);
282                        return Ok(Event::Disconnected);
283                    }
284                    debug!("wire reset received, awaiting handshake");
285                    continue;
286                }
287                // Valid COBS packet
288                Ok(Some(packet)) => packet,
289            };
290            let receiver = match self.receiver.as_mut() {
291                None => {
292                    debug!("discarding data outside session");
293                    self.send_dropped();
294                    continue;
295                }
296                Some(receiver) => receiver,
297            };
298            // Finish after decrypting, ordering message acceptance with a send
299            // failure without ever waiting for the writer on a successful receive.
300            let sealer = self
301                .sealer
302                .as_ref()
303                .expect("receiver has a sending context");
304            let opened = sealing::open(receiver, packet);
305            let undecryptable = opened.is_err();
306            let message = match self.outbound.finish_receive(sealer, opened) {
307                Err(err) => {
308                    if undecryptable {
309                        warn!("ending session {}: {}", self.log_id, err);
310                    } else {
311                        debug!(
312                            "discarding message read after session {} ended",
313                            self.log_id
314                        );
315                    }
316                    self.end_session();
317                    self.send_dropped();
318                    return Ok(Event::Disconnected);
319                }
320                Ok(message) => message,
321            };
322            trace!("received host-to-ark message ({} bytes)", packet.len());
323            return Ok(Event::Message(message));
324        }
325    }
326
327    /// Stores the negotiated contexts and returns a sender for the new session.
328    /// The sending context's allocation identifies the session. The server owns
329    /// both contexts and shares the sending context with active sends. Idle
330    /// senders hold weak references and keep neither context nor stream alive.
331    ///
332    /// Takes the writer lock, then the binding lock. An old write that already
333    /// holds the writer lock may finish first. Once the binding is replaced,
334    /// old sends cannot write and old received messages cannot be accepted.
335    /// This method performs no handshake, crypto or stream I/O.
336    fn new_session(&mut self, sender: xhpke::Sender, receiver: xhpke::Receiver) -> Sender<W> {
337        let sealer = Arc::new(Mutex::new(sender));
338        let sender = self.outbound.bind(&sealer);
339        self.log_id = sender.log_id();
340        self.receiver = Some(receiver);
341        self.sealer = Some(sealer);
342        sender
343    }
344
345    /// Ends the current binding before releasing the server's crypto contexts.
346    /// Waits for the writer. After this returns, no write or flush for that
347    /// session is running or can start. A send that gets the writer first may
348    /// finish. A send still sealing after removal cannot write its packet.
349    /// This takes no encryption lock and does not wait for crypto work.
350    ///
351    /// This does not close the stream or send a notification. An active write
352    /// may delay ending until its frame deadline. Another thread can use the
353    /// Closer to cancel I/O without taking the writer lock.
354    ///
355    /// Returns true if it removed a receive context, even if a send failure
356    /// already ended the binding. The receive loop uses this removal to emit
357    /// Disconnected once. Local disconnect ignores the result because its caller
358    /// already knows the session ended.
359    fn end_session(&mut self) -> bool {
360        if let Some(sealer) = self.sealer.as_ref() {
361            self.outbound.end(sealer);
362        }
363        self.sealer = None;
364        self.receiver.take().is_some()
365    }
366
367    /// Sends an empty frame to tell the client it has no session. Logs failures.
368    fn send_dropped(&self) {
369        if let Err(err) = self.outbound.send_dropped(None) {
370            warn!("failed to signal dropped session: {}", err);
371        }
372    }
373
374    /// Ends the encrypted session and tells the client with an empty frame.
375    /// The stream remains available for the client to connect again. Notification
376    /// failures are logged. This does not produce a Disconnected event because
377    /// the caller already knows the session ended.
378    ///
379    /// Waits for the current writer and its flush, then retires the binding.
380    /// The notification gets its own frame budget. Another thread can use the
381    /// Closer to cancel output earlier.
382    pub fn disconnect(&mut self) {
383        if self.end_session() {
384            debug!("wire session {} dropped locally", self.log_id);
385        }
386        self.send_dropped();
387    }
388
389    /// Responds to the handshake after a session reset, establishing the
390    /// HPKE contexts of both directions:
391    ///
392    ///   1. Client -> Server: HostHello { host_signer, host_crypto }           (plain CBOR)
393    ///   2. Server -> Client: ArkHello  { ark_attest, ark_crypto, a2h_encap }  (cose::seal)
394    ///   3. Client -> Server: HostAck   { h2a_encap }                          (cose::seal)
395    fn handshake(&mut self, deadline: Instant) -> Result<(xhpke::Sender, xhpke::Receiver), Error> {
396        self.outbound.unbind();
397        loop {
398            // Message 1: Read the HostHello (skip any trailing empty reset frames)
399            let packet = loop {
400                if let Some(packet) = self.reader.next_packet(Some(deadline))? {
401                    break packet;
402                }
403            };
404            let host_hello: handshake::HostHello = cbor::decode(packet)
405                .map_err(|err| Error::HandshakeFailed(format!("invalid client hello: {}", err)))?;
406
407            // Generate ephemeral keys and set up server-to-client encryption
408            let ark_crypto_key = xhpke::SecretKey::generate();
409            let ark_crypto_pub = ark_crypto_key.public_key();
410
411            let (sender, a2h_encap) = host_hello
412                .host_crypto
413                .new_sender(CRYPTO_DOMAIN_WIRE_ARK_TO_HOST)
414                .map_err(|err| {
415                    Error::HandshakeFailed(format!("server sender setup failed: {}", err))
416                })?;
417
418            // Message 2: Seal and send the ArkHello
419            let ark_hello = handshake::ArkHello {
420                ark_attest: self.attester.attest().into_bytes(),
421                ark_crypto: ark_crypto_pub.clone(),
422                a2h_encap: a2h_encap.to_vec(),
423            };
424            let auth = handshake::ArkHelloAuth {
425                host_signer: host_hello.host_signer.clone(),
426                host_crypto: host_hello.host_crypto.clone(),
427            };
428            #[cfg(not(any(test, feature = "bench", feature = "fuzz")))]
429            let sealed = cose::seal(
430                &ark_hello,
431                &auth,
432                &self.signer,
433                &host_hello.host_crypto,
434                CRYPTO_DOMAIN_WIRE,
435            );
436            #[cfg(any(test, feature = "bench", feature = "fuzz"))]
437            let sealed = match self.timestamp {
438                Some(timestamp) => cose::seal_at(
439                    &ark_hello,
440                    &auth,
441                    &self.signer,
442                    &host_hello.host_crypto,
443                    CRYPTO_DOMAIN_WIRE,
444                    timestamp,
445                ),
446                None => cose::seal(
447                    &ark_hello,
448                    &auth,
449                    &self.signer,
450                    &host_hello.host_crypto,
451                    CRYPTO_DOMAIN_WIRE,
452                ),
453            };
454            let ark_hello = sealed.map_err(|err| {
455                Error::HandshakeFailed(format!("failed to seal server hello: {}", err))
456            })?;
457
458            self.outbound.send_packet(&ark_hello, Some(deadline))?;
459
460            // Message 3: Read and open HostAck. An empty frame is another reset;
461            // discard this attempt and wait for the next HostHello.
462            let Some(packet) = self.reader.next_packet(Some(deadline))? else {
463                debug!("wire reset received during handshake");
464                continue;
465            };
466            let host_ack: handshake::HostAck = cose::open(
467                packet,
468                &handshake::HostAckAuth {
469                    ark_signer: self.signer.public_key(),
470                    ark_crypto: ark_crypto_pub.clone(),
471                },
472                &ark_crypto_key,
473                &host_hello.host_signer,
474                CRYPTO_DOMAIN_WIRE,
475                None, // clock possibly unset, ephemeral keys guarantee freshness
476            )
477            .map_err(|err| Error::HandshakeFailed(format!("invalid client ack: {}", err)))?;
478
479            // Set up client-to-server decryption
480            let enc_h2a: [u8; xhpke::ENCAP_KEY_SIZE] = host_ack
481                .h2a_encap
482                .try_into()
483                .map_err(|_| Error::HandshakeFailed("invalid h2a_encap size".into()))?;
484
485            let receiver = ark_crypto_key
486                .new_receiver(&enc_h2a, CRYPTO_DOMAIN_WIRE_HOST_TO_ARK)
487                .map_err(|err| {
488                    Error::HandshakeFailed(format!("server receiver setup failed: {}", err))
489                })?;
490
491            // Session established
492            check_deadline(deadline).map_err(Error::RecvFailed)?;
493            return Ok((sender, receiver));
494        }
495    }
496}
497
498impl<R: Read, W: Write, A: Attester> Drop for Server<R, W, A> {
499    /// Closes the stream to cancel blocked I/O, then ends the binding before
500    /// releasing the contexts. Shutdown must precede waiting for the writer.
501    /// Idle senders hold weak references and cannot extend the stream's lifetime.
502    fn drop(&mut self) {
503        self.outbound.close();
504        self.end_session();
505    }
506}
507
508#[cfg(test)]
509#[cfg_attr(coverage_nightly, coverage(off))]
510mod tests {
511    use super::*;
512    #[cfg(unix)]
513    use crate::testing::Socket;
514    use crate::transport::mock::payload;
515    use crate::transport::testing::Memory;
516    use crate::transport::{Client, MAX_FRAME_SIZE, Verifier};
517    use crate::{memory, testing};
518    use darkbio_cobs as cobs;
519    #[cfg(unix)]
520    use std::io::Write;
521    #[cfg(unix)]
522    use std::os::unix::net::UnixStream;
523
524    /// Self-signed attestation for a device that has not been onboarded.
525    fn self_attestation(signer: &xdsa::SecretKey) -> Attestation {
526        use darkbio_crypto::cwt::claims::{self, eat};
527
528        let claims = darkbio_trust::device::HardwareClaims {
529            sub: claims::Subject { sub: "".into() },
530            cnf: claims::Confirm::new(signer.public_key()),
531            nbf: claims::NotBefore { nbf: 0 },
532            iat: claims::IssuedAt { iat: 0 },
533            oem: eat::Oemid::new_pen(0),
534            hwm: eat::HwModel { hw_model: vec![] },
535            hwv: eat::HwVersion::new("".into()),
536        };
537        let cwt = cwt::issue(
538            &claims,
539            signer,
540            darkbio_trust::CRYPTO_DOMAIN_DEVICE_ATTESTATION,
541        )
542        .unwrap();
543        Attestation::new(cwt).unwrap()
544    }
545
546    /// COBS-encodes data and appends the frame delimiter.
547    fn cobs_frame(data: &[u8]) -> Vec<u8> {
548        let mut buf = vec![0u8; cobs::encode_buffer(data.len())];
549        let n = cobs::encode(data, &mut buf).unwrap();
550        buf.truncate(n);
551        buf.push(0x00);
552        buf
553    }
554
555    // Tests that an oversized server hello produces one failure notification.
556    // It never reaches adapter I/O, so the handshake owns that notification;
557    // the writer must not emit another one with its own budget. Dummy attestation
558    // bytes isolate the framing limit: the server forwards them without parsing.
559    #[test]
560    fn test_oversized_hello_notifies_once() {
561        testing::init_tracing();
562
563        let hello = cbor::encode(&handshake::HostHello {
564            host_signer: xdsa::SecretKey::generate().public_key(),
565            host_crypto: xhpke::SecretKey::generate().public_key(),
566        })
567        .unwrap();
568        let mut input = vec![0, 0];
569        input.extend_from_slice(&cobs_frame(&hello));
570        let mut output = Vec::new();
571        let mut server = Server::new(
572            Stream::new(Memory::new(&input[..]), Memory::new(&mut output), || {}),
573            xdsa::SecretKey::generate(),
574            Attestation(vec![0; MAX_FRAME_SIZE]),
575        );
576
577        assert!(matches!(server.recv(), Err(Error::Terminated)));
578        drop(server);
579        assert_eq!(output, [0]);
580    }
581
582    // Tests the two real sides against each other. The handshake hands the
583    // attestation to the client's verifier unchanged and a request gets its
584    // response. The server's signal for a dropped session then surfaces on the
585    // client as a reset, which a fresh handshake recovers from.
586    #[test]
587    #[cfg(unix)]
588    fn test_message_round_trip() {
589        testing::init_tracing();
590
591        let signer_key = xdsa::SecretKey::generate();
592        let signer_pub = signer_key.public_key();
593        let attestation = self_attestation(&signer_key);
594        let presented = attestation.clone();
595
596        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
597        let ark_reader = Socket::new(ark_sock.try_clone().unwrap());
598        let ark_writer = Socket::new(ark_sock);
599
600        // Server side: receive two messages (across two sessions), echo each back.
601        let ark_thread = std::thread::spawn(move || {
602            let mut server = Server::new(
603                Stream::new(ark_reader, ark_writer, || {}),
604                signer_key,
605                attestation,
606            );
607            let mut sender = None;
608            let mut requests = Vec::new();
609            for _ in 0..2 {
610                let req = testing::served(&mut server, &mut sender).unwrap();
611                sender.as_ref().unwrap().send(&req).unwrap();
612                requests.push(req);
613            }
614            requests
615        });
616
617        // Raw handle to inject bytes past the client side.
618        let mut raw_sock = host_sock.try_clone().unwrap();
619
620        // Session 1: handshake, checking the attestation, exchange one message.
621        let mut client = Client::new(Stream::new(
622            Socket::new(host_sock.try_clone().unwrap()),
623            Socket::new(host_sock),
624            || {},
625        ));
626        let (sender, attest) = client.connect(&signer_pub).unwrap();
627        assert_eq!(attest.as_bytes(), presented.as_bytes());
628        sender.send(&payload(1)).unwrap();
629        assert_eq!(client.recv().unwrap(), payload(1));
630
631        // Inject a frame the server cannot decrypt. It drops the session and
632        // signals it. The client's next read reports a reset, and its old sender
633        // cannot send again.
634        raw_sock
635            .write_all(&cobs_frame(b"interrupted transfer"))
636            .unwrap();
637        let result = client.recv();
638        assert!(matches!(result, Err(Error::SessionReset)), "{result:?}");
639        let result = sender.send(&payload(2));
640        assert!(
641            matches!(result, Err(Error::EncryptionFailed(_))),
642            "{result:?}"
643        );
644
645        // Session 2: new handshake on the same wire, exchange one message.
646        let (sender, _) = client.connect(&signer_pub).unwrap();
647        sender.send(&payload(2)).unwrap();
648        assert_eq!(client.recv().unwrap(), payload(2));
649
650        let requests = ark_thread.join().unwrap();
651        assert_eq!(requests, vec![payload(1), payload(2)]);
652    }
653
654    // Tests that an untrusting verifier rejects the session on the client side.
655    #[test]
656    fn test_verifier_rejects() {
657        testing::init_tracing();
658
659        /// Verifier refusing every attestation.
660        struct Untrusting;
661
662        impl Verifier for Untrusting {
663            type Info = ();
664
665            fn verify(&self, _: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String> {
666                Err("attestation rejected".into())
667            }
668        }
669
670        let signer_key = xdsa::SecretKey::generate();
671
672        let (host, ark) = memory::duplex(64 * 1024);
673
674        // Server side: serve handshakes until the transport drops. The client aborts
675        // mid-handshake, so the server never delivers a message.
676        let ark_thread = std::thread::spawn(move || {
677            let attestation = self_attestation(&signer_key);
678            let mut server = Server::new(ark, signer_key, attestation);
679            let mut sender = None;
680            testing::served(&mut server, &mut sender)
681        });
682
683        // Client side: refuse the attestation in the verifier.
684        let mut client = Client::new(host);
685        let result = client.connect(&Untrusting);
686        assert!(result.is_err());
687
688        // Dropping the client tears down the transport, unblocking the server.
689        drop(client);
690        assert!(ark_thread.join().unwrap().is_err());
691    }
692
693    // Tests that the roots verifier accepts hardware and emulator attestations
694    // under the configured roots and returns the verified identity. Unknown
695    // roots and self-signed attestations are refused.
696    #[test]
697    fn test_roots_verifier() {
698        testing::init_tracing();
699
700        use crate::transport::Roots;
701        use darkbio_crypto::cwt;
702        use darkbio_crypto::cwt::claims::{self, eat};
703        use darkbio_trust::device::{EmulatorClaims, HardwareClaims};
704        use darkbio_trust::{CRYPTO_DOMAIN_DEVICE_ATTESTATION, Realm};
705        use std::time::{SystemTime, UNIX_EPOCH};
706
707        let now = SystemTime::now()
708            .duration_since(UNIX_EPOCH)
709            .unwrap()
710            .as_secs();
711
712        /// Runs a handshake with the given attestation and trusted roots.
713        /// Returns the client's verification result.
714        fn handshake(
715            signer_key: xdsa::SecretKey,
716            attestation: Attestation,
717            hardware: &[xdsa::PublicKey],
718            emulator: &[xdsa::PublicKey],
719        ) -> Result<darkbio_trust::device::Device, Error> {
720            let (host, ark) = memory::duplex(64 * 1024);
721
722            let ark_thread = std::thread::spawn(move || {
723                let mut server = Server::new(ark, signer_key, attestation);
724                let mut sender = None;
725                testing::served(&mut server, &mut sender)
726            });
727            let mut client = Client::new(host);
728            let result = client
729                .connect(&Roots { hardware, emulator })
730                .map(|(_, info)| info);
731
732            // Dropping the client tears down the transport, unblocking the server
733            drop(client);
734            let _ = ark_thread.join().unwrap();
735            result
736        }
737
738        let hardware_root = xdsa::SecretKey::generate();
739        let emulator_root = xdsa::SecretKey::generate();
740        let hardware_roots = [hardware_root.public_key()];
741        let emulator_roots = [emulator_root.public_key()];
742
743        // A hardware server attested by a hardware root is accepted with its identity
744        let signer_key = xdsa::SecretKey::generate();
745        let attestation = cwt::issue(
746            &HardwareClaims {
747                sub: claims::Subject {
748                    sub: "ark-1234".into(),
749                },
750                cnf: claims::Confirm::new(signer_key.public_key()),
751                nbf: claims::NotBefore { nbf: now - 10 },
752                iat: claims::IssuedAt { iat: now - 10 },
753                oem: eat::Oemid::new_pen(65145),
754                hwm: eat::HwModel {
755                    hw_model: b"Ark I".to_vec(),
756                },
757                hwv: eat::HwVersion::new("Ark I - 1.0.0".into()),
758            },
759            &hardware_root,
760            CRYPTO_DOMAIN_DEVICE_ATTESTATION,
761        )
762        .map(|cwt| Attestation::new(cwt).unwrap())
763        .unwrap();
764        let device = handshake(signer_key, attestation.clone(), &hardware_roots, &[]).unwrap();
765        assert_eq!(device.realm, Realm::Hardware);
766        assert_eq!(device.serial, "ark-1234");
767
768        // A hardware attestation is refused when only emulator roots are trusted
769        let signer_key = xdsa::SecretKey::generate();
770        assert!(handshake(signer_key, attestation, &[], &emulator_roots).is_err());
771
772        // An emulated server attested by an emulator root is accepted with its expiry
773        let signer_key = xdsa::SecretKey::generate();
774        let attestation = cwt::issue(
775            &EmulatorClaims {
776                sub: claims::Subject {
777                    sub: "emu-1234".into(),
778                },
779                cnf: claims::Confirm::new(signer_key.public_key()),
780                nbf: claims::NotBefore { nbf: now - 10 },
781                exp: claims::Expiration { exp: now + 1000 },
782                iat: claims::IssuedAt { iat: now - 10 },
783                oem: eat::Oemid::new_pen(65145),
784                hwm: eat::HwModel {
785                    hw_model: b"Ark I".to_vec(),
786                },
787                hwv: eat::HwVersion::new("Ark I - 1.0.0".into()),
788            },
789            &emulator_root,
790            CRYPTO_DOMAIN_DEVICE_ATTESTATION,
791        )
792        .map(|cwt| Attestation::new(cwt).unwrap())
793        .unwrap();
794        let device = handshake(signer_key, attestation, &hardware_roots, &emulator_roots).unwrap();
795        assert_eq!(device.realm, Realm::Emulator);
796        assert_eq!(device.expiry, Some(now + 1000));
797
798        // A never onboarded server presenting a self-signed attestation is refused
799        let signer_key = xdsa::SecretKey::generate();
800        let attestation = cwt::issue(
801            &HardwareClaims {
802                sub: claims::Subject { sub: "".into() },
803                cnf: claims::Confirm::new(signer_key.public_key()),
804                nbf: claims::NotBefore { nbf: 0 },
805                iat: claims::IssuedAt { iat: 0 },
806                oem: eat::Oemid::new_pen(0),
807                hwm: eat::HwModel { hw_model: vec![] },
808                hwv: eat::HwVersion::new("".into()),
809            },
810            &signer_key,
811            CRYPTO_DOMAIN_DEVICE_ATTESTATION,
812        )
813        .map(|cwt| Attestation::new(cwt).unwrap())
814        .unwrap();
815        assert!(handshake(signer_key, attestation, &hardware_roots, &emulator_roots).is_err());
816    }
817
818    // Tests that attestation construction accepts hardware and emulator claims
819    // and rejects junk or CWTs containing other claim types.
820    #[test]
821    fn test_attestation_shapes() {
822        use darkbio_crypto::cwt::claims;
823        use darkbio_trust::CRYPTO_DOMAIN_DEVICE_ATTESTATION;
824
825        let signer = xdsa::SecretKey::generate();
826        let _ = self_attestation(&signer);
827
828        let emulator = darkbio_trust::device::EmulatorClaims {
829            sub: claims::Subject { sub: "".into() },
830            cnf: claims::Confirm::new(signer.public_key()),
831            nbf: claims::NotBefore { nbf: 0 },
832            exp: claims::Expiration { exp: u64::MAX },
833            iat: claims::IssuedAt { iat: 0 },
834            oem: claims::eat::Oemid::new_pen(0),
835            hwm: claims::eat::HwModel { hw_model: vec![] },
836            hwv: claims::eat::HwVersion::new("".into()),
837        };
838        let cwt = cwt::issue(&emulator, &signer, CRYPTO_DOMAIN_DEVICE_ATTESTATION).unwrap();
839        Attestation::new(cwt).expect("emulator attestation refused");
840
841        let cloud = darkbio_trust::cloud::SignerClaims {
842            iss: claims::Issuer { iss: "".into() },
843            sub: claims::Subject { sub: "".into() },
844            nbf: claims::NotBefore { nbf: 0 },
845            exp: claims::Expiration { exp: 1 },
846            cnf: claims::Confirm::new(signer.public_key()),
847        };
848        let cwt = cwt::issue(&cloud, &signer, CRYPTO_DOMAIN_DEVICE_ATTESTATION).unwrap();
849        let result = Attestation::new(cwt).map(|_| ());
850        assert!(
851            matches!(result, Err(Error::InvalidAttestation)),
852            "{result:?}"
853        );
854        let result = Attestation::new(b"junk".to_vec()).map(|_| ());
855        assert!(
856            matches!(result, Err(Error::InvalidAttestation)),
857            "{result:?}"
858        );
859    }
860
861    // Tests sending from other threads while the server blocks in a read.
862    // The client must receive every message in encryption order to decrypt it.
863    #[test]
864    fn test_senders() {
865        testing::init_tracing();
866
867        let signer_key = xdsa::SecretKey::generate();
868        let signer_pub = signer_key.public_key();
869        let attestation = self_attestation(&signer_key);
870
871        let (host, ark) = memory::duplex(64 * 1024);
872
873        // Server side: on the first request, push messages from a few threads
874        // while waiting for the second request.
875        let ark_thread = std::thread::spawn(move || {
876            let mut server = Server::new(ark, signer_key, attestation);
877            let mut sender = None;
878            testing::served(&mut server, &mut sender).unwrap();
879
880            let pushers: Vec<_> = (0..4)
881                .map(|thread| {
882                    let sender = sender.as_ref().unwrap().clone();
883                    std::thread::spawn(move || {
884                        for i in 0..25 {
885                            sender.send(&payload(thread * 100 + i)).unwrap();
886                        }
887                    })
888                })
889                .collect();
890            let stop = testing::served(&mut server, &mut sender).unwrap();
891            for pusher in pushers {
892                pusher.join().unwrap();
893            }
894            stop
895        });
896
897        // Client side: request the push, receive it all, then request the stop.
898        let mut client = Client::new(host);
899        let (sender, _) = client.connect(&signer_pub).unwrap();
900        sender.send(&payload(1)).unwrap();
901
902        let mut pushed: Vec<Vec<u8>> = (0..100).map(|_| client.recv().unwrap()).collect();
903        pushed.sort_unstable();
904        let mut expected: Vec<Vec<u8>> = (0..4)
905            .flat_map(|thread| (0..25).map(move |i| payload(thread * 100 + i)))
906            .collect();
907        expected.sort_unstable();
908        assert_eq!(pushed, expected);
909
910        sender.send(&payload(2)).unwrap();
911        assert_eq!(ark_thread.join().unwrap(), payload(2));
912    }
913}