refactor(client): make authentication explicit and redact HMAC keys

This commit is contained in:
Nils Andreas Svee committed 2026-10-04 16:58:44 +07:00
1 parent 9c703ecff2
commit 156db4c720
16 files changed
+184 -119

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
use std::{fmt, sync::Arc};
/// Shared HMAC key bytes. Debug output never includes the bytes or a fingerprint.
///
/// An empty key is still an explicitly configured HMAC key.
#[derive(Clone, PartialEq, Eq)]
pub struct HmacKey(Arc<[u8]>);
impl HmacKey {
/// Store key bytes without interpreting textual key syntax.
pub fn new(bytes: impl Into<Arc<[u8]>>) -> Self {
Self(bytes.into())
}
/// Explicitly borrow the key material for authentication.
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl fmt::Debug for HmacKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("HmacKey([REDACTED])")
}
}
/// Concrete authentication for a low-level client session.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Authentication {
/// Send and accept packets without HMAC authentication.
#[default]
Unauthenticated,
/// Authenticate packets with this key, including when it is empty.
Hmac(HmacKey),
}
impl Authentication {
pub(crate) fn hmac_key(&self) -> Option<&[u8]> {
match self {
Self::Unauthenticated => None,
Self::Hmac(key) => Some(key.as_bytes()),
}
}
}
+6 -29
View File
@@ -1,5 +1,7 @@
use std::{net::SocketAddr, time::Duration};
use crate::Authentication;
/// Protocol compatibility bound for a requested `server_fill` value, in UTF-8
/// bytes.
///
@@ -118,11 +120,10 @@ pub struct ClientConfig {
/// This is the six-bit DSCP value, not the full traffic-class byte, and
/// must be less than or equal to [`MAX_DSCP_CODEPOINT`].
pub dscp: u8,
/// Optional HMAC key used to authenticate IRTT packets.
/// Concrete authentication for open, echo, and close packets.
///
/// When present, open, echo, and close packets are encoded and decoded with
/// HMAC authentication. The peer must be configured with the same key.
pub hmac_key: Option<Vec<u8>>,
/// HMAC peers must use the same key. An empty HMAC key remains authenticated.
pub auth: Authentication,
/// Optional server payload fill request.
///
/// `None` leaves server fill behavior unspecified. `Some(value)` requests
@@ -155,30 +156,6 @@ pub struct ClientConfig {
pub max_pending_probes: usize,
}
/// Authentication settings used by client sessions.
///
/// `ClientConfig` keeps its existing top-level HMAC field for source
/// compatibility. Managed multi-target callers can use this smaller type for
/// per-target auth overrides without carrying a full independent
/// [`ClientConfig`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ClientAuthConfig {
/// Optional HMAC key used to authenticate IRTT packets.
pub hmac_key: Option<Vec<u8>>,
}
impl From<Option<Vec<u8>>> for ClientAuthConfig {
fn from(hmac_key: Option<Vec<u8>>) -> Self {
Self { hmac_key }
}
}
impl From<ClientAuthConfig> for Option<Vec<u8>> {
fn from(auth: ClientAuthConfig) -> Self {
auth.hmac_key
}
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
@@ -190,7 +167,7 @@ impl Default for ClientConfig {
stamp_at: StampAt::Both,
clock: Clock::Both,
dscp: 0,
hmac_key: None,
auth: Authentication::Unauthenticated,
server_fill: None,
open_timeouts: DEFAULT_OPEN_TIMEOUTS.to_vec(),
run_mode: RunMode::Normal,
+4 -2
View File
@@ -34,6 +34,7 @@
#[cfg(feature = "tokio")]
mod async_client;
mod auth;
mod client;
mod config;
mod error;
@@ -49,10 +50,11 @@ mod timing;
#[cfg(feature = "tokio")]
pub use async_client::AsyncClient;
pub use auth::{Authentication, HmacKey};
pub use client::Client;
pub use config::{
ClientAuthConfig, ClientConfig, NegotiationPolicy, RecvBudget, RunMode, SocketConfig,
MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH,
ClientConfig, NegotiationPolicy, RecvBudget, RunMode, SocketConfig, MAX_DSCP_CODEPOINT,
MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH,
};
pub use error::ClientError;
pub use event::{
+26 -4
View File
@@ -2,7 +2,7 @@ use std::{fmt, net::SocketAddr, sync::Arc, time::Duration};
use thiserror::Error;
use crate::{ClientAuthConfig, ClientConfig, ClientError, ClientEvent};
use crate::{Authentication, ClientConfig, ClientError, ClientEvent};
use super::TargetId;
@@ -26,12 +26,34 @@ pub struct TargetInstance {
pub generation: u64,
}
/// Endpoint and optional authentication override for one managed target.
/// Authentication specification for a managed target.
///
/// Equality preserves the specification: inheritance and an explicit override
/// remain distinct even when they currently resolve to the same authentication.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum TargetAuth {
/// Use the shared client configuration's authentication.
#[default]
Inherit,
/// Replace shared authentication. `Unauthenticated` explicitly disables it.
Override(Authentication),
}
impl TargetAuth {
pub(crate) fn resolve(&self, shared: &Authentication) -> Authentication {
match self {
Self::Inherit => shared.clone(),
Self::Override(auth) => auth.clone(),
}
}
}
/// Endpoint and authentication specification for one managed target.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ManagedTargetConfig {
pub id: TargetId,
pub server_addr: String,
pub auth: Option<ClientAuthConfig>,
pub auth: TargetAuth,
}
impl ManagedTargetConfig {
@@ -39,7 +61,7 @@ impl ManagedTargetConfig {
Self {
id: id.into(),
server_addr: server_addr.into(),
auth: None,
auth: TargetAuth::Inherit,
}
}
}
+2 -6
View File
@@ -945,9 +945,7 @@ impl ManagedClientTask {
}
let mut client_config = self.config.client.clone();
client_config.server_addr.clone_from(&target.server_addr);
if let Some(auth) = &target.auth {
client_config.hmac_key.clone_from(&auth.hmac_key);
}
client_config.auth = target.auth.resolve(&self.config.client.auth);
validate_target_config(&client_config).map_err(|source| {
ManagedCommandApplyError::InvalidTarget {
id: target.id.clone(),
@@ -2251,9 +2249,7 @@ fn build_task(
.ok_or(ManagedConfigError::GenerationExhausted)?;
let mut client_config = config.client.clone();
client_config.server_addr.clone_from(&target.server_addr);
if let Some(auth) = &target.auth {
client_config.hmac_key.clone_from(&auth.hmac_key);
}
client_config.auth = target.auth.resolve(&config.client.auth);
validate_target_config(&client_config).map_err(|source| {
ManagedConfigError::InvalidTarget {
id: target.id.clone(),
+6 -6
View File
@@ -201,7 +201,7 @@ impl SessionMachine {
}
pub(crate) fn has_hmac(&self) -> bool {
self.config.hmac_key.is_some()
self.config.auth.hmac_key().is_some()
}
pub(crate) fn prepare_open_request(&self) -> Result<PreparedOpenRequest, ClientError> {
@@ -211,7 +211,7 @@ impl SessionMachine {
params: &self.requested,
no_test: self.config.run_mode == RunMode::NoTest,
},
self.config.hmac_key.as_deref(),
self.config.auth.hmac_key(),
)?;
Ok(PreparedOpenRequest {
bytes: bytes.into_boxed_slice(),
@@ -223,7 +223,7 @@ impl SessionMachine {
packet: &[u8],
) -> Result<OpenDatagramDisposition, ClientError> {
self.ensure_connected()?;
match irtt_proto::decode_open_reply(packet, self.config.hmac_key.as_deref()) {
match irtt_proto::decode_open_reply(packet, self.config.auth.hmac_key()) {
Ok(reply) => Ok(OpenDatagramDisposition::Trusted(reply)),
Err(error @ irtt_proto::ProtoError::ZeroToken) => Err(ClientError::Protocol(error)),
Err(
@@ -250,7 +250,7 @@ impl SessionMachine {
let cleanup_close = if !reply_is_close && reply.token != 0 {
let bytes = encode_request(
RequestToEncode::Close { token: reply.token },
self.config.hmac_key.as_deref(),
self.config.auth.hmac_key(),
)
.map_err(ClientError::from)
.map_err(OpenAcceptanceFailure::without_cleanup)?;
@@ -313,7 +313,7 @@ impl SessionMachine {
params: &session.negotiated.params,
payload: &[],
},
self.config.hmac_key.as_deref(),
self.config.auth.hmac_key(),
)?;
Ok(PreparedProbe {
bytes: bytes.into_boxed_slice(),
@@ -679,7 +679,7 @@ impl SessionMachine {
decode_echo_reply(
packet,
&session.negotiated.params,
self.config.hmac_key.as_deref(),
self.config.auth.hmac_key(),
)
.ok()
}