use crate::{MessageType, errors::OpenVTCError};
use affinidi_tdk::didcomm::Message;
use dtg_credentials::DTGCredential;
use serde::{Deserialize, Serialize};
use std::{
collections::{
HashMap,
hash_map::{Keys, Values},
},
sync::Arc,
time::SystemTime,
};
use tracing::{debug, warn};
use uuid::Uuid;
#[derive(Serialize, Debug, Clone, Default)]
pub struct Vrcs {
vrcs: HashMap<Arc<String>, HashMap<Arc<String>, Arc<DTGCredential>>>,
#[serde(default, skip_serializing_if = "is_zero")]
retired: usize,
}
fn is_zero(n: &usize) -> bool {
*n == 0
}
impl<'de> Deserialize<'de> for Vrcs {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Raw {
#[serde(default)]
vrcs: HashMap<Arc<String>, HashMap<Arc<String>, serde_json::Value>>,
#[serde(default)]
retired: usize,
}
let raw = Raw::deserialize(deserializer)?;
let mut vrcs: HashMap<Arc<String>, HashMap<Arc<String>, Arc<DTGCredential>>> =
HashMap::new();
let mut retired = raw.retired;
for (remote, by_id) in raw.vrcs {
for (vrc_id, value) in by_id {
match serde_json::from_value::<DTGCredential>(value) {
Ok(vrc) => {
vrcs.entry(remote.clone())
.or_default()
.insert(vrc_id, Arc::new(vrc));
}
Err(e) => {
retired += 1;
warn!(
remote = %remote,
reason = %e,
"dropping a stored VRC that does not conform to DTG Credentials v1 — ask the peer to issue a fresh one"
);
}
}
}
}
Ok(Vrcs { vrcs, retired })
}
}
impl Vrcs {
#[must_use]
pub fn retired(&self) -> usize {
self.retired
}
pub fn clear_retired(&mut self) {
self.retired = 0;
}
pub fn values(&self) -> Values<'_, Arc<String>, HashMap<Arc<String>, Arc<DTGCredential>>> {
self.vrcs.values()
}
pub fn keys(&self) -> Keys<'_, Arc<String>, HashMap<Arc<String>, Arc<DTGCredential>>> {
self.vrcs.keys()
}
pub fn get(&self, id: &Arc<String>) -> Option<&HashMap<Arc<String>, Arc<DTGCredential>>> {
self.vrcs.get(id)
}
pub fn insert(
&mut self,
remote_p_did: &Arc<String>,
vrc: Arc<DTGCredential>,
) -> Result<(), OpenVTCError> {
let hash = Arc::new(
vrc.proof_value()
.ok_or_else(|| OpenVTCError::InvalidMessage("VRC has no proof value".to_string()))?
.to_string(),
);
self.vrcs
.entry(remote_p_did.clone())
.and_modify(|hm| {
hm.insert(hash.clone(), vrc.clone());
})
.or_insert({
let mut hm = HashMap::new();
hm.insert(hash, vrc);
hm
});
Ok(())
}
pub fn remove_vrc(&mut self, vrc_id: &Arc<String>) {
debug!("removing VRC {}", vrc_id);
for r in self.vrcs.values_mut() {
r.retain(|vrc_id_key, _| vrc_id_key != vrc_id);
}
}
pub fn remove_relationship(&mut self, remote_p_did: &Arc<String>) -> bool {
let removed = self.vrcs.remove(remote_p_did).is_some();
if removed {
debug!("removing VRCs for relationship {}", remote_p_did);
}
removed
}
}
pub trait DtgCredentialMessage {
fn message(&self, from: &str, to: &str, thid: Option<&str>) -> Result<Message, OpenVTCError>;
}
impl DtgCredentialMessage for DTGCredential {
fn message(&self, from: &str, to: &str, thid: Option<&str>) -> Result<Message, OpenVTCError> {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| OpenVTCError::Config(format!("System clock error: {e}")))?
.as_secs();
let mut builder = Message::build(
Uuid::new_v4().to_string(),
String::from(MessageType::VRCIssued),
serde_json::to_value(self)?,
)
.from(from.to_string())
.to(to.to_string())
.created_time(now)
.expires_time(now + 60 * 60 * 48);
if let Some(thid_value) = thid {
builder = builder.thid(thid_value.to_string());
}
Ok(builder.finalize())
}
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct VrcRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl VrcRequest {
pub fn create_message(
&self,
to: &Arc<String>,
from: &Arc<String>,
) -> Result<Message, OpenVTCError> {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| OpenVTCError::Config(format!("System clock error: {e}")))?
.as_secs();
Ok(Message::build(
Uuid::new_v4().to_string(),
crate::protocol_urls::VRC_REQUEST.to_string(),
serde_json::to_value(self)?,
)
.from(from.to_string())
.to(to.to_string())
.created_time(now)
.expires_time(now + 60 * 60 * 48) .finalize())
}
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct VRCRequestReject {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl VRCRequestReject {
pub fn create_message(
to: &Arc<String>,
from: &Arc<String>,
thid: &Arc<String>,
reason: Option<String>,
) -> Result<Message, OpenVTCError> {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| OpenVTCError::Config(format!("System clock error: {e}")))?
.as_secs();
Ok(Message::build(
Uuid::new_v4().to_string(),
crate::protocol_urls::VRC_REJECTED.to_string(),
serde_json::to_value(VRCRequestReject { reason })?,
)
.from(from.to_string())
.to(to.to_string())
.thid(thid.to_string())
.created_time(now)
.expires_time(now + 60 * 60 * 48) .finalize())
}
}
pub fn new_identified_vrc(
issuer: &str,
issuer_scope: dtg_credentials::IssuerScope,
subject: &str,
valid_from: chrono::DateTime<chrono::Utc>,
valid_until: Option<chrono::DateTime<chrono::Utc>>,
) -> DTGCredential {
DTGCredential::new_vrc(
issuer.to_string(),
issuer_scope,
subject.to_string(),
valid_from,
valid_until,
)
.with_id(format!("urn:uuid:{}", uuid::Uuid::new_v4()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_issued_vrc_carries_its_own_identifier() {
let vrc = new_identified_vrc(
"did:key:zIssuerR",
dtg_credentials::IssuerScope::Pairwise,
"did:key:zSubjectR",
chrono::Utc::now(),
None,
);
let value = serde_json::to_value(&vrc).expect("serialise");
assert_eq!(value["issuerScope"], "pairwise");
assert_eq!(value["@context"][1], dtg_credentials::DTG_CONTEXT_V1);
let id = value["id"].as_str().expect("a top-level id");
assert!(id.starts_with("urn:uuid:"), "got {id}");
assert_eq!(value["credentialSubject"]["id"], "did:key:zSubjectR");
assert_eq!(value["issuer"], "did:key:zIssuerR");
}
#[test]
fn each_issued_vrc_gets_a_fresh_identifier() {
let now = chrono::Utc::now();
let scope = dtg_credentials::IssuerScope::Pairwise;
let a = new_identified_vrc("did:key:zI", scope, "did:key:zS", now, None);
let b = new_identified_vrc("did:key:zI", scope, "did:key:zS", now, None);
assert_ne!(a.id(), b.id());
assert!(a.id().is_some());
}
#[test]
fn a_pre_v1_stored_vrc_is_set_aside_on_load() {
let current = serde_json::to_value(new_identified_vrc(
"did:key:zI",
dtg_credentials::IssuerScope::Pairwise,
"did:key:zS",
chrono::Utc::now(),
None,
))
.unwrap();
let mut old = current.clone();
old["@context"][1] = serde_json::json!(crate::dtg::fixtures::RETIRED_CONTEXT);
old.as_object_mut().unwrap().remove("issuerScope");
let stored = serde_json::json!({
"vrcs": { "did:key:zRemote": { "zOld": old, "zNew": current } }
});
let vrcs: Vrcs = serde_json::from_value(stored).expect("loads despite the old VRC");
assert_eq!(vrcs.retired(), 1);
let remote = Arc::new("did:key:zRemote".to_string());
let kept = vrcs.get(&remote).expect("the conformant VRC is kept");
assert_eq!(kept.len(), 1);
assert!(kept.contains_key(&Arc::new("zNew".to_string())));
let again: Vrcs = serde_json::from_value(serde_json::to_value(&vrcs).unwrap()).unwrap();
assert_eq!(again.retired(), 1);
}
#[test]
fn test_vrcs_default_empty() {
let vrcs = Vrcs::default();
assert_eq!(
vrcs.keys().count(),
0,
"Default Vrcs should have no entries"
);
assert_eq!(vrcs.values().count(), 0);
}
#[test]
fn test_vrcs_remove_relationship() {
let mut vrcs = Vrcs::default();
let key = Arc::new("did:remote:1".to_string());
assert!(!vrcs.remove_relationship(&key));
}
#[test]
fn test_vrcs_get_missing_key() {
let vrcs = Vrcs::default();
let key = Arc::new("did:nonexistent".to_string());
assert!(
vrcs.get(&key).is_none(),
"get on missing key should return None"
);
}
#[test]
fn test_vrc_request_default() {
let req = VrcRequest::default();
assert!(req.reason.is_none());
}
#[test]
fn test_vrc_request_serde_roundtrip() {
let req = VrcRequest {
reason: Some("testing".to_string()),
};
let json = serde_json::to_string(&req).expect("serialize");
let restored: VrcRequest = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored.reason.as_deref(), Some("testing"));
}
#[test]
fn test_vrc_request_reject_serde_roundtrip() {
let reject = VRCRequestReject {
reason: Some("not trusted".to_string()),
};
let json = serde_json::to_string(&reject).expect("serialize");
let restored: VRCRequestReject = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored.reason.as_deref(), Some("not trusted"));
}
}