Merge pull request #142 from prabhpreet/feat/cookie-mechanism

Add cookie mechanism
This commit is contained in:
Alice Michaela Bowman
2024-04-17 13:48:14 +02:00
committed by GitHub
12 changed files with 1410 additions and 101 deletions

269
Cargo.lock generated
View File

@@ -380,7 +380,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex 0.7.0",
"strsim",
"strsim 0.11.1",
]
[[package]]
@@ -502,12 +502,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crypto-common"
@@ -519,6 +516,54 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.3",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
@@ -530,6 +575,37 @@ dependencies = [
"syn",
]
[[package]]
name = "derive_builder"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -582,6 +658,89 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -691,6 +850,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -942,6 +1107,29 @@ version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.48.5",
]
[[package]]
name = "paste"
version = "1.0.14"
@@ -954,6 +1142,18 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "plotters"
version = "0.3.5"
@@ -1086,6 +1286,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.10.2"
@@ -1122,6 +1331,7 @@ dependencies = [
"anyhow",
"clap 4.5.4",
"criterion",
"derive_builder",
"env_logger",
"home",
"log",
@@ -1136,6 +1346,7 @@ dependencies = [
"rosenpass-to",
"rosenpass-util",
"serde",
"serial_test",
"stacker",
"static_assertions",
"test_bin",
@@ -1330,12 +1541,52 @@ dependencies = [
"serde",
]
[[package]]
name = "serial_test"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d"
dependencies = [
"dashmap",
"futures",
"lazy_static",
"log",
"parking_lot",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "spin"
version = "0.9.8"
@@ -1370,6 +1621,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"

View File

@@ -59,3 +59,5 @@ blake2 = "0.10.6"
chacha20poly1305 = { version = "0.10.1", default-features = false, features = [ "std", "heapless" ] }
zerocopy = { version = "0.7.32", features = ["derive"] }
home = "0.5.9"
serial_test = "3.0.0"
derive_builder = "0.20.0"

View File

@@ -6,6 +6,7 @@ author:
- Benjamin Lipp = Max Planck Institute for Security and Privacy (MPI-SP)
- Wanja Zaeske
- Lisa Schmidt = {Scientific Illustrator \\url{mullana.de}}
- Prabhpreet Dua
abstract: |
Rosenpass is used to create post-quantum-secure VPNs. Rosenpass computes a shared key, WireGuard (WG) [@wg] uses the shared key to establish a secure connection. Rosenpass can also be used without WireGuard, deriving post-quantum-secure symmetric keys for another application. The Rosenpass protocol builds on “Post-quantum WireGuard” (PQWG) [@pqwg] and improves it by using a cookie mechanism to provide security against state disruption attacks.
@@ -218,6 +219,7 @@ The server needs to store the following variables:
* `spkm`
* `biscuit_key` Randomly chosen key used to encrypt biscuits
* `biscuit_ctr` Retransmission protection for biscuits
* `cookie_secret`- A randomized cookie secret to derive cookies sent to peer when under load. This secret changes every 120 seconds
Not mandated per se, but required in practice:
@@ -243,6 +245,7 @@ The initiator stores the following local state for each ongoing handshake:
* `ck` The chaining key
* `eski` The initiator's ephemeral secret key
* `epki` The initiator's ephemeral public key
* `cookie_value`- Cookie value sent by an initiator peer under load, used to compute cookie field in outgoing handshake to peer under load. This value expires 120 seconds from when a peer sends this value using the CookieReply message
The responder stores no state. While the responder has access to all of the above variables except for `eski`, the responder discards them after generating the RespHello message. Instead, the responder state is contained inside a cookie called a biscuit. This value is returned to the responder inside the InitConf packet. The biscuit consists of:
@@ -428,12 +431,92 @@ The responder code handling InitConf needs to deal with the biscuits and package
ICR5 and ICR6 perform biscuit replay protection using the biscuit number. This is not handled in `load_biscuit()` itself because there is the case that `biscuit_no = biscuit_used` which needs to be dealt with for retransmission handling.
### Denial of Service Mitigation and Cookies
Rosenpass derives its cookie-based DoS mitigation technique for a responder when receiving InitHello messages from Wireguard [@wg].
When the responder is under load, it may choose to not process further InitHello handshake messages, but instead to respond with a cookie reply message (see Figure \ref{img:MessageTypes}).
The sender of the exchange then uses this cookie in order to resend the message and have it accepted the following time by the reciever.
For an initiator, Rosenpass ignores all messages when under load.
#### Cookie Reply Message
The cookie reply message is sent by the responder on receiving an InitHello message when under load. It consists of the `sidi` of the initiator, a random 24-byte bitstring `nonce` and encrypting `cookie_value` into a `cookie_encrypted` reply field which consists of the following:
```pseudorust
cookie_value = lhash("cookie-value", cookie_secret, initiator_host_info)[0..16]
cookie_encrypted = XAEAD(lhash("cookie-key", spkm), nonce, cookie_value, mac_peer)
```
where `cookie_secret` is a secret variable that changes every two minutes to a random value. `initiator_host_info` is used to identify the initiator host, and is implementation-specific for the client. This paramaters used to identify the host must be carefully chosen to ensure there is a unique mapping, especially when using IPv4 and IPv6 addresses to identify the host (such as taking care of IPv6 link-local addresses). `cookie_value` is a truncated 16 byte value from the above hash operation. `mac_peer` is the `mac` field of the peer's handshake message to which message is the reply.
#### Envelope `mac` Field
Similar to `mac.1` in Wireguard handshake messages, the `mac` field of a Rosenpass envelope from a handshake packet sender's point of view consists of the following:
```pseudorust
mac = lhash("mac", spkt, MAC_WIRE_DATA)[0..16]
```
where `MAC_WIRE_DATA` represents all bytes of msg prior to `mac` field in the envelope.
If a client receives an invalid `mac` value for any message, it will discard the message.
#### Envelope cookie field
The initiator, on receiving a CookieReply message, decrypts `cookie_encrypted` and stores the `cookie_value` for the session into `peer[sid].cookie_value` for a limited time (120 seconds). This value is then used to set `cookie` field set for subsequent messages and retransmissions to the responder as follows:
```pseudorust
if (peer.cookie_value.is_none() || seconds_since_update(peer[sid].cookie_value) >= 120) {
cookie.zeroize(); //zeroed out 16 bytes bitstring
}
else {
cookie = lhash("cookie",peer.cookie_value.unwrap(),COOKIE_WIRE_DATA)
}
```
Here, `seconds_since_update(peer.cookie_value)` is the amount of time in seconds ellapsed since last cookie was received, and `COOKIE_WIRE_DATA` are the message contents of all bytes of the retransmitted message prior to the `cookie` field.
The inititator can use an invalid value for the `cookie` value, when the responder is not under load, and the responder must ignore this value.
However, when the responder is under load, it may reject InitHello messages with the invalid `cookie` value, and issue a cookie reply message.
### Conditions to trigger DoS Mechanism
This whitepaper does not mandate any specific mechanism to detect responder contention (also mentioned as the under load condition) that would trigger use of the cookie mechanism.
For the reference implemenation, Rosenpass has derived inspiration from the linux implementation of Wireguard. This implementation suggests that the reciever keep track of the number of messages it is processing at a given time.
On receiving an incoming message, if the length of the message queue to be processed exceeds a threshold `MAX_QUEUED_INCOMING_HANDSHAKES_THRESHOLD`, the client is considered under load and its state is stored as under load. In addition, the timestamp of this instant when the client was last under load is stored. When recieving subsequent messages, if the client is still in an under load state, the client will check if the time ellpased since the client was last under load has exceeded `LAST_UNDER_LOAD_WINDOW` seconds. If this is the case, the client will update its state to normal operation, and process the message in a normal fashion.
Currently, the following constants are derived from the Linux kernel implementation of Wireguard:
```pseudorust
MAX_QUEUED_INCOMING_HANDSHAKES_THRESHOLD = 4096
LAST_UNDER_LOAD_WINDOW = 1 //seconds
```
## Dealing with Packet Loss
The initiator deals with packet loss by storing the messages it sends to the responder and retransmitting them in randomized, exponentially increasing intervals until they get a response. Receiving RespHello terminates retransmission of InitHello. A Data or EmptyData message serves as acknowledgement of receiving InitConf and terminates its retransmission.
The responder does not need to do anything special to handle RespHello retransmission if the RespHello package is lost, the initiator retransmits InitHello and the responder can generate another RespHello package from that. InitConf retransmission needs to be handled specifically in the responder code because accepting an InitConf retransmission would reset the live session including the nonce counter, which would cause nonce reuse. Implementations must detect the case that `biscuit_no = biscuit_used` in ICR5, skip execution of ICR6 and ICR7, and just transmit another EmptyData package to confirm that the initiator can stop transmitting InitConf.
### Interaction with cookie reply system
The cookie reply system does not interfere with the retransmission logic discussed above.
When the initator is under load, it will ignore processing any incoming messages.
When a responder is under load and it receives an InitHello handshake message, the InitHello message will be discarded and a cookie reply message is sent. The initiator, then on the reciept of the cookie reply message, will store a decrypted `cookie_value` to set the `cookie` field to subsequently sent messages. As per the retransmission mechanism above, the initiator will send a retransmitted InitHello message with a valid `cookie` value appended. On receiving the retransmitted handshake message, the responder will validate the `cookie` value and resume with the handshake process.
When the responder is under load and it recieves an InitConf message, the message will be directly processed without checking the validity of the cookie field.
# Changelog
- Added section "Denial of Service Mitigation and Cookies", and modify "Dealing with Packet Loss" for DoS cookie mechanism
\printbibliography
\setupimage{landscape,fullpage,label=img:HandlingCode}

View File

@@ -9,6 +9,10 @@ homepage = "https://rosenpass.eu/"
repository = "https://github.com/rosenpass/rosenpass"
readme = "readme.md"
[[bin]]
name = "rosenpass"
path = "src/main.rs"
[[bench]]
name = "handshake"
harness = false
@@ -34,6 +38,7 @@ mio = { workspace = true }
rand = { workspace = true }
zerocopy = { workspace = true }
home = { workspace = true }
derive_builder = {workspace = true}
[build-dependencies]
anyhow = { workspace = true }
@@ -42,3 +47,4 @@ anyhow = { workspace = true }
criterion = { workspace = true }
test_bin = { workspace = true }
stacker = { workspace = true }
serial_test = {workspace = true}

View File

@@ -1,6 +1,7 @@
use anyhow::bail;
use anyhow::Result;
use derive_builder::Builder;
use log::{debug, error, info, warn};
use mio::Interest;
use mio::Token;
@@ -22,7 +23,9 @@ use std::process::Stdio;
use std::slice;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use crate::protocol::HostIdentification;
use crate::{
config::Verbosity,
protocol::{CryptoServer, MsgBuf, PeerPtr, SPk, SSk, SymKey, Timing},
@@ -33,6 +36,9 @@ use rosenpass_util::b64::{b64_writer, fmt_b64};
const IPV4_ANY_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
const IPV6_ANY_ADDR: Ipv6Addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
const UNDER_LOAD_RATIO: f64 = 0.5;
const DURATION_UPDATE_UNDER_LOAD_STATUS: Duration = Duration::from_millis(500);
fn ipv4_any_binding() -> SocketAddr {
// addr, port
SocketAddr::V4(SocketAddrV4::new(IPV4_ANY_ADDR, 0))
@@ -67,6 +73,23 @@ pub struct WireguardOut {
pub extra_params: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DoSOperation {
UnderLoad,
Normal,
}
/// Integration test helpers for AppServer
#[derive(Debug, Builder)]
#[builder(pattern = "owned")]
pub struct AppServerTest {
/// Enable DoS operation permanently
#[builder(default = "false")]
pub enable_dos_permanently: bool,
/// Terminate application signal
#[builder(default = "None")]
pub termination_handler: Option<std::sync::mpsc::Receiver<()>>,
}
/// Holds the state of the application, namely the external IO
///
/// Responsible for file IO, network IO
@@ -80,6 +103,12 @@ pub struct AppServer {
pub peers: Vec<AppPeer>,
pub verbosity: Verbosity,
pub all_sockets_drained: bool,
pub under_load: DoSOperation,
pub blocking_polls_count: usize,
pub non_blocking_polls_count: usize,
pub unpolled_count: usize,
pub last_update_time: Instant,
pub test_helpers: Option<AppServerTest>,
}
/// A socket pointer is an index assigned to a socket;
@@ -162,13 +191,7 @@ pub enum Endpoint {
/// at the same time. It also would reply on the same port RespHello was
/// sent to when listening on multiple ports on the same interface. This
/// may be required for some arcane firewall setups.
SocketBoundAddress {
/// The socket the address can be reached under; this is generally
/// determined when we actually receive an RespHello message
socket: SocketPtr,
/// Just the address
addr: SocketAddr,
},
SocketBoundAddress(SocketBoundEndpoint),
// A host name or IP address; storing the hostname here instead of an
// ip address makes sure that we look up the host name whenever we try
// to make a connection; this may be beneficial in some setups where a host-name
@@ -176,6 +199,85 @@ pub enum Endpoint {
Discovery(HostPathDiscoveryEndpoint),
}
impl std::fmt::Display for Endpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Endpoint::SocketBoundAddress(host) => write!(f, "{}", host),
Endpoint::Discovery(host) => write!(f, "{}", host),
}
}
}
#[derive(Debug)]
pub struct SocketBoundEndpoint {
/// The socket the address can be reached under; this is generally
/// determined when we actually receive an RespHello message
socket: SocketPtr,
/// Just the address
addr: SocketAddr,
/// identifier
bytes: (usize, [u8; SocketBoundEndpoint::BUFFER_SIZE]),
}
impl std::fmt::Display for SocketBoundEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.addr)
}
}
impl SocketBoundEndpoint {
const SOCKET_SIZE: usize = usize::BITS as usize / 8;
const IPV6_SIZE: usize = 16;
const PORT_SIZE: usize = 2;
const SCOPE_ID_SIZE: usize = 4;
const BUFFER_SIZE: usize = SocketBoundEndpoint::SOCKET_SIZE
+ SocketBoundEndpoint::IPV6_SIZE
+ SocketBoundEndpoint::PORT_SIZE
+ SocketBoundEndpoint::SCOPE_ID_SIZE;
pub fn new(socket: SocketPtr, addr: SocketAddr) -> Self {
let bytes = Self::to_bytes(&socket, &addr);
Self {
socket,
addr,
bytes,
}
}
fn to_bytes(
socket: &SocketPtr,
addr: &SocketAddr,
) -> (usize, [u8; SocketBoundEndpoint::BUFFER_SIZE]) {
let mut buf = [0u8; SocketBoundEndpoint::BUFFER_SIZE];
let addr = match addr {
SocketAddr::V4(addr) => {
//Map IPv4-mapped to IPv6 addresses
let ip = addr.ip().to_ipv6_mapped();
SocketAddrV6::new(ip, addr.port(), 0, 0)
}
SocketAddr::V6(addr) => addr.clone(),
};
let mut len: usize = 0;
buf[len..len + SocketBoundEndpoint::SOCKET_SIZE].copy_from_slice(&socket.0.to_be_bytes());
len += SocketBoundEndpoint::SOCKET_SIZE;
buf[len..len + SocketBoundEndpoint::IPV6_SIZE].copy_from_slice(&addr.ip().octets());
len += SocketBoundEndpoint::IPV6_SIZE;
buf[len..len + SocketBoundEndpoint::PORT_SIZE].copy_from_slice(&addr.port().to_be_bytes());
len += SocketBoundEndpoint::PORT_SIZE;
buf[len..len + SocketBoundEndpoint::SCOPE_ID_SIZE]
.copy_from_slice(&addr.scope_id().to_be_bytes());
len += SocketBoundEndpoint::SCOPE_ID_SIZE;
(len, buf)
}
}
impl HostIdentification for SocketBoundEndpoint {
fn encode(&self) -> &[u8] {
&self.bytes.1[0..self.bytes.0]
}
}
impl Endpoint {
/// Start discovery from some addresses
pub fn discovery_from_addresses(addresses: Vec<SocketAddr>) -> Self {
@@ -216,7 +318,7 @@ impl Endpoint {
pub fn send(&self, srv: &AppServer, buf: &[u8]) -> anyhow::Result<()> {
use Endpoint::*;
match self {
SocketBoundAddress { socket, addr } => socket.send_to(srv, buf, *addr),
SocketBoundAddress(host) => host.socket.send_to(srv, buf, host.addr),
Discovery(host) => host.send_scouting(srv, buf),
}
}
@@ -224,7 +326,7 @@ impl Endpoint {
fn addresses(&self) -> &[SocketAddr] {
use Endpoint::*;
match self {
SocketBoundAddress { addr, .. } => slice::from_ref(addr),
SocketBoundAddress(host) => slice::from_ref(&host.addr),
Discovery(host) => host.addresses(),
}
}
@@ -262,6 +364,12 @@ pub struct HostPathDiscoveryEndpoint {
addresses: Vec<SocketAddr>,
}
impl std::fmt::Display for HostPathDiscoveryEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.addresses)
}
}
impl HostPathDiscoveryEndpoint {
pub fn from_addresses(addresses: Vec<SocketAddr>) -> Self {
let scouting_state = Cell::new((0, 0));
@@ -327,7 +435,7 @@ impl HostPathDiscoveryEndpoint {
.to_string()
.starts_with("Address family not supported by protocol");
if !ignore {
warn!("Socket #{} refusing to send to {}: ", sock_no, addr);
warn!("Socket #{} refusing to send to {}: {}", sock_no, addr, err);
}
}
}
@@ -342,10 +450,11 @@ impl AppServer {
pk: SPk,
addrs: Vec<SocketAddr>,
verbosity: Verbosity,
test_helpers: Option<AppServerTest>,
) -> anyhow::Result<Self> {
// setup mio
let mio_poll = mio::Poll::new()?;
let events = mio::Events::with_capacity(8);
let events = mio::Events::with_capacity(20);
// bind each SocketAddr to a socket
let maybe_sockets: Result<Vec<_>, _> =
@@ -435,6 +544,12 @@ impl AppServer {
events,
mio_poll,
all_sockets_drained: false,
under_load: DoSOperation::Normal,
blocking_polls_count: 0,
non_blocking_polls_count: 0,
unpolled_count: 0,
last_update_time: Instant::now(),
test_helpers,
})
}
@@ -525,6 +640,17 @@ impl AppServer {
use crate::protocol::HandleMsgResult;
use AppPollResult::*;
use KeyOutputReason::*;
if let Some(AppServerTest {
termination_handler: Some(terminate),
..
}) = &self.test_helpers
{
if let Ok(_) = terminate.try_recv() {
return Ok(());
}
}
match self.poll(&mut *rx)? {
#[allow(clippy::redundant_closure_call)]
SendInitiation(peer) => tx_maybe_with!(peer, || self
@@ -549,11 +675,17 @@ impl AppServer {
}
ReceivedMessage(len, endpoint) => {
match self.crypt.handle_msg(&rx[..len], &mut *tx) {
let msg_result = match self.under_load {
DoSOperation::UnderLoad => {
self.handle_msg_under_load(&endpoint, &rx[..len], &mut *tx)
}
DoSOperation::Normal => self.crypt.handle_msg(&rx[..len], &mut *tx),
};
match msg_result {
Err(ref e) => {
self.verbose().then(|| {
info!(
"error processing incoming message from {:?}: {:?} {}",
"error processing incoming message from {}: {:?} {}",
endpoint,
e,
e.backtrace()
@@ -584,6 +716,22 @@ impl AppServer {
}
}
fn handle_msg_under_load(
&mut self,
endpoint: &Endpoint,
rx: &[u8],
tx: &mut [u8],
) -> Result<crate::protocol::HandleMsgResult> {
match endpoint {
Endpoint::SocketBoundAddress(socket) => {
self.crypt.handle_msg_under_load(&rx, &mut *tx, socket)
}
Endpoint::Discovery(_) => {
anyhow::bail!("Host-path discovery is not supported under load")
}
}
}
pub fn output_key(
&self,
peer: AppPeerPtr,
@@ -706,9 +854,56 @@ impl AppServer {
// only poll if we drained all sockets before
if self.all_sockets_drained {
self.mio_poll.poll(&mut self.events, Some(timeout))?;
//Non blocked polling
self.mio_poll
.poll(&mut self.events, Some(Duration::from_secs(0)))?;
if self.events.iter().peekable().peek().is_none() {
// if there are no events, then add to blocking poll count
self.blocking_polls_count += 1;
//Execute blocking poll
self.mio_poll.poll(&mut self.events, Some(timeout))?;
} else {
self.non_blocking_polls_count += 1;
}
} else {
self.unpolled_count += 1;
}
if let Some(AppServerTest {
enable_dos_permanently: true,
..
}) = self.test_helpers
{
self.under_load = DoSOperation::UnderLoad;
} else {
//Reset blocking poll count if waiting for more than BLOCKING_POLL_COUNT_DURATION
if self.last_update_time.elapsed() > DURATION_UPDATE_UNDER_LOAD_STATUS {
self.last_update_time = Instant::now();
let total_polls = self.blocking_polls_count + self.non_blocking_polls_count;
let load_ratio = if total_polls > 0 {
self.non_blocking_polls_count as f64 / total_polls as f64
} else if self.unpolled_count > 0 {
//There are no polls, so we are under load
1.0
} else {
0.0
};
if load_ratio > UNDER_LOAD_RATIO {
self.under_load = DoSOperation::UnderLoad;
} else {
self.under_load = DoSOperation::Normal;
}
self.blocking_polls_count = 0;
self.non_blocking_polls_count = 0;
self.unpolled_count = 0;
}
}
// drain all sockets
let mut would_block_count = 0;
for (sock_no, socket) in self.sockets.iter_mut().enumerate() {
match socket.recv_from(buf) {
@@ -717,10 +912,10 @@ impl AppServer {
self.all_sockets_drained = false;
return Ok(Some((
n,
Endpoint::SocketBoundAddress {
socket: SocketPtr(sock_no),
Endpoint::SocketBoundAddress(SocketBoundEndpoint::new(
SocketPtr(sock_no),
addr,
},
)),
)));
}
Err(e) if e.kind() == ErrorKind::WouldBlock => {

View File

@@ -6,8 +6,8 @@ use rosenpass_secret_memory::file::StoreSecret;
use rosenpass_util::file::{LoadValue, LoadValueB64};
use std::path::PathBuf;
use crate::app_server;
use crate::app_server::AppServer;
use crate::app_server::{self, AppServerTest};
use crate::protocol::{SPk, SSk, SymKey};
use super::config;
@@ -150,7 +150,7 @@ impl CliCommand {
///
/// ## TODO
/// - This method consumes the [`CliCommand`] value. It might be wise to use a reference...
pub fn run(self) -> anyhow::Result<()> {
pub fn run(self, test_helpers: Option<AppServerTest>) -> anyhow::Result<()> {
use CliCommand::*;
match self {
Man => {
@@ -257,7 +257,7 @@ impl CliCommand {
let config = config::Rosenpass::load(config_file)?;
config.validate()?;
Self::event_loop(config)?;
Self::event_loop(config, test_helpers)?;
}
Exchange {
@@ -274,7 +274,7 @@ impl CliCommand {
config.config_file_path = p;
}
config.validate()?;
Self::event_loop(config)?;
Self::event_loop(config, test_helpers)?;
}
Validate { config_files } => {
@@ -296,7 +296,10 @@ impl CliCommand {
Ok(())
}
fn event_loop(config: config::Rosenpass) -> anyhow::Result<()> {
fn event_loop(
config: config::Rosenpass,
test_helpers: Option<AppServerTest>,
) -> anyhow::Result<()> {
// load own keys
let sk = SSk::load(&config.secret_key)?;
let pk = SPk::load(&config.public_key)?;
@@ -307,6 +310,7 @@ impl CliCommand {
pk,
config.listen,
config.verbosity,
test_helpers,
)?);
for cfg_peer in config.peers {

View File

@@ -448,9 +448,8 @@ impl Default for Verbosity {
#[cfg(test)]
mod test {
use std::net::IpAddr;
use super::*;
use std::net::IpAddr;
fn split_str(s: &str) -> Vec<String> {
s.split(' ').map(|s| s.to_string()).collect()

View File

@@ -31,6 +31,8 @@ pub fn protocol() -> Result<HashDomain> {
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");

View File

@@ -26,7 +26,7 @@ pub fn main() {
// error!("error dummy");
}
match args.command.run() {
match args.command.run(None) {
Ok(_) => {}
Err(e) => {
error!("{e}");

View File

@@ -7,13 +7,19 @@
//! to the concept of lenses in function programming; more on that here:
//! [https://sinusoid.es/misc/lager/lenses.pdf](https://sinusoid.es/misc/lager/lenses.pdf)
//! To achieve this we utilize the zerocopy library.
//!
use std::mem::size_of;
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};
use std::mem::size_of;
use zerocopy::{AsBytes, FromBytes, FromZeroes};
pub const MSG_SIZE_LEN: usize = 1;
pub const RESERVED_LEN: usize = 3;
pub const MAC_SIZE: usize = 16;
pub const COOKIE_SIZE: usize = 16;
pub const SID_LEN: usize = 4;
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
@@ -104,10 +110,24 @@ pub struct DataMsg {
pub dummy: [u8; 4],
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct CookieReplyInner {
/// [MsgType] of this message
pub msg_type: u8,
/// Reserved for future use
pub reserved: [u8; 3],
/// Session ID of the sender (initiator)
pub sid: [u8; 4],
/// Encrypted cookie with authenticated initiator `mac`
pub cookie_encrypted: [u8; xaead::NONCE_LEN + COOKIE_SIZE + xaead::TAG_LEN],
}
#[repr(packed)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct CookieReply {
pub dummy: [u8; 4],
pub inner: CookieReplyInner,
pub padding: [u8; size_of::<Envelope<InitHello>>() - size_of::<CookieReplyInner>()],
}
// Traits /////////////////////////////////////////////////////////////////////
@@ -156,6 +176,12 @@ impl TryFrom<u8> for MsgType {
}
}
impl Into<u8> for MsgType {
fn into(self) -> u8 {
self as u8
}
}
/// length in bytes of an unencrypted Biscuit (plain text)
pub const BISCUIT_PT_LEN: usize = size_of::<Biscuit>();

View File

@@ -65,14 +65,18 @@
//! # }
//! ```
use std::collections::hash_map::{
Entry::{Occupied, Vacant},
HashMap,
};
use std::convert::Infallible;
use std::mem::size_of;
use std::{
collections::hash_map::{
Entry::{Occupied, Vacant},
HashMap,
},
fmt::Display,
};
use anyhow::{bail, ensure, Context, Result};
use rand::Fill as Randomize;
use memoffset::span_of;
use rosenpass_cipher_traits::Kem;
@@ -113,6 +117,18 @@ pub const REKEY_AFTER_TIME_RESPONDER: Timing = 120.0;
pub const REKEY_AFTER_TIME_INITIATOR: Timing = 130.0;
pub const REJECT_AFTER_TIME: Timing = 180.0;
// From the wireguard paper; "under no circumstances send an initiation message more than once every 5 seconds"
pub const REKEY_TIMEOUT: Timing = 5.0;
// Cookie Secret `cookie_secret` in the whitepaper
pub const COOKIE_SECRET_LEN: usize = MAC_SIZE;
pub const COOKIE_SECRET_EPOCH: Timing = 120.0;
// Cookie value len in whitepaper
pub const COOKIE_VALUE_LEN: usize = MAC_SIZE;
// Peer `cookie_value` validity
pub const PEER_COOKIE_VALUE_EPOCH: Timing = 120.0;
// Seconds until the biscuit key is changed; we issue biscuits
// using one biscuit key for one epoch and store the biscuit for
// decryption for a second epoch
@@ -188,18 +204,27 @@ pub struct CryptoServer {
// Tick handling
pub peer_poll_off: usize,
// Random state which changes every COOKIE_SECRET_EPOCH seconds
pub cookie_secrets: [CookieSecret; 2],
}
/// Container for storing cookie types: Biscuit, CookieSecret, CookieValue
#[derive(Debug)]
pub struct CookieStore<const N: usize> {
pub created_at: Timing,
pub value: Secret<N>,
}
/// Stores cookie secret, which is used to create a rotating the cookie value
pub type CookieSecret = CookieStore<COOKIE_SECRET_LEN>;
/// A Biscuit is like a fancy cookie. To avoid state disruption attacks,
/// the responder doesn't store state. Instead the state is stored in a
/// Biscuit, that is encrypted using the [BiscuitKey] which is only known to
/// the Responder. Thus secrecy of the Responder state is not violated, still
/// the responder can avoid storing this state.
#[derive(Debug)]
pub struct BiscuitKey {
pub created_at: Timing,
pub key: SymKey,
}
pub type BiscuitKey = CookieStore<KEY_LEN>;
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum IndexKey {
@@ -279,6 +304,9 @@ pub struct InitiatorHandshake {
pub tx_count: usize,
pub tx_len: usize,
pub tx_buf: MsgBuf,
// Cookie storage for retransmission, expires PEER_COOKIE_VALUE_EPOCH seconds after creation
pub cookie_value: CookieStore<COOKIE_VALUE_LEN>,
}
#[derive(Debug)]
@@ -346,6 +374,13 @@ pub struct SessionPtr(pub usize);
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct BiscuitKeyPtr(pub usize);
/// Valid index to [CryptoServer::cookie_secrets] cookie value
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct ServerCookieSecretPtr(pub usize);
/// Valid index to [CryptoServer::peers] cookie value
pub struct PeerCookieValuePtr(usize);
impl PeerPtr {
pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a Peer {
&srv.peers[self.0]
@@ -362,6 +397,10 @@ impl PeerPtr {
pub fn hs(&self) -> IniHsPtr {
IniHsPtr(self.0)
}
pub fn cv(&self) -> PeerCookieValuePtr {
PeerCookieValuePtr(self.0)
}
}
impl IniHsPtr {
@@ -435,6 +474,41 @@ impl BiscuitKeyPtr {
}
}
impl ServerCookieSecretPtr {
pub fn get<'a>(&self, srv: &'a CryptoServer) -> &'a CookieSecret {
&srv.cookie_secrets[self.0]
}
pub fn get_mut<'a>(&self, srv: &'a mut CryptoServer) -> &'a mut CookieSecret {
&mut srv.cookie_secrets[self.0]
}
}
impl PeerCookieValuePtr {
pub fn get<'a>(&self, srv: &'a CryptoServer) -> Option<&'a CookieStore<COOKIE_SECRET_LEN>> {
srv.peers[self.0]
.handshake
.as_ref()
.map(|v| &v.cookie_value)
}
pub fn update_mut<'a>(&self, srv: &'a mut CryptoServer) -> Option<&'a mut [u8]> {
let timebase = srv.timebase.clone();
if let Some(cs) = PeerPtr(self.0)
.hs()
.get_mut(srv)
.as_mut()
.map(|v| &mut v.cookie_value)
{
cs.created_at = timebase.now();
Some(cs.value.secret_mut())
} else {
None
}
}
}
// DATABASE //////////////////////////////////////
impl CryptoServer {
@@ -449,10 +523,11 @@ impl CryptoServer {
// Defaults
timebase: tb,
biscuit_ctr: BiscuitId::new([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), // 1, LSB
biscuit_keys: [BiscuitKey::new(), BiscuitKey::new()],
biscuit_keys: [CookieStore::new(), CookieStore::new()],
peers: Vec::new(),
index: HashMap::new(),
peer_poll_off: 0,
cookie_secrets: [CookieStore::new(), CookieStore::new()],
}
}
@@ -461,6 +536,10 @@ impl CryptoServer {
(0..self.biscuit_keys.len()).map(BiscuitKeyPtr)
}
pub fn cookie_secret_ptrs(&self) -> impl Iterator<Item = ServerCookieSecretPtr> {
(0..self.cookie_secrets.len()).map(ServerCookieSecretPtr)
}
#[rustfmt::skip]
pub fn pidm(&self) -> Result<PeerId> {
Ok(Public::new(
@@ -577,6 +656,36 @@ impl CryptoServer {
r.get_mut(self).randomize(&tb);
r
}
// Return cookie secrets in order of youthfulness (youngest first)
pub fn active_or_retired_cookie_secrets(&mut self) -> [Option<ServerCookieSecretPtr>; 2] {
let (a, b) = (ServerCookieSecretPtr(0), ServerCookieSecretPtr(1));
let (t, u) = (a.get(self).created_at, b.get(self).created_at);
let mut return_arr = [None, None];
let mut index_top = 0;
// Add the youngest but only if it's youthful first (being added first in case of a tie)
let (young, old) = if t >= u { (a, b) } else { (b, a) };
if young.lifecycle(self) == Lifecycle::Young || young.lifecycle(self) == Lifecycle::Retired
{
return_arr[index_top] = Some(young);
index_top += 1;
}
if old.lifecycle(self) == Lifecycle::Young || old.lifecycle(self) == Lifecycle::Retired {
return_arr[index_top] = Some(old);
index_top += 1;
}
if index_top == 0 {
// Reap the oldest biscuit key and spawn a new young one
let tb = self.timebase.clone();
old.get_mut(self).randomize(&tb);
return_arr[index_top] = Some(old);
}
return_arr
}
}
impl Peer {
@@ -616,29 +725,30 @@ impl Session {
}
}
// BISCUIT KEY ///////////////////////////////////
/// Biscuit Keys are always randomized, so that even if through a bug some
/// secrete is encrypted with an initialized [BiscuitKey], nobody instead of
/// everybody may read the secret.
impl BiscuitKey {
// COOKIE STORE ///////////////////////////////////
impl<const N: usize> CookieStore<N> {
// new creates a random value, that might be counterintuitive for a Default
// impl
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
created_at: BCE,
key: SymKey::random(),
value: Secret::<N>::random(),
}
}
pub fn erase(&mut self) {
self.key.randomize();
self.value.randomize();
self.created_at = BCE;
}
pub fn randomize(&mut self, tb: &Timebase) {
self.key.randomize();
self.value.randomize();
self.created_at = tb.now();
}
pub fn update(&mut self, tb: &Timebase, value: &[u8]) {
self.value.secret_mut().copy_from_slice(value);
self.created_at = tb.now();
}
}
@@ -705,6 +815,44 @@ impl Mortal for BiscuitKeyPtr {
}
}
impl Mortal for ServerCookieSecretPtr {
fn created_at(&self, srv: &CryptoServer) -> Option<Timing> {
let t = self.get(srv).created_at;
if t < 0.0 {
None
} else {
Some(t)
}
}
fn retire_at(&self, srv: &CryptoServer) -> Option<Timing> {
self.created_at(srv).map(|t| t + COOKIE_SECRET_EPOCH)
}
fn die_at(&self, srv: &CryptoServer) -> Option<Timing> {
self.retire_at(srv).map(|t| t + COOKIE_SECRET_EPOCH)
}
}
impl Mortal for PeerCookieValuePtr {
fn created_at(&self, srv: &CryptoServer) -> Option<Timing> {
if let Some(cs) = self.get(srv) {
if cs.created_at < 0.0 {
return None;
}
Some(cs.created_at)
} else {
None
}
}
fn retire_at(&self, srv: &CryptoServer) -> Option<Timing> {
self.die_at(srv)
}
fn die_at(&self, srv: &CryptoServer) -> Option<Timing> {
self.created_at(srv).map(|t| t + PEER_COOKIE_VALUE_EPOCH)
}
}
/// Trait extension to the [Mortal] Trait, that enables nicer access to timing
/// information
trait MortalExt: Mortal {
@@ -757,8 +905,148 @@ pub struct HandleMsgResult {
pub resp: Option<usize>,
}
/// Trait for host identification types
pub trait HostIdentification: Display {
// Byte slice representing the host identification encoding
fn encode(&self) -> &[u8];
}
impl CryptoServer {
/// Respond to an incoming message
/// Process a message under load
/// This is one of the main entry point for the protocol.
/// Keeps track of messages processed, and qualifies messages using
/// cookie based DoS mitigation.
/// If recieving a InitHello message, it dispatches message for further processing
/// to `process_msg` handler if cookie is valid otherwise sends a cookie reply
/// message for sender to process and verify for messages part of the handshake phase
/// Directly processes InitConf messages.
/// Bails on messages sent by responder and non-handshake messages.
pub fn handle_msg_under_load<H: HostIdentification>(
&mut self,
rx_buf: &[u8],
tx_buf: &mut [u8],
host_identification: &H,
) -> Result<HandleMsgResult> {
let mut active_cookie_value: Option<[u8; COOKIE_SIZE]> = None;
let mut rx_cookie = [0u8; COOKIE_SIZE];
let mut rx_mac = [0u8; MAC_SIZE];
let mut rx_sid = [0u8; 4];
let msg_type: Result<MsgType, _> = rx_buf[0].try_into();
match msg_type {
Ok(MsgType::InitConf) => {
log::debug!(
"Rx {:?} from {} under load, skip cookie validation",
msg_type,
host_identification
);
return self.handle_msg(rx_buf, tx_buf);
}
Ok(MsgType::InitHello) => {
//Process message (continued below)
}
_ => {
bail!(
"Rx {:?} from {} is not processed under load",
msg_type,
host_identification
);
}
}
for cookie_secret in self.active_or_retired_cookie_secrets() {
if let Some(cookie_secret) = cookie_secret {
let cookie_secret = cookie_secret.get(self).value.secret();
let mut cookie_value = [0u8; 16];
cookie_value.copy_from_slice(
&hash_domains::cookie_value()?
.mix(cookie_secret)?
.mix(host_identification.encode())?
.into_value()[..16],
);
//Most recently filled value is active cookie value
if active_cookie_value.is_none() {
active_cookie_value = Some(cookie_value);
}
let mut expected = [0u8; COOKIE_SIZE];
let msg_in = Ref::<&[u8], Envelope<InitHello>>::new(rx_buf)
.ok_or(RosenpassError::BufferSizeMismatch)?;
expected.copy_from_slice(
&hash_domains::cookie()?
.mix(&cookie_value)?
.mix(&msg_in.as_bytes()[span_of!(Envelope<InitHello>, msg_type..cookie)])?
.into_value()[..16],
);
rx_cookie.copy_from_slice(&msg_in.cookie);
rx_mac.copy_from_slice(&msg_in.mac);
rx_sid.copy_from_slice(&msg_in.payload.sidi);
//If valid cookie is found, process message
if constant_time::memcmp(&rx_cookie, &expected) {
log::debug!(
"Rx {:?} from {} under load, valid cookie",
msg_type,
host_identification
);
let result = self.handle_msg(rx_buf, tx_buf)?;
return Ok(result);
}
} else {
break;
}
}
//Otherwise send cookie reply
if active_cookie_value.is_none() {
bail!("No active cookie value found");
}
log::debug!(
"Rx {:?} from {} under load, tx cookie reply message",
msg_type,
host_identification
);
let cookie_value = active_cookie_value.unwrap();
let cookie_key = hash_domains::cookie_key()?
.mix(self.spkm.secret())?
.into_value();
let mut msg_out = truncating_cast_into::<CookieReply>(tx_buf)?;
let nonce = XAEADNonce::random();
msg_out.inner.msg_type = MsgType::CookieReply.into();
msg_out.inner.sid = rx_sid;
xaead::encrypt(
&mut msg_out.inner.cookie_encrypted[..],
&cookie_key,
&nonce.value,
&rx_mac,
&cookie_value,
)?;
msg_out
.padding
.try_fill(&mut rosenpass_secret_memory::rand::rng())
.unwrap();
// length of the response
let _len = Some(size_of::<CookieReply>());
Ok(HandleMsgResult {
exchanged_with: None,
resp: Some(size_of::<CookieReply>()),
})
}
/// Handle an incoming message
/// This is one of the main entry point for the protocol.
///
/// # Overview
///
@@ -794,7 +1082,11 @@ impl CryptoServer {
ensure!(!rx_buf.is_empty(), "received empty message, ignoring it");
let peer = match rx_buf[0].try_into() {
let msg_type = rx_buf[0].try_into();
log::debug!("Rx {:?}, processing", msg_type);
let peer = match msg_type {
Ok(MsgType::InitHello) => {
let msg_in: Ref<&[u8], Envelope<InitHello>> =
Ref::new(rx_buf).ok_or(RosenpassError::BufferSizeMismatch)?;
@@ -837,7 +1129,13 @@ impl CryptoServer {
self.handle_resp_conf(&msg_in.payload)?
}
Ok(MsgType::DataMsg) => bail!("DataMsg handling not implemented!"),
Ok(MsgType::CookieReply) => bail!("CookieReply handling not implemented!"),
Ok(MsgType::CookieReply) => {
let msg_in: Ref<&[u8], CookieReply> =
Ref::new(rx_buf).ok_or(RosenpassError::BufferSizeMismatch)?;
let peer = self.handle_cookie_reply(&msg_in)?;
len = 0;
peer
}
Err(_) => {
bail!("CookieReply handling not implemented!")
}
@@ -850,7 +1148,8 @@ impl CryptoServer {
}
/// Serialize message to `tx_buf`, generating the `mac` in the process of
/// doing so
/// doing so. If `cookie_secret` is also present, a `cookie` value is also generated
/// and added to the message
///
/// The message type is explicitly required here because it is very easy to
/// forget setting that, which creates subtle but far ranging errors.
@@ -1053,6 +1352,7 @@ impl CryptoServer {
pub fn poll(&mut self) -> Result<PollResult> {
let r = begin_poll() // Poll each biscuit and peer until an event is found
.poll_children(self, self.biscuit_key_ptrs())?
.poll_children(self, self.cookie_secret_ptrs())?
.poll_children(self, self.peer_ptrs_off(self.peer_poll_off))?;
self.peer_poll_off = match r.peer() {
Some(p) => p.0 + 1, // Event found while polling peer p; will poll peer p+1 next
@@ -1070,6 +1370,14 @@ impl Pollable for BiscuitKeyPtr {
}
}
impl Pollable for ServerCookieSecretPtr {
fn poll(&self, srv: &mut CryptoServer) -> Result<PollResult> {
begin_poll()
.sched(self.life_left(srv), void_poll(|| self.get_mut(srv).erase())) // Erase stale cookie secrets
.ok()
}
}
impl Pollable for PeerPtr {
fn poll(&self, srv: &mut CryptoServer) -> Result<PollResult> {
let (ses, hs) = (self.session(), self.hs());
@@ -1131,12 +1439,22 @@ impl IniHsPtr {
}
pub fn apply_retransmission(&self, srv: &mut CryptoServer, tx_buf: &mut [u8]) -> Result<usize> {
let ih = self
.get_mut(srv)
.as_mut()
.with_context(|| format!("No current handshake for peer {:?}", self.peer()))?;
cpy_min(&ih.tx_buf[..ih.tx_len], tx_buf);
Ok(ih.tx_len)
let ih_tx_len: usize;
{
let ih = self
.get_mut(srv)
.as_mut()
.with_context(|| format!("No current handshake for peer {:?}", self.peer()))?;
cpy_min(&ih.tx_buf[..ih.tx_len], tx_buf);
ih_tx_len = ih.tx_len;
}
// Add cookie to retransmitted message
let mut envelope = truncating_cast_into::<Envelope<InitHello>>(tx_buf)?;
envelope.seal_cookie(self.peer(), srv)?;
Ok(ih_tx_len)
}
pub fn register_retransmission(&self, srv: &mut CryptoServer) -> Result<()> {
@@ -1159,6 +1477,17 @@ impl IniHsPtr {
Ok(())
}
pub fn register_immediate_retransmission(&self, srv: &mut CryptoServer) -> Result<()> {
let tb = srv.timebase.clone();
let ih = self
.get_mut(srv)
.as_mut()
.with_context(|| format!("No current handshake for peer {:?}", self.peer()))?;
ih.tx_retry_at = tb.now();
ih.tx_count += 1;
Ok(())
}
pub fn retransmission_in(&self, srv: &mut CryptoServer) -> Option<Timing> {
self.get(srv)
.as_ref()
@@ -1172,12 +1501,25 @@ impl<M> Envelope<M>
where
M: AsBytes + FromBytes,
{
/// Calculate the message authentication code (`mac`)
/// Calculate the message authentication code (`mac`) and also append cookie value
pub fn seal(&mut self, peer: PeerPtr, srv: &CryptoServer) -> Result<()> {
let mac = hash_domains::mac()?
.mix(peer.get(srv).spkt.secret())?
.mix(&self.as_bytes()[span_of!(Self, msg_type..mac)])?;
self.mac.copy_from_slice(mac.into_value()[..16].as_ref());
self.seal_cookie(peer, srv)?;
Ok(())
}
/// Calculate and append the cookie value if `cookie_key` exists (`cookie`)
pub fn seal_cookie(&mut self, peer: PeerPtr, srv: &CryptoServer) -> Result<()> {
if let Some(cookie_key) = &peer.cv().get(srv) {
let cookie = hash_domains::cookie()?
.mix(cookie_key.value.secret())?
.mix(&self.as_bytes()[span_of!(Self, msg_type..cookie)])?;
self.cookie
.copy_from_slice(cookie.into_value()[..16].as_ref());
}
Ok(())
}
}
@@ -1211,6 +1553,7 @@ impl InitiatorHandshake {
tx_count: 0,
tx_len: 0,
tx_buf: MsgBuf::zero(),
cookie_value: CookieStore::new(),
}
}
}
@@ -1308,7 +1651,7 @@ impl HandshakeState {
n[0] &= 0b0111_1111;
n[0] |= (bk.0 as u8 & 0x1) << 7;
let k = bk.get(srv).key.secret();
let k = bk.get(srv).value.secret();
let pt = biscuit.as_bytes();
xaead::encrypt(biscuit_ct, k, &*n, &ad, pt)?;
@@ -1339,7 +1682,7 @@ impl HandshakeState {
Ref::new(biscuit.secret_mut().as_mut_slice()).unwrap();
xaead::decrypt(
biscuit.as_bytes_mut(),
bk.get(srv).key.secret(),
bk.get(srv).value.secret(),
&ad,
biscuit_ct,
)?;
@@ -1718,16 +2061,102 @@ impl CryptoServer {
Ok(hs.peer())
}
pub fn handle_cookie_reply(&mut self, cr: &CookieReply) -> Result<PeerPtr> {
let peer_ptr: Option<PeerPtr> = self
.lookup_session(Public::new(cr.inner.sid))
.map(|v| PeerPtr(v.0))
.or_else(|| {
self.lookup_handshake(Public::new(cr.inner.sid))
.map(|v| PeerPtr(v.0))
});
if let Some(peer) = peer_ptr {
// Get last transmitted handshake message
if let Some(ih) = &peer.get(self).handshake {
let mut mac = [0u8; MAC_SIZE];
// TODO: Handle buffer overflow in ih.tx_buf[0] (i.e. the case where the )
match ih.tx_buf[0].try_into() {
Ok(MsgType::InitHello) => {
match truncating_cast_into_nomut::<Envelope<InitHello>>(&ih.tx_buf.value) {
Ok(t) => {
mac = t.mac;
Ok(())
}
Err(e) => Err(e),
}
}
Ok(MsgType::InitConf) => {
match truncating_cast_into_nomut::<Envelope<InitConf>>(&ih.tx_buf.value) {
Ok(t) => {
mac = t.mac;
Ok(())
}
Err(e) => Err(e),
}
}
_ => bail!(
"No last sent message for peer {pidr:?} to decrypt cookie reply.",
pidr = cr.inner.sid
),
}?;
let spkt = peer.get(self).spkt.secret();
let cookie_key = hash_domains::cookie_key()?.mix(spkt)?.into_value();
let cookie_value = peer.cv().update_mut(self).unwrap();
xaead::decrypt(cookie_value, &cookie_key, &mac, &cr.inner.cookie_encrypted)?;
// Immediately retransmit on recieving a cookie reply message
peer.hs().register_immediate_retransmission(self)?;
Ok(peer)
} else {
bail!(
"No last sent message for peer {pidr:?} to decrypt cookie reply.",
pidr = cr.inner.sid
);
}
} else {
bail!("No such peer {pidr:?}.", pidr = cr.inner.sid);
}
}
}
fn truncating_cast_into<T: FromBytes>(buf: &mut [u8]) -> Result<Ref<&mut [u8], T>, RosenpassError> {
Ok(Ref::new(&mut buf[..size_of::<T>()]).ok_or(RosenpassError::BufferSizeMismatch)?)
}
// TODO: This is bad…
fn truncating_cast_into_nomut<T: FromBytes>(buf: &[u8]) -> Result<Ref<&[u8], T>, RosenpassError> {
Ok(Ref::new(&buf[..size_of::<T>()]).ok_or(RosenpassError::BufferSizeMismatch)?)
}
#[cfg(test)]
mod test {
use std::{net::SocketAddrV4, thread::sleep, time::Duration};
use super::*;
struct VecHostIdentifier(Vec<u8>);
impl HostIdentification for VecHostIdentifier {
fn encode(&self) -> &[u8] {
&self.0
}
}
impl Display for VecHostIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl From<Vec<u8>> for VecHostIdentifier {
fn from(v: Vec<u8>) -> Self {
VecHostIdentifier(v)
}
}
#[test]
/// Ensure that the protocol implementation can deal with truncated
/// messages and with overlong messages.
@@ -1812,4 +2241,156 @@ mod test {
b.add_peer(Some(psk), pka)?;
Ok((a, b))
}
#[test]
fn cookie_reply_mechanism_responder_under_load() {
stacker::grow(8 * 1024 * 1024, || {
type MsgBufPlus = Public<MAX_MESSAGE_LEN>;
let (mut a, mut b) = make_server_pair().unwrap();
let mut a_to_b_buf = MsgBufPlus::zero();
let mut b_to_a_buf = MsgBufPlus::zero();
let ip_a: SocketAddrV4 = "127.0.0.1:8080".parse().unwrap();
let mut ip_addr_port_a = ip_a.ip().octets().to_vec();
ip_addr_port_a.extend_from_slice(&ip_a.port().to_be_bytes());
let _ip_b: SocketAddrV4 = "127.0.0.1:8081".parse().unwrap();
let init_hello_len = a.initiate_handshake(PeerPtr(0), &mut *a_to_b_buf).unwrap();
let socket_addr_a = std::net::SocketAddr::V4(ip_a);
let mut ip_addr_port_a = match socket_addr_a.ip() {
std::net::IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
std::net::IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
};
ip_addr_port_a.extend_from_slice(&socket_addr_a.port().to_be_bytes());
let ip_addr_port_a: VecHostIdentifier = ip_addr_port_a.into();
//B handles handshake under load, should send cookie reply message with invalid cookie
let HandleMsgResult { resp, .. } = b
.handle_msg_under_load(
&a_to_b_buf.as_slice()[..init_hello_len],
&mut *b_to_a_buf,
&ip_addr_port_a,
)
.unwrap();
let cookie_reply_len = resp.unwrap();
//A handles cookie reply message
a.handle_msg(&b_to_a_buf[..cookie_reply_len], &mut *a_to_b_buf)
.unwrap();
assert_eq!(PeerPtr(0).cv().lifecycle(&a), Lifecycle::Young);
let expected_cookie_value = hash_domains::cookie_value()
.unwrap()
.mix(
b.active_or_retired_cookie_secrets()[0]
.unwrap()
.get(&b)
.value
.secret(),
)
.unwrap()
.mix(&ip_addr_port_a.encode())
.unwrap()
.into_value()[..16]
.to_vec();
assert_eq!(
PeerPtr(0).cv().get(&a).map(|x| &x.value.secret()[..]),
Some(&expected_cookie_value[..])
);
let retx_init_hello_len = loop {
match a.poll().unwrap() {
PollResult::SendRetransmission(peer) => {
break (a.retransmit_handshake(peer, &mut *a_to_b_buf).unwrap());
}
PollResult::Sleep(time) => {
sleep(Duration::from_secs_f64(time));
}
_ => {}
}
};
let retx_msg_type: MsgType = a_to_b_buf.value[0].try_into().unwrap();
assert_eq!(retx_msg_type, MsgType::InitHello);
//B handles retransmitted message
let HandleMsgResult { resp, .. } = b
.handle_msg_under_load(
&a_to_b_buf.as_slice()[..retx_init_hello_len],
&mut *b_to_a_buf,
&ip_addr_port_a,
)
.unwrap();
let _resp_hello_len = resp.unwrap();
let resp_msg_type: MsgType = b_to_a_buf.value[0].try_into().unwrap();
assert_eq!(resp_msg_type, MsgType::RespHello);
});
}
#[test]
fn cookie_reply_mechanism_initiator_bails_on_message_under_load() {
stacker::grow(8 * 1024 * 1024, || {
type MsgBufPlus = Public<MAX_MESSAGE_LEN>;
let (mut a, mut b) = make_server_pair().unwrap();
let mut a_to_b_buf = MsgBufPlus::zero();
let mut b_to_a_buf = MsgBufPlus::zero();
let ip_a: SocketAddrV4 = "127.0.0.1:8080".parse().unwrap();
let mut ip_addr_port_a = ip_a.ip().octets().to_vec();
ip_addr_port_a.extend_from_slice(&ip_a.port().to_be_bytes());
let ip_b: SocketAddrV4 = "127.0.0.1:8081".parse().unwrap();
//A initiates handshake
let init_hello_len = a.initiate_handshake(PeerPtr(0), &mut *a_to_b_buf).unwrap();
//B handles InitHello message, should respond with RespHello
let HandleMsgResult { resp, .. } = b
.handle_msg(&a_to_b_buf.as_slice()[..init_hello_len], &mut *b_to_a_buf)
.unwrap();
let resp_hello_len = resp.unwrap();
let resp_msg_type: MsgType = b_to_a_buf.value[0].try_into().unwrap();
assert_eq!(resp_msg_type, MsgType::RespHello);
let socket_addr_b = std::net::SocketAddr::V4(ip_b);
let mut ip_addr_port_b = [0u8; 18];
let mut ip_addr_port_b_len = 0;
match socket_addr_b.ip() {
std::net::IpAddr::V4(ipv4) => {
ip_addr_port_b[0..4].copy_from_slice(&ipv4.octets());
ip_addr_port_b_len += 4;
}
std::net::IpAddr::V6(ipv6) => {
ip_addr_port_b[0..16].copy_from_slice(&ipv6.octets());
ip_addr_port_b_len += 16;
}
};
ip_addr_port_b[ip_addr_port_b_len..ip_addr_port_b_len + 2]
.copy_from_slice(&socket_addr_b.port().to_be_bytes());
ip_addr_port_b_len += 2;
let ip_addr_port_b: VecHostIdentifier =
ip_addr_port_b[..ip_addr_port_b_len].to_vec().into();
//A handles RespHello message under load, should not send cookie reply
assert!(a
.handle_msg_under_load(
&b_to_a_buf[..resp_hello_len],
&mut *a_to_b_buf,
&ip_addr_port_b
)
.is_err());
});
}
}

View File

@@ -1,4 +1,9 @@
use std::{fs, net::UdpSocket, path::PathBuf, process::Stdio, time::Duration};
use std::{fs, net::UdpSocket, path::PathBuf, time::Duration};
use clap::Parser;
use rosenpass::{app_server::AppServerTestBuilder, cli::CliArgs};
use serial_test::serial;
use std::io::Write;
const BIN: &str = "rosenpass";
@@ -28,26 +33,34 @@ fn generate_keys() {
fs::remove_dir_all(&tmpdir).unwrap();
}
fn find_udp_socket() -> u16 {
fn find_udp_socket() -> Option<u16> {
for port in 1025..=u16::MAX {
if UdpSocket::bind(("127.0.0.1", port)).is_ok() {
return port;
if UdpSocket::bind(("::1", port)).is_ok() {
return Some(port);
}
}
panic!("no free UDP port found");
None
}
// check that we can exchange keys
#[test]
fn check_exchange() {
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange");
fs::create_dir_all(&tmpdir).unwrap();
fn setup_logging() {
let mut log_builder = env_logger::Builder::from_default_env(); // sets log level filter from environment (or defaults)
log_builder.filter_level(log::LevelFilter::Debug);
log_builder.format_timestamp_nanos();
log_builder.format(|buf, record| {
let ts_format = buf.timestamp_nanos().to_string();
writeln!(
buf,
"\x1b[1m{:?}\x1b[0m {}: {}",
std::thread::current().id(),
&ts_format[14..],
record.args()
)
});
let secret_key_paths = [tmpdir.join("secret-key-0"), tmpdir.join("secret-key-1")];
let public_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")];
let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")];
let _ = log_builder.try_init();
}
// generate key pairs
fn generate_key_pairs(secret_key_paths: &[PathBuf], public_key_paths: &[PathBuf]) {
for (secret_key_path, pub_key_path) in secret_key_paths.iter().zip(public_key_paths.iter()) {
let output = test_bin::get_test_bin(BIN)
.args(["gen-keys", "--secret-key"])
@@ -61,11 +74,86 @@ fn check_exchange() {
assert!(secret_key_path.is_file());
assert!(pub_key_path.is_file());
}
}
fn run_server_client_exchange(
(server_cmd, server_test_builder): (&std::process::Command, AppServerTestBuilder),
(client_cmd, client_test_builder): (&std::process::Command, AppServerTestBuilder),
) {
let (server_terminate, server_terminate_rx) = std::sync::mpsc::channel();
let (client_terminate, client_terminate_rx) = std::sync::mpsc::channel();
let cli = CliArgs::try_parse_from(
[server_cmd.get_program()]
.into_iter()
.chain(server_cmd.get_args()),
)
.unwrap();
std::thread::spawn(move || {
cli.command
.run(Some(
server_test_builder
.termination_handler(Some(server_terminate_rx))
.build()
.unwrap(),
))
.unwrap();
});
let cli = CliArgs::try_parse_from(
[client_cmd.get_program()]
.into_iter()
.chain(client_cmd.get_args()),
)
.unwrap();
std::thread::spawn(move || {
cli.command
.run(Some(
client_test_builder
.termination_handler(Some(client_terminate_rx))
.build()
.unwrap(),
))
.unwrap();
});
// give them some time to do the key exchange under load
std::thread::sleep(Duration::from_secs(10));
// time's up, kill the childs
server_terminate.send(()).unwrap();
client_terminate.send(()).unwrap();
}
// check that we can exchange keys
#[test]
#[serial]
fn check_exchange_under_normal() {
setup_logging();
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange");
fs::create_dir_all(&tmpdir).unwrap();
let secret_key_paths = [tmpdir.join("secret-key-0"), tmpdir.join("secret-key-1")];
let public_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")];
let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")];
// generate key pairs
generate_key_pairs(&secret_key_paths, &public_key_paths);
// start first process, the server
let port = find_udp_socket();
let listen_addr = format!("localhost:{port}");
let mut server = test_bin::get_test_bin(BIN)
let port = loop {
if let Some(port) = find_udp_socket() {
break port;
}
};
let listen_addr = format!("::1:{port}");
let mut server_cmd = std::process::Command::new(BIN);
server_cmd
.args(["exchange", "secret-key"])
.arg(&secret_key_paths[0])
.arg("public-key")
@@ -73,16 +161,12 @@ fn check_exchange() {
.args(["listen", &listen_addr, "verbose", "peer", "public-key"])
.arg(&public_key_paths[1])
.arg("outfile")
.arg(&shared_key_paths[0])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start {BIN}");
.arg(&shared_key_paths[0]);
std::thread::sleep(Duration::from_millis(500));
let server_test_builder = AppServerTestBuilder::default();
// start second process, the client
let mut client = test_bin::get_test_bin(BIN)
let mut client_cmd = std::process::Command::new(BIN);
client_cmd
.args(["exchange", "secret-key"])
.arg(&secret_key_paths[1])
.arg("public-key")
@@ -91,18 +175,88 @@ fn check_exchange() {
.arg(&public_key_paths[0])
.args(["endpoint", &listen_addr])
.arg("outfile")
.arg(&shared_key_paths[1])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start {BIN}");
.arg(&shared_key_paths[1]);
// give them some time to do the key exchange
std::thread::sleep(Duration::from_secs(2));
let client_test_builder = AppServerTestBuilder::default();
// time's up, kill the childs
server.kill().unwrap();
client.kill().unwrap();
run_server_client_exchange(
(&server_cmd, server_test_builder),
(&client_cmd, client_test_builder),
);
// read the shared keys they created
let shared_keys: Vec<_> = shared_key_paths
.iter()
.map(|p| fs::read_to_string(p).unwrap())
.collect();
// check that they created two equal keys
assert_eq!(shared_keys.len(), 2);
assert_eq!(shared_keys[0], shared_keys[1]);
// cleanup
fs::remove_dir_all(&tmpdir).unwrap();
}
// check that we can trigger a DoS condition and we can exchange keys under DoS
// This test creates a responder (server) with the feature flag "integration_test_always_under_load" to always be under load condition for the test.
#[test]
#[serial]
fn check_exchange_under_dos() {
setup_logging();
//Generate binary with responder with feature integration_test
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange-dos");
fs::create_dir_all(&tmpdir).unwrap();
let secret_key_paths = [tmpdir.join("secret-key-0"), tmpdir.join("secret-key-1")];
let public_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")];
let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")];
// generate key pairs
generate_key_pairs(&secret_key_paths, &public_key_paths);
// start first process, the server
let port = loop {
if let Some(port) = find_udp_socket() {
break port;
}
};
let listen_addr = format!("::1:{port}");
let mut server_cmd = std::process::Command::new(BIN);
server_cmd
.args(["exchange", "secret-key"])
.arg(&secret_key_paths[0])
.arg("public-key")
.arg(&public_key_paths[0])
.args(["listen", &listen_addr, "verbose", "peer", "public-key"])
.arg(&public_key_paths[1])
.arg("outfile")
.arg(&shared_key_paths[0]);
let server_test_builder = AppServerTestBuilder::default().enable_dos_permanently(true);
let mut client_cmd = std::process::Command::new(BIN);
client_cmd
.args(["exchange", "secret-key"])
.arg(&secret_key_paths[1])
.arg("public-key")
.arg(&public_key_paths[1])
.args(["verbose", "peer", "public-key"])
.arg(&public_key_paths[0])
.args(["endpoint", &listen_addr])
.arg("outfile")
.arg(&shared_key_paths[1]);
let client_test_builder = AppServerTestBuilder::default();
run_server_client_exchange(
(&server_cmd, server_test_builder),
(&client_cmd, client_test_builder),
);
// read the shared keys they created
let shared_keys: Vec<_> = shared_key_paths