use std::collections::HashMap;
use affinidi_openid4vp::dcql::{
CandidateCredential, TrustedAuthoritiesQuery, TrustedAuthorityType,
};
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use tracing::{debug, warn};
use vti_common::error::AppError;
use crate::ceremony::{CredentialStatus, Invitation, Presentation};
use crate::routes::join_requests::manifest::{ServedCriterion, manifest_criterion};
use crate::schemas::accepts::{
AcceptsCriterion, Admission, CredentialIssuers, published_criteria, superseded_version,
};
use crate::server::AppState;
use crate::vetting::VettingFacts;
use crate::vetting::challenge_refusal::ChallengeRefusal;
pub const LDP_VC_FORMAT: &str = "ldp_vc";
pub const SD_JWT_VC_FORMAT: &str = "dc+sd-jwt";
pub const NEED_CREDENTIALS: &str = "credentials";
pub const NEED_INVITATION: &str = "invitation";
pub const NEED_CRITERION: &str = "criterion";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CriterionFact {
pub id: String,
pub requirements_digest: String,
pub admission: Admission,
pub met: bool,
#[serde(default)]
pub needs: Vec<String>,
pub cited: bool,
#[serde(default)]
pub superseded: bool,
}
#[derive(Debug, Clone)]
pub struct Governing {
pub served: ServedCriterion,
pub fact: CriterionFact,
pub vetting: Option<VettingFacts>,
}
impl Governing {
pub fn presentation_definition(&self) -> Option<JsonValue> {
self.fact
.needs
.iter()
.any(|n| n == NEED_CREDENTIALS)
.then(|| self.served.stored.query.clone())
.flatten()
}
}
#[derive(Debug)]
pub enum CriterionRefusal {
NotAccepting,
Unknown(String),
Challenge(ChallengeRefusal),
Other(AppError),
}
impl From<AppError> for CriterionRefusal {
fn from(e: AppError) -> Self {
Self::Other(e)
}
}
impl From<crate::vetting::FactsError> for CriterionRefusal {
fn from(e: crate::vetting::FactsError) -> Self {
match e {
crate::vetting::FactsError::Challenge(r) => Self::Challenge(r),
crate::vetting::FactsError::App(e) => Self::Other(e),
}
}
}
#[derive(Debug, Clone)]
pub struct PresentedCredential {
pub formats: Vec<&'static str>,
pub types: Vec<String>,
pub claims: JsonValue,
pub issuer: String,
pub issuer_authority_key_ids: Vec<String>,
pub only_for: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Presented {
pub credentials: Vec<PresentedCredential>,
pub invitation: Option<Invitation>,
pub vp: Option<JsonValue>,
pub extensions: JsonValue,
}
pub async fn govern(
state: &AppState,
applicant_did: &str,
presented: &Presented,
cited: Option<&str>,
now: DateTime<Utc>,
) -> Result<Governing, CriterionRefusal> {
let published = published_criteria(&state.schemas_ks).await?;
if published.is_empty() {
return Err(CriterionRefusal::NotAccepting);
}
let mut ctx = Evaluator::new(state, applicant_did, presented, now).await;
if let Some(digest) = cited {
for stored in &published {
let served = manifest_criterion(stored.clone())?;
if served.digest == digest {
return ctx.evaluate(served, true, false).await;
}
}
if let Some(version) = superseded_version(&state.schemas_ks, digest).await?
&& version.within_grace(now)
{
let served = manifest_criterion(version.criterion)?;
return ctx.evaluate(served, true, true).await;
}
return Err(CriterionRefusal::Unknown(digest.to_string()));
}
let mut first = None;
for stored in published {
let served = manifest_criterion(stored)?;
let governing = ctx.evaluate(served, false, false).await?;
if governing.fact.met {
return Ok(governing);
}
first.get_or_insert(governing);
}
Ok(first.expect("published is not empty"))
}
pub async fn served_by_digest(
state: &AppState,
digest: &str,
now: DateTime<Utc>,
) -> Result<Option<ServedCriterion>, AppError> {
for stored in published_criteria(&state.schemas_ks).await? {
let served = manifest_criterion(stored)?;
if served.digest == digest {
return Ok(Some(served));
}
}
match superseded_version(&state.schemas_ks, digest).await? {
Some(version) if version.within_grace(now) => {
Ok(Some(manifest_criterion(version.criterion)?))
}
_ => Ok(None),
}
}
pub async fn issuer_admitted(
state: &AppState,
own_did: Option<&str>,
issuers: CredentialIssuers,
issuer: &str,
) -> bool {
let own = own_did == Some(issuer);
match issuers {
CredentialIssuers::Any => true,
CredentialIssuers::Community => own,
CredentialIssuers::Recognised => {
if own
|| crate::routes::join_requests::present::issuer_trusted(
state.registry_client.as_deref(),
own_did,
issuer,
)
.await
{
return true;
}
let swiyu = state.config.read().await.oid4vp.swiyu_trust.clone();
match swiyu {
Some(swiyu) => {
crate::oid4vp::swiyu_issuer::trusted(state, &swiyu, issuer, chrono::Utc::now())
.await
}
None => false,
}
}
}
}
pub async fn govern_by_id(
state: &AppState,
applicant_did: &str,
presented: &Presented,
id: Option<&str>,
now: DateTime<Utc>,
) -> Result<Governing, CriterionRefusal> {
let Some(id) = id else {
return govern(state, applicant_did, presented, None, now).await;
};
let Some(stored) = crate::schemas::accepts::get_accepts(&state.schemas_ks, id).await? else {
return Err(CriterionRefusal::Unknown(id.to_string()));
};
let served = manifest_criterion(stored)?;
let mut ctx = Evaluator::new(state, applicant_did, presented, now).await;
ctx.evaluate(served, true, false).await
}
pub async fn govern_again(
state: &AppState,
applicant_did: &str,
presented: &Presented,
id: &str,
digest: &str,
now: DateTime<Utc>,
) -> Result<Governing, CriterionRefusal> {
let mut ctx = Evaluator::new(state, applicant_did, presented, now).await;
let current = crate::schemas::accepts::get_accepts(&state.schemas_ks, id).await?;
if let Some(stored) = current.clone() {
let served = manifest_criterion(stored)?;
if served.digest == digest {
return ctx.evaluate(served, true, false).await;
}
}
if let Some(version) = superseded_version(&state.schemas_ks, digest).await?
&& version.within_grace(now)
{
let served = manifest_criterion(version.criterion)?;
return ctx.evaluate(served, true, true).await;
}
match current {
Some(stored) => {
let served = manifest_criterion(stored)?;
ctx.evaluate(served, false, false).await
}
None => Err(CriterionRefusal::Unknown(digest.to_string())),
}
}
struct Evaluator<'a> {
state: &'a AppState,
applicant_did: &'a str,
presented: &'a Presented,
now: DateTime<Utc>,
own_did: Option<String>,
recognised: HashMap<String, bool>,
}
impl<'a> Evaluator<'a> {
async fn new(
state: &'a AppState,
applicant_did: &'a str,
presented: &'a Presented,
now: DateTime<Utc>,
) -> Self {
let own_did = state.config.read().await.vtc_did.clone();
Self {
state,
applicant_did,
presented,
now,
own_did,
recognised: HashMap::new(),
}
}
async fn evaluate(
&mut self,
served: ServedCriterion,
cited: bool,
superseded: bool,
) -> Result<Governing, CriterionRefusal> {
let stored = &served.stored;
let mut needs = Vec::new();
if let Some(query) = stored.dcql()?
&& !self.credentials_meet(stored, &served.digest, &query).await
{
needs.push(NEED_CREDENTIALS.to_string());
}
let vetting = match &self.presented.vp {
Some(vp) => {
crate::vetting::vetting_facts(
self.state,
self.applicant_did,
vp,
&self.presented.extensions,
&served,
cited,
self.now,
)
.await?
}
None => None,
};
if stored.vetting.is_some() {
match &vetting {
Some(facts) => needs.extend(vetting_shortfall(facts, self.vetting_invitation())),
None => needs.push(crate::vetting::NEED_VETTING.to_string()),
}
}
if stored.invitation_required && !self.own_invitation() {
needs.push(NEED_INVITATION.to_string());
}
let fact = CriterionFact {
id: stored.id.clone(),
requirements_digest: served.digest.clone(),
admission: stored.admission,
met: needs.is_empty(),
needs,
cited,
superseded,
};
debug!(
criterion = %fact.id,
met = fact.met,
needs = ?fact.needs,
"join criterion evaluated"
);
Ok(Governing {
served,
fact,
vetting,
})
}
fn own_invitation(&self) -> bool {
self.presented.invitation.as_ref().is_some_and(|inv| {
inv.verified && !inv.consumed && self.own_did.as_deref() == Some(inv.issuer.as_str())
})
}
fn vetting_invitation(&self) -> bool {
self.presented
.invitation
.as_ref()
.is_some_and(|inv| inv.verified && inv.issuer_trusted && !inv.consumed)
}
async fn credentials_meet(
&mut self,
stored: &AcceptsCriterion,
digest: &str,
query: &affinidi_openid4vp::DcqlQuery,
) -> bool {
let issuers = stored
.credential_issuers
.unwrap_or(CredentialIssuers::Community);
let mut admitted = Vec::new();
for c in &self.presented.credentials {
if c.only_for.as_deref().is_some_and(|d| d != digest) {
continue;
}
if self.issuer_admitted(issuers, &c.issuer).await {
admitted.push(c);
}
}
query_met_by(query, &admitted)
}
async fn issuer_admitted(&mut self, issuers: CredentialIssuers, issuer: &str) -> bool {
if issuers != CredentialIssuers::Recognised {
return issuer_admitted(self.state, self.own_did.as_deref(), issuers, issuer).await;
}
if let Some(known) = self.recognised.get(issuer) {
return *known;
}
let known = issuer_admitted(self.state, self.own_did.as_deref(), issuers, issuer).await;
self.recognised.insert(issuer.to_string(), known);
known
}
}
fn query_met_by(
query: &affinidi_openid4vp::DcqlQuery,
credentials: &[&PresentedCredential],
) -> bool {
let (query, type_selectors) = with_type_values_as_keys(query);
let (query, authority_gates) = with_trusted_authorities_as_keys(&query);
let mut candidates = Vec::new();
for (n, c) in credentials.iter().enumerate() {
let selected_by = type_selectors
.iter()
.filter(|(_, alternatives)| {
alternatives
.iter()
.any(|all| all.iter().all(|t| c.types.contains(t)))
})
.map(|(key, _)| key.clone());
let mut keys: Vec<String> = c.types.iter().cloned().chain(selected_by).collect();
let gated: Vec<String> = authority_gates
.iter()
.filter(|(_, authorities)| {
meets_trusted_authorities(authorities, &c.issuer_authority_key_ids)
})
.flat_map(|(id, _)| {
std::iter::once(trusted_key(id, None))
.chain(keys.iter().map(|k| trusted_key(id, Some(k))))
})
.collect();
keys.extend(gated);
for format in &c.formats {
for vct in &keys {
candidates.push(CandidateCredential {
id: n.to_string(),
format: (*format).to_string(),
claims: c.claims.clone(),
vct: Some(vct.clone()),
doctype: None,
supports_holder_binding: true,
});
}
}
}
query.match_credentials(&candidates).is_ok()
}
fn with_type_values_as_keys(
query: &affinidi_openid4vp::DcqlQuery,
) -> (
affinidi_openid4vp::DcqlQuery,
Vec<(String, Vec<Vec<String>>)>,
) {
let mut query = query.clone();
let mut selectors = Vec::new();
for cq in &mut query.credentials {
let Some(meta) = cq.meta.as_mut() else {
continue;
};
if meta.contains_key("vct_values") {
continue;
}
let Some(type_values) = meta.remove("type_values") else {
continue;
};
let alternatives: Vec<Vec<String>> = type_values
.as_array()
.into_iter()
.flatten()
.map(|alt| match alt {
JsonValue::Array(types) => types
.iter()
.filter_map(JsonValue::as_str)
.map(str::to_string)
.collect(),
JsonValue::String(t) => vec![t.clone()],
_ => Vec::new(),
})
.filter(|alt: &Vec<String>| !alt.is_empty())
.collect();
let key = format!("urn:vtc:dcql:type-values:{}", cq.id);
meta.insert("vct_values".into(), json!([key]));
selectors.push((key, alternatives));
}
(query, selectors)
}
fn trusted_key(query_id: &str, type_key: Option<&str>) -> String {
match type_key {
Some(k) => format!("urn:vtc:dcql:trusted:{query_id}:{k}"),
None => format!("urn:vtc:dcql:trusted:{query_id}"),
}
}
fn with_trusted_authorities_as_keys(
query: &affinidi_openid4vp::DcqlQuery,
) -> (
affinidi_openid4vp::DcqlQuery,
Vec<(String, Vec<TrustedAuthoritiesQuery>)>,
) {
let mut query = query.clone();
let mut gates = Vec::new();
for cq in &mut query.credentials {
let Some(authorities) = cq.trusted_authorities.take() else {
continue;
};
let meta = cq.meta.get_or_insert_with(Default::default);
let keys: Vec<String> = match meta.get("vct_values").and_then(JsonValue::as_array) {
Some(types) => types
.iter()
.filter_map(JsonValue::as_str)
.map(|t| trusted_key(&cq.id, Some(t)))
.collect(),
None => vec![trusted_key(&cq.id, None)],
};
meta.insert("vct_values".into(), json!(keys));
gates.push((cq.id.clone(), authorities));
}
(query, gates)
}
pub fn meets_trusted_authorities(
authorities: &[TrustedAuthoritiesQuery],
authority_key_ids: &[String],
) -> bool {
let decode = |v: &str| URL_SAFE_NO_PAD.decode(v.trim_end_matches('=')).ok();
let held: Vec<Vec<u8>> = authority_key_ids.iter().filter_map(|v| decode(v)).collect();
authorities.iter().any(|entry| {
entry.authority_type == TrustedAuthorityType::Aki
&& entry
.values
.iter()
.filter_map(|v| decode(v))
.any(|id| !id.is_empty() && held.contains(&id))
})
}
fn vetting_shortfall(facts: &VettingFacts, invitation_held: bool) -> Vec<String> {
let mut needs = Vec::new();
if !facts.commitments_consistent {
needs.push("vetting:consistency".to_string());
}
needs.extend(facts.needs.iter().cloned());
if facts.commitments_consistent && facts.needs.is_empty() && !facts.independence_ok {
needs.push("vetting:independence".to_string());
}
if !facts.satisfied && needs.is_empty() {
needs.push(crate::vetting::NEED_VETTING.to_string());
}
if facts.satisfied && facts.invitation_required && !invitation_held {
needs.push(crate::vetting::NEED_INVITATION.to_string());
}
needs
}
pub fn expand_needs(needs: &mut Vec<String>, fact: Option<&CriterionFact>) {
let Some(fact) = fact else {
return;
};
if fact.needs.is_empty() {
return;
}
if let Some(pos) = needs.iter().position(|n| n == NEED_CRITERION) {
needs.splice(pos..=pos, fact.needs.iter().cloned());
}
}
pub async fn verify_embedded_credentials(
state: &AppState,
applicant_did: &str,
vp: &JsonValue,
now: DateTime<Utc>,
) -> Result<Vec<PresentedCredential>, AppError> {
let Some(vcs) = vp.get("verifiableCredential").and_then(JsonValue::as_array) else {
return Ok(Vec::new());
};
if vcs.is_empty() {
return Ok(Vec::new());
}
let resolver = crate::credentials::vm_resolver::DidVmResolver::new(state.did_resolver.clone());
let fetcher = crate::routes::join_requests::present::credential_status_fetcher(state);
let aliases = type_aliases(state).await?;
let trusted_witnesses = crate::credentials::trusted_witnesses::live_for(state).await;
let vm_resolver = state.trust_task_vm_resolver();
let witness_ctx = crate::credentials::witness::WitnessContext {
relationships_ks: &state.relationships_ks,
trusted: &trusted_witnesses,
evidence: &[],
resolver: &vm_resolver,
};
let mut out = Vec::new();
for vc in vcs.iter().filter(|v| v.is_object()) {
match verify_one_credential(vc, applicant_did, &resolver, &fetcher, now).await {
Ok((issuer, concrete)) if crate::credentials::witness::is_witness_statement(vc) => {
let verdict =
crate::credentials::witness::assess_or_unresolved(&witness_ctx, &issuer, vc)
.await;
if !verdict.is_bound() {
warn!(
applicant = %applicant_did,
witness = %issuer,
?verdict,
"presented witness credential does not count here — it meets no join \
criterion"
);
continue;
}
out.push(presented_credential(vc, issuer, concrete, &aliases));
}
Ok((issuer, concrete)) => {
out.push(presented_credential(vc, issuer, concrete, &aliases))
}
Err(reason) => warn!(
applicant = %applicant_did,
credential = vc.get("id").and_then(JsonValue::as_str).unwrap_or("<no id>"),
%reason,
"presented credential did not verify — it meets no join criterion"
),
}
}
Ok(out)
}
fn presented_credential(
vc: &JsonValue,
issuer: String,
concrete: String,
aliases: &HashMap<String, Vec<String>>,
) -> PresentedCredential {
let mut types: Vec<String> = match vc.get("type") {
Some(JsonValue::Array(t)) => t
.iter()
.filter_map(JsonValue::as_str)
.map(str::to_string)
.collect(),
Some(JsonValue::String(t)) => vec![t.clone()],
_ => Vec::new(),
};
if !types.contains(&concrete) {
types.push(concrete.clone());
}
if let Some(more) = aliases.get(&concrete) {
types.extend(more.iter().cloned());
}
PresentedCredential {
formats: vec![LDP_VC_FORMAT],
types,
claims: vc.clone(),
issuer,
issuer_authority_key_ids: Vec::new(),
only_for: None,
}
}
async fn type_aliases(state: &AppState) -> Result<HashMap<String, Vec<String>>, AppError> {
let mut aliases: HashMap<String, Vec<String>> = HashMap::new();
for entry in crate::schemas::list_schemas(&state.schemas_ks).await? {
if let Some(dtg) = entry.dtg_type
&& dtg != entry.type_uri
{
aliases.entry(dtg).or_default().push(entry.type_uri);
}
}
Ok(aliases)
}
async fn verify_one_credential(
vc: &JsonValue,
applicant_did: &str,
resolver: &dyn vti_common::auth::PurposeVmResolver,
fetcher: &dyn crate::recognition::StatusListFetcher,
now: DateTime<Utc>,
) -> Result<(String, String), String> {
let issuer = match vc.get("issuer") {
Some(JsonValue::String(s)) => s.clone(),
Some(JsonValue::Object(o)) => o
.get("id")
.and_then(JsonValue::as_str)
.ok_or("issuer has no id")?
.to_string(),
_ => return Err("no issuer".into()),
};
let subject = vc
.pointer("/credentialSubject/id")
.and_then(JsonValue::as_str)
.ok_or("no credentialSubject.id")?;
if subject.split('#').next().unwrap_or(subject) != applicant_did {
return Err(format!("its subject ({subject}) is not the applicant"));
}
let concrete = crate::credentials::ingress::concrete_type(vc).ok_or("no concrete type")?;
crate::credentials::ingress::check_validity_window(vc, now, "credential")
.map_err(|e| e.to_string())?;
crate::credentials::proof_set::verify_issued(vc, &issuer, resolver).await?;
match crate::routes::join_requests::present::resolve_presented_status(
vc.get("credentialStatus"),
Some(&issuer),
fetcher,
)
.await
{
CredentialStatus::Valid => Ok((issuer, concrete)),
other => Err(format!("its status is {other:?}")),
}
}
pub fn presented_from_verified(
presentation: &Presentation,
verified: &[crate::credentials::VerifiedPresentation],
) -> Vec<PresentedCredential> {
let aligned = presentation.credentials.len() == verified.len();
presentation
.credentials
.iter()
.enumerate()
.filter(|(_, c)| c.holder_bound && c.status == CredentialStatus::Valid)
.filter(|(_, c)| {
c.witness_binding
.as_ref()
.is_none_or(crate::credentials::witness::WitnessBinding::is_bound)
})
.map(|(n, c)| PresentedCredential {
formats: vec![SD_JWT_VC_FORMAT, LDP_VC_FORMAT],
types: vec![c.credential_type.clone()],
claims: match c.claims.get("credentialSubject") {
Some(_) => c.claims.clone(),
None => {
let mut flat = c.claims.clone();
if let Some(map) = flat.as_object_mut() {
map.insert("credentialSubject".into(), json!(c.claims));
}
flat
}
},
issuer: c.issuer.clone(),
issuer_authority_key_ids: verified
.get(n)
.filter(|_| aligned)
.map(|p| p.issuer_authority_key_ids.clone())
.unwrap_or_default(),
only_for: None,
})
.collect()
}
pub fn presented_from_wallet(
requirements_digest: &str,
credentials: &[crate::oid4vp::session::EvidenceCredential],
) -> Vec<PresentedCredential> {
credentials
.iter()
.filter_map(|c| {
let format = match c.format.as_str() {
SD_JWT_VC_FORMAT => SD_JWT_VC_FORMAT,
LDP_VC_FORMAT => LDP_VC_FORMAT,
_ => return None,
};
Some(PresentedCredential {
formats: vec![format],
types: c.vct.iter().cloned().collect(),
claims: c.claims.clone(),
issuer: c.issuer.clone(),
issuer_authority_key_ids: c.issuer_authority_key_ids.clone(),
only_for: Some(requirements_digest.to_string()),
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn vetting(
consistent: bool,
independent: bool,
needs: &[&str],
satisfied: bool,
) -> VettingFacts {
VettingFacts {
criterion_id: "k".into(),
requirements_digest: "zK".into(),
applicant_digest_matches: true,
statements: vec![],
distinct_counted_vetters: 0,
by_method: Default::default(),
commitments_consistent: consistent,
independence_ok: independent,
invitation_required: false,
satisfied,
needs: needs.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn an_unmet_vetting_requirement_always_names_what_is_missing() {
assert_eq!(
vetting_shortfall(
&vetting(true, true, &["vetting:statements:1"], false),
false
),
["vetting:statements:1"]
);
assert_eq!(
vetting_shortfall(&vetting(false, true, &[], false), false),
["vetting:consistency"]
);
assert_eq!(
vetting_shortfall(&vetting(true, false, &[], false), false),
["vetting:independence"]
);
assert!(vetting_shortfall(&vetting(true, true, &[], true), false).is_empty());
}
#[test]
fn a_vetting_invitation_requirement_is_part_of_the_vetting() {
let mut facts = vetting(true, true, &[], true);
facts.invitation_required = true;
assert_eq!(
vetting_shortfall(&facts, false),
[crate::vetting::NEED_INVITATION]
);
assert!(vetting_shortfall(&facts, true).is_empty());
}
fn ldp(types: &[&str], subject: serde_json::Value) -> PresentedCredential {
PresentedCredential {
formats: vec![LDP_VC_FORMAT],
types: types.iter().map(|t| t.to_string()).collect(),
claims: json!({ "type": types, "credentialSubject": subject }),
issuer: "did:web:issuer.example".into(),
issuer_authority_key_ids: Vec::new(),
only_for: None,
}
}
fn query(v: serde_json::Value) -> affinidi_openid4vp::DcqlQuery {
affinidi_openid4vp::DcqlQuery::from_json(&v).unwrap()
}
fn sd_jwt(vct: &str, akis: &[&str]) -> PresentedCredential {
PresentedCredential {
formats: vec![SD_JWT_VC_FORMAT],
types: vec![vct.to_string()],
claims: json!({ "vct": vct }),
issuer: "https://pid.example.org".into(),
issuer_authority_key_ids: akis.iter().map(|a| a.to_string()).collect(),
only_for: None,
}
}
const PID: &str = "urn:eudi:pid:1";
const TRUSTED: &str = "s9tIpPmhxdiuNkHMEWNpYim8S8Y";
const OTHER: &str = "AQIDBAUGBwgJCgsMDQ4PEBESExQ";
#[test]
fn trusted_authorities_admits_only_an_issuer_a_named_authority_certifies() {
let q = query(json!({ "credentials": [{
"id": "pid",
"format": "dc+sd-jwt",
"meta": { "vct_values": [PID] },
"trusted_authorities": [{ "type": "aki", "values": [OTHER, TRUSTED] }]
}]}));
assert!(query_met_by(&q, &[&sd_jwt(PID, &[TRUSTED])]));
assert!(query_met_by(
&q,
&[&sd_jwt(PID, &["s9tIpPmhxdiuNkHMEWNpYim8S8Y="])]
));
assert!(!query_met_by(&q, &[&sd_jwt(PID, &["AAAA"])]));
assert!(!query_met_by(&q, &[&sd_jwt(PID, &[])]));
assert!(!query_met_by(&q, &[&sd_jwt("urn:other:1", &[TRUSTED])]));
}
#[test]
fn trusted_authorities_gates_only_its_own_query() {
let q = query(json!({ "credentials": [
{
"id": "pid",
"format": "dc+sd-jwt",
"meta": { "vct_values": [PID] },
"trusted_authorities": [{ "type": "aki", "values": [TRUSTED] }]
},
{ "id": "email", "format": "dc+sd-jwt", "meta": { "vct_values": ["urn:email:1"] } }
]}));
let pid = sd_jwt(PID, &[TRUSTED]);
let email = sd_jwt("urn:email:1", &[]);
assert!(query_met_by(&q, &[&pid, &email]));
assert!(!query_met_by(&q, &[&sd_jwt(PID, &[OTHER]), &email]));
}
#[test]
fn an_unchecked_trusted_authority_type_is_met_by_nothing() {
let entry = |t: &str| {
serde_json::from_value::<Vec<TrustedAuthoritiesQuery>>(
json!([{ "type": t, "values": [TRUSTED] }]),
)
.unwrap()
};
assert!(meets_trusted_authorities(&entry("aki"), &[TRUSTED.into()]));
assert!(!meets_trusted_authorities(
&entry("etsi_tl"),
&[TRUSTED.into()]
));
assert!(!meets_trusted_authorities(
&entry("openid_federation"),
&[TRUSTED.into()]
));
}
#[test]
fn a_type_values_query_selects_only_credentials_of_that_type() {
let q = query(json!({ "credentials": [{
"id": "membership",
"format": "ldp_vc",
"meta": { "type_values": [["VerifiableCredential", "MembershipCredential"]] }
}]}));
let membership = ldp(&["VerifiableCredential", "MembershipCredential"], json!({}));
let other = ldp(&["VerifiableCredential", "EmailCredential"], json!({}));
assert!(query_met_by(&q, &[&membership]));
assert!(!query_met_by(&q, &[&other]));
assert!(!query_met_by(&q, &[]));
}
#[test]
fn any_one_type_values_alternative_selects() {
let q = query(json!({ "credentials": [{
"id": "proof-of-employment",
"format": "ldp_vc",
"meta": { "type_values": [["EmployeeCredential"], ["ContractorCredential", "Vetted"]] }
}]}));
assert!(query_met_by(
&q,
&[&ldp(&["EmployeeCredential"], json!({}))]
));
assert!(!query_met_by(
&q,
&[&ldp(&["ContractorCredential"], json!({}))]
));
assert!(query_met_by(
&q,
&[&ldp(&["ContractorCredential", "Vetted"], json!({}))]
));
}
#[test]
fn a_claims_constraint_reads_the_credential() {
let q = query(json!({ "credentials": [{
"id": "membership",
"format": "ldp_vc",
"meta": { "type_values": [["MembershipCredential"]] },
"claims": [{ "path": ["credentialSubject", "level"], "values": ["full"] }]
}]}));
let full = ldp(&["MembershipCredential"], json!({ "level": "full" }));
let associate = ldp(&["MembershipCredential"], json!({ "level": "associate" }));
assert!(query_met_by(&q, &[&full]));
assert!(!query_met_by(&q, &[&associate]));
}
#[test]
fn the_default_credential_criterion_is_met_by_a_membership_credential() {
let defaults = crate::schemas::accepts::default_criteria(true);
let q = defaults[1].dcql().unwrap().expect("a query");
let vmc = ldp(
&[
"VerifiableCredential",
"DTGCredential",
"MembershipCredential",
],
json!({}),
);
let vic = ldp(
&[
"VerifiableCredential",
"DTGCredential",
"InvitationCredential",
],
json!({}),
);
assert!(query_met_by(&q, &[&vmc]));
assert!(!query_met_by(&q, &[&vic]));
}
#[test]
fn the_generic_need_becomes_what_the_criterion_lacks() {
let fact = CriterionFact {
id: "k".into(),
requirements_digest: "zK".into(),
admission: Admission::Automatic,
met: false,
needs: vec![NEED_INVITATION.into(), NEED_CREDENTIALS.into()],
cited: false,
superseded: false,
};
let mut needs = vec!["agreed:code-of-conduct".to_string(), NEED_CRITERION.into()];
expand_needs(&mut needs, Some(&fact));
assert_eq!(
needs,
["agreed:code-of-conduct", NEED_INVITATION, NEED_CREDENTIALS]
);
}
#[test]
fn a_witness_counts_toward_a_criterion_only_when_bound() {
use crate::ceremony::{Credential, CredentialStatus, Presentation};
use crate::credentials::witness::WitnessBinding;
let credential = |binding: Option<WitnessBinding>| Credential {
credential_type: "StatementCredential".into(),
predicate: binding
.as_ref()
.map(|_| dtg_credentials::WITNESSED_V1.into()),
issuer: "did:key:z6MkWitness".into(),
issuer_trusted: true,
status: CredentialStatus::Valid,
holder_bound: true,
claims: json!({ "credentialSubject": { "id": "did:key:zApplicant" } }),
valid_until: None,
witness_binding: binding,
task_context: None,
};
let bound = WitnessBinding::Bound {
relationship_id: uuid::Uuid::new_v4(),
};
for (binding, counts) in [
(None, true),
(Some(bound), true),
(Some(WitnessBinding::UntrustedWitness), false),
(Some(WitnessBinding::NoOutcomeEvidence), false),
(Some(WitnessBinding::OutcomeInvalid), false),
(Some(WitnessBinding::Unresolved), false),
] {
let presentation = Presentation {
verified: true,
holder: "did:key:zApplicant".into(),
credentials: vec![credential(binding.clone())],
};
assert_eq!(
presented_from_verified(&presentation, &[]).len(),
usize::from(counts),
"{binding:?}"
);
}
}
}