mirror of
https://github.com/orbien-org/orbien.git
synced 2026-09-21 16:01:31 +00:00
fix: improve login authentication against digest replay and stale timestamps (#24)
Add a ±180s timestamp window and in-process digest replay cache; warn when auth.token is empty
This commit is contained in:
+3
@@ -58,6 +58,9 @@ public final class OrbienClient implements AutoCloseable {
|
||||
started.set(false);
|
||||
throw new IllegalStateException("tcpMux is not supported; set tcpMux=false on client and server");
|
||||
}
|
||||
if (config.getToken() == null || config.getToken().isEmpty()) {
|
||||
log.warn("auth.token is empty; authentication is disabled");
|
||||
}
|
||||
|
||||
group = new NioEventLoopGroup();
|
||||
CompletableFuture<String> loginFuture = new CompletableFuture<>();
|
||||
|
||||
@@ -32,6 +32,9 @@ pub struct Service {
|
||||
|
||||
impl Service {
|
||||
pub fn new(cfg: ClientConfig) -> Self {
|
||||
if cfg.auth.token.is_empty() {
|
||||
tracing::warn!("auth.token is empty; authentication is disabled");
|
||||
}
|
||||
Self { cfg }
|
||||
}
|
||||
|
||||
|
||||
+38
-1
@@ -1,3 +1,40 @@
|
||||
mod replay;
|
||||
mod token;
|
||||
|
||||
pub use token::{compute_auth_digest, verify_auth_digest, verify_login};
|
||||
pub use replay::ReplayCache;
|
||||
pub use token::{compute_auth_digest, unix_now_secs, verify_auth_digest, verify_login};
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub const AUTH_SKEW_SECS: i64 = 180;
|
||||
|
||||
pub const REPLAY_TTL_SECS: u64 = (AUTH_SKEW_SECS as u64) * 2;
|
||||
|
||||
pub const REPLAY_MAX_ENTRIES: usize = 100_000;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AuthFailure {
|
||||
EmptyDigest,
|
||||
InvalidDigest,
|
||||
TimestampSkew,
|
||||
Replay,
|
||||
Capacity,
|
||||
}
|
||||
|
||||
impl AuthFailure {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::EmptyDigest => "empty authentication digest",
|
||||
Self::InvalidDigest => "invalid authentication digest",
|
||||
Self::TimestampSkew => "timestamp outside allowed window",
|
||||
Self::Replay => "authentication digest reused",
|
||||
Self::Capacity => "authentication replay cache full",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AuthFailure {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::{AuthFailure, REPLAY_MAX_ENTRIES, REPLAY_TTL_SECS};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ReplayCache {
|
||||
inner: Mutex<HashMap<String, Instant>>,
|
||||
}
|
||||
|
||||
impl ReplayCache {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn accept(&self, digest: &str) -> Result<(), AuthFailure> {
|
||||
self.accept_at(digest, Instant::now())
|
||||
}
|
||||
|
||||
pub fn accept_at(&self, digest: &str, now: Instant) -> Result<(), AuthFailure> {
|
||||
let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
purge_expired(&mut map, now);
|
||||
|
||||
if map.contains_key(digest) {
|
||||
return Err(AuthFailure::Replay);
|
||||
}
|
||||
if map.len() >= REPLAY_MAX_ENTRIES {
|
||||
return Err(AuthFailure::Capacity);
|
||||
}
|
||||
|
||||
map.insert(
|
||||
digest.to_owned(),
|
||||
now + Duration::from_secs(REPLAY_TTL_SECS),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn purge_expired(map: &mut HashMap<String, Instant>, now: Instant) {
|
||||
map.retain(|_, exp| *exp > now);
|
||||
}
|
||||
+41
-14
@@ -1,3 +1,4 @@
|
||||
use super::{AuthFailure, ReplayCache, AUTH_SKEW_SECS};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
@@ -10,23 +11,49 @@ pub fn compute_auth_digest(token: &str, timestamp: i64) -> String {
|
||||
hex::encode(mac.finalize().into_bytes())
|
||||
}
|
||||
|
||||
pub fn verify_login(token: &str, auth_digest: &str, timestamp: i64) -> bool {
|
||||
verify_auth_digest(token, auth_digest, timestamp)
|
||||
}
|
||||
|
||||
pub fn verify_auth_digest(token: &str, auth_digest: &str, timestamp: i64) -> bool {
|
||||
pub fn verify_auth_digest(
|
||||
token: &str,
|
||||
auth_digest: &str,
|
||||
timestamp: i64,
|
||||
now_secs: i64,
|
||||
replay: Option<&ReplayCache>,
|
||||
) -> Result<(), AuthFailure> {
|
||||
if token.is_empty() {
|
||||
return true;
|
||||
return Ok(());
|
||||
}
|
||||
if auth_digest.is_empty() {
|
||||
return false;
|
||||
return Err(AuthFailure::EmptyDigest);
|
||||
}
|
||||
let Ok(expected) = hex::decode(auth_digest) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(mut mac) = HmacSha256::new_from_slice(token.as_bytes()) else {
|
||||
return false;
|
||||
};
|
||||
if (now_secs - timestamp).abs() > AUTH_SKEW_SECS {
|
||||
return Err(AuthFailure::TimestampSkew);
|
||||
}
|
||||
|
||||
let expected = hex::decode(auth_digest).map_err(|_| AuthFailure::InvalidDigest)?;
|
||||
let mut mac =
|
||||
HmacSha256::new_from_slice(token.as_bytes()).map_err(|_| AuthFailure::InvalidDigest)?;
|
||||
mac.update(timestamp.to_string().as_bytes());
|
||||
mac.verify_slice(&expected).is_ok()
|
||||
mac.verify_slice(&expected)
|
||||
.map_err(|_| AuthFailure::InvalidDigest)?;
|
||||
|
||||
if let Some(cache) = replay {
|
||||
cache.accept(auth_digest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_login(
|
||||
token: &str,
|
||||
auth_digest: &str,
|
||||
timestamp: i64,
|
||||
now_secs: i64,
|
||||
replay: &ReplayCache,
|
||||
) -> Result<(), AuthFailure> {
|
||||
verify_auth_digest(token, auth_digest, timestamp, now_secs, Some(replay))
|
||||
}
|
||||
|
||||
pub fn unix_now_secs() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::tunnel::{
|
||||
};
|
||||
use agent_registry::AgentRegistry;
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::auth::ReplayCache;
|
||||
use orbien_core::config::ServerConfig;
|
||||
use orbien_core::transport;
|
||||
use session_table::SessionMap;
|
||||
@@ -24,6 +25,7 @@ pub struct Service {
|
||||
cfg: ServerConfig,
|
||||
pub(crate) controls: Arc<Mutex<SessionMap>>,
|
||||
pub(crate) agents: Arc<AgentRegistry>,
|
||||
pub(crate) auth_replay: Arc<ReplayCache>,
|
||||
http_gw: Option<Arc<HttpGw>>,
|
||||
https_gw: Option<Arc<HttpsGw>>,
|
||||
tls_config: Arc<rustls::ServerConfig>,
|
||||
@@ -36,6 +38,9 @@ pub struct Service {
|
||||
|
||||
impl Service {
|
||||
pub fn new(cfg: ServerConfig) -> Result<Self> {
|
||||
if cfg.auth.token.is_empty() {
|
||||
tracing::warn!("auth.token is empty; authentication is disabled");
|
||||
}
|
||||
let http_gw = if cfg.http_gw_enabled() {
|
||||
Some(Arc::new(HttpGw::new(cfg.http_gw_port)))
|
||||
} else {
|
||||
@@ -56,6 +61,7 @@ impl Service {
|
||||
cfg,
|
||||
controls: Arc::new(Mutex::new(HashMap::new())),
|
||||
agents: Arc::new(AgentRegistry::new()),
|
||||
auth_replay: Arc::new(ReplayCache::new()),
|
||||
http_gw,
|
||||
https_gw,
|
||||
tls_config,
|
||||
|
||||
@@ -22,7 +22,14 @@ impl Service {
|
||||
login: Login,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
if !auth::verify_login(&self.cfg.auth.token, &login.auth_digest, login.timestamp) {
|
||||
if let Err(reason) = auth::verify_login(
|
||||
&self.cfg.auth.token,
|
||||
&login.auth_digest,
|
||||
login.timestamp,
|
||||
auth::unix_now_secs(),
|
||||
&self.auth_replay,
|
||||
) {
|
||||
tracing::warn!(%reason, %peer, "login rejected");
|
||||
let mut stream = stream;
|
||||
let _ = msg::write_msg(
|
||||
&mut stream,
|
||||
@@ -167,9 +174,20 @@ impl Service {
|
||||
if nw.session_id.trim().is_empty() {
|
||||
return Err(anyhow!("empty session_id for data conn"));
|
||||
}
|
||||
if !auth::verify_auth_digest(&self.cfg.auth.token, &nw.auth_digest, nw.timestamp) {
|
||||
if let Err(reason) = auth::verify_auth_digest(
|
||||
&self.cfg.auth.token,
|
||||
&nw.auth_digest,
|
||||
nw.timestamp,
|
||||
auth::unix_now_secs(),
|
||||
None,
|
||||
) {
|
||||
tracing::warn!(
|
||||
%reason,
|
||||
session_id = %nw.session_id,
|
||||
"data connection authentication failed"
|
||||
);
|
||||
return Err(anyhow!(
|
||||
"data conn auth failed for session_id={}",
|
||||
"data connection authentication failed for session_id={}",
|
||||
nw.session_id
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user