Files
rosenpass/rosenpass/tests/api-integration-tests-api-setup.rs
Karolin Varner b1a7d94295 feat: Support for custom osk (output key) domain separators in Rosenpass app
This allows for custom protocol extensions with custom domain
separators to be used without modifying the Rosenpass source code
2025-06-25 19:48:29 +02:00

354 lines
12 KiB
Rust

use std::{
borrow::Borrow,
io::{BufRead, BufReader, Write},
os::unix::net::UnixStream,
process::Stdio,
thread::sleep,
time::Duration,
};
use anyhow::{bail, Context};
use command_fds::{CommandFdExt, FdMapping};
use hex_literal::hex;
use rosenpass::api::{
self, add_listen_socket_response_status, add_psk_broker_response_status,
supply_keypair_response_status,
};
use rosenpass::config::ProtocolVersion;
use rosenpass::protocol::basic_types::SymKey;
use rosenpass_util::{
b64::B64Display,
file::LoadValueB64,
io::IoErrorKind,
length_prefix_encoding::{decoder::LengthPrefixDecoder, encoder::LengthPrefixEncoder},
mem::{DiscardResultExt, MoveExt},
mio::WriteWithFileDescriptors,
zerocopy::ZerocopySliceExt,
};
use std::os::fd::{AsFd, AsRawFd};
use tempfile::TempDir;
use zerocopy::AsBytes;
struct KillChild(std::process::Child);
impl Drop for KillChild {
fn drop(&mut self) {
use rustix::process::{kill_process, Pid, Signal::Term};
let pid = Pid::from_child(&self.0);
// We seriously need to start handling signals with signalfd, our current signal handling
// system is a bit broken; there is probably a few functions that just restart on EINTR
// so the signal is absorbed
loop {
kill_process(pid, Term).discard_result();
if self.0.try_wait().unwrap().is_some() {
break;
}
}
}
}
#[test]
fn api_integration_api_setup_v02() -> anyhow::Result<()> {
api_integration_api_setup(ProtocolVersion::V02)
}
#[test]
fn api_integration_api_setup_v03() -> anyhow::Result<()> {
api_integration_api_setup(ProtocolVersion::V03)
}
fn api_integration_api_setup(protocol_version: ProtocolVersion) -> anyhow::Result<()> {
rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets();
let dir = TempDir::with_prefix("rosenpass-api-integration-test")?;
macro_rules! tempfile {
($($lst:expr),+) => {{
let mut buf = dir.path().to_path_buf();
$(buf.push($lst);)*
buf
}}
}
let peer_a_endpoint = "[::1]:0";
let peer_a_listen = std::net::UdpSocket::bind(peer_a_endpoint)?;
let peer_a_endpoint = format!("{}", peer_a_listen.local_addr()?);
let peer_a_keypair = config::Keypair::new(tempfile!("a.pk"), tempfile!("a.sk"));
let peer_b_osk = tempfile!("b.osk");
let peer_b_wg_device = "mock_device";
let peer_b_wg_peer_id = hex!(
"
93 0f ee 77 0c 6b 54 7e 13 5f 13 92 21 97 26 53
7d 77 4a 6a 0f 6c eb 1a dd 6e 5b c4 1b 92 cd 99
"
);
use rosenpass::config;
let peer_a = config::Rosenpass {
config_file_path: tempfile!("a.config"),
keypair: None,
listen: vec![], // TODO: This could collide by accident
verbosity: config::Verbosity::Verbose,
api: api::config::ApiConfig {
listen_path: vec![tempfile!("a.sock")],
listen_fd: vec![],
stream_fd: vec![],
},
peers: vec![config::RosenpassPeer {
public_key: tempfile!("b.pk"),
key_out: None,
endpoint: None,
pre_shared_key: None,
wg: Some(config::WireGuard {
device: peer_b_wg_device.to_string(),
peer: format!("{}", peer_b_wg_peer_id.fmt_b64::<8129>()),
extra_params: vec![],
}),
protocol_version: protocol_version.clone(),
osk_domain_separator: Default::default(),
}],
};
let peer_b_keypair = config::Keypair::new(tempfile!("b.pk"), tempfile!("b.sk"));
let peer_b = config::Rosenpass {
config_file_path: tempfile!("b.config"),
keypair: Some(peer_b_keypair.clone()),
listen: vec![],
verbosity: config::Verbosity::Verbose,
api: api::config::ApiConfig {
listen_path: vec![tempfile!("b.sock")],
listen_fd: vec![],
stream_fd: vec![],
},
peers: vec![config::RosenpassPeer {
public_key: tempfile!("a.pk"),
key_out: Some(peer_b_osk.clone()),
endpoint: Some(peer_a_endpoint.to_owned()),
pre_shared_key: None,
wg: None,
protocol_version: protocol_version.clone(),
osk_domain_separator: Default::default(),
}],
};
// Generate the keys
rosenpass::cli::testing::generate_and_save_keypair(
peer_a_keypair.secret_key.clone(),
peer_a_keypair.public_key.clone(),
)?;
rosenpass::cli::testing::generate_and_save_keypair(
peer_b_keypair.secret_key.clone(),
peer_b_keypair.public_key.clone(),
)?;
// Write the configuration files
peer_a.commit()?;
peer_b.commit()?;
let (deliberate_fail_api_client, deliberate_fail_api_server) =
std::os::unix::net::UnixStream::pair()?;
let deliberate_fail_child_fd = 3;
// Start peer a
let _proc_a = KillChild(
std::process::Command::new(env!("CARGO_BIN_EXE_rosenpass"))
.args(["--api-stream-fd", &deliberate_fail_child_fd.to_string()])
.fd_mappings(vec![FdMapping {
parent_fd: deliberate_fail_api_server.move_here().as_raw_fd(),
child_fd: 3,
}])?
.args([
"exchange-config",
peer_a.config_file_path.to_str().context("")?,
])
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()?,
);
// Start peer b
let mut proc_b = KillChild(
std::process::Command::new(env!("CARGO_BIN_EXE_rosenpass"))
.args([
"exchange-config",
peer_b.config_file_path.to_str().context("")?,
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.spawn()?,
);
// Acquire stdout
let mut out_b = BufReader::new(proc_b.0.stdout.take().context("")?).lines();
// Now connect to the peers
let api_path = peer_a.api.listen_path[0].as_path();
// Wait for the socket to be created
let attempt = 0;
while !api_path.exists() {
sleep(Duration::from_millis(200));
assert!(
attempt < 50,
"Api failed to be created even after 50 seconds"
);
}
let api = UnixStream::connect(api_path)?;
let (psk_broker_sock, psk_broker_server_sock) = UnixStream::pair()?;
// Send AddListenSocket request
{
let fd = peer_a_listen.as_fd();
let mut fds = vec![&fd].into();
let mut api = WriteWithFileDescriptors::<UnixStream, _, _, _>::new(&api, &mut fds);
LengthPrefixEncoder::from_message(api::AddListenSocketRequest::new().as_bytes())
.write_all_to_stdio(&mut api)?;
assert!(fds.is_empty(), "Failed to write all file descriptors");
std::mem::forget(peer_a_listen);
}
// Read response
{
let mut decoder = LengthPrefixDecoder::new([0u8; api::MAX_RESPONSE_LEN]);
let res = decoder.read_all_from_stdio(&api)?;
let res = res.zk_parse::<api::AddListenSocketResponse>()?;
assert_eq!(
*res,
api::AddListenSocketResponse::new(add_listen_socket_response_status::OK)
);
}
// Deliberately break API connection given via FD; this checks that the
// API connections are closed when invalid data is received and it also
// implicitly checks that other connections are unaffected
{
use std::io::ErrorKind as K;
let client = deliberate_fail_api_client;
let err = loop {
if let Err(e) = client.borrow().write(&[0xffu8; 16]) {
break e;
}
};
// NotConnected happens on Mac
assert!(matches!(
err.io_error_kind(),
K::ConnectionReset | K::BrokenPipe | K::NotConnected
));
}
// Send SupplyKeypairRequest
{
use rustix::fs::{open, Mode, OFlags};
let sk = open(peer_a_keypair.secret_key, OFlags::RDONLY, Mode::empty())?;
let pk = open(peer_a_keypair.public_key, OFlags::RDONLY, Mode::empty())?;
let mut fds = vec![&sk, &pk].into();
let mut api = WriteWithFileDescriptors::<UnixStream, _, _, _>::new(&api, &mut fds);
LengthPrefixEncoder::from_message(api::SupplyKeypairRequest::new().as_bytes())
.write_all_to_stdio(&mut api)?;
assert!(fds.is_empty(), "Failed to write all file descriptors");
}
// Read response
{
let mut decoder = LengthPrefixDecoder::new([0u8; api::MAX_RESPONSE_LEN]);
let res = decoder.read_all_from_stdio(&api)?;
let res = res.zk_parse::<api::SupplyKeypairResponse>()?;
assert_eq!(
*res,
api::SupplyKeypairResponse::new(supply_keypair_response_status::OK)
);
}
// Send AddPskBroker request
{
let mut fds = vec![psk_broker_server_sock.as_fd()].into();
let mut api = WriteWithFileDescriptors::<UnixStream, _, _, _>::new(&api, &mut fds);
LengthPrefixEncoder::from_message(api::AddPskBrokerRequest::new().as_bytes())
.write_all_to_stdio(&mut api)?;
assert!(fds.is_empty(), "Failed to write all file descriptors");
}
// Read response
{
let mut decoder = LengthPrefixDecoder::new([0u8; api::MAX_RESPONSE_LEN]);
let res = decoder.read_all_from_stdio(&api)?;
let res = res.zk_parse::<api::AddPskBrokerResponse>()?;
assert_eq!(
*res,
api::AddPskBrokerResponse::new(add_psk_broker_response_status::OK)
);
}
// Wait for the keys to successfully exchange a key
let mut attempt = 0;
loop {
// Read OSK generated by A
let osk_a = {
use rosenpass_wireguard_broker::api::msgs as M;
type SetPskReqPkg = M::Envelope<M::SetPskRequest>;
type SetPskResPkg = M::Envelope<M::SetPskResponse>;
// Receive request
let mut decoder = LengthPrefixDecoder::new([0u8; M::REQUEST_MSG_BUFFER_SIZE]);
let req = decoder.read_all_from_stdio(&psk_broker_sock)?;
let req = req.zk_parse::<SetPskReqPkg>()?;
assert_eq!(req.msg_type, M::MsgType::SetPsk as u8);
assert_eq!(req.payload.peer_id, peer_b_wg_peer_id);
assert_eq!(req.payload.iface()?, peer_b_wg_device);
// Send response
let res = SetPskResPkg {
msg_type: M::MsgType::SetPsk as u8,
reserved: [0u8; 3],
payload: M::SetPskResponse {
return_code: M::SetPskResponseReturnCode::Success as u8,
},
};
LengthPrefixEncoder::from_message(res.as_bytes())
.write_all_to_stdio(&psk_broker_sock)?;
SymKey::from_slice(&req.payload.psk)
};
// Read OSK generated by B
let osk_b = {
let line = out_b.next().context("")??;
let words = line.split(' ').collect::<Vec<_>>();
// FIXED FIXED PEER-ID FIXED FILENAME STATUS
// output-key peer KZqXTZ4l2aNnkJtLPhs4D8JxHTGmRSL9w3Qr+X8JxFk= key-file "client-A-osk" exchanged
let peer_id = words
.get(2)
.with_context(|| format!("Bad rosenpass output: `{line}`"))?;
assert_eq!(
line,
format!(
"output-key peer {peer_id} key-file \"{}\" exchanged",
peer_b_osk.to_str().context("")?
)
);
SymKey::load_b64::<64, _>(peer_b_osk.clone())?
};
// TODO: This may be flaky. Both rosenpass instances are not guaranteed to produce
// the same number of output events; they merely guarantee eventual consistency of OSK.
// Correctly, we should use tokio to read any number of generated OSKs and indicate
// success on consensus
match osk_a.secret() == osk_b.secret() {
true => break,
false if attempt > 10 => bail!("Peers did not produce a matching key even after ten attempts. Something is wrong with the key exchange!"),
false => {},
};
attempt += 1;
}
Ok(())
}