//! AZ-678 — operator command authentication contract. //! //! `OperatorCommandValidator` is the boundary every operator-bound //! command crosses before any business logic runs. The default //! implementation (`HmacOperatorValidator` in //! `operator_bridge::internal::auth`) uses HMAC-SHA256 over //! `(session_token || sequence_number || payload_bytes)`. The trait //! lives here so the dispatch surface (`scan_controller`, //! `mission_executor`) can depend on the contract without importing //! `operator_bridge`. use thiserror::Error; use crate::models::operator::{OperatorCommand, OperatorCommandKind}; /// A command as it arrives over the operator-link, prior to any /// authentication. Mirrors the validated `OperatorCommand` shape /// closely so a successful `validate` is a near-identity transform. #[derive(Debug, Clone)] pub struct SignedCommand { pub session_token: String, pub sequence_number: u64, pub kind: OperatorCommandKind, pub payload: serde_json::Value, /// HMAC over `(session_token || sequence_number || canonical /// JSON of payload)`. Length depends on the scheme; for HMAC-SHA256 /// this is exactly 32 bytes. pub signature: Vec, /// Wall-clock time the Ground Station stamped the command. Carried /// through `validate` for downstream audit logging; not used by /// the auth check itself. pub issued_at_wallclock: chrono::DateTime, pub command_id: uuid::Uuid, } impl SignedCommand { /// Convert into a canonical [`OperatorCommand`] once validation /// has succeeded. The signature is retained on the result for /// downstream audit logging. pub fn into_command(self) -> OperatorCommand { OperatorCommand { command_id: self.command_id, session_token: self.session_token, sequence_number: self.sequence_number, issued_at_wallclock: self.issued_at_wallclock, kind: self.kind, payload: self.payload, signature: self.signature, } } } /// Validated command. Returned by `OperatorCommandValidator::validate` /// on the happy path. Holding a `ValidatedCommand` is the proof that /// dispatching the inner command is safe. #[derive(Debug, Clone)] pub struct ValidatedCommand { pub command: OperatorCommand, } /// Why an operator command was rejected. Each variant maps 1-1 to a /// `auth_rejections_total{reason}` metric counter and to a structured /// log line. Order MUST match /// `operator_bridge::internal::auth::REJECTION_REASONS` for the /// counter array layout. #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] pub enum AuthError { #[error("signature does not match computed HMAC")] SignatureInvalid, #[error("replay detected — sequence number not greater than last seen")] ReplayDetected, #[error("session token unknown or never established")] SessionUnknown, #[error("session token expired (TTL elapsed)")] SessionExpired, } impl AuthError { /// Stable kebab-case label for the rejection-reason metric. pub fn reason_label(&self) -> &'static str { match self { Self::SignatureInvalid => "signature_invalid", Self::ReplayDetected => "replay_detected", Self::SessionUnknown => "session_unknown", Self::SessionExpired => "session_expired", } } } /// Contract every operator-command validator must satisfy. The /// default `HmacOperatorValidator` lives in `operator_bridge`; other /// schemes (e.g. Q9 resolution to a JWS-based one) implement the /// same trait and can be swapped behind the same callsite. pub trait OperatorCommandValidator: Send + Sync { /// Validate one signed command. On `Ok`, the per-session /// sequence-number tracker advances; on `Err`, it does NOT /// advance (so the rejected `seq` does not poison the session). fn validate(&self, cmd: SignedCommand) -> Result; }