diff --git a/.github/workflows/qc.yaml b/.github/workflows/qc.yaml index 39e71b7..5b4a9c2 100644 --- a/.github/workflows/qc.yaml +++ b/.github/workflows/qc.yaml @@ -194,19 +194,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - run: rustup default nightly - run: rustup component add llvm-tools-preview - run: | cargo install cargo-llvm-cov || true - cargo llvm-cov \ - --workspace\ - --all-features \ - --lcov \ - --output-path coverage.lcov + cargo install grcov || true + ./coverage_report.sh # If using tarapulin #- run: cargo install cargo-tarpaulin #- run: cargo tarpaulin --out Xml - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: - files: ./coverage.lcov + files: ./target/grcov/lcov verbose: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a885854..2412df7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,38 +1,41 @@ -**Making a new Release of Rosenpass — Cooking Recipe** +# Contributing to Rosenpass -If you have to change a file, do what it takes to get the change as commit on the main branch, then **start from step 0**. -If any other issue occurs +## Common operations -0. Make sure you are in the root directory of the project - - `cd "$(git rev-parse --show-toplevel)"` -1. Make sure you locally checked out the head of the main branch - - `git stash --include-untracked && git checkout main && git pull` -2. Make sure all tests pass - - `cargo test --workspace --all-features` -3. Make sure the current version in `rosenpass/Cargo.toml` matches that in the [last release on GitHub](https://github.com/rosenpass/rosenpass/releases) - - Only normal releases count, release candidates and draft releases can be ignored -4. Pick the kind of release that you want to make (`major`, `minor`, `patch`, `rc`, ...) - - See `cargo release --help` for more information on the available release types - - Pick `rc` if in doubt -5. Try to release a new version - - `cargo release rc --package rosenpass` - - An issue was reported? Go fix it, start again with step 0! -6. Actually make the release - - `cargo release rc --package rosenpass --execute` - - Tentatively wait for any interactions, such as entering ssh keys etc. - - You may be asked for your ssh key multiple times! +### Apply code formatting -**Frequently Asked Questions (FAQ)** +Format rust code: -- You have untracked files, which `cargo release` complains about? - - `git stash --include-untracked` -- You cannot push to crates.io because you are not logged in? - - Follow the steps displayed in [`cargo login`](https://doc.rust-lang.org/cargo/commands/cargo-login.html) -- How is the release page added to [GitHub Releases](https://github.com/rosenpass/rosenpass/releases) itself? - - Our CI Pipeline will create the release, once `cargo release` pushed the new version tag to the repo. The new release should pop up almost immediately in [GitHub Releases](https://github.com/rosenpass/rosenpass/releases) after the [Actions/Release](https://github.com/rosenpass/rosenpass/actions/workflows/release.yaml) pipeline started. -- No new release pops up in the `Release` sidebar element on the [main page](https://github.com/rosenpass/rosenpass) - - Did you push a `rc` release? This view only shows non-draft release, but `rc` releases are considered as draft. See [Releases](https://github.com/rosenpass/rosenpass/releases) page to see all (including draft!) releases. -- The release page was created on GitHub, but there are no assets/artifacts other than the source code tar ball/zip? - - The artifacts are generated and pushed automatically to the release, but this takes some time (a couple of minutes). You can check the respective CI pipeline: [Actions/Release](https://github.com/rosenpass/rosenpass/actions/workflows/release.yaml), which should start immediately after `cargo release` pushed the new release tag to the repo. The release artifacts only are added later to the release, once all jobs in bespoke pipeline finished. -- How are the release artifacts generated, and what are they? - - The release artifacts are built using one Nix derivation per platform, `nix build .#release-package`. It contains both statically linked versions of `rosenpass` itself and OCI container images. +```bash +cargo fmt +``` + +Format rust code in markdown files: + +```bash +./format_rust_code.sh --mode fix +``` + +### Spawn a development environment with nix + +```bash +nix develop .#fullEnv +``` + +You need to [install this nix package manager](https://wiki.archlinux.org/title/Nix) first. + +### Run our test + +Make sure to increase the stack size available; some of our cryptography operations require a lot of stack memory. + +```bash +RUST_MIN_STACK=8388608 cargo test --workspace --all-features +``` + +### Generate coverage reports + +Keep in mind that many of Rosenpass' tests are doctests, so to get an accurate read on our code coverage, you have to include doctests: + +```bash +./coverage_report.sh +``` diff --git a/coverage_report.sh b/coverage_report.sh new file mode 100755 index 0000000..b819c8b --- /dev/null +++ b/coverage_report.sh @@ -0,0 +1,44 @@ +#! /usr/bin/env bash + +set -e -o pipefail + +OUTPUT_DIR="target/grcov" + +log() { + echo >&2 "$@" +} + +exc() { + echo '$' "$@" + "$@" +} + +main() { + exc cd "$(dirname "$0")" + + local open="0" + if [[ "$1" == "--open" ]]; then + open="1" + fi + + exc cargo llvm-cov --all-features --workspace --doctests + + exc rm -rf "${OUTPUT_DIR}" + exc mkdir -p "${OUTPUT_DIR}" + exc grcov target/llvm-cov-target/ --llvm -s . --branch \ + --binary-path ./target/llvm-cov-target/debug/deps \ + --ignore-not-existing --ignore '../*' --ignore "/*" \ + --excl-line '^\s*#\[(derive|repr)\(' \ + -t lcov,html,markdown -o "${OUTPUT_DIR}" + + if (( "${open}" == 1 )); then + xdg-open "${PWD}/${OUTPUT_DIR}/html/index.html" + fi + + log "" + log "Generated reports in \"${PWD}/${OUTPUT_DIR}\"." + log "Open \"${PWD}/${OUTPUT_DIR}/html/index.html\" to view HTML report." + log "" +} + +main "$@" diff --git a/flake.nix b/flake.nix index 6fbbee7..c11d007 100644 --- a/flake.nix +++ b/flake.nix @@ -121,6 +121,7 @@ proverif-patched inputs.fenix.packages.${system}.complete.toolchain pkgs.cargo-llvm-cov + pkgs.grcov ]; }; devShells.coverage = pkgs.mkShell { @@ -128,6 +129,7 @@ nativeBuildInputs = [ inputs.fenix.packages.${system}.complete.toolchain pkgs.cargo-llvm-cov + pkgs.grcov ]; }; diff --git a/readme.md b/readme.md index 6cbd6f6..8975ce5 100644 --- a/readme.md +++ b/readme.md @@ -23,6 +23,12 @@ rosenpass help Follow [quick start instructions](https://rosenpass.eu/#start) to get a VPN up and running. +## Contributing + +Contributions are generally welcome. Join our [Matrix Chat](https://matrix.to/#/#rosenpass:matrix.org) if you are looking for guidance on how to contribute or for people to collaborate with. + +We also have a – as of now, very minimal – [contributors guide](CONTRIBUTING.md). + ## Software architecture The [rosenpass tool](./src/) is written in Rust and uses liboqs[^liboqs]. The tool establishes a symmetric key and provides it to WireGuard. Since it supplies WireGuard with key through the PSK feature using Rosenpass+WireGuard is cryptographically no less secure than using WireGuard on its own ("hybrid security"). Rosenpass refreshes the symmetric key every two minutes. diff --git a/rosenpass/src/api/mod.rs b/rosenpass/src/api/mod.rs index da9b1b0..f0f677c 100644 --- a/rosenpass/src/api/mod.rs +++ b/rosenpass/src/api/mod.rs @@ -1,3 +1,5 @@ +//! The bulk code relating to the Rosenpass unix socket API + mod api_handler; mod boilerplate; diff --git a/rosenpass/src/bin/gen-ipc-msg-types.rs b/rosenpass/src/bin/gen-ipc-msg-types.rs index 161968f..028113b 100644 --- a/rosenpass/src/bin/gen-ipc-msg-types.rs +++ b/rosenpass/src/bin/gen-ipc-msg-types.rs @@ -3,6 +3,7 @@ use heck::ToShoutySnakeCase; use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN}; +/// Recursively calculate a concrete hash value for an API message type fn calculate_hash_value(hd: HashDomain, values: &[&str]) -> Result<[u8; KEY_LEN]> { match values.split_first() { Some((head, tail)) => calculate_hash_value(hd.mix(head.as_bytes())?, tail), @@ -10,6 +11,7 @@ fn calculate_hash_value(hd: HashDomain, values: &[&str]) -> Result<[u8; KEY_LEN] } } +/// Print a hash literal for pasting into the Rosenpass source code fn print_literal(path: &[&str]) -> Result<()> { let val = calculate_hash_value(HashDomain::zero(), path)?; let (last, prefix) = path.split_last().context("developer error!")?; @@ -33,6 +35,8 @@ fn print_literal(path: &[&str]) -> Result<()> { Ok(()) } +/// Tree of domain separators where each leaf represents +/// an API message ID #[derive(Debug, Clone)] enum Tree { Branch(String, Vec), @@ -68,6 +72,7 @@ impl Tree { } } +/// Helper for generating hash-based message IDs for the IPC API fn main() -> Result<()> { let tree = Tree::Branch( "Rosenpass IPC API".to_owned(), diff --git a/rosenpass/src/hash_domains.rs b/rosenpass/src/hash_domains.rs index dd3a468..b37981b 100644 --- a/rosenpass/src/hash_domains.rs +++ b/rosenpass/src/hash_domains.rs @@ -1,13 +1,68 @@ //! Pseudo Random Functions (PRFs) with a tree-like label scheme which -//! ensures their uniqueness +//! ensures their uniqueness. +//! +//! This ensures [domain separation](https://en.wikipedia.org/wiki/Domain_separation) is used +//! across the Rosenpass protocol. +//! +//! There is a chart containing all hash domains used in Rosenpass in the +//! [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository). +//! +//! # Tutorial +//! +//! ``` +//! use rosenpass::{hash_domain, hash_domain_ns}; +//! use rosenpass::hash_domains::protocol; +//! +//! // Declaring a custom hash domain +//! hash_domain_ns!(protocol, custom_domain, "my custom hash domain label"); +//! +//! // Declaring a custom hashers +//! hash_domain_ns!(custom_domain, hashers, "hashers"); +//! hash_domain_ns!(hashers, hasher1, "1"); +//! hash_domain_ns!(hashers, hasher2, "2"); +//! +//! // Declaring specific domain separators +//! hash_domain_ns!(custom_domain, domain_separators, "domain separators"); +//! hash_domain!(domain_separators, sep1, "1"); +//! hash_domain!(domain_separators, sep2, "2"); +//! +//! // Generating values under hasher1 with both domain separators +//! let h1 = hasher1()?.mix(b"some data")?.dup(); +//! let h1v1 = h1.mix(&sep1()?)?.mix(b"More data")?.into_value(); +//! let h1v2 = h1.mix(&sep2()?)?.mix(b"More data")?.into_value(); +//! +//! // Generating values under hasher2 with both domain separators +//! let h2 = hasher2()?.mix(b"some data")?.dup(); +//! let h2v1 = h2.mix(&sep1()?)?.mix(b"More data")?.into_value(); +//! let h2v2 = h2.mix(&sep2()?)?.mix(b"More data")?.into_value(); +//! +//! // All of the domain separators are now different, random strings +//! let values = [h1v1, h1v2, h2v1, h2v2]; +//! for i in 0..values.len() { +//! for j in (i+1)..values.len() { +//! assert_ne!(values[i], values[j]); +//! } +//! } +//! +//! Ok::<(), anyhow::Error>(()) +//! ``` use anyhow::Result; -use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN}; +use rosenpass_ciphers::hash_domain::HashDomain; +/// Declare a hash function +/// +/// # Examples +/// +/// See the source file for details about how this is used concretely. +/// +/// See the [module](self) documentation on how to use the hash domains in general // TODO Use labels that can serve as identifiers +#[macro_export] macro_rules! hash_domain_ns { - ($base:ident, $name:ident, $($lbl:expr),* ) => { - pub fn $name() -> Result { + ($(#[$($attrss:tt)*])* $base:ident, $name:ident, $($lbl:expr),+ ) => { + $(#[$($attrss)*])* + pub fn $name() -> ::anyhow::Result<::rosenpass_ciphers::hash_domain::HashDomain> { let t = $base()?; $( let t = t.mix($lbl.as_bytes())?; )* Ok(t) @@ -15,9 +70,18 @@ macro_rules! hash_domain_ns { } } +/// Declare a concrete hash value +/// +/// # Examples +/// +/// See the source file for details about how this is used concretely. +/// +/// See the [module](self) documentation on how to use the hash domains in general +#[macro_export] macro_rules! hash_domain { - ($base:ident, $name:ident, $($lbl:expr),* ) => { - pub fn $name() -> Result<[u8; KEY_LEN]> { + ($(#[$($attrss:tt)*])* $base:ident, $name:ident, $($lbl:expr),+ ) => { + $(#[$($attrss)*])* + pub fn $name() -> ::anyhow::Result<[u8; ::rosenpass_ciphers::KEY_LEN]> { let t = $base()?; $( let t = t.mix($lbl.as_bytes())?; )* Ok(t.into_value()) @@ -25,24 +89,227 @@ macro_rules! hash_domain { } } +/// The hash domain containing the protocol string. +/// +/// This serves as a global [domain separator](https://en.wikipedia.org/wiki/Domain_separation) +/// used in various places in the rosenpass protocol. +/// +/// This is generally used to create further hash-domains for specific purposes. See +/// +/// # Examples +/// +/// See the source file for details about how this is used concretely. +/// +/// See the [module](self) documentation on how to use the hash domains in general pub fn protocol() -> Result { HashDomain::zero().mix("Rosenpass v1 mceliece460896 Kyber512 ChaChaPoly1305 BLAKE2s".as_bytes()) } -hash_domain_ns!(protocol, mac, "mac"); -hash_domain_ns!(protocol, cookie, "cookie"); -hash_domain_ns!(protocol, cookie_value, "cookie-value"); -hash_domain_ns!(protocol, cookie_key, "cookie-key"); -hash_domain_ns!(protocol, peerid, "peer id"); -hash_domain_ns!(protocol, biscuit_ad, "biscuit additional data"); -hash_domain_ns!(protocol, ckinit, "chaining key init"); -hash_domain_ns!(protocol, _ckextract, "chaining key extract"); +hash_domain_ns!( + /// Hash domain based on [protocol] for calculating [crate::msgs::Envelope::mac]. + /// + /// # Examples + /// + /// See the source of [crate::msgs::Envelope::seal] and [crate::msgs::Envelope::check_seal] + /// to figure out how this is concretely used. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, mac, "mac"); +hash_domain_ns!( + /// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie]. + /// + /// # Examples + /// + /// See the source of [crate::msgs::Envelope::seal_cookie], + /// [crate::protocol::CryptoServer::handle_msg_under_load], and + /// [crate::protocol::CryptoServer::handle_cookie_reply] + /// to figure out how this is concretely used. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, cookie, "cookie"); +hash_domain_ns!( + /// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie]. + /// + /// # Examples + /// + /// See the source of [crate::msgs::Envelope::seal_cookie], + /// [crate::protocol::CryptoServer::handle_msg_under_load], and + /// [crate::protocol::CryptoServer::handle_cookie_reply] + /// to figure out how this is concretely used. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, cookie_value, "cookie-value"); +hash_domain_ns!( + /// Hash domain based on [protocol] involved in calculating [crate::msgs::Envelope::cookie]. + /// + /// # Examples + /// + /// See the source of [crate::msgs::Envelope::seal_cookie], + /// [crate::protocol::CryptoServer::handle_msg_under_load], and + /// [crate::protocol::CryptoServer::handle_cookie_reply] + /// to figure out how this is concretely used. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, cookie_key, "cookie-key"); +hash_domain_ns!( + /// Hash domain based on [protocol] for calculating the peer id as transmitted (encrypted) + /// in [crate::msgs::InitHello::pidic]. + /// + /// # Examples + /// + /// See the source of [crate::protocol::CryptoServer::pidm] and + /// [crate::protocol::Peer::pidt] + /// to figure out how this is concretely used. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, peerid, "peer id"); +hash_domain_ns!( + /// Hash domain based on [protocol] for calculating the additional data + /// during [crate::msgs::Biscuit] encryption, storing the biscuit into + /// [crate::msgs::RespHello::biscuit]. + /// + /// # Examples + /// + /// To understand how the biscuit is used, it is best to read + /// the code of [crate::protocol::HandshakeState::store_biscuit] and + /// [crate::protocol::HandshakeState::load_biscuit] + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, biscuit_ad, "biscuit additional data"); +hash_domain_ns!( + /// This hash domain begins our actual handshake procedure, initializing the + /// chaining key [crate::protocol::HandshakeState::ck]. + /// + /// # Examples + /// + /// To understand how the chaining key is used, study + /// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init] + /// and [crate::protocol::HandshakeState::mix]. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, ckinit, "chaining key init"); +hash_domain_ns!( + /// Namespace for chaining key usage domain separators. + /// + /// During the execution of the Rosenpass protocol, we use the chaining key for multiple + /// purposes, so to make sure that we have unique value domains, we mix a domain separator + /// into the chaining key before using it for any particular purpose. + /// + /// We could use the full domain separation strings, but using a hash value here is nice + /// because it does not lead to any constraints about domain separator format and we can + /// even allow third parties to define their own separators by claiming a namespace. + /// + /// # Examples + /// + /// To understand how the chaining key is used, study + /// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init] + /// and [crate::protocol::HandshakeState::mix]. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + protocol, _ckextract, "chaining key extract"); -hash_domain!(_ckextract, mix, "mix"); -hash_domain!(_ckextract, hs_enc, "handshake encryption"); -hash_domain!(_ckextract, ini_enc, "initiator handshake encryption"); -hash_domain!(_ckextract, res_enc, "responder handshake encryption"); +hash_domain!( + /// Used to mix in further values into the chaining key during the handshake. + /// + /// See [_ckextract]. + /// + /// # Examples + /// + /// To understand how the chaining key is used, study + /// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init] + /// and [crate::protocol::HandshakeState::mix]. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + _ckextract, mix, "mix"); +hash_domain!( + /// Chaining key domain separator for generating encryption keys that can + /// encrypt parts of the handshake. + /// + /// See [_ckextract]. + /// + /// # Examples + /// + /// Encryption of data during the handshake happens in + /// [crate::protocol::HandshakeState::encrypt_and_mix] and decryption happens in + /// [crate::protocol::HandshakeState::decrypt_and_mix]. See their source code + /// for details. + /// + /// To understand how the chaining key is used, study + /// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init] + /// and [crate::protocol::HandshakeState::mix]. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + _ckextract, hs_enc, "handshake encryption"); +hash_domain!( + /// Chaining key domain separator for live data encryption. + /// Live data encryption is only used to send confirmation of handshake + /// done in [crate::msgs::EmptyData]. + /// + /// See [_ckextract]. + /// + /// # Examples + /// + /// This domain separator finds use in [crate::protocol::HandshakeState::enter_live]. + /// + /// To understand how the chaining key is used, study + /// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init] + /// and [crate::protocol::HandshakeState::mix]. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + _ckextract, ini_enc, "initiator handshake encryption"); +hash_domain!( + /// Chaining key domain separator for live data encryption. + /// Live data encryption is only used to send confirmation of handshake + /// done in [crate::msgs::EmptyData]. + /// + /// See [_ckextract]. + /// + /// # Examples + /// + /// This domain separator finds use in [crate::protocol::HandshakeState::enter_live]. + /// Check out its source code! + /// + /// To understand how the chaining key is used, study + /// [crate::protocol::HandshakeState], especially [crate::protocol::HandshakeState::init] + /// and [crate::protocol::HandshakeState::mix]. + /// + /// See the [module](self) documentation on how to use the hash domains in general. + _ckextract, res_enc, "responder handshake encryption"); -hash_domain_ns!(_ckextract, _user, "user"); -hash_domain_ns!(_user, _rp, "rosenpass.eu"); -hash_domain!(_rp, osk, "wireguard psk"); +hash_domain_ns!( + /// Chaining key domain separator for any usage specific purposes. + /// + /// We do recommend that third parties base their specific domain separators + /// on a internet domain and/or mix in much more specific information. + /// + /// We only really use this to derive a output key for wireguard; see [osk]. + /// + /// See [_ckextract]. + /// + /// # Examples + /// + /// See the [module](self) documentation on how to use the hash domains in general. + _ckextract, _user, "user"); +hash_domain_ns!( + /// Chaining key domain separator for any rosenpass specific purposes. + /// + /// We only really use this to derive a output key for wireguard; see [osk]. + /// + /// See [_ckextract]. + /// + /// # Examples + /// + /// See the [module](self) documentation on how to use the hash domains in general. + _user, _rp, "rosenpass.eu"); +hash_domain!( + /// Chaining key domain separator for deriving the key sent to WireGuard. + /// + /// See [_ckextract]. + /// + /// # Examples + /// + /// This domain separator finds use in [crate::protocol::CryptoServer::osk]. + /// Check out its source code! + /// + /// See the [module](self) documentation on how to use the hash domains in general. + _rp, osk, "wireguard psk"); diff --git a/rosenpass/src/lib.rs b/rosenpass/src/lib.rs index b186bd2..b389a07 100644 --- a/rosenpass/src/lib.rs +++ b/rosenpass/src/lib.rs @@ -1,3 +1,18 @@ +//! This is the central rosenpass crate implementing the rosenpass protocol. +//! +//! - [crate::app_server] contains the business logic of rosenpass, handling networking +//! - [crate::cli] contains the cli parsing logic and contains quite a bit of startup logic; the +//! main function quickly hands over to [crate::cli::CliArgs::run] which contains quite a bit +//! of our startup logic +//! - [crate::config] has the code to parse and generate configuration files +//! - [crate::hash_domains] lists the different hash function domains used in the Rosenpass +//! protocol +//! - [crate::msgs] provides declarations of the Rosenpass protocol network messages and facilities +//! to parse those messages through the [::zerocopy] crate +//! - [crate::protocol] this is where the bulk of our code lives; this module contains the actual +//! cryptographic protocol logic +//! - crate::api implements the Rosenpass unix socket API, if feature "experiment_api" is active + #[cfg(feature = "experiment_api")] pub mod api; pub mod app_server; @@ -7,14 +22,25 @@ pub mod hash_domains; pub mod msgs; pub mod protocol; +/// Error types used in diverse places across Rosenpass #[derive(thiserror::Error, Debug)] pub enum RosenpassError { + /// Usually indicates that parsing a struct through the + /// [::zerocopy] crate failed #[error("buffer size mismatch")] BufferSizeMismatch, + /// Mostly raised by the `TryFrom` implementation for [crate::msgs::MsgType] + /// to indicate that a message type is not defined #[error("invalid message type")] - InvalidMessageType(u8), + InvalidMessageType( + /// The message type that could not be parsed + u8, + ), + /// Raised by the `TryFrom` (crate::api::RawMsgType) implementation for crate::api::RequestMsgType + /// and crate::api::RequestMsgType to indicate that a message type is not defined #[error("invalid API message type")] - InvalidApiMessageType(u128), - #[error("could not parse API message")] - InvalidApiMessage, + InvalidApiMessageType( + /// The message type that could not be parsed + u128, + ), } diff --git a/rosenpass/src/main.rs b/rosenpass/src/main.rs index 93b41f5..42369c5 100644 --- a/rosenpass/src/main.rs +++ b/rosenpass/src/main.rs @@ -1,3 +1,5 @@ +//! For the main function + use clap::CommandFactory; use clap::Parser; use clap_mangen::roff::{roman, Roff}; @@ -5,6 +7,7 @@ use log::error; use rosenpass::cli::CliArgs; use std::process::exit; +/// Printing custom man sections when generating the man page fn print_custom_man_section(section: &str, text: &str, file: &mut std::fs::File) { let mut roff = Roff::default(); roff.control("SH", [section]); @@ -13,6 +16,8 @@ fn print_custom_man_section(section: &str, text: &str, file: &mut std::fs::File) } /// Catches errors, prints them through the logger, then exits +/// +/// The bulk of the command line logic is handled inside [crate::cli::CliArgs::run]. pub fn main() { // parse CLI arguments let args = CliArgs::parse(); @@ -81,21 +86,27 @@ pub fn main() { } } } + +/// Custom main page section: Exit Status static EXIT_STATUS_MAN: &str = r" The rosenpass utility exits 0 on success, and >0 if an error occurs."; +/// Custom main page section: See also. static SEE_ALSO_MAN: &str = r" rp(1), wg(1) Karolin Varner, Benjamin Lipp, Wanja Zaeske, and Lisa Schmidt, Rosenpass, https://rosenpass.eu/whitepaper.pdf, 2023."; +/// Custom main page section: Standards. static STANDARDS_MAN: &str = r" This tool is the reference implementation of the Rosenpass protocol, as specified within the whitepaper referenced above."; +/// Custom main page section: Authors. static AUTHORS_MAN: &str = r" Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske, Marei Peischl, Stephan Ajuvo, and Lisa Schmidt."; +/// Custom main page section: Bugs. static BUGS_MAN: &str = r" The bugs are tracked at https://github.com/rosenpass/rosenpass/issues."; diff --git a/rosenpass/src/msgs.rs b/rosenpass/src/msgs.rs index 79bf9da..fd281c6 100644 --- a/rosenpass/src/msgs.rs +++ b/rosenpass/src/msgs.rs @@ -9,21 +9,73 @@ //! To achieve this we utilize the zerocopy library. //! use std::mem::size_of; +use std::u8; use zerocopy::{AsBytes, FromBytes, FromZeroes}; use super::RosenpassError; use rosenpass_cipher_traits::Kem; use rosenpass_ciphers::kem::{EphemeralKem, StaticKem}; use rosenpass_ciphers::{aead, xaead, KEY_LEN}; -pub const MSG_SIZE_LEN: usize = 1; -pub const RESERVED_LEN: usize = 3; + +/// Length of a session ID such as [InitHello::sidi] +pub const SESSION_ID_LEN: usize = 4; +/// Length of a biscuit ID; i.e. size of the value in [Biscuit::biscuit_no] +pub const BISCUIT_ID_LEN: usize = 12; + +/// TODO: Unused, remove! +pub const WIRE_ENVELOPE_LEN: usize = 1 + 3 + 16 + 16; // TODO verify this + +/// Size required to fit any message in binary form +pub const MAX_MESSAGE_LEN: usize = 2500; // TODO fix this + +/// length in bytes of an unencrypted Biscuit (plain text) +pub const BISCUIT_PT_LEN: usize = size_of::(); + +/// Length in bytes of an encrypted Biscuit (cipher text) +pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN; + +/// Size of the field [Envelope::mac] pub const MAC_SIZE: usize = 16; -pub const COOKIE_SIZE: usize = 16; -pub const SID_LEN: usize = 4; +/// Size of the field [Envelope::cookie] +pub const COOKIE_SIZE: usize = MAC_SIZE; -pub type MsgEnvelopeMac = [u8; 16]; -pub type MsgEnvelopeCookie = MsgEnvelopeMac; +/// Type of the mac field in [Envelope] +pub type MsgEnvelopeMac = [u8; MAC_SIZE]; +/// Type of the cookie field in [Envelope] +pub type MsgEnvelopeCookie = [u8; COOKIE_SIZE]; + +/// Header and footer included in all our packages, +/// including a type field. +/// +/// # Examples +/// +/// ``` +/// use rosenpass::msgs::{Envelope, InitHello}; +/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes}; +/// use memoffset::offset_of; +/// +/// // Zero-initialization +/// let mut ih = Envelope::::new_zeroed(); +/// +/// // Edit fields normally +/// ih.mac[0] = 1; +/// +/// // Edit as binary +/// ih.as_bytes_mut()[offset_of!(Envelope, msg_type)] = 23; +/// assert_eq!(ih.msg_type, 23);; +/// +/// // Conversion to bytes +/// let mut ih2 = ih.as_bytes().to_owned(); +/// +/// // Setting msg_type field, again +/// ih2[0] = 42; +/// +/// // Zerocopy parsing +/// let ih3 = Ref::<&mut [u8], Envelope>::new(&mut ih2).unwrap(); +/// assert_ne!(ih.as_bytes(), ih3.as_bytes()); +/// assert_eq!(ih3.msg_type, 42); +/// ``` #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes, Clone)] pub struct Envelope { @@ -40,6 +92,40 @@ pub struct Envelope { pub cookie: MsgEnvelopeCookie, } +/// This is the first message sent by the initiator to the responder +/// during the execution of the Rosenpass protocol. +/// +/// When transmitted on the wire, this type will generally be wrapped into [Envelope]. +/// +/// # Examples +/// +/// Check out the code of [crate::protocol::CryptoServer::handle_initiation] (generation on +/// iniatiator side) and [crate::protocol::CryptoServer::handle_init_hello] (processing on +/// responder side) to understand how this is used. +/// +/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate. +/// +/// ``` +/// use rosenpass::msgs::{Envelope, InitHello}; +/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes}; +/// use memoffset::span_of; +/// +/// // Zero initialization +/// let mut ih = Envelope::::new_zeroed(); +/// +/// // Conversion to byte representation +/// let ih = ih.as_bytes_mut(); +/// +/// // Set value on byte representation +/// ih[span_of!(Envelope, payload)][span_of!(InitHello, sidi)] +/// .copy_from_slice(&[1,2,3,4]); +/// +/// // Conversion from bytes +/// let ih = Ref::<&mut [u8], Envelope>::new(ih).unwrap(); +/// +/// // Check that write above on byte representation was effective +/// assert_eq!(ih.payload.sidi, [1,2,3,4]); +/// ``` #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes)] pub struct InitHello { @@ -55,6 +141,40 @@ pub struct InitHello { pub auth: [u8; aead::TAG_LEN], } +/// This is the second message sent by the responder to the initiator +/// during the execution of the Rosenpass protocol in response to [InitHello]. +/// +/// When transmitted on the wire, this type will generally be wrapped into [Envelope]. +/// +/// # Examples +/// +/// Check out the code of [crate::protocol::CryptoServer::handle_init_hello] (generation on +/// responder side) and [crate::protocol::CryptoServer::handle_resp_hello] (processing on +/// initiator side) to understand how this is used. +/// +/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate. +/// +/// ``` +/// use rosenpass::msgs::{Envelope, RespHello}; +/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes}; +/// use memoffset::span_of; +/// +/// // Zero initialization +/// let mut ih = Envelope::::new_zeroed(); +/// +/// // Conversion to byte representation +/// let ih = ih.as_bytes_mut(); +/// +/// // Set value on byte representation +/// ih[span_of!(Envelope, payload)][span_of!(RespHello, sidi)] +/// .copy_from_slice(&[1,2,3,4]); +/// +/// // Conversion from bytes +/// let ih = Ref::<&mut [u8], Envelope>::new(ih).unwrap(); +/// +/// // Check that write above on byte representation was effective +/// assert_eq!(ih.payload.sidi, [1,2,3,4]); +/// ``` #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes)] pub struct RespHello { @@ -72,6 +192,40 @@ pub struct RespHello { pub biscuit: [u8; BISCUIT_CT_LEN], } +/// This is the third message sent by the initiator to the responder +/// during the execution of the Rosenpass protocol in response to [RespHello]. +/// +/// When transmitted on the wire, this type will generally be wrapped into [Envelope]. +/// +/// # Examples +/// +/// Check out the code of [crate::protocol::CryptoServer::handle_resp_hello] (generation on +/// initiator side) and [crate::protocol::CryptoServer::handle_init_conf] (processing on +/// responder side) to understand how this is used. +/// +/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate. +/// +/// ``` +/// use rosenpass::msgs::{Envelope, InitConf}; +/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes}; +/// use memoffset::span_of; +/// +/// // Zero initialization +/// let mut ih = Envelope::::new_zeroed(); +/// +/// // Conversion to byte representation +/// let ih = ih.as_bytes_mut(); +/// +/// // Set value on byte representation +/// ih[span_of!(Envelope, payload)][span_of!(InitConf, sidi)] +/// .copy_from_slice(&[1,2,3,4]); +/// +/// // Conversion from bytes +/// let ih = Ref::<&mut [u8], Envelope>::new(ih).unwrap(); +/// +/// // Check that write above on byte representation was effective +/// assert_eq!(ih.payload.sidi, [1,2,3,4]); +/// ``` #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes, Debug)] pub struct InitConf { @@ -85,6 +239,51 @@ pub struct InitConf { pub auth: [u8; aead::TAG_LEN], } +/// This is the fourth message sent by the initiator to the responder +/// during the execution of the Rosenpass protocol in response to [RespHello]. +/// +/// When transmitted on the wire, this type will generally be wrapped into [Envelope]. +/// +/// This message does not serve a cryptographic purpose; it just tells the initiator +/// to stop package retransmission. +/// +/// This message should really be called `RespConf`, but when we wrote the protocol, +/// we initially designed the protocol we still though Rosenpass itself should do +/// payload transmission at some point so `EmptyData` could have served as a more generic +/// mechanism. +/// +/// We might add payload transmission in the future again, but we will treat +/// it as a protocol extension if we do. +/// +/// # Examples +/// +/// Check out the code of [crate::protocol::CryptoServer::handle_init_conf] (generation on +/// responder side) and [crate::protocol::CryptoServer::handle_resp_conf] (processing on +/// initiator side) to understand how this is used. +/// +/// [Envelope] contains some extra examples on how to use structures from the [::zerocopy] crate. +/// +/// ``` +/// use rosenpass::msgs::{Envelope, EmptyData}; +/// use zerocopy::{AsBytes, FromBytes, Ref, FromZeroes}; +/// use memoffset::span_of; +/// +/// // Zero initialization +/// let mut ih = Envelope::::new_zeroed(); +/// +/// // Conversion to byte representation +/// let ih = ih.as_bytes_mut(); +/// +/// // Set value on byte representation +/// ih[span_of!(Envelope, payload)][span_of!(EmptyData, sid)] +/// .copy_from_slice(&[1,2,3,4]); +/// +/// // Conversion from bytes +/// let ih = Ref::<&mut [u8], Envelope>::new(ih).unwrap(); +/// +/// // Check that write above on byte representation was effective +/// assert_eq!(ih.payload.sid, [1,2,3,4]); +/// ``` #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes, Clone, Copy)] pub struct EmptyData { @@ -96,6 +295,22 @@ pub struct EmptyData { pub auth: [u8; aead::TAG_LEN], } +/// Cookie encrypted and sent to the initiator by the responder in [RespHello] +/// and returned by the initiator in [InitConf]. +/// +/// The encryption key is randomly chosen by the responder and frequently regenerated. +/// Using this biscuit value in the protocol allows us to make sure that the responder +/// is mostly stateless until full initiator authentication is achieved, which is needed +/// to prevent denial of service attacks. See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) +/// ([/papers/whitepaper.md] in this repository). +/// +/// # Examples +/// +/// To understand how the biscuit is used, it is best to read +/// the code of [crate::protocol::HandshakeState::store_biscuit] and +/// [crate::protocol::HandshakeState::load_biscuit] +/// +/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate. #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes)] pub struct Biscuit { @@ -107,12 +322,20 @@ pub struct Biscuit { pub ck: [u8; KEY_LEN], } -#[repr(packed)] -#[derive(AsBytes, FromBytes, FromZeroes)] -pub struct DataMsg { - pub dummy: [u8; 4], -} - +/// Specialized message for use in the cookie mechanism. +/// +/// See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository) for details. +/// +/// Generally used together with [CookieReply] which brings this up to the size +/// of [InitHello] to avoid amplification Denial of Service attacks. +/// +/// # Examples +/// +/// To understand how the biscuit is used, it is best to read +/// the code of [crate::protocol::CryptoServer::handle_cookie_reply] and +/// [crate::protocol::CryptoServer::handle_msg_under_load]. +/// +/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate. #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes)] pub struct CookieReplyInner { @@ -126,6 +349,20 @@ pub struct CookieReplyInner { pub cookie_encrypted: [u8; xaead::NONCE_LEN + COOKIE_SIZE + xaead::TAG_LEN], } +/// Specialized message for use in the cookie mechanism. +/// +/// This just brings [CookieReplyInner] up to the size +/// of [InitHello] to avoid amplification Denial of Service attacks. +/// +/// See the [whitepaper](https://rosenpass.eu/whitepaper.pdf) ([/papers/whitepaper.md] in this repository) for details. +/// +/// # Examples +/// +/// To understand how the biscuit is used, it is best to read +/// the code of [crate::protocol::CryptoServer::handle_cookie_reply] and +/// [crate::protocol::CryptoServer::handle_msg_under_load]. +/// +/// [Envelope] and [InitHello] contain some extra examples on how to use structures from the [::zerocopy] crate. #[repr(packed)] #[derive(AsBytes, FromBytes, FromZeroes)] pub struct CookieReply { @@ -133,33 +370,46 @@ pub struct CookieReply { pub padding: [u8; size_of::>() - size_of::()], } -// Traits ///////////////////////////////////////////////////////////////////// - -pub trait WireMsg: std::fmt::Debug { - const MSG_TYPE: MsgType; - const MSG_TYPE_U8: u8 = Self::MSG_TYPE as u8; - const BYTES: usize; -} - -// Constants ////////////////////////////////////////////////////////////////// - -pub const SESSION_ID_LEN: usize = 4; -pub const BISCUIT_ID_LEN: usize = 12; - -pub const WIRE_ENVELOPE_LEN: usize = 1 + 3 + 16 + 16; // TODO verify this - -/// Size required to fit any message in binary form -pub const MAX_MESSAGE_LEN: usize = 2500; // TODO fix this - /// Recognized message types +/// +/// # Examples +/// +/// ``` +/// use rosenpass::msgs::MsgType; +/// use rosenpass::msgs::MsgType as M; +/// +/// let values = [M::InitHello, M::RespHello, M::InitConf, M::EmptyData, M::CookieReply]; +/// let values_u8 = values.map(|v| -> u8 { v.into() }); +/// +/// // Can be converted to and from u8 using [::std::convert::Into] or [::std::convert::From] +/// for v in values.iter().copied() { +/// let v_u8 : u8 = v.into(); +/// let v2 : MsgType = v_u8.try_into()?; +/// assert_eq!(v, v2); +/// } +/// +/// // Converting an unsupported type produces an error +/// let invalid_values = (u8::MIN..=u8::MAX) +/// .filter(|v| !values_u8.contains(v)); +/// for v in invalid_values { +/// let res : Result = v.try_into(); +/// assert!(res.is_err()); +/// } +/// +/// Ok::<(), anyhow::Error>(()) +/// ``` #[repr(u8)] #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] pub enum MsgType { + /// MsgType for [InitHello] InitHello = 0x81, + /// MsgType for [RespHello] RespHello = 0x82, + /// MsgType for [InitConf] InitConf = 0x83, + /// MsgType for [EmptyData] EmptyData = 0x84, - DataMsg = 0x85, + /// MsgType for [CookieReply] CookieReply = 0x86, } @@ -172,7 +422,6 @@ impl TryFrom for MsgType { 0x82 => MsgType::RespHello, 0x83 => MsgType::InitConf, 0x84 => MsgType::EmptyData, - 0x85 => MsgType::DataMsg, 0x86 => MsgType::CookieReply, _ => return Err(RosenpassError::InvalidMessageType(value)), }) @@ -185,12 +434,6 @@ impl From for u8 { } } -/// length in bytes of an unencrypted Biscuit (plain text) -pub const BISCUIT_PT_LEN: usize = size_of::(); - -/// Length in bytes of an encrypted Biscuit (cipher text) -pub const BISCUIT_CT_LEN: usize = BISCUIT_PT_LEN + xaead::NONCE_LEN + xaead::TAG_LEN; - #[cfg(test)] mod test_constants { use crate::msgs::{BISCUIT_CT_LEN, BISCUIT_PT_LEN}; diff --git a/rosenpass/src/protocol/mod.rs b/rosenpass/src/protocol/mod.rs index 68d48bb..aa41d77 100644 --- a/rosenpass/src/protocol/mod.rs +++ b/rosenpass/src/protocol/mod.rs @@ -1,3 +1,75 @@ +//! Module containing the cryptographic protocol implementation +//! +//! # Overview +//! +//! The most important types in this module probably are [PollResult] +//! & [CryptoServer]. Once a [CryptoServer] is created, the server is +//! provided with new messages via the [CryptoServer::handle_msg] method. +//! The [CryptoServer::poll] method can be used to let the server work, which +//! will eventually yield a [PollResult]. Said [PollResult] contains +//! prescriptive activities to be carried out. [CryptoServer::osk] can than +//! be used to extract the shared key for two peers, once a key-exchange was +//! successful. +//! +//! TODO explain briefly the role of epki +//! +//! # Example Handshake +//! +//! This example illustrates a minimal setup for a key-exchange between two +//! [CryptoServer]. +//! +//! ``` +//! use std::ops::DerefMut; +//! use rosenpass_secret_memory::policy::*; +//! use rosenpass_cipher_traits::Kem; +//! use rosenpass_ciphers::kem::StaticKem; +//! use rosenpass::{ +//! protocol::{SSk, SPk, MsgBuf, PeerPtr, CryptoServer, SymKey}, +//! }; +//! # fn main() -> anyhow::Result<()> { +//! // Set security policy for storing secrets +//! +//! secret_policy_try_use_memfd_secrets(); +//! +//! // initialize secret and public key for peer a ... +//! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero()); +//! StaticKem::keygen(peer_a_sk.secret_mut(), peer_a_pk.deref_mut())?; +//! +//! // ... and for peer b +//! let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero()); +//! StaticKem::keygen(peer_b_sk.secret_mut(), peer_b_pk.deref_mut())?; +//! +//! // initialize server and a pre-shared key +//! let psk = SymKey::random(); +//! let mut a = CryptoServer::new(peer_a_sk, peer_a_pk.clone()); +//! let mut b = CryptoServer::new(peer_b_sk, peer_b_pk.clone()); +//! +//! // introduce peers to each other +//! a.add_peer(Some(psk.clone()), peer_b_pk)?; +//! b.add_peer(Some(psk), peer_a_pk)?; +//! +//! // declare buffers for message exchange +//! let (mut a_buf, mut b_buf) = (MsgBuf::zero(), MsgBuf::zero()); +//! +//! // let a initiate a handshake +//! let mut maybe_len = Some(a.initiate_handshake(PeerPtr(0), a_buf.as_mut_slice())?); +//! +//! // let a and b communicate +//! while let Some(len) = maybe_len { +//! maybe_len = b.handle_msg(&a_buf[..len], &mut b_buf[..])?.resp; +//! std::mem::swap(&mut a, &mut b); +//! std::mem::swap(&mut a_buf, &mut b_buf); +//! } +//! +//! // all done! Extract the shared keys and ensure they are identical +//! let a_key = a.osk(PeerPtr(0))?; +//! let b_key = b.osk(PeerPtr(0))?; +//! assert_eq!(a_key.secret(), b_key.secret(), +//! "the key exchanged failed to establish a shared secret"); +//! # Ok(()) +//! # } +//! ``` + mod build_crypto_server; #[allow(clippy::module_inception)] mod protocol; diff --git a/rosenpass/src/protocol/protocol.rs b/rosenpass/src/protocol/protocol.rs index f417c32..5d15442 100644 --- a/rosenpass/src/protocol/protocol.rs +++ b/rosenpass/src/protocol/protocol.rs @@ -1,75 +1,3 @@ -//! Module containing the cryptographic protocol implementation -//! -//! # Overview -//! -//! The most important types in this module probably are [PollResult] -//! & [CryptoServer]. Once a [CryptoServer] is created, the server is -//! provided with new messages via the [CryptoServer::handle_msg] method. -//! The [CryptoServer::poll] method can be used to let the server work, which -//! will eventually yield a [PollResult]. Said [PollResult] contains -//! prescriptive activities to be carried out. [CryptoServer::osk] can than -//! be used to extract the shared key for two peers, once a key-exchange was -//! successful. -//! -//! TODO explain briefly the role of epki -//! -//! # Example Handshake -//! -//! This example illustrates a minimal setup for a key-exchange between two -//! [CryptoServer]. -//! -//! ``` -//! use std::ops::DerefMut; -//! use rosenpass_secret_memory::policy::*; -//! use rosenpass_cipher_traits::Kem; -//! use rosenpass_ciphers::kem::StaticKem; -//! use rosenpass::{ -//! protocol::{SSk, SPk, MsgBuf, PeerPtr, CryptoServer, SymKey}, -//! }; -//! # fn main() -> anyhow::Result<()> { -//! // Set security policy for storing secrets -//! -//! secret_policy_try_use_memfd_secrets(); -//! -//! // initialize secret and public key for peer a ... -//! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero()); -//! StaticKem::keygen(peer_a_sk.secret_mut(), peer_a_pk.deref_mut())?; -//! -//! // ... and for peer b -//! let (mut peer_b_sk, mut peer_b_pk) = (SSk::zero(), SPk::zero()); -//! StaticKem::keygen(peer_b_sk.secret_mut(), peer_b_pk.deref_mut())?; -//! -//! // initialize server and a pre-shared key -//! let psk = SymKey::random(); -//! let mut a = CryptoServer::new(peer_a_sk, peer_a_pk.clone()); -//! let mut b = CryptoServer::new(peer_b_sk, peer_b_pk.clone()); -//! -//! // introduce peers to each other -//! a.add_peer(Some(psk.clone()), peer_b_pk)?; -//! b.add_peer(Some(psk), peer_a_pk)?; -//! -//! // declare buffers for message exchange -//! let (mut a_buf, mut b_buf) = (MsgBuf::zero(), MsgBuf::zero()); -//! -//! // let a initiate a handshake -//! let mut maybe_len = Some(a.initiate_handshake(PeerPtr(0), a_buf.as_mut_slice())?); -//! -//! // let a and b communicate -//! while let Some(len) = maybe_len { -//! maybe_len = b.handle_msg(&a_buf[..len], &mut b_buf[..])?.resp; -//! std::mem::swap(&mut a, &mut b); -//! std::mem::swap(&mut a_buf, &mut b_buf); -//! } -//! -//! // all done! Extract the shared keys and ensure they are identical -//! let a_key = a.osk(PeerPtr(0))?; -//! let b_key = b.osk(PeerPtr(0))?; -//! assert_eq!(a_key.secret(), b_key.secret(), -//! "the key exchanged failed to establish a shared secret"); -//! # Ok(()) -//! # } -//! ``` - use std::borrow::Borrow; use std::convert::Infallible; use std::fmt::Debug; @@ -1358,7 +1286,6 @@ impl CryptoServer { self.handle_resp_conf(&msg_in.payload)? } - Ok(MsgType::DataMsg) => bail!("DataMsg handling not implemented!"), Ok(MsgType::CookieReply) => { let msg_in: Ref<&[u8], CookieReply> = Ref::new(rx_buf).ok_or(RosenpassError::BufferSizeMismatch)?; diff --git a/rosenpass/tests/main-fn-generates-manpages.rs b/rosenpass/tests/main-fn-generates-manpages.rs new file mode 100644 index 0000000..41124c9 --- /dev/null +++ b/rosenpass/tests/main-fn-generates-manpages.rs @@ -0,0 +1,99 @@ +use rosenpass_util::functional::ApplyExt; + +fn expect_section(manpage: &str, section: &str) -> anyhow::Result<()> { + anyhow::ensure!(manpage.lines().any(|line| { line.starts_with(section) })); + Ok(()) +} + +fn expect_sections(manpage: &str, sections: &[&str]) -> anyhow::Result<()> { + for section in sections.iter().copied() { + expect_section(manpage, section)?; + } + Ok(()) +} + +fn expect_contents(manpage: &str, patterns: &[&str]) -> anyhow::Result<()> { + for pat in patterns.iter().copied() { + anyhow::ensure!(manpage.contains(pat)) + } + Ok(()) +} + +fn filter_backspace(str: &str) -> anyhow::Result { + let mut out = String::new(); + for chr in str.chars() { + if chr == '\x08' { + anyhow::ensure!(out.pop().is_some()); + } else { + out.push(chr); + } + } + Ok(out) +} + +/// Spot tests about man page generation; these are by far not exhaustive. +#[test] +fn main_fn_generates_manpages() -> anyhow::Result<()> { + let dir = tempfile::TempDir::with_prefix("rosenpass-test-main-fn-generates-mangapges")?; + let cmd_out = test_bin::get_test_bin("rosenpass") + .args(["--generate-manpage", dir.path().to_str().unwrap()]) + .output()?; + assert!(cmd_out.status.success()); + + let expected_manpages = [ + "rosenpass.1", + "rosenpass-exchange.1", + "rosenpass-exchange-config.1", + "rosenpass-gen-config.1", + "rosenpass-gen-keys.1", + "rosenpass-keygen.1", + "rosenpass-validate.1", + ]; + + let man_texts: std::collections::HashMap<&str, String> = expected_manpages + .iter() + .copied() + .map(|name| (name, dir.path().join(name))) + .map(|(name, path)| { + let res = std::process::Command::new("man").arg(path).output()?; + assert!(res.status.success()); + let body = res + .stdout + .apply(String::from_utf8)? + .apply(|s| filter_backspace(&s))?; + Ok((name, body)) + }) + .collect::>()?; + + for (name, body) in man_texts.iter() { + expect_sections(body, &["NAME", "SYNOPSIS", "OPTIONS"])?; + + if *name != "rosenpass.1" { + expect_section(body, "DESCRIPTION")?; + } + } + + { + let body = man_texts.get("rosenpass.1").unwrap(); + expect_sections( + body, + &["EXIT STATUS", "SEE ALSO", "STANDARDS", "AUTHORS", "BUGS"], + )?; + expect_contents( + body, + &[ + "[--log-level]", + "rosenpass-exchange-config(1)", + "Start Rosenpass key exchanges based on a configuration file", + "https://rosenpass.eu/whitepaper.pdf", + ], + )?; + } + + { + let body = man_texts.get("rosenpass-exchange.1").unwrap(); + expect_contents(body, &["[-c|--config-file]", "PSK := preshared-key"])?; + } + + Ok(()) +} diff --git a/rosenpass/tests/main-fn-prints-errors.rs b/rosenpass/tests/main-fn-prints-errors.rs new file mode 100644 index 0000000..5b24871 --- /dev/null +++ b/rosenpass/tests/main-fn-prints-errors.rs @@ -0,0 +1,10 @@ +#[test] +fn main_fn_prints_errors() -> anyhow::Result<()> { + let out = test_bin::get_test_bin("rosenpass") + .args(["exchange-config", "/"]) + .output()?; + assert!(!out.status.success()); + assert!(String::from_utf8(out.stderr)?.contains("Is a directory (os error 21)")); + + Ok(()) +}