chio-proof-room 0.1.2

Focused Proof Room quickstart server
use std::{collections::BTreeSet, path::Path};

use chio_core_types::{PublicKey, Signature};

use super::{
    verify_manifest_ref, ProofRoomArtifactRef, ProofRoomReceiptCoverage, CHIO_RECEIPT_SCHEMA,
    PROOF_ROOM_RECEIPT_EVIDENCE_SCHEMA, REQUIRED_RECEIPT_COVERAGE_CATEGORIES,
    RUNTIME_TERMINAL_RECEIPT_SCHEMA,
};

const RUNTIME_TERMINAL_RECEIPT_SIGNATURE_SCHEMA: &str =
    "chio.runtime.terminal-receipt-signature.v1";

pub(crate) fn verify(
    bundle_root: &Path,
    coverage: &[ProofRoomReceiptCoverage],
    artifacts: &[ProofRoomArtifactRef],
    trusted_kernel_keys: &BTreeSet<String>,
    require_full_matrix: bool,
) -> Result<(), String> {
    if coverage.is_empty() {
        return if require_full_matrix {
            Err("proof-room.receipt-coverage.missing".to_string())
        } else {
            Ok(())
        };
    }

    let mut categories = BTreeSet::new();
    for entry in coverage {
        if !categories.insert(entry.category.as_str()) {
            return Err(format!(
                "proof-room.receipt-coverage.duplicate: {}",
                entry.category
            ));
        }
        if !REQUIRED_RECEIPT_COVERAGE_CATEGORIES.contains(&entry.category.as_str()) {
            return Err(format!(
                "proof-room.receipt-coverage.category-unsupported: {}",
                entry.category
            ));
        }
        match entry.status.as_str() {
            "covered" => {
                verify_covered_category(bundle_root, entry, artifacts, trusted_kernel_keys)?
            }
            "excluded" => verify_excluded_category(entry, require_full_matrix)?,
            _ => {
                return Err(format!(
                    "proof-room.receipt-coverage.status-unsupported: {}",
                    entry.category
                ));
            }
        }
    }

    if require_full_matrix {
        for required_category in REQUIRED_RECEIPT_COVERAGE_CATEGORIES {
            if !categories.contains(required_category) {
                return Err(format!(
                    "proof-room.receipt-coverage.category-missing: {required_category}"
                ));
            }
        }
    }
    Ok(())
}

fn verify_covered_category(
    bundle_root: &Path,
    entry: &ProofRoomReceiptCoverage,
    artifacts: &[ProofRoomArtifactRef],
    trusted_kernel_keys: &BTreeSet<String>,
) -> Result<(), String> {
    let artifact_path = entry.artifact_path.as_deref().ok_or_else(|| {
        format!(
            "proof-room.receipt-coverage.artifact-missing: {}",
            entry.category
        )
    })?;
    let declared_status = entry.terminal_status.as_deref().ok_or_else(|| {
        format!(
            "proof-room.receipt-coverage.terminal-status-missing: {}",
            entry.category
        )
    })?;
    if !terminal_status_matches_category(&entry.category, declared_status) {
        return Err(format!(
            "proof-room.receipt-coverage.status-mismatch: {}",
            entry.category
        ));
    }
    let artifact = artifacts
        .iter()
        .find(|artifact| artifact.path == artifact_path)
        .ok_or_else(|| {
            format!(
                "proof-room.receipt-coverage.artifact-unregistered: {} -> {artifact_path}",
                entry.category
            )
        })?;
    if !is_receipt_coverage_schema(&artifact.schema) {
        return Err(format!(
            "proof-room.schema-mismatch: receipt_coverage expected {PROOF_ROOM_RECEIPT_EVIDENCE_SCHEMA}"
        ));
    }
    let receipt = verify_manifest_ref(bundle_root, artifact, "receipt_coverage", None)?;
    let value: serde_json::Value = serde_json::from_slice(&receipt.bytes).map_err(|error| {
        format!(
            "proof-room.receipt-coverage.invalid-json: {}: {error}",
            entry.category
        )
    })?;
    let actual_status = value
        .get("terminal_status")
        .and_then(serde_json::Value::as_str)
        .ok_or_else(|| {
            format!(
                "proof-room.receipt-coverage.terminal-status-missing: {}",
                entry.category
            )
        })?;
    if actual_status != declared_status
        || !terminal_status_matches_category(&entry.category, actual_status)
    {
        return Err(format!(
            "proof-room.receipt-coverage.status-mismatch: {}",
            entry.category
        ));
    }
    let receipt_id = required_receipt_field(&value, "receipt_id", &entry.category)?;
    let _policy_digest = required_receipt_field(&value, "policy_digest", &entry.category)?;
    let _signature = required_receipt_field(&value, "signature", &entry.category)?;
    let _kernel_key = required_receipt_field(&value, "kernel_key", &entry.category)?;
    verify_receipt_signature(&entry.category, receipt_id, &value, trusted_kernel_keys)?;
    Ok(())
}

