refactor(client): make authentication explicit and redact HMAC keys
This commit is contained in:
16 files changed
+184
-119
No files matched your search
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user