archy/core/archipelago/src/api/rpc/credentials.rs
Dorian 5019e4ec11 feat: add did:dht support to verifiable credentials
- Add dht_did field to IdentityRecord (optional, serde-compatible)
- Add prefer_dht_did param to identity.issue-credential RPC
- When true and dht_did is set, uses did:dht as VC issuer
- Credential system already format-agnostic for any DID type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:53:14 +00:00

253 lines
9.1 KiB
Rust

use super::RpcHandler;
use crate::credentials;
use crate::identity_manager::IdentityManager;
use anyhow::Result;
impl RpcHandler {
/// Issue a Verifiable Credential from one of the user's identities.
pub(super) async fn handle_identity_issue_credential(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let issuer_id = params
.get("issuer_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing issuer_id"))?;
let subject_did = params
.get("subject_did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing subject_did"))?;
let credential_type = params
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("VerifiableCredential");
let claims = params
.get("claims")
.cloned()
.unwrap_or(serde_json::json!({}));
let expires_at = params.get("expires_at").and_then(|v| v.as_str());
let prefer_dht = params
.get("prefer_dht_did")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let manager = IdentityManager::new(&self.config.data_dir).await?;
let issuer_record = manager.get(issuer_id).await?;
// Use did:dht if available and preferred, otherwise did:key
let issuer_did = if prefer_dht {
issuer_record
.dht_did
.as_deref()
.unwrap_or(&issuer_record.did)
.to_string()
} else {
issuer_record.did.clone()
};
// Capture identity_id for the signing closure
let data_dir = self.config.data_dir.clone();
let sign_id = issuer_id.to_string();
let vc = credentials::issue_credential(
&self.config.data_dir,
&issuer_did,
subject_did,
credential_type,
claims,
expires_at,
|bytes| {
// Use block_in_place to avoid deadlocking the tokio runtime
let hex_msg = hex::encode(bytes);
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let mgr = IdentityManager::new(&data_dir).await?;
mgr.sign(&sign_id, hex_msg.as_bytes()).await
})
})
},
)
.await?;
let status = if credentials::is_revoked(&vc) { "revoked" } else { "active" };
Ok(serde_json::json!({
"id": vc.id,
"issuer": vc.issuer,
"subject": vc.credential_subject.id,
"type": vc.credential_type,
"issued_at": vc.issuance_date,
"status": status,
}))
}
/// Verify a credential by its ID.
pub(super) async fn handle_identity_verify_credential(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let credential_id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing id"))?;
let store = credentials::load_credentials(&self.config.data_dir).await?;
let vc = store
.credentials
.iter()
.find(|c| c.id == credential_id)
.ok_or_else(|| anyhow::anyhow!("Credential not found"))?;
let data_dir = self.config.data_dir.clone();
let valid = credentials::verify_credential(vc, |did, bytes, signature| {
let hex_msg = hex::encode(bytes);
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let mgr = IdentityManager::new(&data_dir).await?;
mgr.verify(did, hex_msg.as_bytes(), signature).await
})
})
})?;
let status = if credentials::is_revoked(vc) { "revoked" } else { "active" };
Ok(serde_json::json!({
"id": vc.id,
"valid": valid,
"status": status,
}))
}
/// List all credentials, optionally filtered by DID.
pub(super) async fn handle_identity_list_credentials(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let filter_did = params
.as_ref()
.and_then(|p| p.get("did"))
.and_then(|v| v.as_str());
let creds = credentials::list_credentials(&self.config.data_dir, filter_did).await?;
let items: Vec<serde_json::Value> = creds
.into_iter()
.map(|c| {
let status = if credentials::is_revoked(&c) { "revoked" } else { "active" };
serde_json::json!({
"@context": c.context,
"id": c.id,
"type": c.credential_type,
"issuer": c.issuer,
"credentialSubject": c.credential_subject,
"issuanceDate": c.issuance_date,
"expirationDate": c.expiration_date,
"proof": c.proof,
"status": status,
})
})
.collect();
Ok(serde_json::json!({ "credentials": items }))
}
/// Revoke a credential.
pub(super) async fn handle_identity_revoke_credential(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing id"))?;
credentials::revoke_credential(&self.config.data_dir, id).await?;
Ok(serde_json::json!({ "ok": true }))
}
/// Create a Verifiable Presentation bundling selected credentials.
pub(super) async fn handle_identity_create_presentation(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let holder_id = params
.get("holder_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing holder_id"))?;
let credential_ids: Vec<&str> = params
.get("credential_ids")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow::anyhow!("Missing credential_ids array"))?
.iter()
.filter_map(|v| v.as_str())
.collect();
if credential_ids.is_empty() {
return Err(anyhow::anyhow!("credential_ids must not be empty"));
}
let manager = IdentityManager::new(&self.config.data_dir).await?;
let holder_record = manager.get(holder_id).await?;
let holder_did = holder_record.did.clone();
let store = credentials::load_credentials(&self.config.data_dir).await?;
let data_dir = self.config.data_dir.clone();
let sign_id = holder_id.to_string();
let vp = credentials::create_presentation(
&holder_did,
&credential_ids,
&store.credentials,
|bytes| {
let hex_msg = hex::encode(bytes);
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let mgr = IdentityManager::new(&data_dir).await?;
mgr.sign(&sign_id, hex_msg.as_bytes()).await
})
})
},
)?;
Ok(serde_json::to_value(&vp)?)
}
/// Verify a Verifiable Presentation: check holder proof and all embedded credentials.
pub(super) async fn handle_identity_verify_presentation(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let presentation = params
.get("presentation")
.ok_or_else(|| anyhow::anyhow!("Missing presentation"))?;
let vp: credentials::VerifiablePresentation =
serde_json::from_value(presentation.clone())?;
let data_dir = self.config.data_dir.clone();
let result = credentials::verify_presentation(&vp, |did, bytes, signature| {
let hex_msg = hex::encode(bytes);
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let mgr = IdentityManager::new(&data_dir).await?;
mgr.verify(did, hex_msg.as_bytes(), signature).await
})
})
})?;
Ok(serde_json::json!({
"valid": result.valid,
"holder_valid": result.holder_valid,
"credentials": result.credentials,
}))
}
}