//! Verifiable Credentials (VC) management following W3C VC Data Model. //! Allows issuing, verifying, and managing credentials tied to DIDs. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; use tokio::fs; use tracing::debug; const CREDENTIALS_DIR: &str = "credentials"; /// A Verifiable Credential following W3C VC Data Model (simplified). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VerifiableCredential { pub id: String, pub issuer: String, pub subject: String, pub credential_type: String, pub claims: serde_json::Value, pub issued_at: String, pub expires_at: Option, pub signature: String, pub status: CredentialStatus, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum CredentialStatus { Active, Revoked, Expired, } impl std::fmt::Display for CredentialStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CredentialStatus::Active => write!(f, "active"), CredentialStatus::Revoked => write!(f, "revoked"), CredentialStatus::Expired => write!(f, "expired"), } } } /// Stored credentials index. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CredentialStore { pub credentials: Vec, } async fn ensure_dir(data_dir: &Path) -> Result<()> { let dir = data_dir.join(CREDENTIALS_DIR); if !dir.exists() { fs::create_dir_all(&dir).await.context("Creating credentials dir")?; } Ok(()) } fn store_path(data_dir: &Path) -> std::path::PathBuf { data_dir.join(CREDENTIALS_DIR).join("credentials.json") } pub async fn load_credentials(data_dir: &Path) -> Result { ensure_dir(data_dir).await?; let path = store_path(data_dir); if !path.exists() { return Ok(CredentialStore::default()); } let data = fs::read_to_string(&path).await.context("Reading credentials")?; serde_json::from_str(&data).context("Parsing credentials") } pub async fn save_credentials(data_dir: &Path, store: &CredentialStore) -> Result<()> { ensure_dir(data_dir).await?; let path = store_path(data_dir); let data = serde_json::to_string_pretty(store)?; fs::write(&path, data).await.context("Writing credentials") } /// Issue a new Verifiable Credential. /// The issuer signs the credential claims with their identity key. pub async fn issue_credential( data_dir: &Path, issuer_did: &str, subject_did: &str, credential_type: &str, claims: serde_json::Value, expires_at: Option<&str>, sign_fn: impl FnOnce(&[u8]) -> Result, ) -> Result { let id = format!("vc:{}", uuid::Uuid::new_v4()); let issued_at = chrono::Utc::now().to_rfc3339(); // Build the credential body for signing let body = serde_json::json!({ "id": id, "issuer": issuer_did, "subject": subject_did, "type": credential_type, "claims": claims, "issued_at": issued_at, }); let body_bytes = serde_json::to_vec(&body)?; let signature = sign_fn(&body_bytes)?; let vc = VerifiableCredential { id: id.clone(), issuer: issuer_did.to_string(), subject: subject_did.to_string(), credential_type: credential_type.to_string(), claims, issued_at, expires_at: expires_at.map(|s| s.to_string()), signature, status: CredentialStatus::Active, }; let mut store = load_credentials(data_dir).await?; debug!(id = %vc.id, "Issued credential"); store.credentials.push(vc.clone()); save_credentials(data_dir, &store).await?; Ok(vc) } /// Verify a credential's signature against the issuer DID. pub fn verify_credential( vc: &VerifiableCredential, verify_fn: impl FnOnce(&str, &[u8], &str) -> Result, ) -> Result { let body = serde_json::json!({ "id": vc.id, "issuer": vc.issuer, "subject": vc.subject, "type": vc.credential_type, "claims": vc.claims, "issued_at": vc.issued_at, }); let body_bytes = serde_json::to_vec(&body)?; verify_fn(&vc.issuer, &body_bytes, &vc.signature) } /// Revoke a credential by ID. pub async fn revoke_credential(data_dir: &Path, credential_id: &str) -> Result<()> { let mut store = load_credentials(data_dir).await?; let vc = store .credentials .iter_mut() .find(|c| c.id == credential_id) .ok_or_else(|| anyhow::anyhow!("Credential not found: {}", credential_id))?; vc.status = CredentialStatus::Revoked; save_credentials(data_dir, &store).await } /// List all credentials, optionally filtering by issuer or subject DID. pub async fn list_credentials( data_dir: &Path, filter_did: Option<&str>, ) -> Result> { let store = load_credentials(data_dir).await?; let creds = if let Some(did) = filter_did { store .credentials .into_iter() .filter(|c| c.issuer == did || c.subject == did) .collect() } else { store.credentials }; Ok(creds) }