parlov-analysis 0.7.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Auth-block signature equivalence and gate decision.
//!
//! Compares two [`AuthBlockSignature`] values to decide whether the auth block fired
//! identically on both sides. Equivalent signatures permit gating; any non-equivalence
//! preserves the differential as oracle evidence.
//!
//! Also owns [`auth_gate_decision`] and [`layer_from_family`] — pure derivations from
//! equivalence that have no dependency on classifier internals.

use parlov_core::{DifferentialSet, ResponseSurface};

use super::auth_classifier::classify_auth_block;
use super::auth_types::{AuthBlockFamily, AuthBlockSignature, AuthChallenge, AuthGateDecision};
use super::precondition::{AuthBlockLayer, PreconditionBlock};

/// True when two auth-block signatures are equivalent — same family, credential state,
/// status, equivalent challenge parameters, and equivalent auth-relevant body content.
///
/// Any difference in these dimensions means the auth block is not the same fact on both
/// sides; the differential must be preserved as oracle evidence.
#[must_use]
pub fn equivalent_auth_block(
    b: &AuthBlockSignature,
    p: &AuthBlockSignature,
    b_res: &ResponseSurface,
    p_res: &ResponseSurface,
) -> bool {
    if b.family != p.family || b.credential_state != p.credential_state || b.status != p.status {
        return false;
    }
    if !equivalent_challenge(b.challenge.as_ref(), p.challenge.as_ref()) {
        return false;
    }
    if !equivalent_body(b, p, b_res, p_res) {
        return false;
    }
    if b.family == AuthBlockFamily::LoginRedirect && !equivalent_login_redirect(b, p) {
        return false;
    }
    true
}

fn equivalent_challenge(a: Option<&AuthChallenge>, b: Option<&AuthChallenge>) -> bool {
    match (a, b) {
        (None, None) => true,
        (Some(x), Some(y)) => {
            x.scheme == y.scheme && x.error == y.error && x.realm == y.realm && x.scope == y.scope
        }
        _ => false,
    }
}

fn equivalent_body(
    b: &AuthBlockSignature,
    p: &AuthBlockSignature,
    b_res: &ResponseSurface,
    p_res: &ResponseSurface,
) -> bool {
    match (&b.body_signal, &p.body_signal) {
        (Some(x), Some(y)) => x.code == y.code,
        (None, None) => b_res.body == p_res.body,
        _ => false,
    }
}

fn equivalent_login_redirect(b: &AuthBlockSignature, p: &AuthBlockSignature) -> bool {
    match (&b.login_redirect, &p.login_redirect) {
        (Some(x), Some(y)) => x.location == y.location,
        _ => false,
    }
}

/// Auth-gate decision for the first baseline+probe pair.
///
/// `Gate` when both sides classify equivalently (same family, credential state, status,
/// challenge, body), `DoNotGate` when an auth-related differential exists, `NoAuthInvolvement`
/// when neither side shows auth involvement.
#[must_use]
pub fn auth_gate_decision(differential: &DifferentialSet) -> AuthGateDecision {
    let Some(b_ex) = differential.baseline.first() else {
        return AuthGateDecision::NoAuthInvolvement;
    };
    let Some(p_ex) = differential.probe.first() else {
        return AuthGateDecision::NoAuthInvolvement;
    };
    let b_sig = classify_auth_block(&b_ex.request, &b_ex.response);
    let p_sig = classify_auth_block(&p_ex.request, &p_ex.response);
    match (b_sig, p_sig) {
        (Some(b), Some(p)) if equivalent_auth_block(&b, &p, &b_ex.response, &p_ex.response) => {
            let layer = layer_from_family(b.family);
            AuthGateDecision::Gate(PreconditionBlock::AuthGateBeforeTechnique {
                credential_state: b.credential_state,
                layer,
            })
        }
        (Some(_) | None, Some(_)) | (Some(_), None) => AuthGateDecision::DoNotGate,
        (None, None) => AuthGateDecision::NoAuthInvolvement,
    }
}

/// Maps an auth-block family to its corresponding layer.
#[must_use]
pub fn layer_from_family(family: AuthBlockFamily) -> AuthBlockLayer {
    match family {
        AuthBlockFamily::OriginAuthentication
        | AuthBlockFamily::OriginAuthorization
        | AuthBlockFamily::AuthErrorEnvelope => AuthBlockLayer::Origin,
        AuthBlockFamily::ProxyAuthentication => AuthBlockLayer::Proxy,
        AuthBlockFamily::NetworkAuthentication => AuthBlockLayer::Network,
        AuthBlockFamily::LoginRedirect => AuthBlockLayer::LoginRedirect,
    }
}