use crate::web5::identity::{DIDManager, Web5Error, Web5Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Clone, Serialize, Deserialize)]
pub struct VerifiableCredential {
#[serde(rename = "@context")]
pub context: Vec<String>,
pub id: String,
#[serde(rename = "type")]
pub credential_type: Vec<String>,
pub issuer: String,
pub issuance_date: String,
pub credential_subject: CredentialSubject,
pub proof: Option<CredentialProof>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CredentialSubject {
pub id: String,
#[serde(flatten)]
pub claims: HashMap<String, serde_json::Value>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CredentialProof {
#[serde(rename = "type")]
pub proof_type: String,
pub created: String,
pub verification_method: String,
pub proof_purpose: String,
pub jws: String,
}
pub struct CredentialManager {
did_manager: DIDManager,
credentials: HashMap<String, VerifiableCredential>,
}
impl CredentialManager {
pub fn new(did_manager: DIDManager) -> Self {
Self {
did_manager,
credentials: HashMap::new(),
}
}
pub fn issue_credential(
&mut self,
issuer_did: &str,
subject_did: &str,
credential_type: &str,
claims: HashMap<String, serde_json::Value>,
) -> Web5Result<VerifiableCredential> {
let id = format!("urn:uuid:{}", generate_uuid());
let credential_subject = CredentialSubject {
id: subject_did.to_string(),
claims,
};
let mut credential = VerifiableCredential {
context: vec![
"https://www.w3.org/2018/credentials/v1".to_string(),
"https://www.w3.org/2018/credentials/examples/v1".to_string(),
],
id,
credential_type: vec![
"VerifiableCredential".to_string(),
credential_type.to_string(),
],
issuer: issuer_did.to_string(),
issuance_date: current_iso_date(),
credential_subject,
proof: None,
};
let proof = self.create_proof(&credential, issuer_did)?;
credential.proof = Some(proof);
self.credentials
.insert(credential.id.clone(), credential.clone());
Ok(credential)
}
pub fn verify_credential(&self, credential: &VerifiableCredential) -> Web5Result<bool> {
let _proof = match &credential.proof {
Some(p) => p,
None => return Err(Web5Error::Credential("No proof in credential".to_string())),
};
Ok(true)
}
pub fn store_credential(&mut self, credential: VerifiableCredential) -> Web5Result<()> {
let id = if credential.id.is_empty() {
generate_uuid()
} else {
credential.id.clone()
};
self.credentials.insert(id, credential);
Ok(())
}
pub fn get_credential(&self, id: &str) -> Web5Result<VerifiableCredential> {
self.credentials
.get(id)
.cloned()
.ok_or_else(|| Web5Error::Credential(format!("Credential not found: {id}")))
}
pub fn list_credentials(&self) -> Vec<&VerifiableCredential> {
self.credentials.values().collect()
}
fn create_proof(
&self,
credential: &VerifiableCredential,
issuer_did: &str,
) -> Web5Result<CredentialProof> {
let credential_json = serde_json::to_string(&credential)
.map_err(|e| Web5Error::Credential(format!("Failed to serialize credential: {e}")))?;
let signature = self
.did_manager
.sign(issuer_did, credential_json.as_bytes())?;
let proof = CredentialProof {
proof_type: "Ed25519Signature2020".to_string(),
created: current_iso_date(),
verification_method: format!("{issuer_did}#keys-1"),
proof_purpose: "assertionMethod".to_string(),
jws: hex::encode(signature),
};
Ok(proof)
}
}
fn generate_uuid() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!(
"{:x}-{:x}-{:x}-{:x}-{:x}",
now & 0xFFFF,
(now >> 16) & 0xFFFF,
(now >> 32) & 0xFFFF,
(now >> 48) & 0xFFFF,
now % 1000
)
}
fn current_iso_date() -> String {
use chrono::Utc;
Utc::now().to_rfc3339()
}
#[derive(Debug)]
pub enum VCError {
Credential(String),
Serialization(String),
Signing(String),
}
impl Display for VCError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{self:?}")
}
}
impl std::error::Error for VCError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::web5::identity::DIDManager;
#[test]
fn test_issue_credential() -> Result<(), Box<dyn std::error::Error>> {
let did_manager = DIDManager::new("ion");
let issuer_did_obj = did_manager.create_did()?;
let subject_did_obj = did_manager.create_did()?;
let issuer_did = &issuer_did_obj.id;
let subject_did = &subject_did_obj.id;
let mut credential_manager = CredentialManager::new(did_manager);
let mut claims = HashMap::new();
claims.insert(
"name".to_string(),
serde_json::Value::String("John Doe".to_string()),
);
claims.insert(
"age".to_string(),
serde_json::Value::Number(serde_json::Number::from(25)),
);
let credential = credential_manager.issue_credential(
issuer_did,
subject_did,
"ExampleCredential",
claims,
)?;
assert_eq!(credential.issuer, *issuer_did);
assert_eq!(credential.credential_subject.id, *subject_did);
assert!(credential
.credential_type
.contains(&"ExampleCredential".to_string()));
assert!(credential.proof.is_some());
Ok(())
}
}