mirror of
https://github.com/rosenpass/rosenpass.git
synced 2025-12-16 01:10:39 -08:00
Compare commits
2 Commits
dev/broker
...
dev/karo/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7632fa047f | ||
|
|
3739042d25 |
@@ -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.
|
||||
|
||||
@@ -428,12 +429,89 @@ 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 from Wireguard [@wg].
|
||||
|
||||
When the responder is under load, it may choose to not process further 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 the message when under load.
|
||||
|
||||
#### Cookie Reply Message
|
||||
|
||||
The cookie reply message consists of the `sid` of the client under load, a random 24-byte bitstring `nonce` and encrypting `cookie_tau` into a `cookie` reply field which consists of the following (from the perspective of the cookie reply sender):
|
||||
|
||||
```
|
||||
cookie_tau = lhash("cookie-tau",r_m, a_m)[0..16]
|
||||
cookie = XAEAD(lhash("cookie-key", spkm), nonce, cookie_tau , mac_peer)
|
||||
```
|
||||
|
||||
where `r_m` is a secret variable that changes every two minutes to a random value. `a_m` is a concatenation of the source IP address and UDP source port of the client's peer. `cookie_tau` will result in 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:
|
||||
|
||||
```
|
||||
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 `cookie_tau` value encrypted as part of `cookie` field in the cookie reply message is decrypted by its receiver and stored as the `last_recvd_cookie` for a limited time (120 seconds). This value is then used by the sender to append a `cookie` field to the sender's message envelope to retransmit the handshake message. This is the equivalent of Wireguard's `mac.2` field and is determined as follows:
|
||||
|
||||
```
|
||||
|
||||
if (is_zero_length_bitstring(last_recvd_cookie) || last_cookie_time_ellapsed >= 120) {
|
||||
cookie = 0^16; //zeroed out 16 bytes bitstring
|
||||
}
|
||||
else {
|
||||
cookie = lhash("cookie",last_recvd_cookie,COOKIE_WIRE_DATA)
|
||||
}
|
||||
```
|
||||
|
||||
Here, `last_recvd_cookie` is the last received decrypted data of the `cookie` field from a cookie reply message by a hanshake message sender, `last_cookie_time_ellapsed` 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 sender can use an invalid value for the `cookie` value, when the receiver is not under load, and the receiver must ignore this value.
|
||||
However, when the receiver is under load, it may reject messages with the invalid `cookie` value, and issue a cookie reply message.
|
||||
|
||||
### Conditions to trigger DoS Mechanism
|
||||
|
||||
Rosenpass implementations are expected to detect conditions in which they are under computational load to trigger the cookie based DoS mitigation mechanism by replying with a cookie reply message.
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
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, a handshake 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_tau` value to use when appending a `cookie` to subsequently sent messages. As per the retransmission mechanism above, the initiator will send a retransmitted InitHello or InitConf 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.
|
||||
|
||||
\printbibliography
|
||||
|
||||
\setupimage{landscape,fullpage,label=img:HandlingCode}
|
||||
|
||||
@@ -3,7 +3,6 @@ use rosenpass::pqkem::KEM;
|
||||
use rosenpass::{
|
||||
pqkem::StaticKEM,
|
||||
protocol::{CryptoServer, HandleMsgResult, MsgBuf, PeerPtr, SPk, SSk, SymKey},
|
||||
sodium::sodium_init,
|
||||
};
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
@@ -58,7 +57,7 @@ fn make_server_pair() -> Result<(CryptoServer, CryptoServer)> {
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
sodium_init().unwrap();
|
||||
rosenpass_sodium::init().unwrap();
|
||||
let (mut a, mut b) = make_server_pair().unwrap();
|
||||
c.bench_function("cca_secret_alloc", |bench| {
|
||||
bench.iter(|| {
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::process::Stdio;
|
||||
use std::slice;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
config::Verbosity,
|
||||
@@ -33,6 +34,11 @@ 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);
|
||||
|
||||
// Using values from Linux Kernel implementation
|
||||
// TODO: Customize values for rosenpass
|
||||
const MAX_QUEUED_INCOMING_HANDSHAKES_THRESHOLD: usize = 4096;
|
||||
const LAST_UNDER_LOAD_WINDOW: Duration = Duration::from_secs(1);
|
||||
|
||||
fn ipv4_any_binding() -> SocketAddr {
|
||||
// addr, port
|
||||
SocketAddr::V4(SocketAddrV4::new(IPV4_ANY_ADDR, 0))
|
||||
@@ -67,6 +73,12 @@ pub struct WireguardOut {
|
||||
pub extra_params: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DoSOperation {
|
||||
UnderLoad { last_under_load: Instant },
|
||||
Normal,
|
||||
}
|
||||
|
||||
/// Holds the state of the application, namely the external IO
|
||||
///
|
||||
/// Responsible for file IO, network IO
|
||||
@@ -80,6 +92,7 @@ pub struct AppServer {
|
||||
pub peers: Vec<AppPeer>,
|
||||
pub verbosity: Verbosity,
|
||||
pub all_sockets_drained: bool,
|
||||
pub under_load: DoSOperation,
|
||||
}
|
||||
|
||||
/// A socket pointer is an index assigned to a socket;
|
||||
@@ -435,6 +448,7 @@ impl AppServer {
|
||||
events,
|
||||
mio_poll,
|
||||
all_sockets_drained: false,
|
||||
under_load: DoSOperation::Normal,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -549,7 +563,37 @@ impl AppServer {
|
||||
}
|
||||
|
||||
ReceivedMessage(len, endpoint) => {
|
||||
match self.crypt.handle_msg(&rx[..len], &mut *tx) {
|
||||
let msg_result = match self.under_load {
|
||||
DoSOperation::UnderLoad { last_under_load: _ } => {
|
||||
//TODO: Lookup peer through addresses (hash)
|
||||
let index = self
|
||||
.peers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_num, p)| {
|
||||
if let Some(ep) = p.endpoint() {
|
||||
ep.addresses() == endpoint.addresses()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.ok_or(anyhow::anyhow!("Received message from unknown endpoint"))?
|
||||
.0;
|
||||
let socket_addr = endpoint
|
||||
.addresses()
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or(anyhow::anyhow!("No socket address for endpoint"))?;
|
||||
self.crypt.handle_msg_under_load(
|
||||
&rx[..len],
|
||||
&mut *tx,
|
||||
PeerPtr(index),
|
||||
socket_addr,
|
||||
)
|
||||
}
|
||||
DoSOperation::Normal => self.crypt.handle_msg(&rx[..len], &mut *tx),
|
||||
};
|
||||
match msg_result {
|
||||
Err(ref e) => {
|
||||
self.verbose().then(|| {
|
||||
info!(
|
||||
@@ -704,11 +748,29 @@ impl AppServer {
|
||||
// desired on a non-dual-stack OS), thus just checking every socket after any
|
||||
// readiness event seems to be good enough™ for now.
|
||||
|
||||
println!("All sockets drained: {}", self.all_sockets_drained);
|
||||
// only poll if we drained all sockets before
|
||||
if self.all_sockets_drained {
|
||||
self.mio_poll.poll(&mut self.events, Some(timeout))?;
|
||||
|
||||
let queue_length = self.events.iter().peekable().count();
|
||||
|
||||
println!("queue length: {}", queue_length);
|
||||
|
||||
if queue_length > MAX_QUEUED_INCOMING_HANDSHAKES_THRESHOLD {
|
||||
self.under_load = DoSOperation::UnderLoad {
|
||||
last_under_load: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let DoSOperation::UnderLoad { last_under_load } = self.under_load {
|
||||
if last_under_load.elapsed() > LAST_UNDER_LOAD_WINDOW {
|
||||
self.under_load = DoSOperation::Normal;
|
||||
}
|
||||
}
|
||||
|
||||
// drain all sockets
|
||||
let mut would_block_count = 0;
|
||||
for (sock_no, socket) in self.sockets.iter_mut().enumerate() {
|
||||
match socket.recv_from(buf) {
|
||||
|
||||
@@ -23,6 +23,8 @@ macro_rules! prflabel {
|
||||
|
||||
prflabel!(protocol, mac, "mac");
|
||||
prflabel!(protocol, cookie, "cookie");
|
||||
prflabel!(protocol, cookie_tau, "cookie-tau");
|
||||
prflabel!(protocol, cookie_key, "cookie-key");
|
||||
prflabel!(protocol, peerid, "peer id");
|
||||
prflabel!(protocol, biscuit_ad, "biscuit additional data");
|
||||
prflabel!(protocol, ckinit, "chaining key init");
|
||||
|
||||
@@ -45,7 +45,10 @@
|
||||
|
||||
use super::RosenpassError;
|
||||
use crate::pqkem::*;
|
||||
use rosenpass_ciphers::{aead, xaead, KEY_LEN};
|
||||
use rosenpass_ciphers::{aead, xaead};
|
||||
|
||||
pub const MAC_SIZE: usize = 16;
|
||||
pub const COOKIE_SIZE: usize = 16;
|
||||
|
||||
// Macro magic ////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -265,9 +268,9 @@ data_lense! { Envelope<M> :=
|
||||
payload: M::LEN,
|
||||
/// Message Authentication Code (mac) over all bytes until (exclusive)
|
||||
/// `mac` itself
|
||||
mac: 16,
|
||||
/// Currently unused, TODO: do something with this
|
||||
cookie: 16
|
||||
mac: MAC_SIZE,
|
||||
/// Cookie value
|
||||
cookie: COOKIE_SIZE
|
||||
}
|
||||
|
||||
data_lense! { InitHello :=
|
||||
@@ -320,11 +323,11 @@ data_lense! { EmptyData :=
|
||||
|
||||
data_lense! { Biscuit :=
|
||||
/// H(spki) – Ident ifies the initiator
|
||||
pidi: KEY_LEN,
|
||||
pidi: aead::KEY_LEN,
|
||||
/// The biscuit number (replay protection)
|
||||
biscuit_no: 12,
|
||||
/// Chaining key
|
||||
ck: KEY_LEN
|
||||
ck: aead::KEY_LEN
|
||||
}
|
||||
|
||||
data_lense! { DataMsg :=
|
||||
@@ -332,7 +335,9 @@ data_lense! { DataMsg :=
|
||||
}
|
||||
|
||||
data_lense! { CookieReply :=
|
||||
dummy: 4
|
||||
sid: 4,
|
||||
nonce: xaead::NONCE_LEN,
|
||||
cookie_encrypted: MAC_SIZE + xaead::TAG_LEN
|
||||
}
|
||||
|
||||
// Traits /////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
use crate::{
|
||||
coloring::*,
|
||||
labeled_prf as lprf,
|
||||
labeled_prf::{self as lprf},
|
||||
msgs::*,
|
||||
pqkem::*,
|
||||
prftree::{SecretPrfTree, SecretPrfTreeBranch},
|
||||
@@ -81,6 +81,7 @@ use std::collections::hash_map::{
|
||||
Entry::{Occupied, Vacant},
|
||||
HashMap,
|
||||
};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
// CONSTANTS & SETTINGS //////////////////////////
|
||||
|
||||
@@ -109,6 +110,16 @@ 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 value Rm composing `cookie_tau` in the whitepaper
|
||||
pub const COOKIE_SECRET_LEN: usize = MAC_SIZE;
|
||||
pub const COOKIE_SECRET_EXP: Timing = 120.0;
|
||||
|
||||
// Peer Cookie Tau expiration
|
||||
pub const PEER_COOKIE_TAU_EXP: 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
|
||||
@@ -184,6 +195,89 @@ pub struct CryptoServer {
|
||||
|
||||
// Tick handling
|
||||
pub peer_poll_off: usize,
|
||||
|
||||
// Random state which changes every COOKIE_SECRET_EXP seconds
|
||||
pub cookie_secret: CookieSecret,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CookieSecret {
|
||||
Some {
|
||||
value: [u8; COOKIE_SECRET_LEN],
|
||||
last_updated: Timebase,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
impl CookieSecret {
|
||||
pub fn new(value: [u8; COOKIE_SECRET_LEN]) -> Self {
|
||||
Self::Some {
|
||||
value,
|
||||
last_updated: Timebase::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Get the cookie secret, does not update the value
|
||||
pub fn get(&self, expiry: Timing) -> Option<&[u8]> {
|
||||
if let CookieSecret::Some {
|
||||
value: _value,
|
||||
last_updated,
|
||||
} = self
|
||||
{
|
||||
if last_updated.now() > expiry {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
match self {
|
||||
CookieSecret::Some { value, .. } => Some(value),
|
||||
CookieSecret::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Get the cookie secret or update the value if it has expired and return the updated value
|
||||
pub fn get_or_update_ellapsed<F: Fn(&mut [u8])>(
|
||||
&mut self,
|
||||
expiry: Timing,
|
||||
update_fn: F,
|
||||
) -> &[u8] {
|
||||
if let CookieSecret::Some {
|
||||
value,
|
||||
last_updated,
|
||||
} = self
|
||||
{
|
||||
if last_updated.now() > expiry {
|
||||
update_fn(value);
|
||||
*last_updated = Timebase::default();
|
||||
}
|
||||
value
|
||||
} else {
|
||||
*self = CookieSecret::Some {
|
||||
value: [0; COOKIE_SECRET_LEN],
|
||||
last_updated: Timebase::default(),
|
||||
};
|
||||
let value = match self {
|
||||
CookieSecret::Some {
|
||||
value,
|
||||
last_updated: _,
|
||||
} => value,
|
||||
CookieSecret::None => unreachable!(),
|
||||
};
|
||||
update_fn(value);
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_some(&self) -> bool {
|
||||
match self {
|
||||
CookieSecret::Some { .. } => true,
|
||||
CookieSecret::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
!self.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Biscuit is like a fancy cookie. To avoid state disruption attacks,
|
||||
@@ -211,6 +305,8 @@ pub struct Peer {
|
||||
pub session: Option<Session>,
|
||||
pub handshake: Option<InitiatorHandshake>,
|
||||
pub initiation_requested: bool,
|
||||
pub cookie_tau: CookieSecret,
|
||||
pub last_sent_mac: Option<Public<MAC_SIZE>>,
|
||||
}
|
||||
|
||||
impl Peer {
|
||||
@@ -222,6 +318,8 @@ impl Peer {
|
||||
session: None,
|
||||
initiation_requested: false,
|
||||
handshake: None,
|
||||
cookie_tau: CookieSecret::None,
|
||||
last_sent_mac: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,6 +547,7 @@ impl CryptoServer {
|
||||
peers: Vec::new(),
|
||||
index: HashMap::new(),
|
||||
peer_poll_off: 0,
|
||||
cookie_secret: CookieSecret::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +581,8 @@ impl CryptoServer {
|
||||
session: None,
|
||||
handshake: None,
|
||||
initiation_requested: false,
|
||||
cookie_tau: CookieSecret::None,
|
||||
last_sent_mac: None,
|
||||
};
|
||||
let peerid = peer.pidt()?;
|
||||
let peerno = self.peers.len();
|
||||
@@ -584,6 +685,8 @@ impl Peer {
|
||||
session: None,
|
||||
handshake: None,
|
||||
initiation_requested: false,
|
||||
cookie_tau: CookieSecret::None,
|
||||
last_sent_mac: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,7 +856,122 @@ pub struct HandleMsgResult {
|
||||
}
|
||||
|
||||
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. 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
|
||||
/// (i.e. InitHello, InitConf messages only). Bails on messages sent by responder and
|
||||
/// non-handshake messages.
|
||||
pub fn handle_msg_under_load(
|
||||
&mut self,
|
||||
rx_buf: &[u8],
|
||||
tx_buf: &mut [u8],
|
||||
peer: PeerPtr,
|
||||
socket_addr: SocketAddr,
|
||||
) -> Result<HandleMsgResult> {
|
||||
//Check cookie value
|
||||
let mut ip_addr_port = match socket_addr.ip() {
|
||||
IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
|
||||
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
|
||||
};
|
||||
|
||||
ip_addr_port.extend_from_slice(&socket_addr.port().to_be_bytes());
|
||||
|
||||
let (rx_bytes_til_cookie, rx_cookie, rx_mac, rx_sid) = match rx_buf[0].try_into() {
|
||||
Ok(MsgType::InitHello) => {
|
||||
let msg_in = rx_buf.envelope::<InitHello<&[u8]>>()?;
|
||||
(
|
||||
msg_in.until_cookie().to_vec(),
|
||||
msg_in.cookie().to_vec(),
|
||||
msg_in.mac().to_vec(),
|
||||
msg_in.payload().init_hello()?.sidi().to_vec(),
|
||||
)
|
||||
}
|
||||
Ok(MsgType::InitConf) => {
|
||||
let msg_in = rx_buf.envelope::<InitConf<&[u8]>>()?;
|
||||
(
|
||||
msg_in.until_cookie().to_vec(),
|
||||
msg_in.cookie().to_vec(),
|
||||
msg_in.mac().to_vec(),
|
||||
msg_in.payload().init_conf()?.sidi().to_vec(),
|
||||
)
|
||||
}
|
||||
Ok(_) => {
|
||||
bail!("Message did not contain cookie or could not be sent a cookie reply message (responder sent-message or non-handshake)")
|
||||
}
|
||||
Err(_) => {
|
||||
bail!("Message type not supported")
|
||||
}
|
||||
};
|
||||
|
||||
let cookie_tau = lprf::cookie_tau()?
|
||||
.mix(self.cookie_secret.get_or_update_ellapsed(
|
||||
COOKIE_SECRET_EXP,
|
||||
rosenpass_sodium::helpers::randombytes_buf,
|
||||
))?
|
||||
.mix(&ip_addr_port)?
|
||||
.into_value()[..16]
|
||||
.to_vec();
|
||||
|
||||
let expected = lprf::cookie()?
|
||||
.mix(&cookie_tau)?
|
||||
.mix(&rx_bytes_til_cookie)?
|
||||
.into_value()[..16]
|
||||
.to_vec();
|
||||
|
||||
//If valid cookie is found, process message
|
||||
if rosenpass_sodium::helpers::memcmp(&rx_cookie, &expected) {
|
||||
let result = self.handle_msg(rx_buf, tx_buf)?;
|
||||
Ok(result)
|
||||
}
|
||||
//Otherwise send cookie reply
|
||||
else {
|
||||
let mut msg_out = tx_buf.envelope_truncating::<CookieReply<&mut [u8]>>()?;
|
||||
let cookie_key = lprf::cookie_key()?.mix(self.spkm.secret())?.into_value();
|
||||
|
||||
let mut cookie_reply_lens = msg_out.payload_mut().cookie_reply()?;
|
||||
let nonce_val = XAEADNonce::random();
|
||||
|
||||
// Copy sender's session id to cookie reply message
|
||||
{
|
||||
let sid = cookie_reply_lens.sid_mut();
|
||||
sid.copy_from_slice(&rx_sid[..]);
|
||||
}
|
||||
|
||||
// Generate random nonce, copy it to message and nonce_val
|
||||
{
|
||||
let nonce = cookie_reply_lens.nonce_mut();
|
||||
nonce.copy_from_slice(&nonce_val.value);
|
||||
}
|
||||
|
||||
// Encrypt cookie
|
||||
{
|
||||
const COOKIE_LENS_SID_LEN: usize = 4;
|
||||
let cookie_ciphertext =
|
||||
&mut cookie_reply_lens.all_bytes_mut()[COOKIE_LENS_SID_LEN..];
|
||||
xaead::encrypt(
|
||||
cookie_ciphertext,
|
||||
&cookie_key,
|
||||
&nonce_val.value,
|
||||
&rx_mac,
|
||||
&cookie_tau,
|
||||
)?;
|
||||
}
|
||||
|
||||
// length of the response
|
||||
let len = Some(self.seal_and_commit_msg(peer, MsgType::CookieReply, msg_out)?);
|
||||
|
||||
Ok(HandleMsgResult {
|
||||
exchanged_with: None,
|
||||
resp: len,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming message
|
||||
/// This is one of the main entry point for the protocol.
|
||||
///
|
||||
/// # Overview
|
||||
///
|
||||
@@ -837,7 +1055,14 @@ impl CryptoServer {
|
||||
self.handle_resp_conf(msg_in.payload().empty_data()?)?
|
||||
}
|
||||
Ok(MsgType::DataMsg) => bail!("DataMsg handling not implemented!"),
|
||||
Ok(MsgType::CookieReply) => bail!("CookieReply handling not implemented!"),
|
||||
Ok(MsgType::CookieReply) => {
|
||||
let msg_in = rx_buf.envelope::<CookieReply<&[u8]>>()?;
|
||||
ensure!(msg_in.check_seal(self)?, seal_broken);
|
||||
|
||||
let peer = self.handle_cookie_reply(msg_in.payload().cookie_reply()?)?;
|
||||
len = 0;
|
||||
peer
|
||||
}
|
||||
Err(_) => {
|
||||
bail!("CookieReply handling not implemented!")
|
||||
}
|
||||
@@ -850,7 +1075,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.
|
||||
@@ -862,6 +1088,15 @@ impl CryptoServer {
|
||||
) -> Result<usize> {
|
||||
msg.msg_type_mut()[0] = msg_type as u8;
|
||||
msg.seal(peer, self)?;
|
||||
msg.seal_cookie(peer, self)?;
|
||||
|
||||
//Store sent mac value as last sent mac (for cookie reply)
|
||||
let mut mac = [0u8; MAC_SIZE];
|
||||
mac.copy_from_slice(msg.mac());
|
||||
|
||||
peer.get_mut(self)
|
||||
.last_sent_mac
|
||||
.replace(Public { value: mac });
|
||||
Ok(<Envelope<(), M> as LenseView>::LEN)
|
||||
}
|
||||
}
|
||||
@@ -1131,12 +1366,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 = tx_buf.envelope_truncating::<InitHello<&mut [u8]>>()?;
|
||||
envelope.seal_cookie(self.peer(), srv)?;
|
||||
|
||||
Ok(ih_tx_len)
|
||||
}
|
||||
|
||||
pub fn register_retransmission(&self, srv: &mut CryptoServer) -> Result<()> {
|
||||
@@ -1181,6 +1426,16 @@ where
|
||||
.copy_from_slice(mac.into_value()[..16].as_ref());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate and append the cookie value if `cookie_tau` exists (`cookie`)
|
||||
pub fn seal_cookie(&mut self, peer: PeerPtr, srv: &CryptoServer) -> Result<()> {
|
||||
if let Some(cookie_tau) = peer.get(srv).cookie_tau.get(PEER_COOKIE_TAU_EXP) {
|
||||
let cookie = lprf::cookie()?.mix(cookie_tau)?.mix(self.until_cookie())?;
|
||||
self.cookie_mut()
|
||||
.copy_from_slice(cookie.into_value()[..16].as_ref());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Envelope<&[u8], M>
|
||||
@@ -1732,10 +1987,55 @@ impl CryptoServer {
|
||||
|
||||
Ok(hs.peer())
|
||||
}
|
||||
|
||||
pub fn handle_cookie_reply(&mut self, cr: CookieReply<&[u8]>) -> Result<PeerPtr> {
|
||||
let session_id = SessionId::from_slice(cr.sid());
|
||||
|
||||
let peer_ptr: Option<PeerPtr> = self
|
||||
.lookup_session(session_id)
|
||||
.map(|v| PeerPtr(v.0))
|
||||
.or_else(|| self.lookup_handshake(session_id).map(|v| PeerPtr(v.0)));
|
||||
if let Some(peer) = peer_ptr {
|
||||
if let Some(mac) = peer.get(self).last_sent_mac {
|
||||
let mut cookie_value = [0u8; COOKIE_SECRET_LEN];
|
||||
let spkt = peer.get(self).spkt.secret();
|
||||
let cookie_key = lprf::cookie_key()?.mix(spkt)?.into_value();
|
||||
|
||||
xaead::decrypt(
|
||||
&mut cookie_value,
|
||||
&cookie_key,
|
||||
&mac.value,
|
||||
&cr.all_bytes()[4..],
|
||||
)?;
|
||||
|
||||
peer.get_mut(self).cookie_tau = CookieSecret::Some {
|
||||
value: cookie_value,
|
||||
last_updated: Timebase::default(),
|
||||
};
|
||||
|
||||
Ok(peer)
|
||||
} else {
|
||||
bail!(
|
||||
"No last sent message for peer {pidr:?} to decrypt cookie reply.",
|
||||
pidr = cr.sid()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let sids: Vec<_> = self
|
||||
.peers
|
||||
.iter()
|
||||
.map(|p| p.session.as_ref().map(|s| s.sidm))
|
||||
.collect();
|
||||
println!("SID: {:?}", sids);
|
||||
bail!("No such peer {pidr:?}.", pidr = cr.sid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{net::SocketAddrV4, thread::sleep, time::Duration};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -1828,4 +2128,129 @@ 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();
|
||||
|
||||
//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,
|
||||
PeerPtr(0),
|
||||
SocketAddr::V4(ip_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!(a.peers[0].cookie_tau.is_some());
|
||||
|
||||
let expected_cookie_tau = lprf::cookie_tau()
|
||||
.unwrap()
|
||||
.mix(b.cookie_secret.get(COOKIE_SECRET_EXP).unwrap())
|
||||
.unwrap()
|
||||
.mix(&ip_addr_port_a)
|
||||
.unwrap()
|
||||
.into_value()[..16]
|
||||
.to_vec();
|
||||
assert_eq!(
|
||||
a.peers[0].cookie_tau.get(PEER_COOKIE_TAU_EXP),
|
||||
Some(&expected_cookie_tau[..])
|
||||
);
|
||||
|
||||
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,
|
||||
PeerPtr(0),
|
||||
SocketAddr::V4(ip_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 sids: Vec<_> = b
|
||||
.peers
|
||||
.iter()
|
||||
.map(|p| p.session.as_ref().map(|s| s.sidt))
|
||||
.collect();
|
||||
println!("Peer SIDs: {:?}", sids);
|
||||
|
||||
//A handles RespHello message under load, should send cookie reply
|
||||
assert!(a
|
||||
.handle_msg_under_load(
|
||||
&b_to_a_buf[..resp_hello_len],
|
||||
&mut *a_to_b_buf,
|
||||
PeerPtr(0),
|
||||
SocketAddr::V4(ip_b),
|
||||
)
|
||||
.is_err());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user