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