#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use crate::primitives::ec::ed25519::ED25519_PUBLIC_KEY_LEN;
use crate::types::traits::{
ContinuousVerifiable, ProofOfPossession, VerificationStatus, ZeroTrustAuthenticable,
};
use crate::unified_api::{
ProofComplexity, ZeroTrustConfig,
error::{CoreError, Result},
};
use crate::{
log_zero_trust_auth_failure, log_zero_trust_auth_success, log_zero_trust_challenge_generated,
log_zero_trust_proof_verified, log_zero_trust_session_created, log_zero_trust_session_expired,
log_zero_trust_session_verification_failed, log_zero_trust_session_verified,
log_zero_trust_unverified_mode,
types::{PrivateKey, PublicKey},
};
pub use crate::types::zero_trust::TrustLevel;
use chrono::{DateTime, Duration, Utc};
use std::sync::Mutex;
use subtle::ConstantTimeEq;
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum SecurityMode<'a> {
Verified(&'a VerifiedSession),
Unverified,
}
impl<'a> SecurityMode<'a> {
#[must_use]
pub fn is_verified(&self) -> bool {
matches!(self, Self::Verified(_))
}
#[must_use]
pub fn is_unverified(&self) -> bool {
matches!(self, Self::Unverified)
}
#[must_use]
pub fn session(&self) -> Option<&'a VerifiedSession> {
match self {
Self::Verified(session) => Some(session),
Self::Unverified => None,
}
}
pub fn validate(&self) -> Result<()> {
match self {
Self::Verified(session) => {
if session.trust_level() == TrustLevel::Untrusted {
log_zero_trust_session_verification_failed!(
hex::encode(session.session_id()),
"trust_level downgraded to Untrusted"
);
return Err(CoreError::ZeroTrustVerificationFailed(
"zero-trust validation failed".to_string(),
));
}
session.verify_valid()
}
Self::Unverified => {
log_zero_trust_unverified_mode!("validate");
Ok(())
}
}
}
}
impl<'a> From<&'a VerifiedSession> for SecurityMode<'a> {
fn from(session: &'a VerifiedSession) -> Self {
Self::Verified(session)
}
}
pub struct VerifiedSession {
session_id: [u8; 32],
authenticated_at: DateTime<Utc>,
trust_level: TrustLevel,
public_key: PublicKey,
expires_at: DateTime<Utc>,
issued_at_monotonic: std::time::Instant,
lifetime: std::time::Duration,
}
impl std::fmt::Debug for VerifiedSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VerifiedSession")
.field("session_id", &"[REDACTED]")
.field("authenticated_at", &self.authenticated_at)
.field("trust_level", &self.trust_level)
.field("public_key", &"[REDACTED]")
.field("expires_at", &self.expires_at)
.finish()
}
}
const DEFAULT_SESSION_LIFETIME_SECS: i64 = 30 * 60;
const _: () =
assert!(DEFAULT_SESSION_LIFETIME_SECS > 0, "DEFAULT_SESSION_LIFETIME_SECS must be > 0",);
impl VerifiedSession {
pub fn establish(public_key: &[u8], private_key: &[u8]) -> Result<Self> {
let pk: PublicKey = PublicKey::new(public_key.to_vec());
let sk: PrivateKey = PrivateKey::new(private_key.to_vec());
let auth = ZeroTrustAuth::new(pk, sk)?;
let mut session = ZeroTrustSession::new(auth);
let challenge = session.initiate_authentication()?;
if !session.auth.verify_challenge_age(&challenge)? {
return Err(CoreError::AuthenticationRequired(
"Challenge expired during establish (replay protection)".to_string(),
));
}
let proof = session.auth.generate_proof(challenge.data())?;
session.verify_response(&proof)?;
session.into_verified()
}
pub(crate) fn from_authenticated(session: &ZeroTrustSession) -> Result<Self> {
if !session.is_authenticated() {
log_zero_trust_auth_failure!(
"pending",
"Session must be authenticated before creating VerifiedSession"
);
return Err(CoreError::AuthenticationRequired(
"Session must be authenticated before creating VerifiedSession".to_string(),
));
}
let session_id_vec = crate::primitives::security::generate_secure_random_bytes(32)
.map_err(|_e| CoreError::EntropyDepleted {
message: "Failed to generate session ID".to_string(),
action: "Check system entropy source".to_string(),
})?;
let mut session_id = [0u8; 32];
session_id.copy_from_slice(&session_id_vec);
let now = Utc::now();
let expires_at = now
.checked_add_signed(Duration::seconds(DEFAULT_SESSION_LIFETIME_SECS))
.ok_or_else(|| {
CoreError::ConfigurationError(
"Cannot compute session expiry: timestamp overflow".to_string(),
)
})?;
let trust_level = TrustLevel::Trusted;
let session_id_hex = hex::encode(session_id);
log_zero_trust_session_created!(session_id_hex, trust_level, expires_at);
log_zero_trust_auth_success!(session_id_hex, trust_level);
let lifetime = std::time::Duration::from_secs(DEFAULT_SESSION_LIFETIME_SECS as u64);
Ok(Self {
session_id,
authenticated_at: now,
trust_level,
public_key: session.auth.public_key.clone(),
expires_at,
issued_at_monotonic: std::time::Instant::now(),
lifetime,
})
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.issued_at_monotonic.elapsed() < self.lifetime
}
#[must_use]
pub fn trust_level(&self) -> TrustLevel {
self.trust_level
}
pub fn downgrade_trust_level(&mut self, new_level: TrustLevel) -> Result<()> {
if new_level >= self.trust_level {
return Err(CoreError::InvalidInput(format!(
"downgrade_trust_level: new={new_level:?} not strictly lower than \
current={current:?}; upgrades require re-authentication",
current = self.trust_level,
)));
}
let prev = self.trust_level;
self.trust_level = new_level;
let session_id_hex = hex::encode(self.session_id);
tracing::warn!(
session_id = %session_id_hex,
previous = ?prev,
new = ?new_level,
"VerifiedSession trust level downgraded"
);
Ok(())
}
#[must_use]
pub fn session_id(&self) -> &[u8; 32] {
&self.session_id
}
#[must_use]
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
#[must_use]
pub fn authenticated_at(&self) -> DateTime<Utc> {
self.authenticated_at
}
#[must_use]
pub fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
pub fn verify_valid(&self) -> Result<()> {
let session_id_hex = hex::encode(self.session_id);
if self.is_valid() {
log_zero_trust_session_verified!(session_id_hex);
Ok(())
} else {
log_zero_trust_session_expired!(session_id_hex);
Err(CoreError::SessionExpired)
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn expired_clone(&self) -> Self {
Self {
session_id: self.session_id,
authenticated_at: self.authenticated_at,
trust_level: self.trust_level,
public_key: self.public_key.clone(),
expires_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap_or_else(Utc::now),
issued_at_monotonic: std::time::Instant::now(),
lifetime: std::time::Duration::from_nanos(0),
}
}
}
pub struct ZeroTrustAuth {
pub(crate) public_key: PublicKey,
private_key: PrivateKey,
config: ZeroTrustConfig,
session_start: DateTime<Utc>,
last_verification: Mutex<Option<DateTime<Utc>>>,
pop_replay_cache: Mutex<PopReplayCache>,
}
struct PopReplayCache {
pk_len: usize,
entries: std::collections::HashMap<Vec<u8>, i64>,
per_pk_counts: std::collections::HashMap<Vec<u8>, usize>,
}
impl PopReplayCache {
fn new(pk_len: usize) -> Self {
Self {
pk_len,
entries: std::collections::HashMap::new(),
per_pk_counts: std::collections::HashMap::new(),
}
}
fn expire_older_than(&mut self, now_secs: i64, max_age_secs: i64) {
let pk_len = self.pk_len;
let entries = &mut self.entries;
let per_pk = &mut self.per_pk_counts;
entries.retain(|key, ts| {
let fresh = now_secs.saturating_sub(*ts) <= max_age_secs;
if !fresh
&& let Some(pk_bytes) = key.get(..pk_len)
&& let Some(count) = per_pk.get_mut(pk_bytes)
{
*count = count.saturating_sub(1);
if *count == 0 {
per_pk.remove(pk_bytes);
}
}
fresh
});
}
fn contains(&self, key: &[u8]) -> bool {
self.entries.contains_key(key)
}
fn count_for_pk(&self, pk_bytes: &[u8]) -> usize {
self.per_pk_counts.get(pk_bytes).copied().unwrap_or(0)
}
fn total(&self) -> usize {
self.entries.len()
}
fn insert(&mut self, pk_bytes: &[u8], full_key: Vec<u8>, ts_secs: i64) {
debug_assert_eq!(
pk_bytes.len(),
self.pk_len,
"PopReplayCache: pk_bytes.len()={} does not match cache pk_len={}; \
mixing PoP key types in one cache silently breaks per-PK quota tracking",
pk_bytes.len(),
self.pk_len,
);
if self.entries.insert(full_key, ts_secs).is_none() {
let slot = self.per_pk_counts.entry(pk_bytes.to_vec()).or_insert(0);
*slot = slot.saturating_add(1);
}
}
}
impl ZeroTrustAuth {
pub fn new(public_key: PublicKey, private_key: PrivateKey) -> Result<Self> {
let config = ZeroTrustConfig::default();
config.validate()?;
let now = Utc::now();
Ok(Self {
public_key,
private_key,
config,
session_start: now,
last_verification: Mutex::new(None),
pop_replay_cache: Mutex::new(PopReplayCache::new(ED25519_PUBLIC_KEY_LEN)),
})
}
pub fn with_config(
public_key: PublicKey,
private_key: PrivateKey,
config: ZeroTrustConfig,
) -> Result<Self> {
config.validate()?;
let now = Utc::now();
Ok(Self {
public_key,
private_key,
config,
session_start: now,
last_verification: Mutex::new(None),
pop_replay_cache: Mutex::new(PopReplayCache::new(ED25519_PUBLIC_KEY_LEN)),
})
}
pub fn generate_challenge(&self) -> Result<Challenge> {
let challenge_data = generate_challenge_data(&self.config.proof_complexity)?;
log_zero_trust_challenge_generated!(self.config.proof_complexity);
Ok(Challenge {
data: challenge_data,
timestamp: Utc::now(),
complexity: self.config.proof_complexity.clone(),
timeout_ms: self.config.challenge_timeout_ms,
})
}
pub fn verify_challenge_age(&self, challenge: &Challenge) -> Result<bool> {
let elapsed = Utc::now().signed_duration_since(challenge.timestamp());
let elapsed_ms = elapsed.num_milliseconds();
let elapsed_u64 = u64::try_from(elapsed_ms).unwrap_or(u64::MAX);
Ok(elapsed_u64 <= challenge.timeout_ms())
}
pub fn start_continuous_verification(&self) -> Result<ContinuousSession> {
let last_verified = match self.last_verification.lock() {
Ok(g) => *g,
Err(poisoned) => {
tracing::warn!(
target: "latticearc::unified_api::zero_trust",
"ZeroTrustAuth.last_verification mutex was poisoned in \
start_continuous_verification; a previous writer panicked. \
Recovering inner guard."
);
*poisoned.into_inner()
}
};
let last_verified = last_verified.ok_or_else(|| {
CoreError::AuthenticationFailed(
"start_continuous_verification requires a prior successful proof verification \
(call verify_proof() against a fresh challenge first)"
.to_string(),
)
})?;
Ok(ContinuousSession {
auth_public_key: self.public_key.clone(),
start_time: Utc::now(),
verification_interval_ms: self.config.verification_interval_ms,
last_verification: last_verified,
})
}
}
impl ZeroTrustAuthenticable for ZeroTrustAuth {
type Proof = ZeroKnowledgeProof;
type Error = CoreError;
fn generate_proof(&self, challenge: &[u8]) -> Result<Self::Proof> {
if challenge.is_empty() {
return Err(CoreError::AuthenticationFailed("Empty challenge".to_string()));
}
let proof_data = self.compute_proof_data(challenge)?;
let timestamp = Utc::now();
Ok(ZeroKnowledgeProof {
challenge: challenge.to_vec(),
proof: proof_data,
timestamp,
complexity: self.config.proof_complexity.clone(),
})
}
fn verify_proof(
&self,
proof: &Self::Proof,
challenge: &[u8],
) -> std::result::Result<bool, Self::Error> {
if proof.complexity() != &self.config.proof_complexity {
tracing::debug!(
expected = ?self.config.proof_complexity,
got = ?proof.complexity(),
"ZK proof rejected: complexity field mismatch"
);
log_zero_trust_proof_verified!(false);
return Ok(false);
}
let len_eq = proof.challenge().len().ct_eq(&challenge.len());
let content_eq = proof.challenge().ct_eq(challenge);
let challenge_matches: bool = (len_eq & content_eq).into();
if !challenge_matches {
log_zero_trust_proof_verified!(false);
return Ok(false);
}
let result = self.verify_proof_data(proof.proof_data(), challenge)?;
if result {
let mut guard = match self.last_verification.lock() {
Ok(g) => g,
Err(poisoned) => {
tracing::warn!(
target: "latticearc::unified_api::zero_trust",
"ZeroTrustAuth.last_verification mutex was poisoned in \
verify_proof success path; a previous writer panicked. \
Recovering inner guard."
);
poisoned.into_inner()
}
};
*guard = Some(Utc::now());
}
log_zero_trust_proof_verified!(result);
Ok(result)
}
}
fn pop_transcript_digest(verifier_pk: &[u8], ts_micros: i64, challenge: &[u8]) -> [u8; 64] {
use sha2::{Digest, Sha512};
let mut hasher = Sha512::new();
hasher.update(crate::types::domains::pop_sig_context());
hasher.update([0x00]);
let pk_len: u32 = verifier_pk.len().try_into().unwrap_or(u32::MAX);
hasher.update(pk_len.to_be_bytes());
hasher.update(verifier_pk);
hasher.update([0x00]);
hasher.update(ts_micros.to_be_bytes());
hasher.update([0x00]);
let ch_len: u32 = challenge.len().try_into().unwrap_or(u32::MAX);
hasher.update(ch_len.to_be_bytes());
hasher.update(challenge);
hasher.finalize().into()
}
impl ProofOfPossession for ZeroTrustAuth {
type Pop = ProofOfPossessionData;
type Error = CoreError;
fn generate_pop(&self, challenge: &[u8]) -> Result<Self::Pop> {
if challenge.is_empty() {
tracing::warn!(
"PoP generate_pop called with empty challenge — cross-verifier-\
instance replay protection requires a per-request nonce. \
verify_pop will refuse to accept an empty challenge."
);
}
let timestamp = Utc::now();
let ts_micros = timestamp.timestamp_micros();
let pop_digest = pop_transcript_digest(self.public_key.as_slice(), ts_micros, challenge);
let signature = crate::unified_api::convenience::ed25519::sign_ed25519_internal(
&pop_digest,
self.private_key.expose_secret(),
)?;
Ok(ProofOfPossessionData { public_key: self.public_key.clone(), signature, timestamp })
}
fn verify_pop(
&self,
pop: &Self::Pop,
expected_challenge: &[u8],
) -> std::result::Result<bool, Self::Error> {
if expected_challenge.is_empty() {
return Err(CoreError::InvalidInput(
"PoP verify_pop requires a non-empty expected_challenge — the \
per-request nonce is the only structural defence against \
cross-verifier-instance replay. Pass the same bytes the \
generator used."
.to_string(),
));
}
const PROOF_OF_POSSESSION_MAX_AGE_SECS: i64 = 5 * 60; let elapsed = Utc::now().signed_duration_since(pop.timestamp());
if elapsed.num_seconds() > PROOF_OF_POSSESSION_MAX_AGE_SECS {
tracing::debug!(
elapsed_secs = elapsed.num_seconds(),
max_age_secs = PROOF_OF_POSSESSION_MAX_AGE_SECS,
"verify_pop rejected: proof-of-possession too old"
);
return Ok(false);
}
if elapsed.num_seconds() < -30 {
tracing::debug!(
elapsed_secs = elapsed.num_seconds(),
"verify_pop rejected: proof-of-possession timestamp is in the future"
);
return Ok(false);
}
use subtle::ConstantTimeEq;
let embedded_pk = pop.public_key().as_slice();
let verifier_pk = self.public_key.as_slice();
if embedded_pk.len() != verifier_pk.len() {
tracing::debug!(
embedded_len = embedded_pk.len(),
verifier_len = verifier_pk.len(),
"verify_pop rejected: embedded public key length differs from \
verifier identity (PoP-H1)"
);
return Ok(false);
}
if embedded_pk.ct_eq(verifier_pk).unwrap_u8() != 1u8 {
tracing::debug!(
"verify_pop rejected: embedded public key does not match verifier \
identity (PoP-H1)"
);
return Ok(false);
}
let ts_micros = pop.timestamp().timestamp_micros();
let pop_digest = pop_transcript_digest(verifier_pk, ts_micros, expected_challenge);
let valid = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
&pop_digest,
pop.signature(),
verifier_pk,
)?;
if valid {
let mut cache = self.pop_replay_cache.lock().map_err(|_poison| {
CoreError::InvalidInput("PoP replay cache poisoned".to_string())
})?;
let pk_bytes = verifier_pk;
let pk_len = pk_bytes.len();
let now_secs = Utc::now().timestamp();
cache.expire_older_than(now_secs, PROOF_OF_POSSESSION_MAX_AGE_SECS);
let key_cap = pk_len.saturating_add(pop.signature().len()).saturating_add(8);
let mut key = Vec::with_capacity(key_cap);
key.extend_from_slice(pk_bytes);
key.extend_from_slice(pop.signature());
key.extend_from_slice(&ts_micros.to_be_bytes());
if cache.contains(&key) {
tracing::debug!(ts_micros, "PoP rejected: replay within 5-min window");
return Ok(false);
}
const POP_CACHE_PER_PK_MAX: usize = 64;
if cache.count_for_pk(pk_bytes) >= POP_CACHE_PER_PK_MAX {
tracing::warn!(
per_pk_cap = POP_CACHE_PER_PK_MAX,
"PoP per-key quota exhausted; rejecting new PoP from this PK \
(other PKs unaffected)"
);
return Ok(false);
}
const POP_CACHE_MAX: usize = 16 * 1024;
if cache.total() >= POP_CACHE_MAX {
tracing::warn!(
cap = POP_CACHE_MAX,
"PoP global replay cache full; rejecting new PoP rather than \
skipping insert (silently skipping would re-open the 5-minute \
replay window)"
);
return Ok(false);
}
cache.insert(pk_bytes, key, now_secs);
}
Ok(valid)
}
}
impl ContinuousVerifiable for ZeroTrustAuth {
type Error = CoreError;
fn verify_continuously(&self) -> Result<VerificationStatus> {
let session_elapsed = Utc::now().signed_duration_since(self.session_start);
let max_session_time: u64 = 30 * 60 * 1000;
let session_elapsed_u64 = u64::try_from(session_elapsed.num_milliseconds()).unwrap_or(0);
if session_elapsed_u64 > max_session_time {
return Ok(VerificationStatus::Expired);
}
if !self.config.continuous_verification {
return Ok(VerificationStatus::Verified);
}
let last_verification_at = match self.last_verification.lock() {
Ok(g) => *g,
Err(poisoned) => {
tracing::warn!(
target: "latticearc::unified_api::zero_trust",
"ZeroTrustAuth.last_verification mutex was poisoned in \
maybe_continuous_verification; a previous writer panicked. \
Recovering inner guard."
);
*poisoned.into_inner()
}
};
let Some(last_verification_at) = last_verification_at else {
return Ok(VerificationStatus::Pending);
};
let verification_elapsed = Utc::now().signed_duration_since(last_verification_at);
let verification_elapsed_u64 =
u64::try_from(verification_elapsed.num_milliseconds()).unwrap_or(0);
if verification_elapsed_u64 > self.config.verification_interval_ms {
return Ok(VerificationStatus::Pending);
}
Ok(VerificationStatus::Verified)
}
fn reauthenticate(&self) -> Result<()> {
let challenge = self.generate_challenge()?;
let proof = self.generate_proof(challenge.data())?;
let proof_valid = self.verify_proof(&proof, challenge.data())?;
if !proof_valid {
return Err(CoreError::AuthenticationFailed(
"Reauthentication proof verification failed".to_string(),
));
}
let mut guard = match self.last_verification.lock() {
Ok(g) => g,
Err(poisoned) => {
tracing::warn!(
target: "latticearc::unified_api::zero_trust",
"ZeroTrustAuth.last_verification mutex was poisoned in \
reauthenticate; a previous writer panicked. Recovering inner guard."
);
poisoned.into_inner()
}
};
*guard = Some(Utc::now());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Challenge {
data: Vec<u8>,
timestamp: DateTime<Utc>,
complexity: ProofComplexity,
timeout_ms: u64,
}
impl Challenge {
#[must_use]
pub fn data(&self) -> &[u8] {
&self.data
}
#[must_use]
pub fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
#[must_use]
pub fn complexity(&self) -> &ProofComplexity {
&self.complexity
}
#[must_use]
pub fn timeout_ms(&self) -> u64 {
self.timeout_ms
}
#[must_use]
pub fn is_expired(&self) -> bool {
let elapsed = Utc::now().signed_duration_since(self.timestamp);
let elapsed_u64 = u64::try_from(elapsed.num_milliseconds()).unwrap_or(u64::MAX);
elapsed_u64 > self.timeout_ms
}
}
#[derive(Debug, Clone)]
pub struct ZeroKnowledgeProof {
challenge: Vec<u8>,
proof: Vec<u8>,
timestamp: DateTime<Utc>,
complexity: ProofComplexity,
}
impl ZeroKnowledgeProof {
#[must_use]
pub fn new(
challenge: Vec<u8>,
proof: Vec<u8>,
timestamp: DateTime<Utc>,
complexity: ProofComplexity,
) -> Self {
Self { challenge, proof, timestamp, complexity }
}
#[must_use]
pub fn challenge(&self) -> &[u8] {
&self.challenge
}
#[must_use]
pub fn proof_data(&self) -> &[u8] {
&self.proof
}
#[must_use]
pub fn proof_data_mut(&mut self) -> &mut Vec<u8> {
&mut self.proof
}
#[must_use]
pub fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
#[must_use]
pub fn complexity(&self) -> &ProofComplexity {
&self.complexity
}
#[must_use]
pub fn is_valid_format(&self) -> bool {
!self.challenge.is_empty() && !self.proof.is_empty() && self.timestamp <= Utc::now()
}
}
#[derive(Debug, Clone)]
pub struct ProofOfPossessionData {
public_key: PublicKey,
signature: Vec<u8>,
timestamp: DateTime<Utc>,
}
impl ProofOfPossessionData {
#[must_use]
pub fn new(public_key: PublicKey, signature: Vec<u8>, timestamp: DateTime<Utc>) -> Self {
Self { public_key, signature, timestamp }
}
#[must_use]
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
#[must_use]
pub fn signature_mut(&mut self) -> &mut Vec<u8> {
&mut self.signature
}
#[must_use]
pub fn signature(&self) -> &[u8] {
&self.signature
}
#[must_use]
pub fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
}
#[derive(Debug)]
pub struct ContinuousSession {
auth_public_key: PublicKey,
start_time: DateTime<Utc>,
verification_interval_ms: u64,
last_verification: DateTime<Utc>,
}
impl ContinuousSession {
#[must_use]
pub fn auth_public_key(&self) -> &PublicKey {
&self.auth_public_key
}
#[must_use]
pub fn last_verification(&self) -> DateTime<Utc> {
self.last_verification
}
#[must_use = "session validity check should not be discarded — act on the boolean"]
pub fn is_valid(&self) -> Result<bool> {
let elapsed = Utc::now().signed_duration_since(self.start_time);
let max_duration: u64 = 60 * 60 * 1000;
let elapsed_u64 = u64::try_from(elapsed.num_milliseconds()).unwrap_or(0);
if elapsed_u64 > max_duration {
return Ok(false);
}
let verification_elapsed = Utc::now().signed_duration_since(self.last_verification);
let verification_elapsed_u64 =
u64::try_from(verification_elapsed.num_milliseconds()).unwrap_or(0);
Ok(verification_elapsed_u64 <= self.verification_interval_ms)
}
}
fn generate_challenge_data(complexity: &ProofComplexity) -> Result<Vec<u8>> {
let size = match complexity {
ProofComplexity::Low => 32,
ProofComplexity::Medium => 64,
ProofComplexity::High => 128,
};
let data = crate::primitives::security::generate_secure_random_bytes(size).map_err(|e| {
CoreError::EntropyDepleted {
message: format!("Failed to generate challenge: {e}"),
action: "Check system entropy source".to_string(),
}
})?;
Ok(data.to_vec())
}
impl ZeroTrustAuth {
fn compute_proof_data(&self, challenge: &[u8]) -> Result<Vec<u8>> {
let timestamp = Utc::now().timestamp_millis().to_le_bytes();
let message_to_sign = match self.config.proof_complexity {
ProofComplexity::Low => {
let mut msg = vec![0x01];
msg.extend_from_slice(challenge);
msg.extend_from_slice(×tamp);
msg.extend_from_slice(self.public_key.as_slice());
msg
}
ProofComplexity::Medium => {
let mut msg = vec![0x02];
msg.extend_from_slice(challenge);
msg.extend_from_slice(×tamp);
msg.extend_from_slice(self.public_key.as_slice());
msg
}
ProofComplexity::High => {
let mut msg = vec![0x03];
msg.extend_from_slice(challenge);
msg.extend_from_slice(×tamp);
msg.extend_from_slice(self.public_key.as_slice());
msg
}
};
use crate::types::domains::{hash_with_context, zk_proof_sig_context};
let zk_digest = hash_with_context(zk_proof_sig_context(), &message_to_sign);
let signature = crate::unified_api::convenience::ed25519::sign_ed25519_internal(
&zk_digest,
self.private_key.expose_secret(),
)?;
let mut proof = signature;
proof.extend_from_slice(×tamp);
Ok(proof)
}
fn verify_proof_data(&self, proof: &[u8], challenge: &[u8]) -> Result<bool> {
if proof.len() < 64 {
return Ok(false);
}
match self.config.proof_complexity {
ProofComplexity::Low => {
if proof.len() < 72 {
return Ok(false);
}
let signature = proof.get(..64).ok_or_else(|| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let timestamp_slice = proof.get(64..72).ok_or_else(|| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let timestamp_bytes: [u8; 8] = timestamp_slice.try_into().map_err(|_e| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let mut message = vec![0x01];
message.extend_from_slice(challenge);
message.extend_from_slice(×tamp_bytes);
message.extend_from_slice(self.public_key.as_slice());
use crate::types::domains::{hash_with_context, zk_proof_sig_context};
let zk_digest = hash_with_context(zk_proof_sig_context(), &message);
let sig_ok = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
&zk_digest,
signature,
self.public_key.as_slice(),
)?;
if !sig_ok {
return Ok(false);
}
let proof_ts_ms = i64::from_le_bytes(timestamp_bytes);
let now_ms = Utc::now().timestamp_millis();
if proof_ts_ms > now_ms.saturating_add(30_000) {
tracing::warn!(
proof_ts_ms,
now_ms,
"proof timestamp more than 30 s in the future"
);
return Ok(false);
}
let drift_ms = now_ms.abs_diff(proof_ts_ms);
if drift_ms > 300_000 {
tracing::warn!(drift_ms, "proof timestamp outside 5-min freshness window");
return Ok(false);
}
Ok(true)
}
ProofComplexity::Medium => {
if proof.len() < 72 {
return Ok(false);
}
let signature = proof.get(..64).ok_or_else(|| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let timestamp_slice = proof.get(64..72).ok_or_else(|| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let timestamp_bytes: [u8; 8] = timestamp_slice.try_into().map_err(|_e| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let mut message = vec![0x02];
message.extend_from_slice(challenge);
message.extend_from_slice(×tamp_bytes);
message.extend_from_slice(self.public_key.as_slice());
use crate::types::domains::{hash_with_context, zk_proof_sig_context};
let zk_digest = hash_with_context(zk_proof_sig_context(), &message);
let sig_ok = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
&zk_digest,
signature,
self.public_key.as_slice(),
)?;
if !sig_ok {
return Ok(false);
}
let proof_ts_ms = i64::from_le_bytes(timestamp_bytes);
let now_ms = Utc::now().timestamp_millis();
if proof_ts_ms > now_ms.saturating_add(30_000) {
tracing::warn!(
proof_ts_ms,
now_ms,
"proof timestamp more than 30 s in the future"
);
return Ok(false);
}
let drift_ms = now_ms.abs_diff(proof_ts_ms);
if drift_ms > 300_000 {
tracing::warn!(drift_ms, "proof timestamp outside 5-min freshness window");
return Ok(false);
}
Ok(true)
}
ProofComplexity::High => {
if proof.len() < 72 {
return Ok(false);
}
let signature = proof.get(..64).ok_or_else(|| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let timestamp_slice = proof.get(64..72).ok_or_else(|| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let timestamp_bytes: [u8; 8] = timestamp_slice.try_into().map_err(|_e| {
CoreError::AuthenticationFailed("Invalid proof format".to_string())
})?;
let mut message = vec![0x03];
message.extend_from_slice(challenge);
message.extend_from_slice(×tamp_bytes);
message.extend_from_slice(self.public_key.as_slice());
use crate::types::domains::{hash_with_context, zk_proof_sig_context};
let zk_digest = hash_with_context(zk_proof_sig_context(), &message);
let sig_ok = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
&zk_digest,
signature,
self.public_key.as_slice(),
)?;
if !sig_ok {
return Ok(false);
}
let proof_ts_ms = i64::from_le_bytes(timestamp_bytes);
let now_ms = Utc::now().timestamp_millis();
if proof_ts_ms > now_ms.saturating_add(30_000) {
tracing::warn!(
proof_ts_ms,
now_ms,
"proof timestamp more than 30 s in the future"
);
return Ok(false);
}
let drift_ms = now_ms.abs_diff(proof_ts_ms);
if drift_ms > 300_000 {
tracing::warn!(drift_ms, "proof timestamp outside 5-min freshness window");
return Ok(false);
}
Ok(true)
}
}
}
}
pub struct ZeroTrustSession {
pub(crate) auth: ZeroTrustAuth,
challenge: Option<Challenge>,
verified: bool,
session_start: DateTime<Utc>,
}
impl ZeroTrustSession {
#[must_use]
pub fn new(auth: ZeroTrustAuth) -> Self {
Self { auth, challenge: None, verified: false, session_start: Utc::now() }
}
pub fn initiate_authentication(&mut self) -> Result<Challenge> {
let challenge = self.auth.generate_challenge()?;
self.challenge = Some(challenge.clone());
Ok(challenge)
}
pub fn verify_response(&mut self, proof: &ZeroKnowledgeProof) -> Result<bool> {
const VERIFY_RESPONSE_FAILED: &str = "challenge verification failed";
let challenge = self.challenge.as_ref().ok_or_else(|| {
log_zero_trust_session_verification_failed!("pending", VERIFY_RESPONSE_FAILED);
tracing::debug!("verify_response rejected: no active challenge");
CoreError::AuthenticationFailed(VERIFY_RESPONSE_FAILED.to_string())
})?;
if challenge.is_expired() {
log_zero_trust_session_verification_failed!("pending", VERIFY_RESPONSE_FAILED);
tracing::debug!("verify_response rejected: challenge expired");
return Err(CoreError::AuthenticationFailed(VERIFY_RESPONSE_FAILED.to_string()));
}
let challenge_data = challenge.data().to_vec();
self.challenge = None;
let verified = self.auth.verify_proof(proof, &challenge_data)?;
self.verified = verified;
if !verified {
log_zero_trust_session_verification_failed!("pending", "Proof verification failed");
}
Ok(self.verified)
}
pub fn generate_proof(&self, challenge: &Challenge) -> Result<ZeroKnowledgeProof> {
self.auth.generate_proof(challenge.data())
}
#[must_use]
pub fn is_authenticated(&self) -> bool {
self.verified
}
pub fn session_age_ms(&self) -> Result<u64> {
let elapsed = Utc::now().signed_duration_since(self.session_start);
let elapsed_u64 = u64::try_from(elapsed.num_milliseconds()).unwrap_or(0);
Ok(elapsed_u64)
}
pub fn into_verified(self) -> Result<VerifiedSession> {
VerifiedSession::from_authenticated(&self)
}
}
#[cfg(test)]
#[expect(
clippy::panic,
clippy::panic_in_result_fn,
clippy::useless_vec,
unused_qualifications,
reason = "test/bench scaffolding: lints suppressed for this module"
)]
mod tests {
use super::*;
use crate::unified_api::generate_keypair;
fn warm_up_auth(auth: &ZeroTrustAuth) -> Result<()> {
let challenge = auth.generate_challenge()?;
let proof = auth.generate_proof(challenge.data())?;
let verified = auth.verify_proof(&proof, challenge.data())?;
assert!(verified, "warm-up proof must verify");
Ok(())
}
#[test]
fn test_trust_level_default_has_correct_value_succeeds() {
let level = TrustLevel::default();
assert_eq!(level, TrustLevel::Untrusted);
}
#[test]
fn test_trust_level_variants_are_correct() {
assert_eq!(TrustLevel::Untrusted as i32, 0);
assert_eq!(TrustLevel::Partial as i32, 1);
assert_eq!(TrustLevel::Trusted as i32, 2);
assert_eq!(TrustLevel::FullyTrusted as i32, 3);
}
#[test]
fn test_trust_level_is_trusted_succeeds() {
assert!(!TrustLevel::Untrusted.is_trusted());
assert!(TrustLevel::Partial.is_trusted());
assert!(TrustLevel::Trusted.is_trusted());
assert!(TrustLevel::FullyTrusted.is_trusted());
}
#[test]
fn test_trust_level_is_fully_trusted_succeeds() {
assert!(!TrustLevel::Untrusted.is_fully_trusted());
assert!(!TrustLevel::Partial.is_fully_trusted());
assert!(!TrustLevel::Trusted.is_fully_trusted());
assert!(TrustLevel::FullyTrusted.is_fully_trusted());
}
#[test]
fn test_trust_level_ordering_is_correct() {
assert!(TrustLevel::Untrusted < TrustLevel::Partial);
assert!(TrustLevel::Partial < TrustLevel::Trusted);
assert!(TrustLevel::Trusted < TrustLevel::FullyTrusted);
}
#[test]
fn test_security_mode_unverified_is_unverified_succeeds() {
let mode = SecurityMode::Unverified;
assert!(mode.is_unverified());
assert!(!mode.is_verified());
}
#[test]
fn test_security_mode_validate_unverified_succeeds() -> Result<()> {
let mode = SecurityMode::Unverified;
mode.validate()?;
Ok(())
}
#[test]
fn test_verified_session_establish_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
assert!(session.is_valid());
assert!(session.trust_level() >= TrustLevel::Partial);
Ok(())
}
#[test]
fn test_verified_session_session_id_is_accessible() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let session_id = session.session_id();
assert_eq!(session_id.len(), 32);
Ok(())
}
#[test]
fn test_verified_session_public_key_is_accessible() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let pk = session.public_key();
assert!(!pk.is_empty());
Ok(())
}
#[test]
fn test_verified_session_timestamps_are_accessible() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let authenticated_at = session.authenticated_at();
let expires_at = session.expires_at();
assert!(expires_at > authenticated_at, "Session should expire after authentication");
Ok(())
}
#[test]
fn test_verified_session_verify_valid_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
session.verify_valid()?;
Ok(())
}
#[test]
fn test_security_mode_verified_with_session_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let mode = SecurityMode::Verified(&session);
assert!(mode.is_verified());
assert!(!mode.is_unverified());
Ok(())
}
#[test]
fn test_security_mode_verified_validate_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let mode = SecurityMode::Verified(&session);
mode.validate()?;
Ok(())
}
#[test]
fn test_security_mode_verified_session_is_accessible() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let mode = SecurityMode::Verified(&session);
assert!(mode.session().is_some());
Ok(())
}
#[test]
fn test_security_mode_unverified_session_returns_none_succeeds() {
let mode = SecurityMode::Unverified;
assert!(mode.session().is_none());
}
#[test]
fn test_zero_trust_auth_new_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
assert!(std::mem::size_of_val(&auth) > 0);
Ok(())
}
#[test]
fn test_zero_trust_auth_with_config_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config =
ZeroTrustConfig::new().with_timeout(10000).with_complexity(ProofComplexity::High);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
assert!(std::mem::size_of_val(&auth) > 0);
Ok(())
}
#[test]
fn test_zero_trust_auth_generate_challenge_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let challenge = auth.generate_challenge()?;
assert!(!challenge.is_expired());
Ok(())
}
#[test]
fn test_zero_trust_auth_multiple_challenges_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let challenge1 = auth.generate_challenge()?;
let challenge2 = auth.generate_challenge()?;
assert!(!challenge1.is_expired());
assert!(!challenge2.is_expired());
Ok(())
}
#[test]
fn test_zero_trust_auth_verify_challenge_age_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let challenge = auth.generate_challenge()?;
let is_valid = auth.verify_challenge_age(&challenge)?;
assert!(is_valid, "Freshly generated challenge should be valid");
Ok(())
}
#[test]
fn test_zero_trust_auth_start_continuous_verification_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
warm_up_auth(&auth)?;
let continuous = auth.start_continuous_verification()?;
let result = continuous.is_valid();
assert!(result.is_ok());
Ok(())
}
#[test]
fn test_start_continuous_verification_rejects_unverified_session() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let result = auth.start_continuous_verification();
match result {
Err(CoreError::AuthenticationFailed(_)) => Ok(()),
other => panic!("expected AuthenticationFailed, got {:?}", other),
}
}
#[test]
fn test_challenge_is_not_expired_when_fresh_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let challenge = auth.generate_challenge()?;
assert!(!challenge.is_expired());
Ok(())
}
#[test]
fn test_zero_trust_session_new_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let session = ZeroTrustSession::new(auth);
assert!(!session.is_authenticated());
Ok(())
}
#[test]
fn test_zero_trust_session_initiate_authentication_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let mut session = ZeroTrustSession::new(auth);
let challenge = session.initiate_authentication()?;
assert!(!challenge.is_expired());
Ok(())
}
#[test]
fn test_zero_trust_session_is_not_authenticated_initially_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let session = ZeroTrustSession::new(auth);
assert!(!session.is_authenticated());
Ok(())
}
#[test]
fn test_proof_complexity_variants_are_correct() {
let _low = ProofComplexity::Low;
let _medium = ProofComplexity::Medium;
let _high = ProofComplexity::High;
assert_eq!(ProofComplexity::Medium, ProofComplexity::Medium);
}
#[test]
fn test_verified_session_with_multiple_instances_succeeds() -> Result<()> {
for _ in 0..3 {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
assert!(session.is_valid());
}
Ok(())
}
#[test]
fn test_zero_trust_config_variations_all_succeed_succeeds() -> Result<()> {
let configs = vec![
ZeroTrustConfig::new().with_timeout(5000),
ZeroTrustConfig::new().with_complexity(ProofComplexity::Low),
ZeroTrustConfig::new().with_complexity(ProofComplexity::High),
ZeroTrustConfig::new().with_continuous_verification(true),
ZeroTrustConfig::new().with_verification_interval(60000),
];
for config in configs {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let _challenge = auth.generate_challenge()?;
}
Ok(())
}
#[test]
fn test_trust_level_progression_is_correct() {
let levels = vec![
TrustLevel::Untrusted,
TrustLevel::Partial,
TrustLevel::Trusted,
TrustLevel::FullyTrusted,
];
for (i, level) in levels.iter().enumerate() {
assert_eq!(*level as usize, i);
}
}
#[test]
fn test_verified_session_multiple_sessions_all_succeed_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session1 =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let session2 =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
assert!(session1.is_valid());
assert!(session2.is_valid());
assert_ne!(session1.session_id(), session2.session_id());
Ok(())
}
#[test]
fn test_challenge_generation_produces_unique_challenges_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let mut challenges = Vec::new();
for _ in 0..5 {
challenges.push(auth.generate_challenge()?);
}
for challenge in &challenges {
assert!(!challenge.is_expired());
}
Ok(())
}
#[test]
fn test_continuous_session_validation_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_continuous_verification(true);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
warm_up_auth(&auth)?;
let continuous = auth.start_continuous_verification()?;
assert!(continuous.is_valid().is_ok());
Ok(())
}
#[test]
fn test_zero_trust_auth_with_all_complexity_levels_succeeds() -> Result<()> {
let complexities =
vec![ProofComplexity::Low, ProofComplexity::Medium, ProofComplexity::High];
for complexity in complexities {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(complexity);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = auth.generate_challenge()?;
assert!(!challenge.is_expired());
}
Ok(())
}
#[test]
fn test_security_mode_explicit_unverified_succeeds() {
let mode = SecurityMode::Unverified;
assert!(mode.is_unverified());
assert!(!mode.is_verified());
assert!(mode.session().is_none());
}
#[test]
fn test_security_mode_from_verified_session_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let mode: SecurityMode = SecurityMode::from(&session);
assert!(mode.is_verified());
assert!(mode.session().is_some());
Ok(())
}
#[test]
fn test_verified_session_from_unauthenticated_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let session = ZeroTrustSession::new(auth);
assert!(!session.is_authenticated());
let result = session.into_verified();
assert!(result.is_err());
Ok(())
}
#[test]
fn test_zero_trust_session_verify_response_returns_error_without_challenge_fails() -> Result<()>
{
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let mut session = ZeroTrustSession::new(auth);
let fake_proof =
ZeroKnowledgeProof::new(vec![1, 2, 3], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
let result = session.verify_response(&fake_proof);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_zero_trust_session_verify_response_consumes_challenge_replay_blocked() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let mut session = ZeroTrustSession::new(auth);
let challenge = session.initiate_authentication()?;
let proof = session.generate_proof(&challenge)?;
assert!(session.verify_response(&proof)?, "first verify_response must accept");
let replay = session.verify_response(&proof);
match replay {
Err(CoreError::AuthenticationFailed(_)) => Ok(()),
other => panic!("replay must reject with AuthenticationFailed, got {other:?}"),
}
}
#[test]
fn test_zero_trust_session_session_age_ms_is_accessible() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let session = ZeroTrustSession::new(auth);
let age = session.session_age_ms()?;
assert!(age < 5000, "Session age should be < 5 seconds, got {}ms", age);
Ok(())
}
#[test]
fn test_continuous_session_auth_public_key_is_accessible() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key.clone(), private_key)?;
warm_up_auth(&auth)?;
let continuous = auth.start_continuous_verification()?;
assert_eq!(continuous.auth_public_key(), &public_key);
Ok(())
}
#[test]
fn test_continuous_session_refresh_via_reauthenticate() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
warm_up_auth(&auth)?;
let continuous = auth.start_continuous_verification()?;
assert!(continuous.is_valid()?);
auth.reauthenticate()?;
Ok(())
}
#[test]
fn test_zero_knowledge_proof_has_valid_format_has_correct_size() {
let valid =
ZeroKnowledgeProof::new(vec![1, 2, 3], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
assert!(valid.is_valid_format());
let empty_challenge =
ZeroKnowledgeProof::new(vec![], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
assert!(!empty_challenge.is_valid_format());
let empty_proof =
ZeroKnowledgeProof::new(vec![1], vec![], Utc::now(), ProofComplexity::Low);
assert!(!empty_proof.is_valid_format());
}
#[test]
fn test_zero_trust_auth_reauthenticate_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
auth.reauthenticate()?;
Ok(())
}
#[test]
fn test_zero_trust_auth_verify_continuously_verified_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
warm_up_auth(&auth)?;
let status = auth.verify_continuously()?;
assert_eq!(status, VerificationStatus::Verified);
Ok(())
}
#[test]
fn test_zero_trust_auth_verify_continuously_with_cv_enabled_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new()
.with_continuous_verification(true)
.with_verification_interval(60000);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
warm_up_auth(&auth)?;
let status = auth.verify_continuously()?;
assert_eq!(status, VerificationStatus::Verified);
Ok(())
}
#[test]
fn test_verify_continuously_pending_before_first_proof() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new()
.with_continuous_verification(true)
.with_verification_interval(60000);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let status = auth.verify_continuously()?;
assert_eq!(status, VerificationStatus::Pending);
Ok(())
}
#[test]
fn test_generate_proof_returns_error_for_empty_challenge_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let result = auth.generate_proof(&[]);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_verify_proof_returns_error_for_wrong_challenge_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let challenge = auth.generate_challenge()?;
let proof = auth.generate_proof(challenge.data())?;
let different_challenge = vec![0xFF; 32];
let result = auth.verify_proof(&proof, &different_challenge)?;
assert!(!result);
Ok(())
}
#[test]
fn test_verify_proof_returns_error_for_short_proof_data_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let challenge_data = vec![1u8; 32];
let short_proof = ZeroKnowledgeProof::new(
challenge_data.clone(),
vec![0u8; 10], Utc::now(),
ProofComplexity::Low,
);
let result = auth.verify_proof(&short_proof, &challenge_data)?;
assert!(!result);
Ok(())
}
#[test]
fn test_full_challenge_response_low_complexity_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = auth.generate_challenge()?;
let proof = auth.generate_proof(challenge.data())?;
let verified = auth.verify_proof(&proof, challenge.data())?;
assert!(verified);
Ok(())
}
#[test]
fn test_full_challenge_response_medium_complexity_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = auth.generate_challenge()?;
let proof = auth.generate_proof(challenge.data())?;
let verified = auth.verify_proof(&proof, challenge.data())?;
assert!(verified);
Ok(())
}
#[test]
fn test_full_challenge_response_high_complexity_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = auth.generate_challenge()?;
let proof = auth.generate_proof(challenge.data())?;
let verified = auth.verify_proof(&proof, challenge.data())?;
assert!(verified);
Ok(())
}
#[test]
fn test_proof_of_possession_roundtrip() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let pop = auth.generate_pop(b"unit-test-challenge")?;
let verified = auth.verify_pop(&pop, b"unit-test-challenge")?;
assert!(verified);
Ok(())
}
#[test]
fn test_full_session_flow_with_into_verified_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let auth = ZeroTrustAuth::new(public_key, private_key)?;
let mut session = ZeroTrustSession::new(auth);
let challenge = session.initiate_authentication()?;
let proof = session.auth.generate_proof(challenge.data())?;
let verified = session.verify_response(&proof)?;
assert!(verified);
assert!(session.is_authenticated());
let verified_session = session.into_verified()?;
assert!(verified_session.is_valid());
assert_eq!(verified_session.trust_level(), TrustLevel::Trusted);
Ok(())
}
#[test]
fn test_verified_session_debug_has_correct_format() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let debug = format!("{:?}", session);
assert!(debug.contains("VerifiedSession"));
Ok(())
}
#[test]
fn test_security_mode_debug_has_correct_format() -> Result<()> {
let mode = SecurityMode::Unverified;
let debug = format!("{:?}", mode);
assert!(debug.contains("Unverified"));
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let verified_mode = SecurityMode::Verified(&session);
let debug2 = format!("{:?}", verified_mode);
assert!(debug2.contains("Verified"));
Ok(())
}
#[test]
fn test_challenge_fields_are_accessible() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config =
ZeroTrustConfig::new().with_timeout(5000).with_complexity(ProofComplexity::High);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = auth.generate_challenge()?;
assert_eq!(challenge.data().len(), 128); assert_eq!(challenge.timeout_ms(), 5000);
assert!(!challenge.is_expired());
let debug = format!("{:?}", challenge);
assert!(debug.contains("Challenge"));
Ok(())
}
#[test]
fn test_verified_session_expired_verify_valid_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let expired_session = VerifiedSession {
session_id: *session.session_id(),
authenticated_at: session.authenticated_at(),
trust_level: session.trust_level(),
public_key: session.public_key().clone(),
expires_at: Utc::now() - Duration::seconds(1),
issued_at_monotonic: std::time::Instant::now(),
lifetime: std::time::Duration::from_nanos(0),
};
assert!(!expired_session.is_valid());
let result = expired_session.verify_valid();
assert!(result.is_err(), "Expired session should fail verify_valid");
match result {
Err(CoreError::SessionExpired) => {} other => panic!("Expected SessionExpired, got: {:?}", other),
}
Ok(())
}
#[test]
fn test_security_mode_validate_expired_session_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let session =
VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
let expired_session = VerifiedSession {
session_id: *session.session_id(),
authenticated_at: session.authenticated_at(),
trust_level: session.trust_level(),
public_key: session.public_key().clone(),
expires_at: Utc::now() - Duration::seconds(1),
issued_at_monotonic: std::time::Instant::now(),
lifetime: std::time::Duration::from_nanos(0),
};
let mode = SecurityMode::Verified(&expired_session);
let result = mode.validate();
assert!(result.is_err(), "Expired session in SecurityMode should fail validation");
Ok(())
}
#[test]
fn test_continuous_verification_pending_after_interval_succeeds() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config =
ZeroTrustConfig::new().with_continuous_verification(true).with_verification_interval(1);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
std::thread::sleep(std::time::Duration::from_millis(5));
let status = auth.verify_continuously()?;
assert_eq!(status, VerificationStatus::Pending, "Should be Pending after interval elapsed");
Ok(())
}
#[test]
fn test_challenge_generation_low_complexity_has_correct_size() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = auth.generate_challenge()?;
assert_eq!(challenge.data().len(), 32, "Low complexity = 32 bytes");
Ok(())
}
#[test]
fn test_challenge_generation_medium_complexity_has_correct_size() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = auth.generate_challenge()?;
assert_eq!(challenge.data().len(), 64, "Medium complexity = 64 bytes");
Ok(())
}
#[test]
fn test_verify_proof_medium_short_proof_rejects_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge_data = vec![1u8; 64];
let short_proof = ZeroKnowledgeProof::new(
challenge_data.clone(),
vec![0u8; 64],
Utc::now(),
ProofComplexity::Medium,
);
let result = auth.verify_proof(&short_proof, &challenge_data)?;
assert!(!result, "Medium-complexity proof without timestamp should fail");
Ok(())
}
#[test]
fn test_verify_proof_high_short_proof_rejects_fails() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge_data = vec![1u8; 128];
let short_proof = ZeroKnowledgeProof::new(
challenge_data.clone(),
vec![0u8; 70],
Utc::now(),
ProofComplexity::High,
);
let result = auth.verify_proof(&short_proof, &challenge_data)?;
assert!(!result, "High-complexity proof too short should fail");
Ok(())
}
fn forge_future_skewed_proof(skew_ms: i64, complexity: ProofComplexity) -> ZeroKnowledgeProof {
let now_ms = Utc::now().timestamp_millis();
let future_ts = now_ms.saturating_add(skew_ms);
let timestamp_bytes = future_ts.to_le_bytes();
let mut proof_data = vec![0u8; 64]; proof_data.extend_from_slice(×tamp_bytes);
ZeroKnowledgeProof::new(vec![1u8; 32], proof_data, Utc::now(), complexity)
}
#[test]
fn test_verify_proof_low_rejects_31s_future_timestamp() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = vec![1u8; 32];
let proof = forge_future_skewed_proof(31_000, ProofComplexity::Low);
let result = auth.verify_proof(&proof, &challenge)?;
assert!(!result, "Low: 31 s future-skew must reject before signature check");
Ok(())
}
#[test]
fn test_verify_proof_medium_rejects_31s_future_timestamp() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = vec![1u8; 32];
let proof = forge_future_skewed_proof(31_000, ProofComplexity::Medium);
let result = auth.verify_proof(&proof, &challenge)?;
assert!(!result, "Medium: 31 s future-skew must reject before signature check");
Ok(())
}
#[test]
fn test_verify_proof_high_rejects_31s_future_timestamp() -> Result<()> {
let (public_key, private_key) = generate_keypair()?;
let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
let challenge = vec![1u8; 32];
let proof = forge_future_skewed_proof(31_000, ProofComplexity::High);
let result = auth.verify_proof(&proof, &challenge)?;
assert!(!result, "High: 31 s future-skew must reject before signature check");
Ok(())
}
#[test]
fn test_zero_knowledge_proof_debug_and_clone_succeeds() {
let proof =
ZeroKnowledgeProof::new(vec![1, 2, 3], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
let cloned = proof.clone();
assert_eq!(cloned.challenge(), proof.challenge());
assert_eq!(cloned.proof_data(), proof.proof_data());
let debug = format!("{:?}", proof);
assert!(debug.contains("ZeroKnowledgeProof"));
}
#[test]
fn test_proof_of_possession_data_debug_and_clone_succeeds() {
let pop =
ProofOfPossessionData::new(PublicKey::new(vec![1, 2, 3]), vec![0u8; 64], Utc::now());
let cloned = pop.clone();
assert_eq!(cloned.public_key(), pop.public_key());
let debug = format!("{:?}", pop);
assert!(debug.contains("ProofOfPossessionData"));
}
#[test]
fn test_challenge_timeout_ms_influences_challenge_timeout_succeeds() -> Result<()> {
let (public_key_a, private_key_a) = generate_keypair()?;
let config_a = ZeroTrustConfig::new().with_timeout(1000);
let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
let challenge_a = auth_a.generate_challenge()?;
let (public_key_b, private_key_b) = generate_keypair()?;
let config_b = ZeroTrustConfig::new().with_timeout(9999);
let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
let challenge_b = auth_b.generate_challenge()?;
assert_ne!(
challenge_a.timeout_ms(),
challenge_b.timeout_ms(),
"challenge_timeout_ms must influence the timeout embedded in generated challenges"
);
Ok(())
}
#[test]
fn test_proof_complexity_influences_challenge_data_size_has_correct_size() -> Result<()> {
let (public_key_a, private_key_a) = generate_keypair()?;
let config_a = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
let challenge_a = auth_a.generate_challenge()?;
let (public_key_b, private_key_b) = generate_keypair()?;
let config_b = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
let challenge_b = auth_b.generate_challenge()?;
assert_ne!(
challenge_a.data().len(),
challenge_b.data().len(),
"proof_complexity must influence the size of generated challenge data"
);
assert_eq!(challenge_a.data().len(), 32, "Low complexity must produce 32-byte challenge");
assert_eq!(
challenge_b.data().len(),
128,
"High complexity must produce 128-byte challenge"
);
Ok(())
}
#[test]
fn test_proof_complexity_influences_proof_data_size_has_correct_size() -> Result<()> {
let (public_key_a, private_key_a) = generate_keypair()?;
let config_a = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
let challenge_a = auth_a.generate_challenge()?;
let proof_a = auth_a.generate_proof(challenge_a.data())?;
let (public_key_b, private_key_b) = generate_keypair()?;
let config_b = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
let challenge_b = auth_b.generate_challenge()?;
let proof_b = auth_b.generate_proof(challenge_b.data())?;
assert!(
proof_a.proof_data().len() >= 72,
"Low proof must be at least 72 bytes (signature + 8-byte timestamp), got {}",
proof_a.proof_data().len()
);
assert!(
proof_b.proof_data().len() >= 72,
"High proof must be at least 72 bytes, got {}",
proof_b.proof_data().len()
);
Ok(())
}
#[test]
fn test_continuous_verification_influences_verify_continuously_succeeds() -> Result<()> {
let (public_key_a, private_key_a) = generate_keypair()?;
let config_a = ZeroTrustConfig::new()
.with_continuous_verification(false)
.with_verification_interval(1); let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
let (public_key_b, private_key_b) = generate_keypair()?;
let config_b =
ZeroTrustConfig::new().with_continuous_verification(true).with_verification_interval(1);
let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
std::thread::sleep(std::time::Duration::from_millis(5));
let status_a = auth_a.verify_continuously()?;
let status_b = auth_b.verify_continuously()?;
assert_eq!(
status_a,
VerificationStatus::Verified,
"continuous_verification=false must return Verified without interval check"
);
assert_eq!(
status_b,
VerificationStatus::Pending,
"continuous_verification=true with 1ms interval must return Pending after elapsed"
);
Ok(())
}
#[test]
fn test_verification_interval_ms_influences_continuous_session_validity_succeeds() -> Result<()>
{
let (public_key_a, private_key_a) = generate_keypair()?;
let config_a =
ZeroTrustConfig::new().with_continuous_verification(true).with_verification_interval(1);
let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
let (public_key_b, private_key_b) = generate_keypair()?;
let config_b = ZeroTrustConfig::new()
.with_continuous_verification(true)
.with_verification_interval(3_600_000);
let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
warm_up_auth(&auth_a)?;
warm_up_auth(&auth_b)?;
let session_a = auth_a.start_continuous_verification()?;
let session_b = auth_b.start_continuous_verification()?;
std::thread::sleep(std::time::Duration::from_millis(5));
let valid_a = session_a.is_valid()?;
let valid_b = session_b.is_valid()?;
assert_ne!(
valid_a, valid_b,
"verification_interval_ms must influence continuous session validity"
);
assert!(!valid_a, "1ms interval session must be invalid after 5ms sleep");
assert!(valid_b, "1-hour interval session must remain valid");
Ok(())
}
}