coreason-urn-authority 0.45.1

Epistemic Ledger & OCI Trust Anchor for CoReason URNs.
Documentation
// Copyright (c) 2026 CoReason, Inc.
// All rights reserved.

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::sync::Mutex;

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum CeremonyStatus {
    Pending,
    Established,
    Fault,
    Retracted,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CeremonySession {
    #[serde(rename = "sessionToken")]
    pub session_token: String,
    pub trust_domain: String,
    pub target_node_urn: String,
    pub required_signers: Vec<String>,
    pub threshold: usize,
    pub status: CeremonyStatus,
    #[serde(rename = "signaturesCollected")]
    pub signatures_collected: usize,
    pub collected_signers: Vec<String>,
    #[serde(rename = "attestationHash", skip_serializing_if = "Option::is_none")]
    pub attestation_hash: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct InitiateRequest {
    pub trust_domain: String,
    pub target_node_urn: String,
    pub required_signers: Vec<String>,
    pub threshold: usize,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SignRequest {
    pub session_token: String,
    pub signer_spiffe_id: String,
    pub signature_share: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RetractRequest {
    pub session_token: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CeremonyResponse {
    pub session_token: String,
    pub status: String,
    pub signatures_collected: usize,
    pub signatures_threshold: usize,
    pub timestamp: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attestation_hash: Option<String>,
}

#[derive(Default)]
pub struct CeremonyManager {
    sessions: Mutex<HashMap<String, CeremonySession>>,
}

impl CeremonyManager {
    pub fn new() -> Self {
        Self {
            sessions: Mutex::new(HashMap::new()),
        }
    }

    pub fn initiate(&self, req: InitiateRequest) -> Result<CeremonyResponse, String> {
        if req.trust_domain.is_empty() {
            return Err("trustDomain is required".to_string());
        }
        if req.target_node_urn.is_empty() {
            return Err("targetNodeUrn is required".to_string());
        }
        if req.required_signers.is_empty() {
            return Err("requiredSigners must not be empty".to_string());
        }
        if req.threshold < 1 || req.threshold > req.required_signers.len() {
            return Err(format!(
                "threshold must be between 1 and {}, got {}",
                req.required_signers.len(),
                req.threshold
            ));
        }

        let now_str = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);

        let token_input = format!("{}:{}:{}", req.trust_domain, req.target_node_urn, now_str);
        let mut hasher = Sha256::new();
        hasher.update(token_input.as_bytes());
        let hash_hex = hex::encode(hasher.finalize());
        let session_token = format!("session_delegation_{}", &hash_hex[..12]);

        let session = CeremonySession {
            session_token: session_token.clone(),
            trust_domain: req.trust_domain,
            target_node_urn: req.target_node_urn,
            required_signers: req.required_signers,
            threshold: req.threshold,
            status: CeremonyStatus::Pending,
            signatures_collected: 0,
            collected_signers: Vec::new(),
            attestation_hash: None,
            created_at: now_str.clone(),
            updated_at: now_str.clone(),
        };

        let mut lock = self.sessions.lock().unwrap();
        lock.insert(session_token.clone(), session);

        Ok(CeremonyResponse {
            session_token,
            status: "PENDING".to_string(),
            signatures_collected: 0,
            signatures_threshold: req.threshold,
            timestamp: now_str,
            attestation_hash: None,
        })
    }

    pub fn submit_signature(&self, req: SignRequest) -> Result<CeremonyResponse, String> {
        let mut lock = self.sessions.lock().unwrap();
        let session = lock
            .get_mut(&req.session_token)
            .ok_or_else(|| format!("Session '{}' not found", req.session_token))?;

        if session.status != CeremonyStatus::Pending {
            return Err(format!(
                "Session is not PENDING, status is {:?}",
                session.status
            ));
        }

        if !session.required_signers.contains(&req.signer_spiffe_id) {
            return Err(format!(
                "Signer '{}' is not in the required signers list",
                req.signer_spiffe_id
            ));
        }

        if session.collected_signers.contains(&req.signer_spiffe_id) {
            return Err(format!(
                "Signer '{}' has already signed",
                req.signer_spiffe_id
            ));
        }

        let now_str = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
        session.updated_at = now_str.clone();

        // Signature validation: basic size check (mimicking Python fallback behavior)
        if req.signature_share.is_empty() || req.signature_share.len() < 8 {
            session.status = CeremonyStatus::Fault;
            return Ok(CeremonyResponse {
                session_token: session.session_token.clone(),
                status: "FAULT".to_string(),
                signatures_collected: session.signatures_collected,
                signatures_threshold: session.threshold,
                timestamp: now_str,
                attestation_hash: None,
            });
        }

        session.collected_signers.push(req.signer_spiffe_id.clone());
        session.signatures_collected += 1;

        if session.signatures_collected >= session.threshold {
            session.status = CeremonyStatus::Established;
            // Generate attestation hash
            let combined = session.collected_signers.join(":");
            let mut hasher = Sha256::new();
            hasher.update(combined.as_bytes());
            session.attestation_hash = Some(format!("0x{}", hex::encode(hasher.finalize())));
        }

        Ok(CeremonyResponse {
            session_token: session.session_token.clone(),
            status: format!("{:?}", session.status).to_uppercase(),
            signatures_collected: session.signatures_collected,
            signatures_threshold: session.threshold,
            timestamp: now_str,
            attestation_hash: session.attestation_hash.clone(),
        })
    }

    pub fn retract(&self, req: RetractRequest) -> Result<CeremonyResponse, String> {
        let mut lock = self.sessions.lock().unwrap();
        let session = lock
            .get_mut(&req.session_token)
            .ok_or_else(|| format!("Session '{}' not found", req.session_token))?;

        let now_str = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
        session.status = CeremonyStatus::Retracted;
        session.updated_at = now_str.clone();

        Ok(CeremonyResponse {
            session_token: session.session_token.clone(),
            status: "RETRACTED".to_string(),
            signatures_collected: session.signatures_collected,
            signatures_threshold: session.threshold,
            timestamp: now_str,
            attestation_hash: None,
        })
    }
}