archy/core/archipelago/src/credentials.rs
Dorian e3aa95a103 fix: prevent tokio runtime deadlock in credential issue/verify
The credential issuance and verification handlers used
Handle::block_on() directly inside the tokio runtime, causing a
deadlock. Wrapped with block_in_place() to properly yield the
runtime thread.

Also completed full feature verification across all 25 test groups
(~175 checks) on live server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:43:12 +00:00

170 lines
5.1 KiB
Rust

//! 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<String>,
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<VerifiableCredential>,
}
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<CredentialStore> {
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<String>,
) -> Result<VerifiableCredential> {
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<bool>,
) -> Result<bool> {
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<Vec<VerifiableCredential>> {
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)
}