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