fn is_receipt_coverage_schema(schema: &str) -> bool {
    schema == PROOF_ROOM_RECEIPT_EVIDENCE_SCHEMA
        || schema == CHIO_RECEIPT_SCHEMA
        || schema == RUNTIME_TERMINAL_RECEIPT_SCHEMA
}

fn required_receipt_field<'a>(
    value: &'a serde_json::Value,
    field: &str,
    category: &str,
) -> Result<&'a str, String> {
    let field_value = value
        .get(field)
        .and_then(serde_json::Value::as_str)
        .ok_or_else(|| format!("proof-room.receipt-coverage.{field}-missing: {category}"))?;
    if field_value.is_empty() {
        return Err(format!(
            "proof-room.receipt-coverage.{field}-missing: {category}"
        ));
    }
    Ok(field_value)
}

fn verify_receipt_signature(
    category: &str,
    receipt_id: &str,
    receipt: &serde_json::Value,
    trusted_kernel_keys: &BTreeSet<String>,
) -> Result<(), String> {
    let signature = required_receipt_field(receipt, "signature", category)?;
    let kernel_key = required_receipt_field(receipt, "kernel_key", category)?;
    let public_key = PublicKey::from_hex(kernel_key).map_err(|error| {
        format!("proof-room.receipt-coverage.signature-invalid: {category}: {error}")
    })?;
    if !trusted_kernel_keys.contains(&public_key.to_hex()) {
        return Err(format!(
            "proof-room.receipt-coverage.signer-untrusted: {category}"
        ));
    }
    let signature = Signature::from_hex(signature).map_err(|error| {
        format!("proof-room.receipt-coverage.signature-invalid: {category}: {error}")
    })?;
    let signed_body = receipt_signature_body(category, receipt)?;
    let verified = public_key
        .verify_canonical(&signed_body, &signature)
        .map_err(|error| {
            format!("proof-room.receipt-coverage.signature-invalid: {category}: {error}")
        })?;
    if verified {
        return Ok(());
    }
    Err(format!(
        "proof-room.receipt-coverage.signature-invalid: {category}: {receipt_id}"
    ))
}

fn receipt_signature_body(
    category: &str,
    receipt: &serde_json::Value,
) -> Result<serde_json::Value, String> {
    if receipt.get("schema").and_then(serde_json::Value::as_str)
        == Some(RUNTIME_TERMINAL_RECEIPT_SCHEMA)
    {
        let mut body = serde_json::json!({
            "schema": RUNTIME_TERMINAL_RECEIPT_SIGNATURE_SCHEMA,
            "receiptId": required_receipt_field(receipt, "receipt_id", category)?,
            "terminalStatus": required_receipt_field(receipt, "terminal_status", category)?,
            "policyDigest": required_receipt_field(receipt, "policy_digest", category)?,
            "kernelKey": required_receipt_field(receipt, "kernel_key", category)?,
        });
        if let Some(execution_lease_ref) = receipt
            .get("execution_lease_ref")
            .and_then(serde_json::Value::as_str)
        {
            body["executionLeaseRef"] = serde_json::Value::String(execution_lease_ref.to_string());
        }
        if let Some(incident_ref) = receipt
            .get("incident_ref")
            .and_then(serde_json::Value::as_str)
        {
            body["incidentRef"] = serde_json::Value::String(incident_ref.to_string());
        }
        return Ok(body);
    }

    let mut signed_body = receipt.clone();
    let Some(signed_body_object) = signed_body.as_object_mut() else {
        return Err(format!(
            "proof-room.receipt-coverage.invalid-json: {category}"
        ));
    };
    signed_body_object.remove("signature");
    Ok(signed_body)
}

fn verify_excluded_category(
    entry: &ProofRoomReceiptCoverage,
    require_full_matrix: bool,
) -> Result<(), String> {
    if require_full_matrix && terminal_category_requires_artifact(&entry.category) {
        return Err(format!(
            "proof-room.receipt-coverage.exclusion-forbidden: {}",
            entry.category
        ));
    }
    let Some(reason) = entry.exclusion_reason.as_deref() else {
        return Err(format!(
            "proof-room.receipt-coverage.exclusion-reason-missing: {}",
            entry.category
        ));
    };
    if reason.is_empty() {
        return Err(format!(
            "proof-room.receipt-coverage.exclusion-reason-missing: {}",
            entry.category
        ));
    }
    if entry.artifact_path.is_some() || entry.terminal_status.is_some() {
        return Err(format!(
            "proof-room.receipt-coverage.exclusion-has-artifact: {}",
            entry.category
        ));
    }
    Ok(())
}

fn terminal_category_requires_artifact(category: &str) -> bool {
    matches!(
        category,
        "runtime_terminal_allow" | "runtime_terminal_denial"
    )
}

fn terminal_status_matches_category(category: &str, status: &str) -> bool {
    match category {
        "runtime_terminal_allow" => status.starts_with("allowed_"),
        "runtime_terminal_denial" => status.starts_with("denied_"),
        "runtime_terminal_failure" => status.starts_with("failed_"),
        _ => false,
    }
}