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}