road-runner-common 0.9.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
//! Snapshot envelope parsing + monotonic version enforcement.
//!
//! NOTE: the Ed25519 signature over the payload is NO LONGER verified — services
//! consume the authz snapshot from Kafka directly and trust the channel (mesh
//! mTLS / broker ACLs). The `signature` field in the envelope is ignored. Only
//! the schema and strict version monotonicity are enforced.

use super::model::{SnapshotEnvelope, SnapshotPayload};

const SCHEMA: &str = "cex.authz.snapshot.v1";

#[derive(Debug, thiserror::Error)]
pub enum VerifyError {
    #[error("envelope parse failed: {0}")]
    Envelope(String),
    #[error("payload parse failed: {0}")]
    Payload(String),
    #[error("unexpected schema: {0}")]
    Schema(String),
    #[error("stale version: {got} <= current {current}")]
    Stale { got: u64, current: u64 },
}

/// Parses authz snapshot envelopes and enforces schema + monotonic version.
/// Keyless: it does not verify the payload signature.
#[derive(Clone, Default)]
pub struct SnapshotVerifier;

impl SnapshotVerifier {
    pub fn new() -> Self {
        Self
    }

    /// Parse a raw envelope and its payload. `current_version` is what the PDP
    /// already holds; a snapshot must strictly advance it.
    pub fn verify(
        &self,
        envelope_bytes: &[u8],
        current_version: u64,
    ) -> Result<SnapshotPayload, VerifyError> {
        let envelope: SnapshotEnvelope = serde_json::from_slice(envelope_bytes)
            .map_err(|e| VerifyError::Envelope(e.to_string()))?;

        // Signature intentionally not verified (channel-trusted consumption).
        let payload: SnapshotPayload = serde_json::from_str(&envelope.payload)
            .map_err(|e| VerifyError::Payload(e.to_string()))?;

        if payload.schema != SCHEMA {
            return Err(VerifyError::Schema(payload.schema));
        }
        if payload.policy_version <= current_version && current_version != 0 {
            return Err(VerifyError::Stale {
                got: payload.policy_version,
                current: current_version,
            });
        }
        Ok(payload)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_envelope(version: u64) -> Vec<u8> {
        let payload = serde_json::json!({
            "schema": SCHEMA,
            "policy_version": version,
            "issued_at": "2026-07-08T00:00:00Z",
            "permissions": [],
            "roles": [],
            "grants": [],
            "bindings": []
        });
        let payload_str = serde_json::to_string(&payload).unwrap();
        let envelope = serde_json::json!({
            "payload": payload_str,
            "signature": "",
            "key_id": "authz-v1"
        });
        serde_json::to_vec(&envelope).unwrap()
    }

    #[test]
    fn parses_and_enforces_monotonic_version() {
        let v = SnapshotVerifier::new();
        let p = v.verify(&make_envelope(5), 0).expect("first snapshot");
        assert_eq!(p.policy_version, 5);
        // Strictly-newer required.
        assert!(matches!(
            v.verify(&make_envelope(5), 5),
            Err(VerifyError::Stale { .. })
        ));
        assert!(v.verify(&make_envelope(6), 5).is_ok());
    }

    #[test]
    fn rejects_wrong_schema() {
        let v = SnapshotVerifier::new();
        let bad = serde_json::to_vec(&serde_json::json!({
            "payload": serde_json::json!({"schema":"nope","policy_version":1,"permissions":[],"roles":[],"grants":[],"bindings":[]}).to_string(),
            "signature": ""
        })).unwrap();
        assert!(matches!(v.verify(&bad, 0), Err(VerifyError::Schema(_))));
    }
}