Skip to main content

breakmancer/
protocol.rs

1//! Network and encryption protocol.
2//!
3//! # Summary
4//!
5//! The breakmancer protocol consists of a cryptographic handshake
6//! followed by a sequence of command/response messages.
7//! This implements a secured reverse shell.
8//!
9//! The protocol follows a client/server model, although in this case the
10//! "client" is usually run on what would normally be termed a server
11//! (likely a CI/CD server), and
12//! the "server" is probably running on the user's development laptop; this is a
13//! *reverse* shell, after all. Due to this ambiguity, more specific
14//! functional terms will be used here instead.
15//!
16//! The user first runs `breakmancer start` to start a session on their
17//! development machine; that process will be termed the "Controller". It
18//! begins listening on a port and prints a `breakmancer break` command to
19//! run on the machine to be debugged.
20//!
21//! The user then copies this break command into the script that needs
22//! debugging. When that line of the script runs, the new breakmancer
23//! process (we'll call this the "Breakpoint") reaches back to the Controller,
24//! performs a handshake, and waits for further instructions.
25//!
26//! The user is then presented with a simple shell-like environment in the
27//! Controller session. They can enter commands to be run. These commands
28//! are sent to the Breakpoint, which executes them and streams the output
29//! back to the Controller for display.
30//!
31//! # Constraints
32//!
33//! The Breakpoint may be in a script running in a public environment,
34//! such as a CI runner for an open source project. This means it may
35//! not always be possible to keep secrets on that side. The dual-pub
36//! protocol therefore does not rely on provisioning a private key or
37//! a shared secret on the Breakpoint side.
38//!
39//! Instead, dual-pub requires that the Breakpoint's output is visible
40//! more or less in realtime. This allows the Breakpoint to generate
41//! an ephemeral keypair and print the public key; the user can then
42//! copy this (or rather a digest of it) to the Controller.
43//!
44//! # Goals
45//!
46//! - The Breakpoint must only execute commands specified by the Controller,
47//!   without alteration (mutation, replay, reordering, etc.) It must not
48//!   be possible for another party to impersonate the Controller, even an
49//!   on-path attacker.
50//! - The Controller must be assured that all outputs are truly from the
51//!   Breakpoint. No party should be able to impersonate the Breakpoint.
52//! - Only the Breakpoint should be able to read or infer the contents
53//!   of commands. This includes defense against:
54//!     - Timing attacks, e.g. inference of text via keystroke timing
55//!       during interactive input. (Protocol is currently line-oriented,
56//!       so this isn't a significant concern.)
57//!     - Plaintext length leakage, to the extent feasible. This would be
58//!       a concern for messages in both directions.
59//!         - **TODO:** Implement padding.
60//! - Confidentiality of traffic must be maintained even if it is stored
61//!   and later attacked with a quantum computer.
62//! - The Breakpoint may run in a low-confidentiality environment and may
63//!   not have secure secrets storage.
64//!
65//! # Non-goals
66//!
67//! - Availability, in the face of an adversary.
68//!
69//! # Cryptographic protocol
70//!
71//! At a high level, the Controller and Breakpoint use X-Wing to send
72//! each other secrets during an initial handshake, then combine those
73//! to generate a pair of XChaCha20-Poly1305 stream keys for all
74//! subsequent messages.
75//!
76//! Both parties generate their X-Wing keypairs at startup, and the
77//! user is enlisted to copy digests of the public keys in both
78//! directions in order to establish trust. (In the CLI we call them
79//! "verification strings"; in the code we say "IDs", for brevity.)
80//!
81//! Details:
82//!
83//! - The Controller creates an X-Wing keypair `ctrl_pub`,
84//!   `ctrl_priv` as well as a digest `ctrl_pub_digest`.
85//!     - The digest is a BLAKE2b digest with `breakmancer.dual-pub.pubkey-digest.controller`
86//!       as the `key` param, and 12 byte digest size. The digest is
87//!       Base58-encoded (using the Bitcoin alphabet) for presentation.
88//!     - The role of the digest is to allow provisioning each party with
89//!       the other's public key in a trusted manner without requiring a user to
90//!       copy and paste a kilobyte+ of text.
91//! - The Breakpoint does the same: `break_pub`, `break_priv`, and
92//!   `break_pub_digest` (with `breakpoint` instead of `controller` in the
93//!   `key` param).
94//! - The digests are communicated to the counterparties out-of-band.
95//! - During the handshake over the network:
96//!     - Each party sends the other their full public key. The parties then
97//!       compute the digest for the received key and compare it to the trusted
98//!       one received out-of-band.
99//!     - Each party uses X-Wing to encapsulate a secret for the other. The
100//!       ciphertexts are shared across the network and decapsulated. The
101//!       resulting shared secrets are `setup_secret_part1` (sent by
102//!       the Controller) and `setup_secret_part2` (sent by the Breakpoint).
103//!     - Each party now computes the stream keys:
104//!         - Concatenate `ctrl_pub`, `break_pub`, `setup_secret_part1`,
105//!           `setup_secret_part2`.
106//!         - Feed this into BLAKE2b with a key of `breakmancer.dual-pub.stream-keys`
107//!           and a digest length of 64.
108//!         - Split the digest into two 32-byte values.
109//!         - These are now the XChaCha20-Poly1305 stream keys
110//!           `ctrl_stream_key` and `break_stream_key`. The first is used by the
111//!           Controller to send messages, the latter by the Breakpoint.
112//!     - Each party initializes a libsodium `crypto_secretstream` with their respective
113//!       transmit key and sends the initializing header to the other party.
114//!     - The handshake is now complete.
115//! - All subsequent messages are encrypted using `crypto_secretstream`.
116//!
117//! # Network layer
118//!
119//! Communication occurs over TCP. Messages are sent length-prefixed
120//! (4-byte little-endian).
121//!
122//! # Insecure message layer
123//!
124//! Each raw message is a CBOR map with one key (naming the message type)
125//! and a type-specific value. There are five types: `BreakpointHello`,
126//! `ControllerHello`, `BreakpointSetupFinalize`, `ControllerSetupFinalize`,
127//! and `EncryptedMsg`. Several of these can contain
128//! encrypted message data, a `crypto_secretstream` message as bytes.
129//!
130//! # Secure message layer
131//!
132//! When an encrypted message is decrypted, it again takes the form of a
133//! CBOR map of one key. The types of these are: `BreakpointIntro`,
134//! `Command`, `OutputLine`, `Finished`, `TerminateCmd`, and `ExitBreak`.
135//!
136//! # Application protocol
137//!
138//! This includes some recap of the cryptographic protocol.
139//!
140//! Any time there is a protocol error (including a message type that is
141//! received in an unexpected context), network fault (such as a closed
142//! TCP connection), or cryptography failure (such as a public key that
143//! doesn't match the expected digest) that party will drop the
144//! connection and start over at the beginning of the handshake.
145//!
146//! ## Initial, out-of-band setup
147//!
148//! - The developer runs the `start` command on their workstation. This
149//!   is the "Controller" process. The Controller creates a fresh keypair.
150//! - The Controller outputs a `break` command to be run in the system
151//!   being inspected. When that command runs, it is the "Breakpoint"
152//!   party. The command contains `ctrl_pub_digest` and the address of the
153//!   Controller.
154//! - When the Breakpoint starts, it also creates a fresh keypair. It then
155//!   connects to the Controller and prints `break_pub_digest` to stdout.
156//!
157//! ## Handshake
158//!
159//! Due to the need to exchange public keys before secrets can be exchanged,
160//! the full handshake requires four messages:
161//!
162//! - The Breakpoint sends a `BreakpointHello` containing `protocol`
163//!   with value (`"dual-pub"`), `break_pub`, and `expecting_controller_id`.
164//!     - The Controller can reject the connection early if the digest
165//!       doesn't match. This is *not* a security measure; it only
166//!       allows early, non-interactive detection of a handshake that
167//!       is doomed to failure.
168//!     - A rejection here may be indicated by sending a `HandshakeRejected`
169//!       message giving the reason (and indicating whether the failure is
170//!       permanent, which it would be at this stage). Or, the controller
171//!       could simply close the connection.
172//! - The Controller asks for `break_pub_digest` as input and validates
173//!   `break_pub` against this. It then sends a `ControllerHello` containing
174//!   `ctrl_pub` and `setup_secret_part1`.
175//!     - A digest mismatch here could again result in a `HandshakeRejected`
176//!       message, although this one should be non-permanent.
177//! - The Breakpoint validates `ctrl_pub`, decapsulates the secret, derives the
178//!   stream keys, and responds with a `BreakpointSetupFinalize` containing
179//!   `setup_secret_part2`, a `cipher_header` for its transmit stream, and an
180//!   `encrypted_intro` (encrypted message containing a `BreakpointIntro`).
181//!     - The intro just contains information about *which* breakpoint is
182//!       running, in case there are multiple.
183//! - The Controller decapsulates the second secret and derives stream keys as
184//!   well, then sends a `ControllerSetupFinalize` with its own `cipher_header`.
185//!
186//! At this point, both parties have a secure channel, and all future
187//! messages are sent encrypted and wrapped in `EncryptedMsg`-type
188//! messages.
189//!
190//! Any message not in this strict order is an error and requires starting a
191//! fresh connection.
192//!
193//! ## Main loop: Controller
194//!
195//! Each time the user enters a command. the Controller sends a `Command`
196//! message (a command string plus a sequence number that increments for
197//! each command). It then switches to waiting for:
198//!
199//! - `OutputLine` messages bearing lines of stdout or stderr for
200//!   display (expecting a matching sequence number.)
201//! - A `Finished` message, at which point it increments the command
202//!   sequence number and goes back to waiting for user input.
203//! - An interrupt from the user, in which case it sends a `TerminateCmd`
204//!   (with sequence number) and waits for more output or the command to
205//!   finish.
206//!
207//! Instead of entering a command, the user can also choose to ask the
208//! breakpoint to exit and continue script execution (via `ExitBreak`
209//! message), or to do that but also exit the controller entirely.
210//!
211//! ## Main loop: Breakpoint
212//!
213//! The Breakpoint waits in a loop for `Command` and `ExitBreak`
214//! messages.
215//!
216//! When a `Command` is received, the Breakpoint executes the command and
217//! streams back stdout and stderr lines as `OutputLine` messages. If a
218//! `TerminateCmd` is received, the Breakpoint checks the sequence number
219//! and ends the child process. Either way, once the command finishes, the
220//! Breakpoint sends a `Finished` message with the command's exit code and
221//! returns to the main loop to wait.
222//!
223//! If an `ExitBreak` is received in the main loop, the Breakpoint process
224//! exits.
225
226use blake2::{
227    digest::{consts::U12, Mac},
228    Blake2bMac, Blake2bMac512,
229};
230use clap::ValueEnum;
231use crypto_secretstream as secret_stream;
232use kem::{Decapsulate, Encapsulate};
233use rand::rngs::OsRng;
234use rust_base58::{FromBase58, ToBase58};
235use serde::{Deserialize, Serialize};
236use strum_macros::IntoStaticStr;
237use subtle::ConstantTimeEq;
238
239use crate::transport::{Transport, TransportCaller, TransportListener};
240
241/// General purpose enum for identifying the parties in the protocol.
242#[derive(Clone)]
243pub enum Party {
244    Controller,
245    Breakpoint,
246}
247
248// TODO: Move AuthVersion to cli module.
249
250/// Authentication protocol version that a set of sessions will use.
251#[derive(ValueEnum, Clone, Debug)]
252pub enum AuthVersion {
253    // Details of dual-pub:
254    //
255    // Authenticates both Controller to Breakpoint, and vice versa,
256    // with each party generating public keys. The keys are X-Wing
257    // (X25519 + ML-KEM) to provide post-quantum security. Each party
258    // provides an ID string to the other party, out of
259    // band, in order to validate the public keys.
260    //
261    // This requires that the user be able to observe streaming logs
262    // from the Breakpoint, which is how the Breakpoint communicates
263    // its ID string. (The Controller's ID string
264    // is provided to the Breakpoint in its invocation in the script.)
265    //
266    // (This is a non-doc comment because the doc comment gets used
267    // for the CLI, and this is more info than is needed for that.)
268    /// Dual public key auth -- requires streaming access to breakpoint logs.
269    DualPub,
270}
271
272/// Wrapper representing an X-Wing keypair.
273#[derive(Clone)]
274pub struct XWingKeypair {
275    pub public_key: x_wing::EncapsulationKey,
276    pub private_key: x_wing::DecapsulationKey,
277}
278
279impl XWingKeypair {
280    /// Create a new, random keypair.
281    #[allow(clippy::new_without_default)]
282    pub fn new() -> XWingKeypair {
283        let (decaps, encaps) = x_wing::generate_key_pair_from_os_rng();
284        XWingKeypair {
285            public_key: encaps,
286            private_key: decaps,
287        }
288    }
289
290    /// Derive the keypair from the private key.
291    pub fn from_decaps(decaps: &x_wing::DecapsulationKey) -> XWingKeypair {
292        XWingKeypair {
293            public_key: decaps.encapsulation_key(),
294            private_key: decaps.clone(),
295        }
296    }
297}
298
299/// IDs are public key digests using Blake2b with output of 12
300/// bytes/96 bits.
301///
302/// Digest entropy chosen to be sufficient that it would take an
303/// interactive attacker 1000 years to compute a matching key if they
304/// could make 1 trillion guesses per second simultaneously on 1
305/// million computers. (In practice, keys should normally live hours
306/// to days.)
307///
308/// That would be about 2^95 guesses; the closest number of bytes
309/// would be 12.
310type Blake2KeyDigests = Blake2bMac<U12>;
311
312#[cfg(test)]
313const DIGEST_BYTE_LEN: usize = 12;
314const DIGEST_MIN_CHARS: usize = 12;
315const DIGEST_MAX_CHARS: usize = 17;
316
317/// ID string (verification digest) for either party.
318pub trait IdString: Sized {
319    /// Export the wrapped ID string.
320    ///
321    /// This should not be compared with a simple equality check; use
322    /// `ids_match` instead, which provides additional security and
323    /// usability support.
324    fn id_string(&self) -> String;
325}
326
327/// This is a Base58 digest of the controller's public key (along with
328/// some domain separation).
329#[derive(Clone)]
330pub struct ControllerId(String);
331
332impl ControllerId {
333    /// Compute the ID string for the controller.
334    pub fn from_key(public_key: &x_wing::EncapsulationKey) -> ControllerId {
335        ControllerId(pubkey_digest(public_key, "controller"))
336    }
337
338    /// Load a controller ID string, assuming the format is valid.
339    pub fn from_received_string(id_string: &str) -> Result<ControllerId, String> {
340        validate_id_format(id_string)?;
341        Ok(ControllerId(id_string.to_owned()))
342    }
343}
344
345impl IdString for ControllerId {
346    fn id_string(&self) -> String {
347        self.0.to_owned()
348    }
349}
350
351/// This is a Base58 digest of the breakpoint's public key (along with
352/// some domain separation).
353#[derive(Clone)]
354pub struct BreakpointId(String);
355
356impl BreakpointId {
357    /// Compute the ID string for the breakpoint.
358    pub fn from_key(public_key: &x_wing::EncapsulationKey) -> BreakpointId {
359        BreakpointId(pubkey_digest(public_key, "breakpoint"))
360    }
361
362    /// Load a breakpoint ID string, assuming the format is valid.
363    pub fn from_received_string(id_string: &str) -> Result<BreakpointId, String> {
364        validate_id_format(id_string)?;
365        Ok(BreakpointId(id_string.to_owned()))
366    }
367}
368
369impl IdString for BreakpointId {
370    fn id_string(&self) -> String {
371        self.0.to_owned()
372    }
373}
374
375/// Check that a received ID is well-formed (Base58 in the range of
376/// expected lengths).
377fn validate_id_format(id: &str) -> Result<(), String> {
378    id.from_base58()
379        .map_err(|err| format!("Not valid Base58: {err}"))?;
380
381    // Now that we know it's using the Base58 alphabet, we can freely
382    // conflate bytes and characters.
383    if id.len() < DIGEST_MIN_CHARS {
384        Err(format!(
385            "Too short (less than {DIGEST_MIN_CHARS} characters long)"
386        ))
387    } else if id.len() > DIGEST_MAX_CHARS {
388        Err(format!(
389            "Too long (more than {DIGEST_MAX_CHARS} characters long)"
390        ))
391    } else {
392        Ok(())
393    }
394}
395
396/// Internal function for computing either the controller or breakpoint digest.
397fn pubkey_digest(public_key: &x_wing::EncapsulationKey, party: &str) -> String {
398    let pub_raw = public_key.as_bytes();
399    // Domain separation provided here, via the `key` parameter
400    let domain = format!("breakmancer.dual-pub.pubkey-digest.{party}");
401
402    let mut hasher =
403        Blake2KeyDigests::new_with_salt_and_personal(domain.as_bytes(), &[], &[]).unwrap();
404    hasher.update(&pub_raw);
405    let digest = hasher.finalize().into_bytes();
406
407    digest.to_base58()
408}
409
410/// Verify an ID by comparing it to a trusted value.
411///
412/// `trusted_id` is the ID that was received out of band and is
413/// trusted. `received_key_id` is the digest of a public key that we
414/// received over the network.
415///
416/// The comparison is performed in constant time.
417///
418/// Some cleanup is performed -- whitespace is trimmed, and lookalike
419/// characters that are not part of the Base58 alphabet are changed to
420/// the expected in-alphabet character.
421fn ids_match<D: IdString>(trusted_id: &D, received_key_id: &D) -> bool {
422    let trusted_digest = trusted_id.id_string();
423    let received_key_digest = received_key_id.id_string();
424
425    // If someone typed this, automatically correct Base58
426    // lookalikes. Also, trim whitespace.
427    let trusted_digest = trusted_digest.replace(['l', 'I'], "1").trim().to_string();
428
429    trusted_digest
430        .as_bytes()
431        .ct_eq(received_key_digest.as_bytes())
432        .into()
433}
434
435/// The protocol version that's implemented here.
436///
437/// (Expecting to later understand multiple protocol versions.)
438const PROTO_DUAL_PUB: &str = "dual-pub";
439
440//************************//
441//                        //
442//  Unencrypted messages  //
443//                        //
444//************************//
445
446/// Message sent any time during handshake to tell counterparty why
447/// the handshake is being rejected and that the connection will be
448/// shut down.
449#[derive(Debug, PartialEq, Serialize, Deserialize)]
450struct HandshakeRejected {
451    /// Human-readable reason for rejection.
452    reason: String,
453    /// Whether the other party should consider this a permanent
454    /// rejection (and should not re-attempt a connection with the
455    /// same parameters).
456    permanent: bool,
457}
458
459/// Initial message sent from breakpoint to controller.
460///
461/// Confirms protocol and delivers breakpoint's public key.
462#[derive(Debug, PartialEq, Serialize, Deserialize)]
463struct BreakpointHello {
464    /// Protocol to use.
465    ///
466    /// The breakpoint will have been provisioned with a list of
467    /// protocols acceptable to the controller, and this is the protocol
468    /// the breakpoint has chosen to use.
469    protocol: String,
470
471    /// The breakpoint's public key.
472    #[serde(with = "serde_bytes")]
473    break_pub: [u8; x_wing::ENCAPSULATION_KEY_SIZE],
474
475    /// The ID string that the breakpoint expects from the
476    /// controller. (Intended only for early cooperative termination,
477    /// not for security.)
478    expecting_controller_id: String,
479}
480
481/// Controller's first message back to the breakpoint.
482///
483/// Delivers the controller's full public key and its contribution to
484/// the setup secret.
485#[derive(Debug, PartialEq, Serialize, Deserialize)]
486struct ControllerHello {
487    /// The controller's public key.
488    #[serde(with = "serde_bytes")]
489    ctrl_pub: [u8; x_wing::ENCAPSULATION_KEY_SIZE],
490
491    /// The controller's half of the setup secret, encapsulated for
492    /// the breakpoint's public key.
493    #[serde(with = "serde_bytes")]
494    setup_secret_part1: [u8; x_wing::CIPHERTEXT_SIZE],
495}
496
497/// Breakpoint's second message.
498///
499/// This completes setup for the breakpoint->controller direction.
500#[derive(Debug, PartialEq, Serialize, Deserialize)]
501struct BreakpointSetupFinalize {
502    /// The breakpoint's half of the setup secret, encapsulated for
503    /// the controller's public key.
504    #[serde(with = "serde_bytes")]
505    setup_secret_part2: [u8; x_wing::CIPHERTEXT_SIZE],
506
507    /// Starts the secure channel from the breakpoint.
508    ///
509    /// This is an `XChaCha20-Poly1305` header for `crypto_secretstream`.
510    #[serde(with = "serde_bytes")]
511    cipher_header: [u8; secret_stream::Header::BYTES],
512
513    /// Encrypted [`BreakpointIntro`], the first `SecureMsg` in the stream.
514    ///
515    /// This must be decrypted so that cipher stream synchronization
516    /// is maintained.
517    encrypted_intro: Vec<u8>,
518}
519
520/// Controller's second message.
521///
522/// This completes setup for the controller->breakpoint direction.
523#[derive(Debug, PartialEq, Serialize, Deserialize)]
524struct ControllerSetupFinalize {
525    /// Starts the secure channel from the controller.
526    ///
527    /// This is an `XChaCha20-Poly1305` header for `crypto_secretstream`.
528    #[serde(with = "serde_bytes")]
529    cipher_header: [u8; secret_stream::Header::BYTES],
530}
531
532/// Contains an encrypted [`SecureMsg`].
533///
534/// A stream of these messages forms the basis of the secure channel.
535#[derive(Debug, PartialEq, Serialize, Deserialize)]
536struct EncryptedMsg {
537    #[serde(with = "serde_bytes")]
538    encrypted: Vec<u8>,
539}
540
541/// A message sent insecurely over the network.
542///
543/// These messages do not have confidentiality or integrity protection
544/// except where they specifically contain encrypted fields.
545#[allow(clippy::large_enum_variant)]
546#[derive(Debug, PartialEq, Serialize, Deserialize, IntoStaticStr)]
547enum InsecureMsg {
548    HandshakeRejected(HandshakeRejected),
549    BreakpointHello(BreakpointHello),
550    ControllerHello(ControllerHello),
551    BreakpointSetupFinalize(BreakpointSetupFinalize),
552    ControllerSetupFinalize(ControllerSetupFinalize),
553    EncryptedMsg(EncryptedMsg),
554}
555
556//***************************************//
557//                                       //
558//  Secure messages (all are encrypted)  //
559//                                       //
560//***************************************//
561
562/// Embedded in the `BreakpointHello`, this tells the controller about
563/// the breakpoint.
564#[derive(Debug, PartialEq, Serialize, Deserialize)]
565pub struct BreakpointIntro {
566    /// Which breakpoint is calling back (if option is in use.)
567    pub which: Option<String>,
568}
569
570/// A command sent by the controller for the breakpoint to execute.
571#[derive(Debug, PartialEq, Serialize, Deserialize)]
572pub struct Command {
573    /// Code to execute on the breakpoint.
574    pub command: String,
575    /// Sequence ID for this command.
576    pub seq: u32,
577}
578
579/// One line of output from a command, sent by breakpoint to controller.
580#[derive(Debug, PartialEq, Serialize, Deserialize)]
581pub struct OutputLine {
582    #[serde(with = "serde_bytes")]
583    pub bytes: Vec<u8>,
584    pub gender: OutputType,
585    /// Must match the most recent [`Command::seq`].
586    pub seq: u32,
587}
588
589#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
590pub enum OutputType {
591    Stdout,
592    Stderr,
593}
594
595/// Final results of a command, sent by breakpoint to controller.
596#[derive(Debug, PartialEq, Serialize, Deserialize)]
597pub struct Finished {
598    /// This is a `std::process::ExitStatus`
599    pub exit_code: Option<i32>,
600    /// Must match the most recent [`Command::seq`].
601    pub seq: u32,
602}
603
604/// Sent by controller to tell breakpoint to terminate a command that is
605/// in-process.
606#[derive(Debug, PartialEq, Serialize, Deserialize)]
607pub struct TerminateCmd {
608    /// Sequence number of the command we're going to try to terminate.
609    pub seq: u32,
610}
611
612/// Controller tells the breakpoint to exit and resume normal script
613/// execution.
614#[derive(Debug, PartialEq, Serialize, Deserialize)]
615pub struct ExitBreak {}
616
617/// All message types sent over the secure channel.
618#[derive(Debug, PartialEq, Serialize, Deserialize, IntoStaticStr)]
619pub enum SecureMsg {
620    BreakpointIntro(BreakpointIntro),
621    Command(Command),
622    OutputLine(OutputLine),
623    Finished(Finished),
624    TerminateCmd(TerminateCmd),
625    /// This variant is special-cased to end the connection when sent or received.
626    ExitBreak(ExitBreak),
627}
628
629//*****************************************//
630//                                         //
631//  Message encryption and key derivation  //
632//                                         //
633//*****************************************//
634
635/// Create the pair of keys that will be used for secure
636/// communication.
637///
638/// Accepts the public keys of both parties and the secrets that each
639/// party has encapsulated for the other via the setup messages.
640///
641/// The first returned key is for the [`crypto_secretstream`] of messages from
642/// the controller to the breakpoint; the second is for the other
643/// direction.
644fn derive_stream_keys_dual_pub(
645    pubkey_controller: &x_wing::EncapsulationKey,
646    pubkey_breakpoint: &x_wing::EncapsulationKey,
647    setup_secret_controller: &x_wing::SharedSecret,
648    setup_secret_breakpoint: &x_wing::SharedSecret,
649) -> (secret_stream::Key, secret_stream::Key) {
650    let mut in_raw = Vec::new();
651    in_raw.extend(&pubkey_controller.as_bytes());
652    in_raw.extend(&pubkey_breakpoint.as_bytes());
653    in_raw.extend(*setup_secret_controller);
654    in_raw.extend(*setup_secret_breakpoint);
655
656    let key_len = secret_stream::Key::BYTES;
657    // Hash input is public keys followed by setup secrets. Key is a
658    // domain separator.
659    let domain = "breakmancer.dual-pub.stream-keys";
660    let mut hasher =
661        Blake2bMac512::new_with_salt_and_personal(domain.as_bytes(), &[], &[]).unwrap();
662    hasher.update(&in_raw);
663    let key_bytes = hasher.finalize().into_bytes();
664    assert_eq!(key_bytes.len(), 2 * key_len);
665
666    // Split output into two halves, as stream keys
667    let controller_stream_key = secret_stream::Key::try_from(&key_bytes[0..key_len]).unwrap();
668    let breakpoint_stream_key = secret_stream::Key::try_from(&key_bytes[key_len..]).unwrap();
669
670    (controller_stream_key, breakpoint_stream_key)
671}
672
673/// Serialize data to CBOR, at any layer of protocol.
674///
675/// This standardizes our approach to data serialization and encoding.
676fn serialize<D>(data: &D) -> Vec<u8>
677where
678    D: Serialize,
679{
680    // Note that serde_cbor serializes structs as maps of strings to
681    // values by default. This is important—if the other party
682    // includes additional, unexpected keys, we can skip over and
683    // ignore them.
684    serde_cbor::to_vec(&data).unwrap()
685}
686
687/// Deserialize data from CBOR, at any layer of protocol.
688///
689/// This standardizes our approach to data serialization and encoding.
690fn deserialize<'a, T>(bytes: &'a [u8]) -> Result<T, String>
691where
692    T: Deserialize<'a>,
693{
694    serde_cbor::from_slice(bytes).map_err(|err| format!("{err}"))
695}
696
697/// Encrypt a [`SecureMsg`] for transport.
698///
699/// This must be done immediately before sending the message in
700/// order to ensure that the cipher state stays in sync.
701fn encrypt_message(msg: &SecureMsg, cipher_tx: &mut secret_stream::PushStream) -> EncryptedMsg {
702    let mut in_place_buffer = serialize(msg);
703    cipher_tx
704        .push(&mut in_place_buffer, &[], secret_stream::Tag::Message)
705        .unwrap();
706    EncryptedMsg {
707        encrypted: in_place_buffer,
708    }
709}
710
711/// Decrypt an encrypted message.
712///
713/// This must be done immediately after receiving the message in
714/// order to ensure that the cipher state stays in sync.
715fn decrypt_message(
716    msg: &EncryptedMsg,
717    cipher_rx: &mut secret_stream::PullStream,
718) -> Result<SecureMsg, String> {
719    let mut in_place_buffer = msg.encrypted.clone();
720    let tag = cipher_rx
721        .pull(&mut in_place_buffer, &[])
722        .map_err(|err| format!("Unable to decrypt message: {err}"))?;
723
724    // We don't use Final in this protocol -- we just close the connection when we're done.
725    if tag != secret_stream::Tag::Message {
726        Err(format!("Unexpected tag in decryption stream: {tag:?}"))?;
727    }
728
729    deserialize(&in_place_buffer).map_err(|err| format!("Could not parse message data: {err}"))
730}
731
732//****************************************//
733//                                        //
734//  Secured connection abstraction layer  //
735//                                        //
736//****************************************//
737
738/// Send a message over the insecure channel.
739async fn send_insecure(transport: &mut Transport, msg: &InsecureMsg) -> Result<(), String> {
740    transport.send_raw(&serialize(msg)).await
741}
742
743/// Receive a message over the insecure channel.
744async fn receive_insecure(transport: &mut Transport) -> Result<Option<InsecureMsg>, String> {
745    let raw = match transport.receive_raw().await? {
746        None => return Ok(None),
747        Some(raw) => raw,
748    };
749    deserialize(&raw).map_err(|err| format!("Could not parse message body: {err}"))
750}
751
752/// A secured connection to the other party (breakpoint or controller).
753pub struct Connection {
754    /// Underlying transport for connection.
755    transport: Transport,
756
757    /// Cipher stream for encrypting messages to be sent over the transport.
758    cipher_tx: secret_stream::PushStream,
759
760    /// Cipher stream for decrypting messages received over the transport.
761    cipher_rx: secret_stream::PullStream,
762}
763
764impl Connection {
765    /// Send a message over the secured channel.
766    pub async fn send(&mut self, msg: SecureMsg) -> Result<(), String> {
767        let encrypted = InsecureMsg::EncryptedMsg(encrypt_message(&msg, &mut self.cipher_tx));
768        send_insecure(&mut self.transport, &encrypted).await
769    }
770
771    /// Receive a message over the secured channel.
772    ///
773    /// Ok(None) signals that the channel is now closed.
774    pub async fn recv(&mut self) -> Result<Option<SecureMsg>, String> {
775        match receive_insecure(&mut self.transport).await? {
776            None => Ok(None),
777            Some(InsecureMsg::EncryptedMsg(enc)) => {
778                Ok(Some(decrypt_message(&enc, &mut self.cipher_rx)?))
779            }
780            Some(clear_msg) => Err(format!("Unexpected insecure message type: {clear_msg:?}")),
781        }
782    }
783}
784
785//*******************************//
786//                               //
787//  Controller connection setup  //
788//                               //
789//*******************************//
790
791/// Create a new controller connection and complete when the
792/// breakpoint's hello has been received.
793///
794/// This does not complete the connection setup; the caller is
795/// responsible for either dropping the returned
796/// [`ControllerReceivedHello`] or calling its
797/// `verify_breakpoint_and_complete_setup` method.
798pub async fn controller_open_connection(
799    controller_keypair: XWingKeypair,
800    transport_listener: &mut TransportListener,
801) -> Result<ControllerReceivedHello, String> {
802    let mut transport = transport_listener.accept_connection().await?;
803
804    // Receive Breakpoint's public key
805    let breakpoint_hello = match receive_insecure(&mut transport).await {
806        Ok(None) => Err(String::from("Breakpoint hung up before BreakpointHello"))?,
807        Ok(Some(InsecureMsg::BreakpointHello(hello))) => hello,
808        Ok(Some(wrong)) => Err(format!(
809            "Unexpected message type when waiting for BreakpointHello: {}",
810            Into::<&str>::into(wrong)
811        ))?,
812        Err(err) => Err(format!(
813            "Couldn't receive data when waiting for a BreakpointHello: {err}"
814        ))?,
815    };
816    if breakpoint_hello.protocol != PROTO_DUAL_PUB {
817        Err(format!(
818            "Unexpected protocol field from breakpoint: {}",
819            breakpoint_hello.protocol
820        ))?;
821    }
822    let breakpoint_pub_key: x_wing::EncapsulationKey = (&breakpoint_hello.break_pub).into();
823    let breakpoint_expecting_id =
824        ControllerId::from_received_string(&breakpoint_hello.expecting_controller_id)?;
825
826    // The Breakpoint gives us an opportunity to close the connection
827    // early on verification mismatch instead of requiring an
828    // interactive step.
829    let ctrl_id = ControllerId::from_key(&controller_keypair.public_key);
830    if !ids_match(&ctrl_id, &breakpoint_expecting_id) {
831        // We specifically do *not* include the received ID. While
832        // that might help the user debug a mis-paste or mis-type, it
833        // also makes it very easy for someone to do the dangerous
834        // thing of copying the string from the error message and
835        // pasting it back in.
836        let err_msg = "Breakpoint sent wrong expected controller verification string, \
837                       exiting early. (Breakpoint had outdated connection string?)";
838        send_insecure(
839            &mut transport,
840            &InsecureMsg::HandshakeRejected(HandshakeRejected {
841                reason: String::from(
842                    "Controller doesn't match expected verification string sent by breakpoint.",
843                ),
844                permanent: true,
845            }),
846        )
847        .await
848        .map_err(|err| format!("Unable to send rejection message. Rejection was: {err}"))?;
849        Err(err_msg)?;
850    }
851
852    Ok(ControllerReceivedHello {
853        controller_keypair,
854        transport,
855        breakpoint_pub_unverified: breakpoint_pub_key,
856    })
857}
858
859/// State where the controller has received a hello from the
860/// breakpoint.
861///
862/// This represents a partially set-up connection requiring some
863/// interaction with the user before setup can complete.
864pub struct ControllerReceivedHello {
865    controller_keypair: XWingKeypair,
866    transport: Transport,
867    /// Unverified public key for a breakpoint, received over the
868    /// network.
869    breakpoint_pub_unverified: x_wing::EncapsulationKey,
870}
871
872impl ControllerReceivedHello {
873    /// Given a breakpoint ID received out-of-band,
874    /// finish the setup process.
875    ///
876    /// If this completes successfully, returns the secured connection
877    /// as well as the decrypted breakpoint introduction message.
878    pub async fn verify_breakpoint_and_complete_setup(
879        mut self,
880        expected_breakpoint_id: &BreakpointId,
881    ) -> Result<(Connection, BreakpointIntro), String> {
882        let received_id = BreakpointId::from_key(&self.breakpoint_pub_unverified);
883        if !ids_match(expected_breakpoint_id, &received_id) {
884            // We specifically do *not* include the received ID. While
885            // that might help the user debug a mis-paste or mis-type,
886            // it also makes it very easy for someone to do the
887            // dangerous thing of copying the string from the error
888            // message and pasting it back in.
889            let err_msg = "Breakpoint verification string did not match. \
890                 Is it possible that you have multiple breakpoints attempting \
891                 to connect here at the same time and entered the verification \
892                 string from the wrong one?";
893            send_insecure(
894                &mut self.transport,
895                &InsecureMsg::HandshakeRejected(HandshakeRejected {
896                    reason: String::from(
897                        "Breakpoint sent a public key that doesn't match \
898                         the verification string the user entered.",
899                    ),
900                    permanent: false, // could be a mis-paste or a typo
901                }),
902            )
903            .await
904            .map_err(|err| format!("Unable to send rejection message. Rejection was: {err}"))?;
905            return Err(err_msg.to_string());
906        }
907        let breakpoint_pubkey = self.breakpoint_pub_unverified; // now verified!
908
909        // We can now trust the breakpoint's public key, which means
910        // we can send them our half of the setup secret. They'll also
911        // need the controller's public key so they can send us
912        // *their* portion of the setup secret.
913
914        let (controller_secret_encap, controller_secret) = breakpoint_pubkey
915            .encapsulate(&mut rand::rngs::OsRng)
916            .map_err(|err| format!("Failed to create controller secret: {err}"))?;
917
918        send_insecure(
919            &mut self.transport,
920            &InsecureMsg::ControllerHello(ControllerHello {
921                ctrl_pub: self.controller_keypair.public_key.as_bytes(),
922                setup_secret_part1: controller_secret_encap.as_bytes(),
923            }),
924        )
925        .await
926        .map_err(|err| format!("Couldn't respond to breakpoint's hello: {err}"))?;
927
928        // The breakpoint now can send us its own half of the setup
929        // secret, after which both parties will have enough
930        // information to compute the stream secrets. So the
931        // breakpoint also sends us the beginning of its stream,
932        // including a first message.
933
934        let breakpoint_finalize = match receive_insecure(&mut self.transport).await {
935            Ok(None) => Err(String::from(
936                "Breakpoint hung up before BreakpointSetupFinalize",
937            ))?,
938            Ok(Some(InsecureMsg::BreakpointSetupFinalize(finalize))) => finalize,
939            Ok(Some(wrong)) => Err(format!(
940                "Unexpected message type when waiting for BreakpointSetupFinalize: {}",
941                Into::<&str>::into(wrong)
942            ))?,
943            Err(err) => Err(format!(
944                "Couldn't receive data when waiting for a BreakpointSetupFinalize: {err}"
945            ))?,
946        };
947
948        let breakpoint_secret_encap: x_wing::Ciphertext =
949            (&breakpoint_finalize.setup_secret_part2).into();
950        let breakpoint_secret = self
951            .controller_keypair
952            .private_key
953            .decapsulate(&breakpoint_secret_encap)
954            .map_err(|err| {
955                format!("Unable to decrypt breakpoint's half of the setup secret: {err}")
956            })?;
957
958        let (controller_stream_key, breakpoint_stream_key) = derive_stream_keys_dual_pub(
959            &self.controller_keypair.public_key,
960            &breakpoint_pubkey,
961            &controller_secret,
962            &breakpoint_secret,
963        );
964
965        // The breakpoint has already included a message for us! We'll
966        // set up the receive stream and decrypt it now, before
967        // setting up the transmit stream.
968
969        let mut rx_stream = secret_stream::PullStream::init(
970            secret_stream::Header::from(breakpoint_finalize.cipher_header),
971            &breakpoint_stream_key,
972        );
973
974        let maybe_intro = decrypt_message(
975            &EncryptedMsg {
976                encrypted: breakpoint_finalize.encrypted_intro,
977            },
978            &mut rx_stream,
979        )
980        .map_err(|err| format!("Could not decrypt breakpoint introduction: {err}"))?;
981        let breakpoint_intro = match maybe_intro {
982            SecureMsg::BreakpointIntro(intro) => intro,
983            _ => Err("Unexpected message type for breakpoint intro.")?,
984        };
985
986        // The last step is setting up the transmit stream and sending
987        // the start of it to the breakpoint.
988
989        let (tx_header, tx_stream) = secret_stream::PushStream::init(OsRng, &controller_stream_key);
990
991        send_insecure(
992            &mut self.transport,
993            &InsecureMsg::ControllerSetupFinalize(ControllerSetupFinalize {
994                cipher_header: *tx_header.as_ref(),
995            }),
996        )
997        .await
998        .map_err(|err| format!("Unable to send controller setup finalization: {err}"))?;
999
1000        // Now both parties should have a fully secured connection.
1001
1002        let secured_conn = Connection {
1003            transport: self.transport,
1004            cipher_tx: tx_stream,
1005            cipher_rx: rx_stream,
1006        };
1007
1008        Ok((secured_conn, breakpoint_intro))
1009    }
1010}
1011
1012//*******************************//
1013//                               //
1014//  Breakpoint connection setup  //
1015//                               //
1016//*******************************//
1017
1018/// Error occurring during breakpoint's connection setup.
1019#[derive(Debug)]
1020pub struct BreakpointConnectError {
1021    /// Error message string.
1022    pub message: String,
1023    /// Whether this should be considered a permanent error,
1024    /// indicating that the same connection parameters should not be
1025    /// retried.
1026    pub permanent: bool,
1027}
1028
1029/// By default, an error shouldn't be considered a permanent error.
1030impl From<String> for BreakpointConnectError {
1031    fn from(message: String) -> Self {
1032        Self {
1033            message,
1034            permanent: false,
1035        }
1036    }
1037}
1038
1039/// Create a new breakpoint connection, completing once a hello has
1040/// been sent to the controller and the breakpoint is ready to show
1041/// its ID to the user.
1042#[allow(clippy::too_many_lines)]
1043pub async fn breakpoint_open_connection(
1044    breakpoint_keypair: &XWingKeypair,
1045    transport_caller: &TransportCaller,
1046    expected_controller_id: &ControllerId,
1047    which: Option<String>,
1048) -> Result<BreakpointProducedId, BreakpointConnectError> {
1049    let mut transport = transport_caller.new_connection().await?;
1050
1051    // Send a hello to the controller with our public key. This will
1052    // need to be verified with the help of the user.
1053
1054    send_insecure(
1055        &mut transport,
1056        &InsecureMsg::BreakpointHello(BreakpointHello {
1057            protocol: PROTO_DUAL_PUB.to_owned(),
1058            break_pub: breakpoint_keypair.public_key.as_bytes(),
1059            expecting_controller_id: expected_controller_id.id_string(),
1060        }),
1061    )
1062    .await
1063    .map_err(|err| format!("Unable to send hello: {err}"))?;
1064
1065    let breakpoint_id = BreakpointId::from_key(&breakpoint_keypair.public_key);
1066
1067    Ok(BreakpointProducedId {
1068        breakpoint_keypair: breakpoint_keypair.clone(),
1069        expected_controller_id: expected_controller_id.clone(),
1070        which,
1071        transport,
1072        breakpoint_id,
1073    })
1074}
1075
1076/// Represents a partially set up connection from the breakpoint side.
1077pub struct BreakpointProducedId {
1078    breakpoint_keypair: XWingKeypair,
1079    expected_controller_id: ControllerId,
1080    which: Option<String>,
1081    transport: Transport,
1082    /// String to be displayed to the user so they can paste it into
1083    /// the controller.
1084    pub breakpoint_id: BreakpointId,
1085}
1086
1087impl BreakpointProducedId {
1088    /// Call this once `breakpoint_id` has
1089    /// been printed or otherwise conveyed to the user.
1090    ///
1091    /// Completes the connection.
1092    pub async fn continue_after_id_printed(mut self) -> Result<Connection, BreakpointConnectError> {
1093        // Once the controller has verified the breakpoint's public key,
1094        // it responds with its own public key (which we'll need to
1095        // verify) as well as its half of the setup secret.
1096
1097        let controller_hello = match receive_insecure(&mut self.transport).await {
1098            Ok(None) => Err(String::from("Controller hung up before ControllerHello"))?,
1099
1100            Ok(Some(InsecureMsg::ControllerHello(hello))) => hello,
1101            Ok(Some(InsecureMsg::HandshakeRejected(HandshakeRejected { reason, permanent }))) => {
1102                Err(BreakpointConnectError {
1103                    message: format!("Controller rejected connection: {reason}"),
1104                    permanent,
1105                })?
1106            }
1107            Ok(Some(wrong)) => Err(format!(
1108                "Unexpected message type when waiting for ControllerHello: {}",
1109                Into::<&str>::into(wrong)
1110            ))?,
1111            Err(err) => Err(format!(
1112                "Couldn't receive data when waiting for a ControllerHello: {err}"
1113            ))?,
1114        };
1115
1116        let unverified_controller_pubkey: x_wing::EncapsulationKey =
1117            (&controller_hello.ctrl_pub).into();
1118        let received_id = ControllerId::from_key(&unverified_controller_pubkey);
1119        if !ids_match(&self.expected_controller_id, &received_id) {
1120            // We specifically do *not* include the received ID. While
1121            // that might help the user debug a mis-paste or mis-type,
1122            // it also makes it very easy for someone to do the
1123            // dangerous thing of copying the string from the error
1124            // message and pasting it back in.
1125            Err("Controller verification string did not match. \
1126                 Is it possible that the controller has since been \
1127                 restarted? (This would mean it is using a new set of keys.)"
1128                .to_string())?;
1129        }
1130        let controller_pubkey = unverified_controller_pubkey; // now verified!
1131
1132        let controller_secret_encap: x_wing::Ciphertext =
1133            (&controller_hello.setup_secret_part1).into();
1134        let controller_secret = self
1135            .breakpoint_keypair
1136            .private_key
1137            .decapsulate(&controller_secret_encap)
1138            .map_err(|err| {
1139                format!("Unable to decrypt controller's half of the setup secret: {err}")
1140            })?;
1141
1142        // We can now trust the controller's public key, which means we
1143        // can send them our half of the setup secret and derive the full
1144        // shared secret for ourselves, and that in turn means we can go
1145        // ahead and include an encrypted introduction message.
1146
1147        let (breakpoint_secret_encap, breakpoint_secret) = controller_pubkey
1148            .encapsulate(&mut rand::rngs::OsRng)
1149            .map_err(|err| format!("Failed to create breakpoint secret: {err}"))?;
1150
1151        let (controller_stream_key, breakpoint_stream_key) = derive_stream_keys_dual_pub(
1152            &controller_pubkey,
1153            &self.breakpoint_keypair.public_key,
1154            &controller_secret,
1155            &breakpoint_secret,
1156        );
1157
1158        let (tx_header, mut tx_stream) =
1159            secret_stream::PushStream::init(OsRng, &breakpoint_stream_key);
1160
1161        let intro = &SecureMsg::BreakpointIntro(BreakpointIntro {
1162            which: self.which.clone(),
1163        });
1164        send_insecure(
1165            &mut self.transport,
1166            &InsecureMsg::BreakpointSetupFinalize(BreakpointSetupFinalize {
1167                setup_secret_part2: breakpoint_secret_encap.as_bytes(),
1168                cipher_header: *tx_header.as_ref(),
1169                encrypted_intro: encrypt_message(intro, &mut tx_stream).encrypted,
1170            }),
1171        )
1172        .await
1173        .map_err(|err| format!("Couldn't send breakpoint setup finalize: {err}"))?;
1174
1175        // Finally, we just need the controller's setup finalize message,
1176        // which will contain the stream header in the other direction.
1177
1178        let controller_finalize = match receive_insecure(&mut self.transport).await {
1179            Ok(None) => Err(String::from(
1180                "Controller hung up before ControllerSetupFinalize",
1181            ))?,
1182            Ok(Some(InsecureMsg::ControllerSetupFinalize(setup))) => setup,
1183            Ok(Some(InsecureMsg::HandshakeRejected(HandshakeRejected { reason, permanent }))) => {
1184                Err(BreakpointConnectError {
1185                    message: format!("Controller rejected connection: {reason}"),
1186                    permanent,
1187                })?
1188            }
1189            Ok(Some(wrong)) => Err(format!(
1190                "Unexpected message type when waiting for ControllerSetupFinalize: {}",
1191                Into::<&str>::into(wrong)
1192            ))?,
1193            Err(err) => Err(format!(
1194                "Couldn't receive data when waiting for a ControllerSetupFinalize: {err}"
1195            ))?,
1196        };
1197        let rx_stream = secret_stream::PullStream::init(
1198            secret_stream::Header::from(controller_finalize.cipher_header),
1199            &controller_stream_key,
1200        );
1201
1202        // Now both parties should have a fully secured connection.
1203
1204        Ok(Connection {
1205            transport: self.transport,
1206            cipher_tx: tx_stream,
1207            cipher_rx: rx_stream,
1208        })
1209    }
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214    use crate::test_utils::all_variant_mapping;
1215
1216    use super::*;
1217
1218    /// Pinning test for digests.
1219    #[test]
1220    fn pubkey_digest() {
1221        let privkey = x_wing::DecapsulationKey::from([7u8; 32]);
1222        let pubkey = privkey.encapsulation_key();
1223        let digest_c = ControllerId::from_key(&pubkey);
1224        let digest_b = BreakpointId::from_key(&pubkey);
1225
1226        // Note that Base58 does not produce the same output length
1227        // for a given input length -- it is content-dependent.
1228        assert_eq!(&digest_c.id_string(), "5TQGToHGhhPzq1dng");
1229        assert_eq!(&digest_b.id_string(), "59ARWFefSuMVrE9QG");
1230    }
1231
1232    /// Validate bounds on ID string size.
1233    #[test]
1234    fn id_size_bounds() {
1235        // The shortest Base58 output is from all-zeroes.
1236        assert_eq!([0u8; DIGEST_BYTE_LEN].to_base58().len(), DIGEST_MIN_CHARS);
1237
1238        // The longest output will be from an input with no leading zero bytes.
1239        assert_eq!([100u8; DIGEST_BYTE_LEN].to_base58().len(), DIGEST_MAX_CHARS);
1240
1241        // Confirm we're using the right constant
1242        let hasher = Blake2KeyDigests::new_with_salt_and_personal(&[], &[], &[]).unwrap();
1243        let hash_out = hasher.finalize();
1244        assert_eq!(hash_out.into_bytes().len(), DIGEST_BYTE_LEN);
1245    }
1246
1247    /// Format checking on ID strings
1248    #[test]
1249    fn id_format() {
1250        validate_id_format("3ZgBvRApjQw64rb8r").unwrap();
1251    }
1252
1253    /// Pinning test for key derivation
1254    #[test]
1255    fn stream_key_pinning() {
1256        let ctrl_pair = x_wing::DecapsulationKey::from([20u8; 32]);
1257        let break_pair = x_wing::DecapsulationKey::from([21u8; 32]);
1258        let ctrl_secret = x_wing::SharedSecret::from([22u8; 32]);
1259        let break_secret = x_wing::SharedSecret::from([23u8; 32]);
1260        let (ctrl_stream_key, break_stream_key) = derive_stream_keys_dual_pub(
1261            &ctrl_pair.encapsulation_key(),
1262            &break_pair.encapsulation_key(),
1263            &ctrl_secret,
1264            &break_secret,
1265        );
1266        assert_eq!(
1267            hex::encode(ctrl_stream_key.as_ref()),
1268            "cab71a3c070baa772ece7eb5f108032527bc3e9d6d51839ec4b394cb3afd175f"
1269        );
1270        assert_eq!(
1271            hex::encode(break_stream_key.as_ref()),
1272            "bc1aeb4d8668dbc18f7fadb2d9f9fa8d3487996016dbbb8795acebaffcfa5b0c"
1273        );
1274    }
1275
1276    /// Test [`InsecureMsg`] de/serialization (including field names)
1277    /// using pinned values.
1278    #[tokio::test]
1279    async fn insecure_message_format_pinning() {
1280        let examples = all_variant_mapping!(
1281            InsecureMsg => Vec<u8>:
1282
1283            InsecureMsg::HandshakeRejected =
1284                (
1285                    HandshakeRejected {
1286                        reason: String::from("not feeling it"),
1287                        permanent: true,
1288                    }
1289                )
1290                => [
1291                    b"\xA1qHandshakeRejected\xA2" as &[u8],
1292                    b"freasonnnot feeling it",
1293                    b"ipermanent\xF5",
1294                ].concat(),
1295
1296            InsecureMsg::BreakpointHello =
1297                (
1298                    BreakpointHello {
1299                        protocol: String::from("SOMEPROTO"),
1300                        break_pub: [6u8; 1216],
1301                        expecting_controller_id: String::from("cdigest"),
1302                    }
1303                )
1304                => [
1305                    b"\xA1oBreakpointHello\xA3" as &[u8],
1306                    b"hprotocoliSOMEPROTO",
1307                    b"ibreak_pubY\x04\xC0", &[b'\x06'; 1216],
1308                    b"wexpecting_controller_idgcdigest",
1309                ].concat(),
1310
1311            InsecureMsg::ControllerHello =
1312                (
1313                    ControllerHello {
1314                        ctrl_pub: [6u8; 1216],
1315                        setup_secret_part1: [4u8; 1120],
1316                    }
1317                )
1318                => [
1319                    b"\xA1oControllerHello\xA2" as &[u8],
1320                    b"hctrl_pubY\x04\xC0", &[b'\x06'; 1216],
1321                    b"rsetup_secret_part1Y\x04\x60", &[b'\x04'; 1120],
1322                ].concat(),
1323
1324            InsecureMsg::BreakpointSetupFinalize =
1325                (
1326                    BreakpointSetupFinalize {
1327                        setup_secret_part2: [5u8; 1120],
1328                        cipher_header: [8u8; 24],
1329                        encrypted_intro: [7u8; 10].to_vec(),
1330                    }
1331                )
1332                => [
1333                    b"\xA1wBreakpointSetupFinalize\xA3" as &[u8],
1334                    b"rsetup_secret_part2Y\x04\x60", &[b'\x05'; 1120],
1335                    b"mcipher_headerX\x18", &[b'\x08'; 24],
1336                    b"oencrypted_intro\x8A", &[b'\x07'; 10],
1337                ].concat(),
1338
1339            InsecureMsg::ControllerSetupFinalize =
1340                (
1341                    ControllerSetupFinalize {
1342                        cipher_header: [9u8; 24],
1343                    }
1344                )
1345                => [
1346                    b"\xA1wControllerSetupFinalize\xA1" as &[u8],
1347                    b"mcipher_headerX\x18", &[b'\x09'; 24],
1348                ].concat(),
1349
1350            InsecureMsg::EncryptedMsg =
1351                (
1352                    EncryptedMsg {
1353                        encrypted: [9u8; 3].to_vec(),
1354                    }
1355                )
1356                => b"\xA1lEncryptedMsg\xA1iencrypted\x43\x09\x09\x09".to_vec(),
1357        );
1358        for (variant_value, serialized) in examples {
1359            assert_eq!(
1360                serialized,
1361                serialize(&variant_value),
1362                "Serialized bytes did not match expected value for input {variant_value:?}"
1363            );
1364            let deserialized = deserialize(&serialized)
1365                .map_err(|err| format!("Could not deserialize: {err}; expected {variant_value:?}"))
1366                .unwrap();
1367            assert_eq!(
1368                variant_value, deserialized,
1369                "Deserialized value did not match expected value {variant_value:?}"
1370            );
1371        }
1372    }
1373
1374    /// Test [`SecureMsg`] de/serialization (including field names) using
1375    /// pinned values.
1376    #[tokio::test]
1377    async fn secure_message_format_pinning() {
1378        let examples = all_variant_mapping!(
1379            SecureMsg => &[u8]:
1380
1381            SecureMsg::BreakpointIntro =
1382                (
1383                    BreakpointIntro {
1384                        which: Some("loop".to_owned()),
1385                    }
1386                )
1387                => b"\xA1oBreakpointIntro\xA1ewhichdloop",
1388
1389            SecureMsg::Command =
1390                (
1391                    Command {
1392                        command: "pwd".to_owned(),
1393                        seq: 5,
1394                    }
1395                )
1396                => b"\xA1gCommand\xA2gcommandcpwdcseq\x05",
1397
1398            SecureMsg::OutputLine =
1399                (
1400                    OutputLine {
1401                        bytes: b"hello world!".to_vec(),
1402                        gender: OutputType::Stdout,
1403                        seq: 7,
1404                    }
1405                )
1406                => b"\xA1jOutputLine\xA3ebytes\x4Chello world!fgenderfStdoutcseq\x07",
1407
1408            SecureMsg::Finished =
1409                (
1410                    Finished {
1411                        exit_code: Some(1),
1412                        seq: 9,
1413                    }
1414                )
1415                => b"\xA1hFinished\xA2iexit_code\x01cseq\x09",
1416
1417            SecureMsg::TerminateCmd =
1418                (
1419                    TerminateCmd { seq: 11 }
1420                )
1421                => b"\xA1lTerminateCmd\xA1cseq\x0b",
1422
1423            SecureMsg::ExitBreak =
1424                (ExitBreak {})
1425                => b"\xA1iExitBreak\xA0",
1426        );
1427        for (variant_value, serialized) in examples {
1428            assert_eq!(
1429                serialized,
1430                serialize(&variant_value),
1431                "Serialized bytes did not match expected value for input {variant_value:?}"
1432            );
1433            let deserialized = deserialize(serialized)
1434                .map_err(|err| format!("Could not deserialize: {err}; expected {variant_value:?}"))
1435                .unwrap();
1436            assert_eq!(
1437                variant_value, deserialized,
1438                "Deserialized value did not match expected value {variant_value:?}"
1439            );
1440        }
1441    }
1442}