Skip to main content

darkbio_wire/
side_ark.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2025 Dark Bio AG. All rights reserved.
3
4use crate::framing::Framing;
5use crate::handshake;
6use crate::protocol::{ArkToHost, HostToArk};
7use crate::session::Session;
8use crate::{
9    CRYPTO_DOMAIN_WIRE, CRYPTO_DOMAIN_WIRE_ARK_TO_HOST, CRYPTO_DOMAIN_WIRE_HOST_TO_ARK, Error,
10};
11use darkbio_crypto::{cbor, cose, cwt, xdsa, xhpke};
12use darkbio_trust as trust;
13use std::io::{Read, Write};
14use tracing::{info, trace, warn};
15
16/// Device attestation an Ark presents in the handshake, a CWT in one of the
17/// shapes darkbio-trust defines (hardware or emulator claims). Only the shape
18/// is checked, so an obviously wrong blob is refused up front; whether it is
19/// accepted is the host's decision.
20#[derive(Clone)]
21pub struct Attestation(Vec<u8>);
22
23impl Attestation {
24    /// Wraps a CWT after checking that it decodes as a device attestation.
25    pub fn new(cwt: Vec<u8>) -> Result<Self, Error> {
26        if cwt::peek::<trust::device::HardwareClaims>(&cwt).is_err()
27            && cwt::peek::<trust::device::EmulatorClaims>(&cwt).is_err()
28        {
29            return Err(Error::InvalidAttestation);
30        }
31        Ok(Self(cwt))
32    }
33
34    /// CWT bytes of the attestation.
35    pub fn as_bytes(&self) -> &[u8] {
36        &self.0
37    }
38
39    /// Unwraps the attestation into its CWT bytes.
40    pub fn into_bytes(self) -> Vec<u8> {
41        self.0
42    }
43}
44
45/// Source of the device attestation the Ark presents in the handshake; queried
46/// on every handshake, so a freshly onboarded attestation can be picked up
47/// without recreating the wire.
48pub trait Attester {
49    /// Returns the device attestation to present to the host (e.g. a root-signed
50    /// CWT read from disk, or a self-signed fallback for pre-onboarding devices).
51    /// The identity key it embeds must be the one signing the wire's handshake.
52    fn attest(&mut self) -> Attestation;
53}
54
55/// A fixed attestation, presented as is on every handshake.
56impl Attester for Attestation {
57    fn attest(&mut self) -> Attestation {
58        self.clone()
59    }
60}
61
62/// Ark side of the wire, an encrypted transport for serving protobuf requests
63/// from a connected host. It waits for session resets (empty frames), responds
64/// to handshake and afterward decrypts inbound and encrypts outbound messages.
65///
66/// The device attestation is not interpreted by the wire, it is provided by an
67/// `Attester` and forwarded to the host verbatim.
68pub struct ArkSide<R: Read, W: Write, A: Attester> {
69    framing: Framing<R, W>, // COBS framed transport for ingress and egress data
70
71    signer: xdsa::SecretKey,  // Ark's identity key, signing the ArkHello
72    attester: A,              // Source of the device attestation for handshakes
73    session: Option<Session>, // Active encrypted session (if handshake completed)
74}
75
76impl<R: Read, W: Write, A: Attester> ArkSide<R, W, A> {
77    /// Creates a new Ark side around a low level reader and writer. The signer is
78    /// the Ark's identity key, which signs the handshake; the host verifies that
79    /// signature against the key it extracts from the attestation, so the two
80    /// must match. Reads block per the transport's semantics, so any timeout
81    /// must be configured on the reader passed in.
82    pub fn new(reader: R, writer: W, signer: xdsa::SecretKey, attester: A) -> Self {
83        Self {
84            framing: Framing::new(reader, writer),
85            signer,
86            attester,
87            session: None,
88        }
89    }
90
91    /// Serves the next host-to-ark message, decrypting and protobuf decoding it.
92    /// Empty frames are session resets and run the handshake inline;
93    /// junk outside a session, undecryptable packets and failed handshakes are
94    /// logged and skipped, so only transport failures and malformed messages
95    /// surface as errors.
96    pub fn next_message(&mut self) -> Result<HostToArk, Error> {
97        // Loop until we can deliver a valid decrypted message. Empty frames
98        // are consumed and trigger a new session handshake.
99        loop {
100            // Retrieve the next COBS encoded packet
101            let size = match self.framing.next_packet() {
102                // Transport errors propagate immediately
103                Err(Error::Terminated) => return Err(Error::Terminated),
104                Err(Error::RecvFailed(err)) => return Err(Error::RecvFailed(err)),
105
106                // Decode errors may be due to session resets, log and ignore.
107                // Within a session the skipped frame may have carried a sealed
108                // message though, leaving the HPKE sequence behind the host's,
109                // so the session cannot continue either way.
110                Err(err) => {
111                    if self.session.take().is_some() {
112                        warn!("failed to decode cobs packet, resetting session: {}", err);
113                    } else {
114                        warn!("failed to decode cobs packet: {}", err);
115                    }
116                    continue;
117                }
118                // Empty frame signals a session reset from the host
119                Ok(None) => {
120                    self.session = None;
121
122                    match self.handshake() {
123                        // Transport errors propagate immediately
124                        Err(Error::Terminated) => return Err(Error::Terminated),
125                        Err(Error::RecvFailed(err)) => return Err(Error::RecvFailed(err)),
126
127                        // Decode or protocol errors are logged and ignored
128                        Err(err) => {
129                            warn!("wire handshake failed: {}", err);
130                            continue;
131                        }
132                        // Handshake successful
133                        Ok(session) => {
134                            info!("new wire session established");
135                            self.session = Some(session);
136                            continue;
137                        }
138                    }
139                }
140                // Valid COBS packet
141                Ok(Some(size)) => size,
142            };
143            // Non-empty packet without a session is considered junk
144            let session = match self.session.as_mut() {
145                None => {
146                    warn!("dropping data outside session");
147                    continue;
148                }
149                Some(s) => s,
150            };
151            // Decrypt the message and parse it with protobuf
152            let req = match session.open(&self.framing.decobs_buffer[..size]) {
153                // If decryption fails, the HPKE context is most probably
154                // broken, no point continuing with it.
155                Err(Error::EncryptionFailed(err)) => {
156                    warn!("decryption failed, resetting session: {}", err);
157                    self.session = None;
158                    continue;
159                }
160                Err(err) => return Err(err),
161                Ok(req) => req,
162            };
163            trace!("read host-to-ark message ({} bytes encrypted)", size);
164            return Ok(req);
165        }
166    }
167
168    /// Protobuf encodes an ark-to-host message, seals it with the session and
169    /// sends it. Fails without an active session, and a failure after sealing
170    /// drops the session, as the host's HPKE sequence can no longer be caught
171    /// up with.
172    pub fn send_message(&mut self, res: ArkToHost) -> Result<(), Error> {
173        // Encode and seal the message, oversized messages are rejected before
174        // the HPKE sequence advances, only a failed seal breaks the session
175        let session = self
176            .session
177            .as_mut()
178            .ok_or_else(|| Error::EncryptionFailed("no active session".into()))?;
179
180        let blob = match session.seal(&res, &mut self.framing.encode_buffer) {
181            Err(err @ Error::EncryptionFailed(_)) => {
182                self.session = None;
183                return Err(err);
184            }
185            Err(err) => return Err(err),
186            Ok(blob) => blob,
187        };
188        // Send the sealed message, tearing down the session if the transport
189        // fails to deliver it
190        if let Err(err) = self.framing.send_packet(&blob) {
191            self.session = None;
192            return Err(err);
193        }
194        trace!("sent ark-to-host message ({} bytes)", blob.len());
195        Ok(())
196    }
197
198    /// Responds to the handshake after a session reset, establishing the
199    /// HPKE contexts of both directions:
200    ///
201    ///   1. Host -> Ark:  HostHello { host_signer, host_crypto }           (plain CBOR)
202    ///   2. Ark -> Host:  ArkHello  { ark_attest, ark_crypto, a2h_encap }  (cose::seal)
203    ///   3. Host -> Ark:  HostAck   { h2a_encap }                          (cose::seal)
204    fn handshake(&mut self) -> Result<Session, Error> {
205        loop {
206            // Message 1: Read the HostHello (skip any trailing empty reset frames)
207            let size = loop {
208                if let Some(n) = self.framing.next_packet()? {
209                    break n;
210                }
211            };
212            let host_hello: handshake::HostHello =
213                cbor::decode(&self.framing.decobs_buffer[..size]).map_err(|err| {
214                    Error::HandshakeFailed(format!("invalid host hello: {}", err))
215                })?;
216
217            // Generate an ephemeral Ark xHPKE keypair and set up the Ark->Host sender
218            let ark_crypto_key = xhpke::SecretKey::generate();
219            let ark_crypto_pub = ark_crypto_key.public_key();
220
221            let (sender, a2h_encap) = host_hello
222                .host_crypto
223                .new_sender(CRYPTO_DOMAIN_WIRE_ARK_TO_HOST)
224                .map_err(|err| {
225                    Error::HandshakeFailed(format!("ark sender setup failed: {}", err))
226                })?;
227
228            // Message 2: Seal and send the ArkHello
229            let ark_hello = cose::seal(
230                &handshake::ArkHello {
231                    ark_attest: self.attester.attest().into_bytes(),
232                    ark_crypto: ark_crypto_pub.clone(),
233                    a2h_encap: a2h_encap.to_vec(),
234                },
235                &handshake::ArkHelloAuth {
236                    host_signer: host_hello.host_signer.clone(),
237                    host_crypto: host_hello.host_crypto.clone(),
238                },
239                &self.signer,
240                &host_hello.host_crypto,
241                CRYPTO_DOMAIN_WIRE,
242            )
243            .map_err(|err| Error::HandshakeFailed(format!("failed to seal ark hello: {}", err)))?;
244
245            self.framing.send_packet(&ark_hello)?;
246
247            // Message 3: Read and open the HostAck. An empty frame probably
248            // means the host is restarting the session, start over.
249            let Some(size) = self.framing.next_packet()? else {
250                warn!("session reset during handshake");
251                continue;
252            };
253            let host_ack: handshake::HostAck = cose::open(
254                &self.framing.decobs_buffer[..size],
255                &handshake::HostAckAuth {
256                    ark_signer: self.signer.public_key(),
257                    ark_crypto: ark_crypto_pub.clone(),
258                },
259                &ark_crypto_key,
260                &host_hello.host_signer,
261                CRYPTO_DOMAIN_WIRE,
262                None, // clock possibly unset, ephemeral keys guarantee freshness
263            )
264            .map_err(|err| Error::HandshakeFailed(format!("invalid host ack: {}", err)))?;
265
266            // Set up the Host->Ark receiver
267            let enc_h2a: [u8; xhpke::ENCAP_KEY_SIZE] = host_ack
268                .h2a_encap
269                .try_into()
270                .map_err(|_| Error::HandshakeFailed("invalid h2a_encap size".into()))?;
271
272            let receiver = ark_crypto_key
273                .new_receiver(&enc_h2a, CRYPTO_DOMAIN_WIRE_HOST_TO_ARK)
274                .map_err(|err| {
275                    Error::HandshakeFailed(format!("ark receiver setup failed: {}", err))
276                })?;
277
278            // Session established
279            return Ok(Session { sender, receiver });
280        }
281    }
282}
283
284#[cfg(all(test, unix))]
285mod tests {
286    use super::*;
287    use crate::testing;
288    use crate::{HostSide, Verifier};
289    use darkbio_cobs as cobs;
290    use std::io::{self, Write};
291    use std::os::unix::net::UnixStream;
292    use std::sync::Arc;
293    use std::sync::atomic::{AtomicBool, Ordering};
294
295    /// Self-signed attestation of a never onboarded device, the placeholder an
296    /// Ark presents before it is attested by a root.
297    fn self_attestation(signer: &xdsa::SecretKey) -> Attestation {
298        use darkbio_crypto::cwt::claims::{self, eat};
299
300        let claims = darkbio_trust::device::HardwareClaims {
301            sub: claims::Subject { sub: "".into() },
302            cnf: claims::Confirm::new(signer.public_key()),
303            nbf: claims::NotBefore { nbf: 0 },
304            iat: claims::IssuedAt { iat: 0 },
305            oem: eat::Oemid::new_pen(0),
306            hwm: eat::HwModel { hw_model: vec![] },
307            hwv: eat::HwVersion::new("".into()),
308        };
309        let cwt = cwt::issue(
310            &claims,
311            signer,
312            darkbio_trust::CRYPTO_DOMAIN_DEVICE_ATTESTATION,
313        )
314        .unwrap();
315        Attestation::new(cwt).unwrap()
316    }
317
318    /// COBS-encodes data and appends the frame delimiter.
319    fn cobs_frame(data: &[u8]) -> Vec<u8> {
320        let mut buf = vec![0u8; cobs::encode_buffer(data.len())];
321        let n = cobs::encode(data, &mut buf).unwrap();
322        buf.truncate(n);
323        buf.push(0x00);
324        buf
325    }
326
327    // Tests a full round trip, the handshake, a host-to-ark request and the
328    // ark-to-host response, and that the device attestation reaches the host's
329    // verifier byte-for-byte.
330    #[test]
331    fn test_message_round_trip() {
332        testing::init_tracing();
333
334        let signer_key = xdsa::SecretKey::generate();
335        let signer_pub = signer_key.public_key();
336        let attestation = self_attestation(&signer_key);
337        let presented = attestation.clone();
338
339        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
340        let ark_reader = ark_sock.try_clone().unwrap();
341        let ark_writer = ark_sock;
342
343        // Ark side: handshake, receive one message, echo it back.
344        let ark_thread = std::thread::spawn(move || {
345            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
346            let req = ark.next_message().unwrap();
347            ark.send_message(ArkToHost {
348                id: req.id,
349                err: None,
350                content: None,
351            })
352            .unwrap();
353            req
354        });
355
356        // Host side: handshake, send a message, read the response.
357        let mut host = HostSide::new(host_sock.try_clone().unwrap(), host_sock);
358        let attest = host.handshake(&signer_pub).unwrap();
359        assert_eq!(
360            attest.as_bytes(),
361            presented.as_bytes(),
362            "attestation mismatch"
363        );
364
365        host.send_message(HostToArk {
366            id: Some(42),
367            content: None,
368        })
369        .unwrap();
370
371        let req = ark_thread.join().unwrap();
372        assert_eq!(req.id, Some(42), "request mismatch");
373
374        let res = host.next_message().unwrap();
375        assert_eq!(res.id, Some(42), "response mismatch");
376    }
377
378    // Tests that a session reset mid-transfer (after a successful handshake and
379    // message exchange) correctly tears down the old session and allows a fresh
380    // handshake to establish a new one.
381    #[test]
382    fn test_reset_mid_transfer() {
383        testing::init_tracing();
384
385        let signer_key = xdsa::SecretKey::generate();
386        let signer_pub = signer_key.public_key();
387
388        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
389        let ark_reader = ark_sock.try_clone().unwrap();
390        let ark_writer = ark_sock;
391
392        // Ark side: receive two messages (across two sessions), echo each back.
393        let ark_thread = std::thread::spawn(move || {
394            let attestation = self_attestation(&signer_key);
395            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
396            let mut ids = Vec::new();
397            for _ in 0..2 {
398                let req = ark.next_message().unwrap();
399                ids.push(req.id);
400                ark.send_message(ArkToHost {
401                    id: req.id,
402                    err: None,
403                    content: None,
404                })
405                .unwrap();
406            }
407            ids
408        });
409
410        // Raw handle to inject bytes past the host side.
411        let mut raw_sock = host_sock.try_clone().unwrap();
412
413        // Session 1: complete handshake, exchange one message.
414        let mut host = HostSide::new(host_sock.try_clone().unwrap(), host_sock);
415        host.handshake(&signer_pub).unwrap();
416        host.send_message(HostToArk {
417            id: Some(1),
418            content: None,
419        })
420        .unwrap();
421        let res = host.next_message().unwrap();
422        assert_eq!(res.id, Some(1), "session 1 response mismatch");
423
424        // Simulate an interrupted transfer by sending a valid COBS frame with a
425        // garbage payload, which the Ark fails to decrypt and drops the session
426        // over.
427        raw_sock
428            .write_all(&cobs_frame(b"interrupted transfer"))
429            .unwrap();
430
431        // Session 2: new handshake on the same wire, exchange one message.
432        host.handshake(&signer_pub).unwrap();
433        host.send_message(HostToArk {
434            id: Some(2),
435            content: None,
436        })
437        .unwrap();
438        let res = host.next_message().unwrap();
439        assert_eq!(res.id, Some(2), "session 2 response mismatch");
440
441        let ids = ark_thread.join().unwrap();
442        assert_eq!(
443            ids,
444            vec![Some(1), Some(2)],
445            "ark received wrong message ids"
446        );
447    }
448
449    // Tests that a response the host never read (e.g. after timing out on it)
450    // does not wedge subsequent handshakes. The Ark answers the request before
451    // it processes the reset, so the stale response precedes the fresh ArkHello
452    // and the host must skip past it.
453    #[test]
454    fn test_reset_unread_response() {
455        testing::init_tracing();
456
457        let signer_key = xdsa::SecretKey::generate();
458        let signer_pub = signer_key.public_key();
459
460        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
461        let ark_reader = ark_sock.try_clone().unwrap();
462        let ark_writer = ark_sock;
463
464        // Ark side: receive two messages (across two sessions), echo each back.
465        let ark_thread = std::thread::spawn(move || {
466            let attestation = self_attestation(&signer_key);
467            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
468            let mut ids = Vec::new();
469            for _ in 0..2 {
470                let req = ark.next_message().unwrap();
471                ids.push(req.id);
472                ark.send_message(ArkToHost {
473                    id: req.id,
474                    err: None,
475                    content: None,
476                })
477                .unwrap();
478            }
479            ids
480        });
481
482        // Session 1: complete handshake, send a message but never read the
483        // response.
484        let mut host = HostSide::new(host_sock.try_clone().unwrap(), host_sock);
485        host.handshake(&signer_pub).unwrap();
486        host.send_message(HostToArk {
487            id: Some(1),
488            content: None,
489        })
490        .unwrap();
491
492        // Session 2: new handshake on the same wire with the unread response
493        // still queued in front of the ArkHello, exchange one message.
494        host.handshake(&signer_pub).unwrap();
495        host.send_message(HostToArk {
496            id: Some(2),
497            content: None,
498        })
499        .unwrap();
500        let res = host.next_message().unwrap();
501        assert_eq!(res.id, Some(2), "session 2 response mismatch");
502
503        let ids = ark_thread.join().unwrap();
504        assert_eq!(
505            ids,
506            vec![Some(1), Some(2)],
507            "ark received wrong message ids"
508        );
509    }
510
511    // Tests that a session reset mid-handshake (after HostHello/ArkHello but
512    // before HostAck) correctly aborts the in-progress handshake and allows a
513    // fresh one to complete, with the abandoned ArkHello left unread for the
514    // fresh handshake to skip past.
515    #[test]
516    fn test_reset_mid_handshake() {
517        testing::init_tracing();
518
519        let signer_key = xdsa::SecretKey::generate();
520        let signer_pub = signer_key.public_key();
521
522        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
523        let ark_reader = ark_sock.try_clone().unwrap();
524        let ark_writer = ark_sock;
525
526        // Ark side: receive one message, echo it back.
527        let ark_thread = std::thread::spawn(move || {
528            let attestation = self_attestation(&signer_key);
529            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
530            let req = ark.next_message().unwrap();
531            ark.send_message(ArkToHost {
532                id: req.id,
533                err: None,
534                content: None,
535            })
536            .unwrap();
537            req
538        });
539
540        let host_read = host_sock.try_clone().unwrap();
541        let mut host_write = host_sock;
542
543        // Start a handshake but abandon it after sending HostHello (message 1),
544        // never reading ArkHello (message 2) nor sending HostAck (message 3).
545        host_write.write_all(&[0x00, 0x00]).unwrap(); // session reset
546
547        let host_signer_key = xdsa::SecretKey::generate();
548        let host_crypto_key = xhpke::SecretKey::generate();
549        let hello = cbor::encode(&handshake::HostHello {
550            host_signer: host_signer_key.public_key(),
551            host_crypto: host_crypto_key.public_key(),
552        })
553        .unwrap();
554        host_write.write_all(&cobs_frame(&hello)).unwrap(); // message 1: HostHello
555
556        // Now do a complete handshake (sends its own reset + full 3 messages).
557        // The Ark sees the reset where it expected HostAck, restarts its
558        // handshake loop, and completes the new one. The host skips the stale
559        // ArkHello of the abandoned attempt to find its own.
560        let mut host = HostSide::new(host_read, host_write);
561        host.handshake(&signer_pub).unwrap();
562        host.send_message(HostToArk {
563            id: Some(99),
564            content: None,
565        })
566        .unwrap();
567
568        let req = ark_thread.join().unwrap();
569        assert_eq!(req.id, Some(99), "request mismatch");
570
571        let res = host.next_message().unwrap();
572        assert_eq!(res.id, Some(99), "response mismatch");
573    }
574
575    // Tests that garbage sent instead of a handshake hello (after a session
576    // reset) aborts the in-progress handshake without wedging the Ark, allowing
577    // a fresh handshake to complete.
578    #[test]
579    fn test_reset_malformed_hello() {
580        testing::init_tracing();
581
582        let signer_key = xdsa::SecretKey::generate();
583        let signer_pub = signer_key.public_key();
584
585        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
586        let ark_reader = ark_sock.try_clone().unwrap();
587        let ark_writer = ark_sock;
588
589        // Ark side: receive one message, echo it back.
590        let ark_thread = std::thread::spawn(move || {
591            let attestation = self_attestation(&signer_key);
592            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
593            let req = ark.next_message().unwrap();
594            ark.send_message(ArkToHost {
595                id: req.id,
596                err: None,
597                content: None,
598            })
599            .unwrap();
600            req
601        });
602
603        // Raw handle to inject bytes past the host side.
604        let mut raw_sock = host_sock.try_clone().unwrap();
605
606        // Signal a session reset, but follow it up with a garbage hello. The Ark
607        // fails to decode it, abandons the handshake and returns to its message
608        // loop.
609        raw_sock.write_all(&[0x00, 0x00]).unwrap();
610        raw_sock.write_all(&cobs_frame(b"not a hello")).unwrap();
611
612        // Now do a complete handshake and exchange one message.
613        let mut host = HostSide::new(host_sock.try_clone().unwrap(), host_sock);
614        host.handshake(&signer_pub).unwrap();
615        host.send_message(HostToArk {
616            id: Some(99),
617            content: None,
618        })
619        .unwrap();
620
621        let req = ark_thread.join().unwrap();
622        assert_eq!(req.id, Some(99), "request mismatch");
623
624        let res = host.next_message().unwrap();
625        assert_eq!(res.id, Some(99), "response mismatch");
626    }
627
628    // Tests that an untrusting verifier rejects the session on the host side.
629    #[test]
630    fn test_verifier_rejects() {
631        testing::init_tracing();
632
633        /// Verifier refusing every attestation.
634        struct Untrusting;
635
636        impl Verifier for Untrusting {
637            type Info = ();
638
639            fn verify(&self, _: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String> {
640                Err("attestation rejected".into())
641            }
642        }
643
644        let signer_key = xdsa::SecretKey::generate();
645
646        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
647        let ark_reader = ark_sock.try_clone().unwrap();
648        let ark_writer = ark_sock;
649
650        // Ark side: serve handshakes until the transport drops. The host aborts
651        // mid-handshake, so the Ark never delivers a message.
652        let ark_thread = std::thread::spawn(move || {
653            let attestation = self_attestation(&signer_key);
654            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
655            ark.next_message()
656        });
657
658        // Host side: refuse the attestation in the verifier.
659        let mut host = HostSide::new(host_sock.try_clone().unwrap(), host_sock);
660        let result = host.handshake(&Untrusting);
661        assert!(result.is_err(), "expected rejected handshake");
662
663        // Dropping the host tears down the transport, unblocking the Ark.
664        drop(host);
665        assert!(
666            ark_thread.join().unwrap().is_err(),
667            "expected torn down wire"
668        );
669    }
670
671    // Tests that the roots verifier opens sessions with root attested Arks of
672    // either realm, handing back their verified identity, and refuses Arks
673    // attested under unknown roots or self-signed ones.
674    #[test]
675    fn test_roots_verifier() {
676        testing::init_tracing();
677
678        use crate::Roots;
679        use darkbio_crypto::cwt;
680        use darkbio_crypto::cwt::claims::{self, eat};
681        use darkbio_trust::device::{EmulatorClaims, HardwareClaims};
682        use darkbio_trust::{CRYPTO_DOMAIN_DEVICE_ATTESTATION, Realm};
683        use std::time::{SystemTime, UNIX_EPOCH};
684
685        let now = SystemTime::now()
686            .duration_since(UNIX_EPOCH)
687            .unwrap()
688            .as_secs();
689
690        /// Drives a handshake with an Ark presenting the attestation and the host
691        /// trusting the roots, returning the host's verdict.
692        fn handshake(
693            signer_key: xdsa::SecretKey,
694            attestation: Attestation,
695            hardware: &[xdsa::PublicKey],
696            emulator: &[xdsa::PublicKey],
697        ) -> Result<darkbio_trust::device::Device, Error> {
698            let (host_sock, ark_sock) = UnixStream::pair().unwrap();
699            let ark_reader = ark_sock.try_clone().unwrap();
700            let ark_writer = ark_sock;
701
702            let ark_thread = std::thread::spawn(move || {
703                let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
704                ark.next_message()
705            });
706            let mut host = HostSide::new(host_sock.try_clone().unwrap(), host_sock);
707            let result = host.handshake(&Roots { hardware, emulator });
708
709            // Dropping the host tears down the transport, unblocking the Ark
710            drop(host);
711            let _ = ark_thread.join().unwrap();
712            result
713        }
714
715        let hardware_root = xdsa::SecretKey::generate();
716        let emulator_root = xdsa::SecretKey::generate();
717        let hardware_roots = [hardware_root.public_key()];
718        let emulator_roots = [emulator_root.public_key()];
719
720        // A hardware Ark attested by a hardware root is accepted with its identity
721        let signer_key = xdsa::SecretKey::generate();
722        let attestation = cwt::issue(
723            &HardwareClaims {
724                sub: claims::Subject {
725                    sub: "ark-1234".into(),
726                },
727                cnf: claims::Confirm::new(signer_key.public_key()),
728                nbf: claims::NotBefore { nbf: now - 10 },
729                iat: claims::IssuedAt { iat: now - 10 },
730                oem: eat::Oemid::new_pen(65145),
731                hwm: eat::HwModel {
732                    hw_model: b"Ark I".to_vec(),
733                },
734                hwv: eat::HwVersion::new("Ark I - 1.0.0".into()),
735            },
736            &hardware_root,
737            CRYPTO_DOMAIN_DEVICE_ATTESTATION,
738        )
739        .map(|cwt| Attestation::new(cwt).unwrap())
740        .unwrap();
741        let device = handshake(signer_key, attestation.clone(), &hardware_roots, &[]).unwrap();
742        assert_eq!(device.realm, Realm::Hardware, "realm mismatch");
743        assert_eq!(device.serial, "ark-1234", "serial mismatch");
744
745        // The same Ark is refused by a host trusting only emulator roots
746        let signer_key = xdsa::SecretKey::generate();
747        assert!(
748            handshake(signer_key, attestation, &[], &emulator_roots).is_err(),
749            "hardware attestation accepted under emulator roots"
750        );
751
752        // An emulated Ark attested by an emulator root is accepted with its expiry
753        let signer_key = xdsa::SecretKey::generate();
754        let attestation = cwt::issue(
755            &EmulatorClaims {
756                sub: claims::Subject {
757                    sub: "emu-1234".into(),
758                },
759                cnf: claims::Confirm::new(signer_key.public_key()),
760                nbf: claims::NotBefore { nbf: now - 10 },
761                exp: claims::Expiration { exp: now + 1000 },
762                iat: claims::IssuedAt { iat: now - 10 },
763                oem: eat::Oemid::new_pen(65145),
764                hwm: eat::HwModel {
765                    hw_model: b"Ark I".to_vec(),
766                },
767                hwv: eat::HwVersion::new("Ark I - 1.0.0".into()),
768            },
769            &emulator_root,
770            CRYPTO_DOMAIN_DEVICE_ATTESTATION,
771        )
772        .map(|cwt| Attestation::new(cwt).unwrap())
773        .unwrap();
774        let device = handshake(signer_key, attestation, &hardware_roots, &emulator_roots).unwrap();
775        assert_eq!(device.realm, Realm::Emulator, "realm mismatch");
776        assert_eq!(device.expiry, Some(now + 1000), "expiry mismatch");
777
778        // A never onboarded Ark presenting a self-signed attestation is refused
779        let signer_key = xdsa::SecretKey::generate();
780        let attestation = cwt::issue(
781            &HardwareClaims {
782                sub: claims::Subject { sub: "".into() },
783                cnf: claims::Confirm::new(signer_key.public_key()),
784                nbf: claims::NotBefore { nbf: 0 },
785                iat: claims::IssuedAt { iat: 0 },
786                oem: eat::Oemid::new_pen(0),
787                hwm: eat::HwModel { hw_model: vec![] },
788                hwv: eat::HwVersion::new("".into()),
789            },
790            &signer_key,
791            CRYPTO_DOMAIN_DEVICE_ATTESTATION,
792        )
793        .map(|cwt| Attestation::new(cwt).unwrap())
794        .unwrap();
795        assert!(
796            handshake(signer_key, attestation, &hardware_roots, &emulator_roots).is_err(),
797            "self-signed attestation accepted"
798        );
799    }
800
801    // Tests that only CWTs in a device attestation shape are accepted as
802    // attestations, junk and other token shapes being refused up front.
803    #[test]
804    fn test_attestation_shapes() {
805        use darkbio_crypto::cwt::claims;
806        use darkbio_trust::CRYPTO_DOMAIN_DEVICE_ATTESTATION;
807
808        let signer = xdsa::SecretKey::generate();
809        let _ = self_attestation(&signer);
810
811        let emulator = darkbio_trust::device::EmulatorClaims {
812            sub: claims::Subject { sub: "".into() },
813            cnf: claims::Confirm::new(signer.public_key()),
814            nbf: claims::NotBefore { nbf: 0 },
815            exp: claims::Expiration { exp: u64::MAX },
816            iat: claims::IssuedAt { iat: 0 },
817            oem: claims::eat::Oemid::new_pen(0),
818            hwm: claims::eat::HwModel { hw_model: vec![] },
819            hwv: claims::eat::HwVersion::new("".into()),
820        };
821        let cwt = cwt::issue(&emulator, &signer, CRYPTO_DOMAIN_DEVICE_ATTESTATION).unwrap();
822        Attestation::new(cwt).expect("emulator attestation refused");
823
824        let cloud = darkbio_trust::cloud::SignerClaims {
825            iss: claims::Issuer { iss: "".into() },
826            sub: claims::Subject { sub: "".into() },
827            nbf: claims::NotBefore { nbf: 0 },
828            exp: claims::Expiration { exp: 1 },
829            cnf: claims::Confirm::new(signer.public_key()),
830        };
831        let cwt = cwt::issue(&cloud, &signer, CRYPTO_DOMAIN_DEVICE_ATTESTATION).unwrap();
832        assert!(
833            matches!(Attestation::new(cwt), Err(Error::InvalidAttestation)),
834            "cloud attestation accepted as device attestation"
835        );
836        assert!(
837            matches!(
838                Attestation::new(b"junk".to_vec()),
839                Err(Error::InvalidAttestation)
840            ),
841            "junk accepted as device attestation"
842        );
843    }
844
845    // Tests that the host refuses a malformed attestation before consulting its
846    // verifier. The Ark bypasses the shape check through the private constructor,
847    // as a misbehaving Ark would by not using this crate at all.
848    #[test]
849    fn test_malformed_attestation_rejected() {
850        testing::init_tracing();
851
852        /// Verifier that must never be consulted.
853        struct Unreachable;
854
855        impl Verifier for Unreachable {
856            type Info = ();
857
858            fn verify(&self, _: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String> {
859                panic!("verifier consulted with a malformed attestation")
860            }
861        }
862
863        let signer_key = xdsa::SecretKey::generate();
864
865        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
866        let ark_reader = ark_sock.try_clone().unwrap();
867        let ark_writer = ark_sock;
868
869        let ark_thread = std::thread::spawn(move || {
870            let attestation = Attestation(b"junk".to_vec());
871            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
872            ark.next_message()
873        });
874        let mut host = HostSide::new(host_sock.try_clone().unwrap(), host_sock);
875        assert!(
876            matches!(host.handshake(&Unreachable), Err(Error::InvalidAttestation)),
877            "malformed attestation not rejected"
878        );
879
880        // Dropping the host tears down the transport, unblocking the Ark
881        drop(host);
882        assert!(
883            ark_thread.join().unwrap().is_err(),
884            "expected torn down wire"
885        );
886    }
887
888    // Tests that a transport failure after sealing drops the session, since
889    // the peer's HPKE sequence can no longer be caught up with, and that a
890    // fresh handshake recovers the wire.
891    #[test]
892    fn test_send_failure_drops_session() {
893        testing::init_tracing();
894
895        /// Writer failing on demand to simulate a transport fault.
896        struct Faulty {
897            inner: UnixStream,
898            fail: Arc<AtomicBool>,
899        }
900
901        impl Write for Faulty {
902            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
903                if self.fail.load(Ordering::Relaxed) {
904                    return Err(io::ErrorKind::BrokenPipe.into());
905                }
906                self.inner.write(buf)
907            }
908
909            fn flush(&mut self) -> io::Result<()> {
910                self.inner.flush()
911            }
912        }
913
914        let signer_key = xdsa::SecretKey::generate();
915        let signer_pub = signer_key.public_key();
916
917        let (host_sock, ark_sock) = UnixStream::pair().unwrap();
918        let ark_reader = ark_sock.try_clone().unwrap();
919        let ark_writer = ark_sock;
920
921        // Ark side: receive one message, echo it back.
922        let ark_thread = std::thread::spawn(move || {
923            let attestation = self_attestation(&signer_key);
924            let mut ark = ArkSide::new(ark_reader, ark_writer, signer_key, attestation);
925            let req = ark.next_message().unwrap();
926            ark.send_message(ArkToHost {
927                id: req.id,
928                err: None,
929                content: None,
930            })
931            .unwrap();
932            req
933        });
934
935        let fail = Arc::new(AtomicBool::new(false));
936        let writer = Faulty {
937            inner: host_sock.try_clone().unwrap(),
938            fail: fail.clone(),
939        };
940        let mut host = HostSide::new(host_sock, writer);
941        host.handshake(&signer_pub).unwrap();
942
943        // Break the transport and send a message. It gets sealed, fails to go
944        // out, and must take the session down with it.
945        fail.store(true, Ordering::Relaxed);
946        let result = host.send_message(HostToArk {
947            id: Some(1),
948            content: None,
949        });
950        assert!(
951            matches!(result, Err(Error::SendFailed(_))),
952            "expected send failure"
953        );
954        let result = host.send_message(HostToArk {
955            id: Some(2),
956            content: None,
957        });
958        assert!(
959            matches!(result, Err(Error::EncryptionFailed(_))),
960            "expected dropped session"
961        );
962
963        // Heal the transport, a fresh handshake resynchronizes both sides.
964        fail.store(false, Ordering::Relaxed);
965        host.handshake(&signer_pub).unwrap();
966        host.send_message(HostToArk {
967            id: Some(3),
968            content: None,
969        })
970        .unwrap();
971
972        let req = ark_thread.join().unwrap();
973        assert_eq!(req.id, Some(3), "request mismatch");
974
975        let res = host.next_message().unwrap();
976        assert_eq!(res.id, Some(3), "response mismatch");
977    }
978}