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>
170 lines
5.1 KiB
Rust
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)
|
|
}
|