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();
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;
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,
})
}
}