refactor(client): make authentication explicit and redact HMAC keys
This commit is contained in:
18 files changed
+492
-119
No files matched your search
@@ -0,0 +1,96 @@
|
||||
use std::{net::UdpSocket, thread, time::Duration};
|
||||
|
||||
use irtt_client::{Authentication, Client, ClientConfig, ClientEvent, HmacKey, SocketConfig};
|
||||
use irtt_proto::{decode_request, verify_packet_hmac};
|
||||
use irtt_server::{ServerConfig, ServerCore};
|
||||
|
||||
// Exercise the runtime-free API against the production server core, including
|
||||
// the wire HMAC flag on Open, Echo and Close for an explicitly empty key.
|
||||
#[test]
|
||||
fn concrete_authentication_preserves_session_and_empty_key_behavior() {
|
||||
for key in [None, Some(b"session-test-key".as_slice()), Some(&[])] {
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.unwrap();
|
||||
let addr = socket.local_addr().unwrap();
|
||||
let server = thread::spawn(move || {
|
||||
let config = key.map_or_else(ServerConfig::default, |key| {
|
||||
ServerConfig::default().with_hmac_key(key)
|
||||
});
|
||||
let mut core = ServerCore::new(config);
|
||||
let mut buffer = [0; 2048];
|
||||
for _ in 0..3 {
|
||||
let (len, peer) = socket.recv_from(&mut buffer).unwrap();
|
||||
let packet = &buffer[..len];
|
||||
assert_eq!(decode_request(packet).unwrap().hmac_present, key.is_some());
|
||||
if let Some(key) = key {
|
||||
verify_packet_hmac(key, packet).unwrap();
|
||||
}
|
||||
if let Some(reply) = core.handle_datagram(peer, packet).unwrap() {
|
||||
socket.send_to(reply.bytes(), peer).unwrap();
|
||||
}
|
||||
}
|
||||
assert_eq!(core.session_count(), 0, "Close must release the session");
|
||||
});
|
||||
let mut client = Client::connect(ClientConfig {
|
||||
server_addr: addr.to_string(),
|
||||
auth: key.map_or(Authentication::Unauthenticated, |key| {
|
||||
Authentication::Hmac(HmacKey::new(key))
|
||||
}),
|
||||
open_timeouts: vec![Duration::from_millis(200)],
|
||||
socket_config: SocketConfig {
|
||||
recv_timeout: Some(Duration::from_secs(1)),
|
||||
..SocketConfig::default()
|
||||
},
|
||||
..ClientConfig::default()
|
||||
})
|
||||
.unwrap();
|
||||
client.open().unwrap();
|
||||
client.send_probe().unwrap();
|
||||
assert!(client
|
||||
.recv_once()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|event| { matches!(event, ClientEvent::EchoReply { .. }) }));
|
||||
client.close().unwrap();
|
||||
server.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_debug_redacts_keys_in_enclosing_configurations() {
|
||||
let sentinel = b"CONSPICUOUS_HMAC_SECRET_DO_NOT_PRINT";
|
||||
let key = HmacKey::new(sentinel.as_slice());
|
||||
let auth = Authentication::Hmac(key.clone());
|
||||
let config = ClientConfig {
|
||||
auth: auth.clone(),
|
||||
..ClientConfig::default()
|
||||
};
|
||||
let mut outputs = vec![
|
||||
format!("{key:?}"),
|
||||
format!("{auth:?}"),
|
||||
format!("{config:?}"),
|
||||
];
|
||||
#[cfg(feature = "tokio")]
|
||||
{
|
||||
use irtt_client::managed::{ManagedClientConfig, ManagedTargetConfig, TargetAuth};
|
||||
let mut target = ManagedTargetConfig::new("one", "127.0.0.1:2112");
|
||||
target.auth = TargetAuth::Override(auth);
|
||||
outputs.push(format!("{target:?}"));
|
||||
outputs.push(format!(
|
||||
"{:?}",
|
||||
ManagedClientConfig {
|
||||
client: config,
|
||||
..ManagedClientConfig::default()
|
||||
}
|
||||
));
|
||||
}
|
||||
// Check both textual and ordinary byte-slice Debug encodings so replacing
|
||||
// the opaque formatter with a derived formatter cannot slip through.
|
||||
for output in outputs.drain(..) {
|
||||
assert!(!output.contains(std::str::from_utf8(sentinel).unwrap()));
|
||||
assert!(!output.contains(&format!("{sentinel:?}")));
|
||||
assert!(output.contains("[REDACTED]"));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use super::*;
|
||||
use irtt_client::{Authentication, HmacKey};
|
||||
|
||||
#[test]
|
||||
fn hmac_open_ignores_missing_hmac_before_valid_reply() {
|
||||
let key = b"secret".to_vec();
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
config.hmac_key = Some(key.clone());
|
||||
config.auth = Authentication::Hmac(HmacKey::new(key.clone()));
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
@@ -26,7 +27,7 @@ fn hmac_open_ignores_bad_hmac_before_valid_reply() {
|
||||
let key = b"secret".to_vec();
|
||||
let wrong_key = b"wrong".to_vec();
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
config.hmac_key = Some(key.clone());
|
||||
config.auth = Authentication::Hmac(HmacKey::new(key.clone()));
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
@@ -47,7 +48,7 @@ fn hmac_open_ignores_bad_hmac_before_valid_reply() {
|
||||
fn post_token_hmac_negotiation_failure_sends_authenticated_cleanup_close() {
|
||||
let key = b"secret".to_vec();
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
config.hmac_key = Some(key.clone());
|
||||
config.auth = Authentication::Hmac(HmacKey::new(key.clone()));
|
||||
let mut returned = default_params();
|
||||
returned.interval_ns += 1;
|
||||
let server_key = key.clone();
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
#![cfg(feature = "tokio")]
|
||||
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
use irtt_client::{managed::*, Authentication, ClientConfig, ClientEvent, HmacKey};
|
||||
use irtt_server::{Server, ServerConfig};
|
||||
|
||||
fn config(auth: Authentication) -> ManagedClientConfig {
|
||||
ManagedClientConfig {
|
||||
client: ClientConfig {
|
||||
auth,
|
||||
duration: None,
|
||||
interval: Duration::from_millis(20),
|
||||
probe_timeout: Duration::from_millis(100),
|
||||
open_timeouts: vec![Duration::from_millis(200)],
|
||||
..ClientConfig::default()
|
||||
},
|
||||
completion: ManagedCompletionPolicy::ExplicitStop,
|
||||
final_drain: Duration::ZERO,
|
||||
..ManagedClientConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime() -> tokio::runtime::Runtime {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_authentication_inherits_disables_and_overrides_on_creation_and_update() {
|
||||
runtime().block_on(async {
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
let shared = Authentication::Hmac(HmacKey::new(b"shared-test-key".as_slice()));
|
||||
let replacement =
|
||||
Authentication::Hmac(HmacKey::new(b"replacement-test-key".as_slice()));
|
||||
let empty = Authentication::Hmac(HmacKey::new(Vec::new()));
|
||||
let mut targets = Vec::new();
|
||||
let mut servers = Vec::new();
|
||||
for (id, key, auth) in [
|
||||
(
|
||||
"inherit",
|
||||
Some(b"shared-test-key".as_slice()),
|
||||
TargetAuth::Inherit,
|
||||
),
|
||||
(
|
||||
"disable",
|
||||
None,
|
||||
TargetAuth::Override(Authentication::Unauthenticated),
|
||||
),
|
||||
(
|
||||
"replace",
|
||||
Some(b"replacement-test-key".as_slice()),
|
||||
TargetAuth::Override(replacement),
|
||||
),
|
||||
("empty", Some(&[]), TargetAuth::Override(empty)),
|
||||
] {
|
||||
let server_config = key.map_or_else(ServerConfig::default, |key| {
|
||||
ServerConfig::default().with_hmac_key(key)
|
||||
});
|
||||
let mut server = Server::bind("127.0.0.1:0".parse().unwrap(), server_config)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut target =
|
||||
ManagedTargetConfig::new(id, server.local_addr().unwrap().to_string());
|
||||
target.auth = auth;
|
||||
targets.push(target);
|
||||
servers.push(tokio::spawn(async move {
|
||||
server.run(std::future::pending()).await
|
||||
}));
|
||||
}
|
||||
for initial in [true, false] {
|
||||
let (task, handle) = ManagedClient::task(
|
||||
config(shared.clone()),
|
||||
if initial { targets.clone() } else { vec![] },
|
||||
)
|
||||
.unwrap();
|
||||
let mut events = handle.subscribe().unwrap();
|
||||
let run = tokio::spawn(task);
|
||||
if !initial {
|
||||
handle
|
||||
.update_targets(targets.clone())
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let mut replied = HashSet::new();
|
||||
while replied.len() < targets.len() {
|
||||
if let ManagedEvent::Client {
|
||||
target,
|
||||
event: ClientEvent::EchoReply { .. },
|
||||
} = events.recv().await.unwrap()
|
||||
{
|
||||
replied.insert(target.id);
|
||||
}
|
||||
}
|
||||
handle.stop().await;
|
||||
let outcome = run.await.unwrap();
|
||||
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
|
||||
assert_eq!(outcome.successful_target_outcomes, targets.len() as u64);
|
||||
assert!(outcome.recent_target_outcomes.iter().all(|target| {
|
||||
target.replies_received > 0 && target.cleanup_failure.is_none()
|
||||
}));
|
||||
}
|
||||
for server in servers {
|
||||
server.abort();
|
||||
assert!(server.await.unwrap_err().is_cancelled());
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("every managed authentication mode must receive replies");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_auth_specification_replaces_generation_even_when_resolved_auth_is_equal() {
|
||||
runtime().block_on(async {
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
for auth in [
|
||||
Authentication::Unauthenticated,
|
||||
Authentication::Hmac(HmacKey::new(b"shared-test-key".as_slice())),
|
||||
] {
|
||||
let server_config = match &auth {
|
||||
Authentication::Unauthenticated => ServerConfig::default(),
|
||||
Authentication::Hmac(key) => {
|
||||
ServerConfig::default().with_hmac_key(key.as_bytes())
|
||||
}
|
||||
};
|
||||
let mut server = Server::bind("127.0.0.1:0".parse().unwrap(), server_config)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut configured =
|
||||
ManagedTargetConfig::new("one", server.local_addr().unwrap().to_string());
|
||||
let service = tokio::spawn(async move { server.run(std::future::pending()).await });
|
||||
let (task, handle) =
|
||||
ManagedClient::task(config(auth.clone()), vec![configured.clone()]).unwrap();
|
||||
let mut events = handle.subscribe().unwrap();
|
||||
let run = tokio::spawn(task);
|
||||
let mut current = handle.status().targets[0].target.clone();
|
||||
// Wait for a real reply before changing the running session.
|
||||
loop {
|
||||
if matches!(events.recv().await.unwrap(), ManagedEvent::Client {
|
||||
target, event: ClientEvent::EchoReply { .. }
|
||||
} if target == current)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
for specification in [TargetAuth::Override(auth.clone()), TargetAuth::Inherit] {
|
||||
configured.auth = specification;
|
||||
let applied = handle
|
||||
.update_targets(vec![configured.clone()])
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
let next = applied
|
||||
.status
|
||||
.targets
|
||||
.iter()
|
||||
.find(|target| target.desired)
|
||||
.unwrap()
|
||||
.target
|
||||
.clone();
|
||||
assert!(next.generation > current.generation);
|
||||
let mut retired = false;
|
||||
let mut replied = false;
|
||||
while !retired || !replied {
|
||||
match events.recv().await.unwrap() {
|
||||
ManagedEvent::TargetFinished { outcome }
|
||||
if outcome.target == current =>
|
||||
{
|
||||
assert_eq!(outcome.end_reason, ManagedTargetEndReason::Replaced);
|
||||
assert!(outcome.cleanup_failure.is_none());
|
||||
retired = true;
|
||||
}
|
||||
ManagedEvent::Client {
|
||||
target,
|
||||
event: ClientEvent::EchoReply { .. },
|
||||
} if target == next => {
|
||||
replied = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let identical = handle
|
||||
.update_targets(vec![configured.clone()])
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
identical
|
||||
.status
|
||||
.targets
|
||||
.iter()
|
||||
.find(|target| target.desired)
|
||||
.unwrap()
|
||||
.target,
|
||||
next
|
||||
);
|
||||
current = next;
|
||||
}
|
||||
handle.stop().await;
|
||||
assert_eq!(run.await.unwrap().successful_target_outcomes, 3);
|
||||
service.abort();
|
||||
assert!(service.await.unwrap_err().is_cancelled());
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("auth specification changes must replace and reopen target generations");
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user