use std::collections::HashMap;
use sha2::{Digest, Sha256};
pub const W3C_VC_CONTEXT: &str = "https://www.w3.org/2018/credentials/v1";
#[derive(Debug, Clone, PartialEq)]
pub enum VerificationStatus {
Valid,
Expired,
NotYetValid,
Revoked,
InvalidProof,
InvalidIssuer,
MissingField(String),
SchemaViolation(String),
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub status: VerificationStatus,
pub checks_passed: Vec<String>,
pub checks_failed: Vec<String>,
pub warnings: Vec<String>,
}
impl VerificationResult {
pub fn is_valid(&self) -> bool {
self.status == VerificationStatus::Valid
}
pub fn add_pass(&mut self, check: impl Into<String>) {
self.checks_passed.push(check.into());
}
pub fn add_fail(&mut self, check: impl Into<String>) {
self.checks_failed.push(check.into());
}
pub fn add_warning(&mut self, warn: impl Into<String>) {
self.warnings.push(warn.into());
}
}
#[derive(Debug, Clone)]
pub struct VerifiableCredential {
pub id: Option<String>,
pub types: Vec<String>,
pub issuer: String,
pub issuance_date: String,
pub expiration_date: Option<String>,
pub credential_subject: HashMap<String, String>,
pub proof: Option<CredentialProof>,
pub context: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CredentialProof {
pub proof_type: String,
pub created: String,
pub verification_method: String,
pub proof_purpose: String,
pub proof_value: String,
}
#[derive(Debug, Clone)]
pub struct VerificationPolicy {
pub check_proof: bool,
pub check_expiry: bool,
pub check_context: bool,
pub check_required_types: bool,
pub trusted_issuers: Vec<String>,
pub check_revocation: bool,
pub revoked_credential_ids: Vec<String>,
pub current_time_ms: u64,
}
impl Default for VerificationPolicy {
fn default() -> Self {
VerificationPolicy {
check_proof: false,
check_expiry: true,
check_context: true,
check_required_types: true,
trusted_issuers: vec![],
check_revocation: true,
revoked_credential_ids: vec![],
current_time_ms: 0,
}
}
}
pub struct VcVerifier;
impl VcVerifier {
pub fn verify(vc: &VerifiableCredential, policy: &VerificationPolicy) -> VerificationResult {
let mut result = VerificationResult {
status: VerificationStatus::Valid,
checks_passed: Vec::new(),
checks_failed: Vec::new(),
warnings: Vec::new(),
};
if policy.check_required_types {
if Self::has_required_type(vc) {
result.add_pass("required_type");
} else {
result.add_fail("required_type");
result.status =
VerificationStatus::MissingField("VerifiableCredential type".to_string());
return result;
}
}
if policy.check_context {
if Self::has_vc_context(vc) {
result.add_pass("vc_context");
} else {
result.add_fail("vc_context");
result.status = VerificationStatus::MissingField("W3C VC context".to_string());
return result;
}
}
if !policy.trusted_issuers.is_empty() {
if Self::is_trusted_issuer(&vc.issuer, &policy.trusted_issuers) {
result.add_pass("trusted_issuer");
} else {
result.add_fail("trusted_issuer");
result.status = VerificationStatus::InvalidIssuer;
return result;
}
} else {
result.add_pass("trusted_issuer");
}
if Self::has_credential_subject(vc) {
result.add_pass("credential_subject");
} else {
result.add_fail("credential_subject");
result.status = VerificationStatus::MissingField("credential subject".to_string());
return result;
}
if policy.check_expiry && policy.current_time_ms > 0 {
let temporal = Self::check_temporal_validity(vc, policy.current_time_ms);
match temporal {
VerificationStatus::Valid => {
result.add_pass("temporal_validity");
}
other => {
result.add_fail("temporal_validity");
result.status = other;
return result;
}
}
} else {
result.add_pass("temporal_validity");
}
if policy.check_revocation {
match &vc.id {
Some(id) if policy.revoked_credential_ids.iter().any(|r| r == id) => {
result.add_fail("revocation");
result.status = VerificationStatus::Revoked;
return result;
}
_ => result.add_pass("revocation"),
}
} else {
result.add_pass("revocation_skipped");
}
if policy.check_proof {
match &vc.proof {
Some(proof) => match Self::verify_proof_cryptographically(vc, proof) {
Ok(true) => result.add_pass("proof"),
Ok(false) => {
result.add_fail("proof");
result.status = VerificationStatus::InvalidProof;
return result;
}
Err(reason) => {
result.add_fail("proof");
result.add_warning(reason);
result.status = VerificationStatus::InvalidProof;
return result;
}
},
None => {
result.add_fail("proof");
result.status = VerificationStatus::InvalidProof;
return result;
}
}
} else {
result.add_pass("proof_skipped");
}
if vc.id.is_none() {
result.add_warning("Credential has no @id — traceability may be limited");
}
result
}
pub fn canonical_signing_input(vc: &VerifiableCredential) -> Vec<u8> {
let mut s = String::new();
s.push_str(vc.id.as_deref().unwrap_or(""));
s.push('\n');
let mut types = vc.types.clone();
types.sort();
s.push_str(&types.join(","));
s.push('\n');
s.push_str(&vc.issuer);
s.push('\n');
s.push_str(&vc.issuance_date);
s.push('\n');
s.push_str(vc.expiration_date.as_deref().unwrap_or(""));
s.push('\n');
let mut subject: Vec<(&String, &String)> = vc.credential_subject.iter().collect();
subject.sort_by(|a, b| a.0.cmp(b.0));
for (k, v) in subject {
s.push_str(k);
s.push('=');
s.push_str(v);
s.push(';');
}
s.push('\n');
let mut context = vc.context.clone();
context.sort();
s.push_str(&context.join(","));
s.into_bytes()
}
fn verify_proof_cryptographically(
vc: &VerifiableCredential,
proof: &CredentialProof,
) -> Result<bool, String> {
if proof.proof_value.is_empty() {
return Err("proofValue is empty".to_string());
}
if proof.proof_type != "Ed25519Signature2020" {
return Err(format!(
"unsupported proof type for cryptographic verification: {}",
proof.proof_type
));
}
let public_key = extract_ed25519_public_key(&proof.verification_method)
.or_else(|| extract_ed25519_public_key(&vc.issuer))
.ok_or_else(|| {
"cannot resolve an Ed25519 public key from verificationMethod or issuer \
(expected a did:key identifier)"
.to_string()
})?;
let signature = decode_multibase_signature(&proof.proof_value)?;
if signature.len() != 64 {
return Ok(false);
}
let hash = Sha256::digest(Self::canonical_signing_input(vc));
crate::proof::ed25519::verify_ed25519(&public_key, &hash, &signature)
.map_err(|e| e.to_string())
}
pub fn parse_date_ms(date_str: &str) -> Option<u64> {
let s = date_str.trim();
if s.len() < 10 {
return None;
}
let year: i64 = s[0..4].parse().ok()?;
if &s[4..5] != "-" {
return None;
}
let month: i64 = s[5..7].parse().ok()?;
if &s[7..8] != "-" {
return None;
}
let day: i64 = s[8..10].parse().ok()?;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let (hour, minute, second) =
if s.len() > 10 && (s[10..].starts_with('T') || s[10..].starts_with(' ')) {
let time_part = &s[11..];
if time_part.len() < 8 {
(0i64, 0i64, 0i64)
} else {
let h: i64 = time_part[0..2].parse().ok()?;
let m: i64 = time_part[3..5].parse().ok()?;
let sec: i64 = time_part[6..8].parse().ok()?;
(h, m, sec)
}
} else {
(0, 0, 0)
};
let jdn = julian_day_number(year, month, day)?;
let epoch_jdn: i64 = 2_440_588;
let days_since_epoch = jdn - epoch_jdn;
let total_seconds = days_since_epoch * 86_400 + hour * 3_600 + minute * 60 + second;
if total_seconds < 0 {
return None;
}
Some((total_seconds as u64) * 1000)
}
pub fn has_vc_context(vc: &VerifiableCredential) -> bool {
vc.context.iter().any(|c| c == W3C_VC_CONTEXT)
}
pub fn has_required_type(vc: &VerifiableCredential) -> bool {
vc.types.iter().any(|t| t == "VerifiableCredential")
}
pub fn is_trusted_issuer(issuer: &str, trusted: &[String]) -> bool {
if trusted.is_empty() {
return true;
}
trusted.iter().any(|t| t == issuer)
}
pub fn check_temporal_validity(
vc: &VerifiableCredential,
current_ms: u64,
) -> VerificationStatus {
if let Some(issued_ms) = Self::parse_date_ms(&vc.issuance_date) {
if current_ms < issued_ms {
return VerificationStatus::NotYetValid;
}
}
if let Some(ref exp_str) = vc.expiration_date {
if let Some(exp_ms) = Self::parse_date_ms(exp_str) {
if current_ms >= exp_ms {
return VerificationStatus::Expired;
}
}
}
VerificationStatus::Valid
}
pub fn has_credential_subject(vc: &VerifiableCredential) -> bool {
!vc.credential_subject.is_empty()
}
pub fn build_test_vc(issuer: &str, subject_id: &str) -> VerifiableCredential {
let mut subject = HashMap::new();
subject.insert("id".to_string(), subject_id.to_string());
subject.insert("name".to_string(), "Test Subject".to_string());
VerifiableCredential {
id: Some("urn:uuid:test-vc-001".to_string()),
types: vec![
"VerifiableCredential".to_string(),
"TestCredential".to_string(),
],
issuer: issuer.to_string(),
issuance_date: "2020-01-01T00:00:00Z".to_string(),
expiration_date: Some("2099-12-31T23:59:59Z".to_string()),
credential_subject: subject,
proof: Some(CredentialProof {
proof_type: "Ed25519Signature2020".to_string(),
created: "2020-01-01T00:00:00Z".to_string(),
verification_method: format!("{}#key-1", issuer),
proof_purpose: "assertionMethod".to_string(),
proof_value: "z3Z2YQjKUQABe2p8VanTFVi4WYJkBfMrS4XTdHq6LGxNdKnZHWxGqPmVMo"
.to_string(),
}),
context: vec![W3C_VC_CONTEXT.to_string()],
}
}
}
fn extract_ed25519_public_key(did_or_url: &str) -> Option<Vec<u8>> {
let did = did_or_url.split('#').next().unwrap_or(did_or_url);
let multibase = did.strip_prefix("did:key:")?;
let b58 = multibase.strip_prefix('z')?;
let decoded = bs58::decode(b58).into_vec().ok()?;
if decoded.len() == 34 && decoded[0] == 0xed && decoded[1] == 0x01 {
Some(decoded[2..].to_vec())
} else if decoded.len() == 32 {
Some(decoded)
} else {
None
}
}
fn decode_multibase_signature(proof_value: &str) -> Result<Vec<u8>, String> {
match proof_value.strip_prefix('z') {
Some(b58) => bs58::decode(b58)
.into_vec()
.map_err(|e| format!("invalid base58 proofValue: {e}")),
None => Err(
"unsupported proofValue encoding (expected multibase base58btc 'z' prefix)".to_string(),
),
}
}
fn julian_day_number(year: i64, month: i64, day: i64) -> Option<i64> {
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let a = (14 - month) / 12;
let y = year + 4800 - a;
let m = month + 12 * a - 3;
let jdn = day + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045;
Some(jdn)
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_vc() -> VerifiableCredential {
VcVerifier::build_test_vc("did:example:issuer", "did:example:subject")
}
fn default_policy() -> VerificationPolicy {
VerificationPolicy::default()
}
#[test]
fn test_build_test_vc_structure() {
let vc = valid_vc();
assert!(vc.types.contains(&"VerifiableCredential".to_string()));
assert!(vc.context.contains(&W3C_VC_CONTEXT.to_string()));
assert!(!vc.credential_subject.is_empty());
assert!(vc.proof.is_some());
}
#[test]
fn test_verify_valid_vc() {
let vc = valid_vc();
let policy = default_policy();
let result = VcVerifier::verify(&vc, &policy);
assert!(result.is_valid(), "status = {:?}", result.status);
assert!(!result.checks_passed.is_empty());
assert!(result.checks_failed.is_empty());
}
#[test]
fn test_verify_missing_required_type_fails() {
let mut vc = valid_vc();
vc.types = vec!["SomeOtherType".to_string()];
let result = VcVerifier::verify(&vc, &default_policy());
assert!(!result.is_valid());
assert!(matches!(result.status, VerificationStatus::MissingField(_)));
assert!(result.checks_failed.contains(&"required_type".to_string()));
}
#[test]
fn test_verify_type_check_disabled() {
let mut vc = valid_vc();
vc.types = vec![];
let mut policy = default_policy();
policy.check_required_types = false;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.checks_failed.contains(&"required_type".to_string()));
}
#[test]
fn test_verify_missing_w3c_context_fails() {
let mut vc = valid_vc();
vc.context = vec!["https://example.org/custom-context".to_string()];
let result = VcVerifier::verify(&vc, &default_policy());
assert!(!result.is_valid());
assert!(matches!(result.status, VerificationStatus::MissingField(_)));
assert!(result.checks_failed.contains(&"vc_context".to_string()));
}
#[test]
fn test_verify_context_check_disabled() {
let mut vc = valid_vc();
vc.context = vec![];
let mut policy = default_policy();
policy.check_context = false;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.checks_failed.contains(&"vc_context".to_string()));
}
#[test]
fn test_verify_trusted_issuer_accepted() {
let vc = valid_vc();
let mut policy = default_policy();
policy.trusted_issuers = vec!["did:example:issuer".to_string()];
let result = VcVerifier::verify(&vc, &policy);
assert!(result.is_valid());
assert!(result.checks_passed.contains(&"trusted_issuer".to_string()));
}
#[test]
fn test_verify_untrusted_issuer_rejected() {
let vc = valid_vc();
let mut policy = default_policy();
policy.trusted_issuers = vec!["did:example:other".to_string()];
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::InvalidIssuer);
}
#[test]
fn test_verify_any_issuer_when_list_empty() {
let mut vc = valid_vc();
vc.issuer = "did:example:unknown-issuer".to_string();
let mut policy = default_policy();
policy.trusted_issuers = vec![];
let result = VcVerifier::verify(&vc, &policy);
assert!(result.checks_passed.contains(&"trusted_issuer".to_string()));
}
#[test]
fn test_verify_empty_credential_subject_fails() {
let mut vc = valid_vc();
vc.credential_subject = HashMap::new();
let result = VcVerifier::verify(&vc, &default_policy());
assert!(!result.is_valid());
assert!(matches!(result.status, VerificationStatus::MissingField(_)));
}
#[test]
fn test_parse_date_ms_valid_datetime() {
let ms = VcVerifier::parse_date_ms("2020-01-01T00:00:00Z");
assert!(ms.is_some());
assert!(ms.expect("some") > 0);
}
#[test]
fn test_parse_date_ms_date_only() {
let ms = VcVerifier::parse_date_ms("2023-06-15");
assert!(ms.is_some(), "date-only format should parse");
}
#[test]
fn test_parse_date_ms_invalid_format() {
assert!(VcVerifier::parse_date_ms("not-a-date").is_none());
assert!(VcVerifier::parse_date_ms("").is_none());
assert!(VcVerifier::parse_date_ms("2020/01/01").is_none());
}
#[test]
fn test_parse_date_ms_epoch() {
let ms = VcVerifier::parse_date_ms("1970-01-01T00:00:00Z");
assert_eq!(ms, Some(0));
}
#[test]
fn test_check_temporal_valid() {
let vc = valid_vc(); let now_ms: u64 = 1_735_000_000_000;
let status = VcVerifier::check_temporal_validity(&vc, now_ms);
assert_eq!(status, VerificationStatus::Valid);
}
#[test]
fn test_check_temporal_expired() {
let mut vc = valid_vc();
vc.expiration_date = Some("2000-01-01T00:00:00Z".to_string());
let now_ms: u64 = 1_735_000_000_000; let status = VcVerifier::check_temporal_validity(&vc, now_ms);
assert_eq!(status, VerificationStatus::Expired);
}
#[test]
fn test_check_temporal_not_yet_valid() {
let mut vc = valid_vc();
vc.issuance_date = "2099-01-01T00:00:00Z".to_string();
let now_ms: u64 = 1_735_000_000_000; let status = VcVerifier::check_temporal_validity(&vc, now_ms);
assert_eq!(status, VerificationStatus::NotYetValid);
}
#[test]
fn test_verify_expired_vc_fails() {
let mut vc = valid_vc();
vc.expiration_date = Some("2000-01-01T00:00:00Z".to_string());
let mut policy = default_policy();
policy.current_time_ms = 1_735_000_000_000;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::Expired);
}
#[test]
fn test_verify_not_yet_valid_vc_fails() {
let mut vc = valid_vc();
vc.issuance_date = "2099-01-01T00:00:00Z".to_string();
let mut policy = default_policy();
policy.current_time_ms = 1_735_000_000_000;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::NotYetValid);
}
#[test]
fn test_verify_expiry_skipped_when_time_zero() {
let mut vc = valid_vc();
vc.expiration_date = Some("2000-01-01T00:00:00Z".to_string());
let mut policy = default_policy();
policy.current_time_ms = 0; let result = VcVerifier::verify(&vc, &policy);
assert!(result.is_valid(), "status = {:?}", result.status);
}
#[test]
fn test_verify_proof_skipped_when_check_proof_false() {
let mut vc = valid_vc();
vc.proof = None;
let mut policy = default_policy();
policy.check_proof = false;
let result = VcVerifier::verify(&vc, &policy);
assert!(result.is_valid());
assert!(result.checks_passed.contains(&"proof_skipped".to_string()));
}
#[test]
fn test_verify_proof_fails_when_missing_and_required() {
let mut vc = valid_vc();
vc.proof = None;
let mut policy = default_policy();
policy.check_proof = true;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::InvalidProof);
}
fn signed_test_vc() -> VerifiableCredential {
use crate::proof::ed25519::Ed25519Signer;
let signer = Ed25519Signer::generate();
let pk = signer.public_key_bytes();
let mut mc = vec![0xed, 0x01];
mc.extend_from_slice(&pk);
let did = format!("did:key:z{}", bs58::encode(&mc).into_string());
let mut subject = HashMap::new();
subject.insert("id".to_string(), "did:example:subject".to_string());
subject.insert("name".to_string(), "Signed Subject".to_string());
let mut vc = VerifiableCredential {
id: Some("urn:uuid:signed-vc-001".to_string()),
types: vec![
"VerifiableCredential".to_string(),
"TestCredential".to_string(),
],
issuer: did.clone(),
issuance_date: "2020-01-01T00:00:00Z".to_string(),
expiration_date: Some("2099-12-31T23:59:59Z".to_string()),
credential_subject: subject,
proof: None,
context: vec![W3C_VC_CONTEXT.to_string()],
};
let hash = Sha256::digest(VcVerifier::canonical_signing_input(&vc));
let signature = signer.sign(&hash);
vc.proof = Some(CredentialProof {
proof_type: "Ed25519Signature2020".to_string(),
created: "2020-01-01T00:00:00Z".to_string(),
verification_method: format!("{did}#key-1"),
proof_purpose: "assertionMethod".to_string(),
proof_value: format!("z{}", bs58::encode(&signature).into_string()),
});
vc
}
#[test]
fn test_verify_proof_passes_when_present_and_required() {
let vc = signed_test_vc(); let mut policy = default_policy();
policy.check_proof = true;
let result = VcVerifier::verify(&vc, &policy);
assert!(result.is_valid(), "status = {:?}", result.status);
assert!(result.checks_passed.contains(&"proof".to_string()));
}
#[test]
fn regression_forged_proof_value_rejected() {
let mut vc = signed_test_vc();
if let Some(proof) = vc.proof.as_mut() {
proof.proof_value = format!("z{}", bs58::encode([0x11u8; 64]).into_string());
}
let mut policy = default_policy();
policy.check_proof = true;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::InvalidProof);
}
#[test]
fn regression_tampered_claim_rejected() {
let mut vc = signed_test_vc();
vc.credential_subject
.insert("name".to_string(), "Attacker".to_string());
let mut policy = default_policy();
policy.check_proof = true;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::InvalidProof);
}
#[test]
fn regression_non_didkey_issuer_proof_fails_loud() {
let vc = valid_vc();
let mut policy = default_policy();
policy.check_proof = true;
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::InvalidProof);
}
#[test]
fn regression_revoked_credential_yields_revoked_status() {
let vc = valid_vc();
let mut policy = default_policy();
policy.revoked_credential_ids = vec![vc.id.clone().expect("test vc has an id")];
let result = VcVerifier::verify(&vc, &policy);
assert!(!result.is_valid());
assert_eq!(result.status, VerificationStatus::Revoked);
}
#[test]
fn regression_non_revoked_credential_passes_revocation() {
let vc = valid_vc();
let mut policy = default_policy();
policy.revoked_credential_ids = vec!["urn:uuid:some-other-vc".to_string()];
let result = VcVerifier::verify(&vc, &policy);
assert!(result.is_valid());
assert!(result.checks_passed.contains(&"revocation".to_string()));
}
#[test]
fn test_is_trusted_issuer_in_list() {
let trusted = vec!["did:example:a".to_string(), "did:example:b".to_string()];
assert!(VcVerifier::is_trusted_issuer("did:example:a", &trusted));
}
#[test]
fn test_is_trusted_issuer_not_in_list() {
let trusted = vec!["did:example:a".to_string()];
assert!(!VcVerifier::is_trusted_issuer(
"did:example:other",
&trusted
));
}
#[test]
fn test_is_trusted_issuer_empty_list() {
assert!(VcVerifier::is_trusted_issuer("did:example:anyone", &[]));
}
#[test]
fn test_has_vc_context_true() {
let vc = valid_vc();
assert!(VcVerifier::has_vc_context(&vc));
}
#[test]
fn test_has_vc_context_false() {
let mut vc = valid_vc();
vc.context = vec!["https://example.org/other".to_string()];
assert!(!VcVerifier::has_vc_context(&vc));
}
#[test]
fn test_has_required_type_true() {
let vc = valid_vc();
assert!(VcVerifier::has_required_type(&vc));
}
#[test]
fn test_has_required_type_false() {
let mut vc = valid_vc();
vc.types = vec!["CustomType".to_string()];
assert!(!VcVerifier::has_required_type(&vc));
}
#[test]
fn test_has_credential_subject_true() {
let vc = valid_vc();
assert!(VcVerifier::has_credential_subject(&vc));
}
#[test]
fn test_has_credential_subject_false() {
let mut vc = valid_vc();
vc.credential_subject = HashMap::new();
assert!(!VcVerifier::has_credential_subject(&vc));
}
#[test]
fn test_verification_result_is_valid() {
let mut r = VerificationResult {
status: VerificationStatus::Valid,
checks_passed: vec![],
checks_failed: vec![],
warnings: vec![],
};
assert!(r.is_valid());
r.status = VerificationStatus::Expired;
assert!(!r.is_valid());
}
#[test]
fn test_verification_result_add_pass_fail_warn() {
let mut r = VerificationResult {
status: VerificationStatus::Valid,
checks_passed: vec![],
checks_failed: vec![],
warnings: vec![],
};
r.add_pass("check_a");
r.add_fail("check_b");
r.add_warning("warn_c");
assert_eq!(r.checks_passed, vec!["check_a"]);
assert_eq!(r.checks_failed, vec!["check_b"]);
assert_eq!(r.warnings, vec!["warn_c"]);
}
#[test]
fn test_checks_passed_on_valid() {
let vc = valid_vc();
let result = VcVerifier::verify(&vc, &default_policy());
assert!(result.checks_passed.contains(&"required_type".to_string()));
assert!(result.checks_passed.contains(&"vc_context".to_string()));
assert!(result
.checks_passed
.contains(&"credential_subject".to_string()));
}
#[test]
fn test_checks_failed_on_invalid_type() {
let mut vc = valid_vc();
vc.types = vec![];
let result = VcVerifier::verify(&vc, &default_policy());
assert!(result.checks_failed.contains(&"required_type".to_string()));
}
#[test]
fn test_warning_for_missing_id() {
let mut vc = valid_vc();
vc.id = None;
let result = VcVerifier::verify(&vc, &default_policy());
assert!(result.is_valid());
assert!(!result.warnings.is_empty());
}
#[test]
fn test_parse_date_ms_monotone() {
let t1 = VcVerifier::parse_date_ms("2020-01-01T00:00:00Z").expect("t1");
let t2 = VcVerifier::parse_date_ms("2021-01-01T00:00:00Z").expect("t2");
assert!(t2 > t1);
}
#[test]
fn test_parse_date_ms_invalid_month() {
assert!(VcVerifier::parse_date_ms("2020-13-01").is_none());
}
}