From 2e584a3aeea2e08b90f60f79e3cef47b5643b0ed Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Wed, 30 Sep 2026 17:18:16 +0200 Subject: [PATCH 1/9] feat(openvtc-core)!: hold every DTG credential to DTG Credentials v1 Every DTG credential the client issues, receives, verifies or stores now carries the v1 context (https://registry.trustoverip.org/dtg/context/v1, the pre-v1 context refused with no alias), a top-level issuerScope and exactly one concrete type, on dtg-credentials 0.12, vta-sdk 0.57, vta-service 0.46 and trust-tasks 0.24.8. - openvtc_core::dtg: the scope each issued credential declares (MEMBER_IDENTIFIER_SCOPE = directed for the persona DID a membership names; relationship_issuer_scope: pairwise for a relationship DID, directed for the persona DID; PERSONA_ANNOTATION_SCOPE = directed), parse_conformant, community_roles, and predicate-based statement display with a fail-closed core accept list. - Member acknowledgement: new_member_vmc_for with the signing persona, declaring directed; build_member_vmc takes the member DID. - VIC validation parses through dtg-credentials: v1 context second, issuerScope public, one concrete type. join::DTG_CONTEXT re-exports DTG_CONTEXT_V1. - CredentialKind::Role is a community role VAC (AuthorityCredential, authority.scope the community, role: actions); from_credential classifies conformant credentials only. - VRCs declare the scope of the identifier the relationship uses. - personhood AssertReply::role_vec -> role_vac (wire roleVac). - Stored credentials in a pre-v1 shape are set aside on load with a logged reason: CommunityRecord::retired_credentials (persisted, cleared by a conformant replacement), Vrcs::retired, dropped VRCIssued tasks, and RejectionReason::Nonconformant on a vault rebuild. The patch block this builds against is its own commit. Signed-off-by: Glenn Gore --- Cargo.toml | 40 ++- openvtc-core/Cargo.toml | 4 +- openvtc-core/src/config/account.rs | 224 +++++++++++++++-- openvtc-core/src/dtg.rs | 379 +++++++++++++++++++++++++++++ openvtc-core/src/identity.rs | 1 + openvtc-core/src/join.rs | 85 +++++-- openvtc-core/src/lib.rs | 79 ++++-- openvtc-core/src/members.rs | 122 +++++++--- openvtc-core/src/messaging.rs | 45 +++- openvtc-core/src/personhood.rs | 17 +- openvtc-core/src/rebuild.rs | 62 ++++- openvtc-core/src/tasks.rs | 37 ++- openvtc-core/src/vrc.rs | 122 +++++++++- 13 files changed, 1093 insertions(+), 124 deletions(-) create mode 100644 openvtc-core/src/dtg.rs diff --git a/Cargo.toml b/Cargo.toml index 10a4d61c..ed52b0a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,22 @@ openvtc-core = { version = "0.3", path = "openvtc-core", default-features = fals # round-trips a grant losslessly — the old hazard that digesting a parsed grant # dropped `credentialStatus` (guarded by a test in `members.rs`) is closed at the # library. -dtg-credentials = "0.9.1" +# +# **0.12 is DTG Credentials v1, and nothing crosses it.** The `@context` is +# `https://registry.trustoverip.org/dtg/context/v1` (the `firstperson.network` +# context is refused, no alias), `issuerScope` is REQUIRED, `type` carries +# exactly one concrete subtype, and the endorsement / witness types are +# statements (VSCs) under registry predicates. A 0.12 verifier refuses every +# credential 0.11 emitted and the reverse, so the whole stack (VTI, trust-tasks, +# this client) moves together; `openvtc-core::dtg` is where the client's side of +# it lives, and credentials an older build stored are set aside on load there. +# It crosses 0.10 and 0.11 too (`new_member_vmc` -> `new_member_vmc_for`, +# `taskDigestMultibase`). +# +# The requirement reads "0.11" only because the unreleased 0.12 is still +# versioned 0.11.0 in its worktree and the patch block at the bottom has to +# match it. Raise it to "0.12" when 0.12 is published, as the patch comes out. +dtg-credentials = "0.11" aes-gcm = "0.11" # The one direct requirement in this workspace that was behind its latest @@ -364,8 +379,14 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # `trust-tasks-rs ^0.24.2`, so this line moves with it — the same follow, not # lead, discipline this note has kept since 0.4. `trust-tasks-capability-client` # moves to 0.24.2 alongside it for the same re-exported-`TrustTask` reason. -trust-tasks-rs = { version = "0.24.2", features = ["git-ns"] } -trust-tasks-capability-client = "0.24.2" +# +# **0.24.8**, following `vta-sdk` 0.57: the DTG VSC conformance specs +# (trustoverip/dtgwg-trust-tasks-tf#691). `roleVec` is `roleVac` in the +# decide / personhood / renew / rotate responses, the vetting specs carry +# `vetted/1` statements and role VACs, and `vtc/endorsements/issue` gains +# `predicateNotIssuable`. +trust-tasks-rs = { version = "0.24.8", features = ["git-ns"] } +trust-tasks-capability-client = "0.24.8" tui-input = "0.15" url = "2.5" uuid = { version = "1.23", features = ["v4", "fast-rng", "serde"] } @@ -515,11 +536,21 @@ uuid = { version = "1.23", features = ["v4", "fast-rng", "serde"] } # node as everything else here. Check `cargo tree -d` after any # `cargo update` rather than assuming it stays closed. # +# **0.57** is the DTG Credentials v1 release: vetting statements are `vetted/1` +# VSCs built from the `vetting/session` document (`StatementDraft::session`, +# `issuer_scope`, `value`), role credentials are community VACs +# (`eligibility::community_roles`, `role_action`), and `role_vec` is `role_vac`. +# +# `did-git-sign` 0.5.2 still requires `vta-sdk ^0.56`, so until VGI releases on +# 0.57 the graph holds a second, registry `vta-sdk` beneath it. It builds — +# `did_git_sign::{config, init}` passes no sdk type across — but `cargo tree -d` +# shows it, and it closes only with a VGI release. +# # `vta-service` is a separate matter this cycle: see the `vta-service` note in # `openvtc-core/Cargo.toml` for why that pin stays at 0.43.0 rather than # following `vta-sdk` here to 0.56 — 0.44.0 is broken independent of what # `vta-sdk` line it is paired with. -vta-sdk = { version = "0.56.0", features = [ +vta-sdk = { version = "0.57.0", features = [ "session", "client", # The SAME per-platform credential-store registration pnm-cli uses @@ -600,3 +631,4 @@ strip = "symbols" # places, and writing down here what has to happen for it to leave again — the # entries removed each carried that note, and it is the only reason this unwind # and the one before it were tractable. + diff --git a/openvtc-core/Cargo.toml b/openvtc-core/Cargo.toml index b705e32a..209ded53 100644 --- a/openvtc-core/Cargo.toml +++ b/openvtc-core/Cargo.toml @@ -165,7 +165,9 @@ affinidi-messaging-test-mediator = "0.14.0" # resolve and never will. 0.45.0 is the republished family on `vta-sdk ^0.56` / # `vti-common ^0.29`, matching the root manifest. `cargo tree -i vti-common` # is the check: one row means the family agrees. -vta-service = { version = "0.45.0", default-features = false, features = ["test-support", "rest", "didcomm"] } +# +# **0.46.0** is the family on `vta-sdk` 0.57 (DTG Credentials v1). +vta-service = { version = "0.46.0", default-features = false, features = ["test-support", "rest", "didcomm"] } base64 = { workspace = true } ed25519-dalek-bip32 = { workspace = true } secrecy = { workspace = true } diff --git a/openvtc-core/src/config/account.rs b/openvtc-core/src/config/account.rs index f998da37..b1546146 100644 --- a/openvtc-core/src/config/account.rs +++ b/openvtc-core/src/config/account.rs @@ -370,9 +370,13 @@ pub struct CommunityRecord { pub vrcs_received: Vrcs, /// Verifiable credentials this VTC has issued to us, keyed by /// [`CredentialKind`]. The membership credential (VMC) lands here on - /// admission and activates the membership; the role endorsement (VEC) - /// arrives alongside. Stored as the signed W3C VC JSON. Empty until the - /// join is accepted and credentials arrive (R-B-8). + /// admission and activates the membership; the role credential (a + /// community VAC) arrives alongside. Stored as the signed W3C VC JSON. Empty + /// until the join is accepted and credentials arrive (R-B-8). + /// + /// Only conformant DTG credentials are held here: one stored by an earlier + /// build in a pre-v1 shape is moved to + /// [`retired_credentials`](Self::retired_credentials) on load. /// /// Persisted as a JSON object keyed by [`CredentialKind::config_key`]. /// Configs written before R19 used flat `membership_credential` / @@ -418,6 +422,17 @@ pub struct CommunityRecord { #[serde(default, skip_serializing_if = "Option::is_none")] pub relationship_identifier_default: Option, + /// Credentials this membership held that do not conform to the current DTG + /// Credentials specification — the retired pre-v1 context, no + /// `issuerScope`, a retired type such as the pre-v1 role endorsement — + /// and were set aside on load. See [`RetiredCredential`]. + /// + /// The credential itself is not kept: a non-conformant credential is not + /// one this client may present or answer, and holding it would only invite + /// that. What is kept is enough to tell the member what went and what to do. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub retired_credentials: Vec, + /// Fields written by a newer build, preserved verbatim (D19). See /// [`Account::extra`] for why. #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")] @@ -472,6 +487,8 @@ struct CommunityRecordShadow { decision: Option, #[serde(default)] relationship_identifier_default: Option, + #[serde(default)] + retired_credentials: Vec, // Legacy pre-R19 flat fields, folded into `credentials` below. #[serde(default)] membership_credential: Option, @@ -504,9 +521,44 @@ impl From for CommunityRecord { if let Some(vmc) = shadow.membership_credential { credentials.entry(CredentialKind::Membership).or_insert(vmc); } - if let Some(vec) = shadow.role_credential { - credentials.entry(CredentialKind::Role).or_insert(vec); + if let Some(role) = shadow.role_credential { + credentials.entry(CredentialKind::Role).or_insert(role); } + // Hold every stored credential to the current specification. A pre-v1 + // one is set aside with its reason rather than kept (a verifier would + // refuse it) or allowed to fail the load (one stale credential must not + // lock the member out of their config). + let mut retired_credentials = shadow.retired_credentials; + let now = Utc::now(); + credentials.retain(|kind, vc| match crate::dtg::nonconformance(vc) { + None => true, + Some(reason) => { + RetiredCredential::record( + &mut retired_credentials, + kind.config_key(), + vc, + reason, + now, + ); + false + } + }); + let member_vmc = match shadow.member_vmc { + Some(vc) => match crate::dtg::nonconformance(&vc) { + None => Some(vc), + Some(reason) => { + RetiredCredential::record( + &mut retired_credentials, + RetiredCredential::MEMBER_ACKNOWLEDGEMENT, + &vc, + reason, + now, + ); + None + } + }, + None => None, + }; CommunityRecord { // D19: carry forward whatever a newer build wrote. extra: shadow.extra, @@ -528,10 +580,86 @@ impl From for CommunityRecord { vrcs_issued: shadow.vrcs_issued, vrcs_received: shadow.vrcs_received, credentials, - member_vmc: shadow.member_vmc, + member_vmc, decision: shadow.decision, relationship_identifier_default: shadow.relationship_identifier_default, + retired_credentials, + } + } +} + +/// A credential a membership held that was set aside on load because it does +/// not conform to the current DTG Credentials specification. +/// +/// # Why credentials are set aside, not migrated +/// +/// DTG Credentials v1 makes every credential issued before it non-conformant: +/// the context changed (`https://registry.trustoverip.org/dtg/context/v1`, with +/// no alias for the old one), `issuerScope` became REQUIRED, and the role endorsement +/// credential was retired in favour of a community VAC. Every +/// digest changed with them. A stored credential cannot be rewritten into the +/// new shape — its proof covers the old bytes, and only its issuer can sign new +/// ones — so the only honest options were to keep it (and present something +/// every current verifier refuses) or to drop it. It is dropped, **explicitly**: +/// +/// - a `warn!` naming the membership, the kind and the parse error is logged; +/// - this record is persisted on the membership, so the member sees what went +/// and why in the Communities view, which asks them to have the community +/// re-issue it (its `vtc/members/renew` re-issues the VMC and role VAC); +/// - a conformant credential of the same kind arriving later clears it +/// ([`CommunityRecord::clear_retired`]). +/// +/// The member's own acknowledgement (`member_vmc`) is held to the same rule; it +/// digests the grant it acknowledges, so once the grant is re-issued a fresh one +/// is owed anyway. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RetiredCredential { + /// [`CredentialKind::config_key`] of what it was stored as, or + /// [`Self::MEMBER_ACKNOWLEDGEMENT`] for our own acknowledgement. + pub kind: String, + /// The credential's `id`, when it had one. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credential_id: Option, + /// Why it does not conform, as `dtg-credentials` put it. + pub reason: String, + /// When it was set aside. + pub retired_at: DateTime, +} + +impl RetiredCredential { + /// [`Self::kind`] of the member-issued acknowledgement VMC. + pub const MEMBER_ACKNOWLEDGEMENT: &'static str = "Acknowledgement"; + + fn record( + into: &mut Vec, + kind: &str, + vc: &serde_json::Value, + reason: String, + now: DateTime, + ) { + let credential_id = vc + .get("id") + .and_then(serde_json::Value::as_str) + .map(str::to_string); + tracing::warn!( + credential_kind = %kind, + credential_id = credential_id.as_deref().unwrap_or("-"), + %reason, + "setting aside a stored credential that does not conform to DTG Credentials v1 — \ + the community must re-issue it" + ); + if into + .iter() + .any(|r| r.kind == kind && r.credential_id == credential_id) + { + return; } + into.push(RetiredCredential { + kind: kind.to_string(), + credential_id, + reason, + retired_at: now, + }); } } @@ -547,6 +675,14 @@ pub const PENDING_TIMEOUT_DAYS: i64 = 7; pub const PENDING_ACK_GRACE_SECS: i64 = 120; impl CommunityRecord { + /// Clear the [`RetiredCredential`] notices of `kind` (a + /// [`CredentialKind::config_key`] or + /// [`RetiredCredential::MEMBER_ACKNOWLEDGEMENT`]) once a conformant + /// replacement has been stored. + pub fn clear_retired(&mut self, kind: &str) { + self.retired_credentials.retain(|r| r.kind != kind); + } + /// Build a fresh `Pending` join record (State-B join request, R-B-*). /// /// `request_id` correlates the VTC's asynchronous accept/reject decision @@ -591,6 +727,7 @@ impl CommunityRecord { // Unknown until the community's profile is read (issue #241); // `None` seeds the pairwise default. relationship_identifier_default: None, + retired_credentials: Vec::new(), } } @@ -1224,6 +1361,7 @@ mod tests { member_vmc: None, decision: None, relationship_identifier_default: None, + retired_credentials: Vec::new(), } } @@ -1288,9 +1426,12 @@ mod tests { /// Pre-R19 configs stored credentials as flat `membership_credential` / /// `role_credential` fields. They must still load, folded into the typed - /// `credentials` registry (config round-trip — no migration). + /// `credentials` registry (config round-trip — no migration) — and, like + /// any stored credential, held to DTG Credentials v1 once folded in. #[test] fn legacy_flat_credential_fields_load_into_registry() { + let mut vmc = crate::dtg::fixtures::grant("did:webvh:vtc.example", "did:webvh:m"); + vmc["id"] = "vmc-1".into(); let legacy = serde_json::json!({ "vtc_did": "did:webvh:vtc.example", "display_name": "Example VTC", @@ -1299,8 +1440,10 @@ mod tests { "status": { "state": "active" }, "member_since": null, "requested_at": null, - "membership_credential": { "type": ["MembershipCredential"], "id": "vmc-1" }, - "role_credential": { "type": ["EndorsementCredential"], "id": "vec-1" }, + "membership_credential": vmc, + "role_credential": crate::dtg::fixtures::retired_role_endorsement( + "did:webvh:vtc.example", "did:webvh:m" + ), }); let rec: CommunityRecord = serde_json::from_value(legacy).unwrap(); assert_eq!( @@ -1310,13 +1453,59 @@ mod tests { .and_then(|v| v.as_str()), Some("vmc-1"), ); + // The pre-v1 role endorsement is set aside, with its reason, not kept. + assert!(!rec.credentials.contains_key(&CredentialKind::Role)); + assert_eq!(rec.retired_credentials.len(), 1); + assert_eq!(rec.retired_credentials[0].kind, "Role"); + } + + /// A stored credential that pre-dates DTG Credentials v1 neither fails the + /// load nor survives it: it is set aside, the reason is kept for the UI, the + /// notice survives a save, and a conformant replacement clears it. + #[test] + fn pre_v1_stored_credentials_are_set_aside_explicitly() { + let vtc = "did:webvh:vtc.example"; + let mut old_vmc = crate::dtg::fixtures::grant(vtc, "did:webvh:m"); + old_vmc["@context"][1] = crate::dtg::fixtures::RETIRED_CONTEXT.into(); + old_vmc.as_object_mut().unwrap().remove("issuerScope"); + let stored = serde_json::json!({ + "vtc_did": vtc, + "display_name": null, + "sub_context_id": "openvtc/x", + "persona_ref": PersonaId::new().0, + "status": { "state": "active" }, + "member_since": null, + "requested_at": null, + "credentials": { + "Membership": old_vmc, + "Role": crate::dtg::fixtures::retired_role_endorsement(vtc, "did:webvh:m"), + }, + "member_vmc": { "type": ["VerifiableCredential", "MembershipCredential"] }, + }); + let mut rec: CommunityRecord = serde_json::from_value(stored).expect("loads"); + assert!(rec.credentials.is_empty()); + assert!(rec.member_vmc.is_none()); + let kinds: Vec<&str> = rec + .retired_credentials + .iter() + .map(|r| r.kind.as_str()) + .collect(); assert_eq!( - rec.credentials - .get(&CredentialKind::Role) - .and_then(|v| v.get("id")) - .and_then(|v| v.as_str()), - Some("vec-1"), + kinds, + [ + "Membership", + "Role", + RetiredCredential::MEMBER_ACKNOWLEDGEMENT + ] ); + assert!(rec.retired_credentials.iter().all(|r| !r.reason.is_empty())); + + let saved = serde_json::to_value(&rec).unwrap(); + let again: CommunityRecord = serde_json::from_value(saved).unwrap(); + assert_eq!(again.retired_credentials, rec.retired_credentials); + + rec.clear_retired("Role"); + assert_eq!(rec.retired_credentials.len(), 2); } /// The new `credentials` object round-trips, serializes under stable @@ -1328,10 +1517,9 @@ mod tests { PersonaId::new(), CommunityStatus::Active, ); - rec.credentials.insert( - CredentialKind::Membership, - serde_json::json!({ "id": "vmc-1" }), - ); + let mut vmc = crate::dtg::fixtures::grant("did:webvh:vtc.example", "did:webvh:m"); + vmc["id"] = "vmc-1".into(); + rec.credentials.insert(CredentialKind::Membership, vmc); let json = serde_json::to_value(&rec).unwrap(); assert!( diff --git a/openvtc-core/src/dtg.rs b/openvtc-core/src/dtg.rs new file mode 100644 index 00000000..5b1bfc33 --- /dev/null +++ b/openvtc-core/src/dtg.rs @@ -0,0 +1,379 @@ +//! DTG Credentials conformance, in one place. +//! +//! Every DTG credential this client issues, receives, verifies, stores or shows +//! is held to the DTG Credentials Core Specification (the frozen v1 context, +//! a top-level `issuerScope`, exactly one concrete type) and, for a statement, +//! to the DTG VSC predicate registry. Three decisions live here rather than at +//! the call sites, so no two of them can disagree: +//! +//! 1. **Which `issuerScope` a credential this client issues declares** — +//! [`MEMBER_IDENTIFIER_SCOPE`], [`relationship_issuer_scope`] and +//! [`PERSONA_ANNOTATION_SCOPE`]. A declaration is about the issuer's own +//! identifier only, and it has to stay true for as long as the identifier is +//! used, so each is chosen from what OpenVTC actually does with that +//! identifier rather than from what a single exchange needs. +//! 2. **Whether a credential is conformant at all** — [`parse_conformant`]. A +//! credential under the retired pre-v1 context, without `issuerScope`, or of a +//! retired type (the endorsement and witness credential types, now statements +//! under a predicate) is refused, with no alias. +//! That is also the test stored state is held to on load +//! ([`crate::config::account::RetiredCredential`]). +//! 3. **What a statement says** — [`describe_statement`]. A VSC is classified by +//! its `credentialSubject.predicate`, never by a type string, and a predicate +//! this client does not know is shown as its IRI rather than refused or +//! guessed at. + +use dtg_credentials::{DTGCredential, DTGCredentialType, StatementObject}; +use serde_json::Value; + +pub use dtg_credentials::{ + DTG_CONTEXT_V1, ENDORSES_V1, IssuerScope, PRESENTED_V1, PredicateAcceptList, VETTED_V1, + W3C_VC_V2_CONTEXT, WITNESSED_V1, +}; + +// --------------------------------------------------------------------------- +// issuerScope: what this client declares +// --------------------------------------------------------------------------- + +/// The scope a member declares for the identifier it uses **with a community**: +/// the persona DID a membership names. +/// +/// `directed`, always, and deliberately not `pairwise`. In OpenVTC the member +/// identifier of a community is an account persona (`CommunityRecord::persona_ref`), +/// and a persona is by construction a set-of-counterparties identifier: +/// +/// - the community's other members see it — it issues relationship +/// credentials under it when the community declares +/// `relationshipIdentifierDefault: attributed`, it vets applicants under it +/// (`vetted/1` requires at least `directed`), and it presents it to them in a +/// vetter-eligibility presentation; +/// - one persona may be worn in several communities (a persona is chosen per +/// join, not minted per join). +/// +/// A `pairwise` declaration would have to stay true for the lifetime of the +/// membership, and none of those uses is one this client can rule out when it +/// acknowledges a grant. DTG Credentials, *Choosing a scope*: an identifier a +/// member "also uses with other members is `directed`", and a persona is +/// "ordinarily asserted" under a `directed` identifier. +/// +/// Used for the member-issued VMC ([`crate::members::build_member_vmc`]) and the +/// vetter's Vetting Statement ([`crate::vetting::VettingBook::statement_draft`]), +/// both of which are issued under that same persona DID. +pub const MEMBER_IDENTIFIER_SCOPE: IssuerScope = IssuerScope::Directed; + +/// The scope a persona annotation (VPC) declares for its issuer, the DID under +/// which the persona is asserted: `directed`, since a persona exists to be +/// recognised across counterparties the holder chooses (DTG Credentials §VPC). +pub const PERSONA_ANNOTATION_SCOPE: IssuerScope = IssuerScope::Directed; + +/// The scope a relationship credential (VRC) declares for its issuer — the +/// identifier **this relationship uses** on our side (`Relationship::our_did`). +/// +/// | our identifier in the relationship | community `relationshipIdentifierDefault` that seeds it | `issuerScope` | +/// |---|---|---| +/// | a relationship DID minted for this one counterparty | `pairwise` (or undeclared) | `pairwise` | +/// | the persona DID | `attributed` | `directed` | +/// +/// The community's `relationshipIdentifierDefault` only seeds the +/// new-relationship form (the member may toggle it per relationship), so the +/// scope is read from the identifier the relationship actually ended up with, +/// not from the default: a declaration is about the identifier in `issuer`, and +/// only that identifier's use decides whether `pairwise` is true. A persona DID +/// in a relationship is the legible graph an `attributed` community asks for — +/// the same identifier recognised by every counterparty the persona relates to, +/// which is `directed`, never `pairwise`. +#[must_use] +pub fn relationship_issuer_scope(issuer_is_persona: bool) -> IssuerScope { + if issuer_is_persona { + IssuerScope::Directed + } else { + IssuerScope::Pairwise + } +} + +// --------------------------------------------------------------------------- +// Conformance +// --------------------------------------------------------------------------- + +/// Parse `value` as a conformant DTG credential: the W3C v2 context first and +/// [`DTG_CONTEXT_V1`] second, a declared `issuerScope`, `VerifiableCredential`, +/// `DTGCredential` and exactly one concrete subtype, and — for a statement — +/// its predicate profile. +/// +/// The proof is set aside first: this answers "is this a DTG credential of the +/// current specification", not "is it genuine". Whether the proof verifies is +/// [`crate::issued_credential`]'s question, asked separately. +/// +/// # Errors +/// +/// A sentence naming what is non-conformant, e.g. the retired context or type. +pub fn parse_conformant(value: &Value) -> Result { + let mut unsigned = value.clone(); + if let Some(object) = unsigned.as_object_mut() { + object.remove("proof"); + } else { + return Err("not a JSON object".to_string()); + } + serde_json::from_value::(unsigned).map_err(|e| e.to_string()) +} + +/// `None` when `value` is a conformant DTG credential, otherwise why it is not. +#[must_use] +pub fn nonconformance(value: &Value) -> Option { + parse_conformant(value).err() +} + +/// The roles a **community role credential** confers: a VAC (`AuthorityCredential`) +/// the community issued in its own scope — `authority.scope` is its `issuer`, +/// `issuerScope` is `public`, no `authority.parent` — carrying `role:` +/// actions. `None` for anything else, including a VAC someone attenuated. +/// +/// This is the shape `vtc/join-requests/decide`, `vtc/members/*` and +/// `vtc/vetting/vetters/grant` deliver a role in, and the one +/// `vta_sdk::vetting::eligibility` accepts from a vetter. +#[must_use] +pub fn community_roles(credential: &DTGCredential) -> Option> { + if credential.type_() != DTGCredentialType::Authority + || credential.issuer_scope() != IssuerScope::Public + { + return None; + } + let authority = credential.credential().authority()?; + if authority.scope != credential.issuer() || authority.parent.is_some() { + return None; + } + let roles: Vec = authority + .actions + .iter() + .filter_map(|a| vta_sdk::protocols::vetting::role_of_action(a)) + .map(str::to_string) + .collect(); + (!roles.is_empty()).then_some(roles) +} + +// --------------------------------------------------------------------------- +// Statements (VSCs): classified and shown by predicate +// --------------------------------------------------------------------------- + +/// The predicates this client accepts in a statement it acts on: the four core +/// profiles of the DTG VSC predicate registry. Fails closed — a statement under +/// any other predicate is shown (see [`describe_statement`]) but never counted. +#[must_use] +pub fn core_accept_list() -> PredicateAcceptList { + PredicateAcceptList::from_iris([ENDORSES_V1, WITNESSED_V1, VETTED_V1, PRESENTED_V1]) + .expect("the core predicate IRIs are absolute NFC IRIs") +} + +/// A short human label for a registry predicate this client knows, or `None`. +#[must_use] +pub fn predicate_label(predicate: &str) -> Option<&'static str> { + Some(match predicate { + ENDORSES_V1 => "endorses", + WITNESSED_V1 => "witnessed", + VETTED_V1 => "vetted (identity vetting)", + PRESENTED_V1 => "presented", + _ => return None, + }) +} + +/// What a statement says, for display. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StatementSummary { + /// The predicate IRI, verbatim. + pub predicate: String, + /// [`predicate_label`], or the IRI itself for a predicate this client does + /// not know. + pub label: String, + /// Whether the predicate is one of the core profiles this client knows. + pub known: bool, + /// The object, one line: an id, a digest, or the value's JSON. + pub object: String, +} + +/// Summarise a statement credential. `None` for any other credential type. +/// +/// Degrades rather than refuses: a statement under a predicate this client has +/// never heard of still shows its predicate IRI and its object, because a +/// holder should be able to see what they hold even where this client cannot +/// act on it. +#[must_use] +pub fn describe_statement(credential: &DTGCredential) -> Option { + let statement = credential.statement()?; + let known = predicate_label(&statement.predicate); + let object = match &statement.object { + StatementObject::Id(id) => format!("id {id}"), + StatementObject::DigestMultibase(digest) => format!("digest {digest}"), + StatementObject::Value(value) => { + let text = value.to_string(); + if text.chars().count() > 160 { + format!("{}…", text.chars().take(159).collect::()) + } else { + text + } + } + }; + Some(StatementSummary { + predicate: statement.predicate.clone(), + label: known.map_or_else(|| statement.predicate.clone(), str::to_string), + known: known.is_some(), + object, + }) +} + +/// One line saying what a credential is — `Membership`, `Role: vetter`, +/// `Statement: vetted (identity vetting)`, … — for the credential views. +/// A non-conformant document says so rather than being described as whatever +/// its `type` array claims. +#[must_use] +pub fn describe(value: &Value) -> String { + let credential = match parse_conformant(value) { + Ok(c) => c, + Err(_) => return "Non-conformant (pre-v1) credential".to_string(), + }; + match credential.type_() { + DTGCredentialType::Membership => "Membership".to_string(), + DTGCredentialType::Authority => match community_roles(&credential) { + Some(roles) => format!("Role: {}", roles.join(", ")), + None => "Authority".to_string(), + }, + DTGCredentialType::Statement => match describe_statement(&credential) { + Some(s) => format!("Statement: {}", s.label), + None => "Statement".to_string(), + }, + DTGCredentialType::Relationship => "Relationship".to_string(), + DTGCredentialType::Invitation => "Invitation".to_string(), + DTGCredentialType::Persona => "Persona".to_string(), + DTGCredentialType::Delegation => "Delegation".to_string(), + other => other.to_string(), + } +} + +#[cfg(test)] +pub(crate) mod fixtures { + //! Conformant DTG credentials for tests, built through `dtg-credentials` so + //! they cannot drift from the specification the way hand-written JSON did. + + use chrono::{Duration, Utc}; + use dtg_credentials::DTGCredential; + use serde_json::{Value, json}; + + /// The pre-v1 DTG context. Named once, here, for the tests that prove it is + /// refused; nothing outside a test may emit or accept it. + pub(crate) const RETIRED_CONTEXT: &str = "https://firstperson.network/credentials/dtg/v1"; + + /// A pre-v1 role credential exactly as a VTC used to deliver it: the + /// retired context and type, no `issuerScope`. Only ever used to prove it is + /// refused or set aside. + pub(crate) fn retired_role_endorsement(community: &str, member: &str) -> Value { + json!({ + "@context": ["https://www.w3.org/ns/credentials/v2", RETIRED_CONTEXT], + "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"], + "id": "urn:uuid:5b0c9d1e-0000-4000-8000-000000000001", + "issuer": community, + "validFrom": "2026-01-01T00:00:00Z", + "credentialSubject": { "id": member, "endorsement": { + "type": "CommunityRole", "role": "vetter", "communityDid": community + } } + }) + } + + /// A community-issued VMC (`issuerScope` public), unsigned. + pub(crate) fn grant(community: &str, member: &str) -> Value { + serde_json::to_value(DTGCredential::new_vmc( + community.to_string(), + member.to_string(), + Utc::now() - Duration::minutes(1), + Some(Utc::now() + Duration::days(365)), + false, + )) + .expect("serialise") + } + + /// A community role VAC conferring `role:`, unsigned. + pub(crate) fn role_vac(community: &str, member: &str, role: &str) -> Value { + serde_json::to_value( + DTGCredential::new_community_role_vac( + community.to_string(), + member.to_string(), + role, + Utc::now() - Duration::minutes(1), + Utc::now() + Duration::days(365), + ) + .expect("a role VAC") + .with_max_attenuation(0) + .expect("a VAC"), + ) + .expect("serialise") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn a_relationship_scope_follows_the_identifier_it_uses() { + assert_eq!(relationship_issuer_scope(false), IssuerScope::Pairwise); + assert_eq!(relationship_issuer_scope(true), IssuerScope::Directed); + // `vetted/1` needs at least `directed`, which the member identifier meets. + assert!(MEMBER_IDENTIFIER_SCOPE.satisfies(IssuerScope::Directed)); + } + + #[test] + fn the_retired_context_and_types_are_nonconformant() { + let old = fixtures::retired_role_endorsement("did:example:c", "did:example:m"); + assert!(nonconformance(&old).is_some()); + // The retired context alone is enough, on an otherwise current credential. + let mut relabelled = fixtures::grant("did:example:c", "did:example:m"); + relabelled["@context"][1] = json!(fixtures::RETIRED_CONTEXT); + assert!(nonconformance(&relabelled).is_some()); + assert_eq!(describe(&old), "Non-conformant (pre-v1) credential"); + + let grant = fixtures::grant("did:example:c", "did:example:m"); + assert_eq!(nonconformance(&grant), None); + assert_eq!(grant["issuerScope"], "public"); + assert_eq!(grant["@context"][1], DTG_CONTEXT_V1); + assert_eq!(describe(&grant), "Membership"); + } + + #[test] + fn a_community_role_vac_names_its_roles() { + let vac = fixtures::role_vac("did:example:c", "did:example:m", "vetter"); + let parsed = parse_conformant(&vac).unwrap(); + assert_eq!(community_roles(&parsed), Some(vec!["vetter".to_string()])); + assert_eq!(describe(&vac), "Role: vetter"); + + // A VAC granting authority in someone else's scope is not a role grant. + let other = DTGCredential::new_vac( + "did:example:c".into(), + IssuerScope::Public, + "did:example:m".into(), + "did:example:elsewhere".into(), + vec!["role:vetter".into()], + chrono::Utc::now(), + chrono::Utc::now() + chrono::Duration::days(1), + ) + .unwrap(); + assert_eq!(community_roles(&other), None); + } + + #[test] + fn an_unknown_predicate_degrades_to_its_iri() { + let vsc = DTGCredential::new_vsc( + "did:example:i".into(), + IssuerScope::Directed, + "did:example:s".into(), + "https://example.org/predicates/likes/1", + StatementObject::Value(json!({ "what": "tea" })), + chrono::Utc::now(), + None, + ) + .unwrap(); + let summary = describe_statement(&vsc).unwrap(); + assert!(!summary.known); + assert_eq!(summary.label, "https://example.org/predicates/likes/1"); + assert_eq!(summary.object, r#"{"what":"tea"}"#); + assert!(core_accept_list().accept(&vsc).is_err(), "fails closed"); + } +} diff --git a/openvtc-core/src/identity.rs b/openvtc-core/src/identity.rs index 0e9b99d4..a6ed85fc 100644 --- a/openvtc-core/src/identity.rs +++ b/openvtc-core/src/identity.rs @@ -188,6 +188,7 @@ mod tests { member_vmc: None, decision: None, relationship_identifier_default: None, + retired_credentials: Vec::new(), extra: serde_json::Map::new(), vtc_did: vtc.into(), display_name: None, diff --git a/openvtc-core/src/join.rs b/openvtc-core/src/join.rs index 281b04d8..b4abf6c0 100644 --- a/openvtc-core/src/join.rs +++ b/openvtc-core/src/join.rs @@ -607,16 +607,21 @@ pub fn invitation_is_expired(vic: &Value, now: DateTime) -> bool { } } -/// The two `@context` entries DTG Credentials §Common Structure requires of -/// every DTG credential, and the base `type` every one of them carries. +/// The two `@context` entries DTG Credentials §Base Structure requires of +/// every DTG credential — the W3C VC 2.0 context first, the frozen DTG v1 +/// context second — and the base `type` every one of them carries. /// -/// `dtg-credentials` builds all three into the credentials it mints but does -/// not export them, so they are named here rather than spelled out inline. -/// Removing this duplication needs a public constant upstream — -/// OpenVTC/dtg-credentials#10. -pub const W3C_VC_V2_CONTEXT: &str = "https://www.w3.org/ns/credentials/v2"; -pub const DTG_CONTEXT: &str = "https://firstperson.network/credentials/dtg/v1"; +/// The contexts are `dtg-credentials`' own constants, re-exported so the +/// client and the library cannot disagree about them. The pre-v1 DTG context +/// is refused, with no alias. +pub use dtg_credentials::{DTG_CONTEXT_V1 as DTG_CONTEXT, W3C_VC_V2_CONTEXT}; pub const DTG_BASE_TYPE: &str = "DTGCredential"; +/// The `issuerScope` a community's invitation declares. The VIC a joining +/// client presents is issued by the community itself +/// ([`invitation_matches_community`]), and a community can truthfully declare +/// nothing narrower than `public`. +pub const INVITATION_ISSUER_SCOPE: dtg_credentials::IssuerScope = + dtg_credentials::IssuerScope::Public; /// Whether a JSON value is an InvitationCredential (its `type` array carries /// the `InvitationCredential` tag). Used to validate a pasted/loaded VIC @@ -642,8 +647,12 @@ pub fn is_invitation_credential(value: &Value) -> bool { /// The required set is the intersection of: /// - **W3C VC Data Model 2.0** mandatory properties: `@context` (ordered set /// whose first value is `https://www.w3.org/ns/credentials/v2`), `type` -/// (here ⊇ `VerifiableCredential` + `InvitationCredential`), `issuer`, -/// `credentialSubject` (with an `id`), and a securing `proof`. +/// (here `VerifiableCredential` + `DTGCredential` + `InvitationCredential`), +/// `issuer`, `credentialSubject` (with an `id`), and a securing `proof`. +/// - **DTG Credentials v1**: the DTG v1 context second, a top-level +/// `issuerScope` — `public` for a community's invitation — and exactly one +/// concrete type, checked by parsing through `dtg-credentials` once every +/// member above is present. /// - the **VIC profile** the receiving VTC enforces: a top-level `id` (the /// single-use consumption handle — W3C makes `id` optional, the VIC profile /// does not), `validUntil` (the invite's expiry), and `credentialStatus` @@ -667,8 +676,14 @@ pub fn validate_invitation_credential(vic: &Value) -> Result<(), String> { "@context (must be an array whose first item is \"https://www.w3.org/ns/credentials/v2\")", ), } - if !ctx.is_some_and(|c| c.iter().any(|v| v.as_str() == Some(DTG_CONTEXT))) { - missing.push("@context entry \"https://firstperson.network/credentials/dtg/v1\""); + if !ctx.is_some_and(|c| c.get(1).and_then(Value::as_str) == Some(DTG_CONTEXT)) { + missing.push( + "@context second item \"https://registry.trustoverip.org/dtg/context/v1\" \ + (an invitation under an older DTG context must be re-issued)", + ); + } + if vic.get("issuerScope").and_then(Value::as_str) != Some(INVITATION_ISSUER_SCOPE.as_str()) { + missing.push("issuerScope (a community's invitation declares \"public\")"); } if !is_invitation_credential(vic) { missing @@ -707,6 +722,14 @@ pub fn validate_invitation_credential(vic: &Value) -> Result<(), String> { } if missing.is_empty() { + // Every member is there; now hold the whole document to the + // specification — the one-subtype rule, the validity window, the + // subject shape — through the library that defines it. + let parsed = crate::dtg::parse_conformant(vic) + .map_err(|e| format!("not a conformant DTG Invitation Credential: {e}"))?; + if parsed.type_() != dtg_credentials::DTGCredentialType::Invitation { + return Err("not an Invitation Credential".to_string()); + } Ok(()) } else { Err(format!( @@ -988,14 +1011,13 @@ mod tests { // previously carried only the W3C half and still called itself // complete — the validator agreed, because it checked only the // same half. - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" - ], + "@context": [W3C_VC_V2_CONTEXT, DTG_CONTEXT], "id": "urn:uuid:vic-1", "type": ["VerifiableCredential", "DTGCredential", "InvitationCredential"], "issuer": "did:webvh:example.com:community", + "issuerScope": "public", "credentialSubject": { "id": "did:webvh:example.com:alice" }, + "validFrom": "2026-01-01T00:00:00Z", "validUntil": "2099-01-01T00:00:00Z", "credentialStatus": { "type": "BitstringStatusListEntry" }, "proof": { "type": "DataIntegrityProof" } @@ -1068,10 +1090,11 @@ mod tests { #[test] fn validate_accepts_a_complete_vic() { assert!(validate_invitation_credential(&complete_vic()).is_ok()); - // Object-form issuer is accepted too. + // A DTG credential names its issuer as a DID string; the W3C object form + // is not one `dtg-credentials` parses, so it is refused. let mut v = complete_vic(); v["issuer"] = json!({ "id": "did:webvh:example.com:community" }); - assert!(validate_invitation_credential(&v).is_ok()); + assert!(validate_invitation_credential(&v).is_err()); } /// DTG Credentials §Common Structure is normative for *every* DTG @@ -1084,10 +1107,34 @@ mod tests { no_ctx["@context"] = json!(["https://www.w3.org/ns/credentials/v2"]); let err = validate_invitation_credential(&no_ctx).expect_err("missing DTG context"); assert!( - err.contains("firstperson.network/credentials/dtg/v1"), + err.contains(DTG_CONTEXT), "error should name the missing context: {err}" ); + // The pre-v1 context is refused outright, not accepted as an alias. + let mut old_ctx = complete_vic(); + old_ctx["@context"][1] = json!(crate::dtg::fixtures::RETIRED_CONTEXT); + assert!(validate_invitation_credential(&old_ctx).is_err()); + + // `issuerScope` is REQUIRED, and a community's invitation is `public`. + let mut no_scope = complete_vic(); + no_scope.as_object_mut().unwrap().remove("issuerScope"); + let err = validate_invitation_credential(&no_scope).expect_err("no issuerScope"); + assert!(err.contains("issuerScope"), "{err}"); + let mut narrow = complete_vic(); + narrow["issuerScope"] = json!("directed"); + assert!(validate_invitation_credential(&narrow).is_err()); + + // Exactly one concrete type. + let mut two = complete_vic(); + two["type"] = json!([ + "VerifiableCredential", + "DTGCredential", + "InvitationCredential", + "MembershipCredential" + ]); + assert!(validate_invitation_credential(&two).is_err()); + let mut no_base = complete_vic(); no_base["type"] = json!(["VerifiableCredential", "InvitationCredential"]); let err = validate_invitation_credential(&no_base).expect_err("missing DTGCredential"); diff --git a/openvtc-core/src/lib.rs b/openvtc-core/src/lib.rs index f55f0312..197d0278 100644 --- a/openvtc-core/src/lib.rs +++ b/openvtc-core/src/lib.rs @@ -30,6 +30,9 @@ pub mod devices; pub mod diagnostics; pub mod didcomm; pub mod display; +/// DTG Credentials conformance: issuer scopes, the conformance check stored and +/// received credentials are held to, and predicate-based statement display. +pub mod dtg; pub mod errors; pub mod git_ns; pub mod health; @@ -298,8 +301,8 @@ impl TryFrom<&Message> for MessageType { /// dispatch ([`messaging::handle_credential_issue`]) /// and the "My Credentials" UI. Adding a credential kind means adding a variant /// here plus its match arms below — the dispatch and UI code iterate -/// [`ALL`](Self::ALL) and match on [`vc_type`](Self::vc_type), so they pick the -/// new kind up without edits. +/// [`ALL`](Self::ALL) and classify through [`from_credential`](Self::from_credential), +/// so they pick the new kind up without edits. /// /// It is kept next to [`MessageType`] deliberately: a `MessageType` identifies /// a DIDComm message, while a `CredentialKind` identifies a credential carried @@ -313,7 +316,11 @@ pub enum CredentialKind { /// The membership credential (VMC) proving admission to the community. /// Receiving it activates the membership. Membership, - /// The role endorsement credential (VEC) issued alongside the VMC. + /// The community role credential issued alongside the VMC: a + /// community-issued VAC (`AuthorityCredential`, `issuerScope` public) whose + /// `authority` is `{ scope: , actions: ["role:"] }` + /// ([`dtg::community_roles`]). It replaced the pre-v1 role endorsement + /// credential, a type DTG Credentials no longer defines. Role, } @@ -322,17 +329,23 @@ impl CredentialKind { /// dispatch, storage and UI iterate; adding a variant extends all three. pub const ALL: &'static [CredentialKind] = &[CredentialKind::Membership, CredentialKind::Role]; - /// The W3C VC `type` value that identifies this kind in an issued credential. + /// The DTG concrete `type` that identifies this kind in an issued credential. pub fn vc_type(self) -> &'static str { match self { CredentialKind::Membership => "MembershipCredential", - CredentialKind::Role => "EndorsementCredential", + CredentialKind::Role => "AuthorityCredential", } } /// Stable key used to persist this kind (the JSON map key in /// [`CommunityRecord::credentials`](crate::config::account::CommunityRecord::credentials)) /// and as the short "My Credentials" display label. + /// + /// `Role` keeps its key across the move from a role endorsement to a role + /// VAC: what sits under it is held to the current specification on load + /// instead ([`crate::config::account::RetiredCredential`]), so a pre-v1 role + /// endorsement under this key is set aside with a reason rather than being + /// read as a VAC. pub fn config_key(self) -> &'static str { match self { CredentialKind::Membership => "Membership", @@ -341,7 +354,7 @@ impl CredentialKind { } /// Whether receiving this credential activates the community membership. - /// The VMC is admission proof; the VEC (role) is supplementary. + /// The VMC is admission proof; the role VAC is supplementary. pub fn activates_membership(self) -> bool { matches!(self, CredentialKind::Membership) } @@ -355,19 +368,27 @@ impl CredentialKind { .find(|k| k.config_key() == key) } - /// Classify an issued W3C VC by matching its `type` array against the - /// registry. Returns the first kind whose [`vc_type`](Self::vc_type) - /// appears, or `None` if the credential is of no known kind. + /// Classify an issued credential. It must first be a conformant DTG + /// credential ([`dtg::parse_conformant`]) — the v1 context, a declared + /// `issuerScope`, exactly one concrete type — and then: + /// + /// - a `MembershipCredential` is [`Membership`](Self::Membership); + /// - an `AuthorityCredential` is [`Role`](Self::Role) only when it is a + /// community role grant ([`dtg::community_roles`]); any other VAC is of + /// no known kind. + /// + /// `None` for anything else, including every credential in a pre-v1 shape. pub fn from_credential(credential: &serde_json::Value) -> Option { - let types = credential - .get("type") - .and_then(serde_json::Value::as_array)?; - CredentialKind::ALL.iter().copied().find(|k| { - types - .iter() - .filter_map(serde_json::Value::as_str) - .any(|t| t == k.vc_type()) - }) + let parsed = dtg::parse_conformant(credential).ok()?; + match parsed.type_() { + dtg_credentials::DTGCredentialType::Membership => Some(CredentialKind::Membership), + dtg_credentials::DTGCredentialType::Authority + if dtg::community_roles(&parsed).is_some() => + { + Some(CredentialKind::Role) + } + _ => None, + } } } @@ -477,7 +498,13 @@ mod tests { // dispatch / storage / UI rely on, so a newly added variant is picked // up everywhere by extending `ALL` + the match arms — and nowhere else. for kind in CredentialKind::ALL { - let cred = serde_json::json!({ "type": ["VerifiableCredential", kind.vc_type()] }); + let cred = match kind { + CredentialKind::Membership => crate::dtg::fixtures::grant("did:ex:c", "did:ex:m"), + CredentialKind::Role => { + crate::dtg::fixtures::role_vac("did:ex:c", "did:ex:m", "member") + } + }; + assert_eq!(cred["type"][2], kind.vc_type()); assert_eq!( CredentialKind::from_credential(&cred), Some(*kind), @@ -493,6 +520,20 @@ mod tests { CredentialKind::from_credential(&serde_json::json!({ "type": ["Other"] })), None, ); + // A bare `type` is not enough: the credential must conform to DTG v1. + assert_eq!( + CredentialKind::from_credential( + &serde_json::json!({ "type": ["VerifiableCredential", "MembershipCredential"] }) + ), + None, + ); + // The retired role endorsement is of no known kind. + assert_eq!( + CredentialKind::from_credential(&crate::dtg::fixtures::retired_role_endorsement( + "did:ex:c", "did:ex:m" + )), + None, + ); assert_eq!(CredentialKind::from_config_key("Nope"), None); } diff --git a/openvtc-core/src/members.rs b/openvtc-core/src/members.rs index 7e78b40b..10b62da2 100644 --- a/openvtc-core/src/members.rs +++ b/openvtc-core/src/members.rs @@ -13,9 +13,9 @@ //! //! ## It carries a digest of the grant //! -//! DTG Core Credentials: "A member-issued VMC whose `digest` does not match a -//! valid community-issued VMC MUST NOT be treated as completing a membership -//! edge." The digest is over the grant **as the community sent it** — the JSON +//! DTG Core Credentials: "A member-issued VMC whose `digestMultibase` does not +//! match a valid community-issued VMC MUST NOT be treated as completing a +//! membership edge." The digest is over the grant **as the community sent it** — the JSON //! stored on the membership record, not a re-serialisation of a parse of it, //! which drops members the local model does not know (`credentialStatus`, which //! every VMC issued against a status list carries). @@ -23,6 +23,14 @@ //! That binding is also what makes renewal safe: a re-issued grant has different //! claims and therefore a different digest, so consent to one membership cannot //! carry over to another. +//! +//! ## It declares the member's scope +//! +//! Each half of the edge declares its own issuer's `issuerScope`: the grant +//! `public` (the community), the acknowledgement the member's +//! [`MEMBER_IDENTIFIER_SCOPE`](crate::dtg::MEMBER_IDENTIFIER_SCOPE) — `directed`, +//! because the persona DID a membership names is recognised by the community, +//! by the members it relates to and vets, and possibly by other communities. use std::sync::Arc; @@ -64,7 +72,7 @@ pub async fn issue_and_send_member_vmc( grant: &Value, closes_request: Option, ) -> Result<(Uuid, Value), OpenVTCError> { - let vc = build_member_vmc(signing_secret, grant).await?; + let vc = build_member_vmc(signing_secret, route.member_did, grant).await?; let msg_id = submit_member_vmc(route, document_signer, vc.clone(), closes_request).await?; Ok((msg_id, vc)) } @@ -93,9 +101,14 @@ pub struct Delivery<'a> { /// without a mediator. /// /// `grant` is the community-issued VMC **as it arrived** — the JSON on the membership -/// record. The member and the community are read off it, so the two halves of the edge -/// cannot disagree about who they are between, and the digest covers the document the -/// community will recompute it over. +/// record. The community is read off it and the digest covers the document the community +/// will recompute it over. `member_did` is the persona whose key signs — established by +/// the caller from the membership record, never read out of the grant — and the grant must +/// name it: an acknowledgement on behalf of whoever a grant happens to name is refused +/// (`NotTheGrantSubject`). The grant must also be a conformant v1 grant declaring +/// `issuerScope: public`; a pre-v1 grant cannot be acknowledged. +/// +/// The acknowledgement declares [`MEMBER_IDENTIFIER_SCOPE`](crate::dtg::MEMBER_IDENTIFIER_SCOPE). /// /// # The credential carries its own `id` /// @@ -111,15 +124,28 @@ pub struct Delivery<'a> { /// verifies. The same is true of the digest, which is why it is set at construction. pub async fn build_member_vmc( signing_secret: &Secret, + member_did: &str, grant: &Value, ) -> Result { - let mut vmc = DTGCredential::new_member_vmc(grant, Utc::now(), None) - .map_err(|e| { - OpenVTCError::Config(format!( - "cannot acknowledge this community's membership credential: {e}" - )) - })? - .with_id(format!("urn:uuid:{}", Uuid::new_v4())); + // An acknowledgement may not outlive what it acknowledges. + let valid_until = grant + .get("validUntil") + .and_then(Value::as_str) + .and_then(|t| chrono::DateTime::parse_from_rfc3339(t).ok()) + .map(|t| t.with_timezone(&Utc)); + let mut vmc = DTGCredential::new_member_vmc_for( + grant, + member_did, + crate::dtg::MEMBER_IDENTIFIER_SCOPE, + Utc::now(), + valid_until, + ) + .map_err(|e| { + OpenVTCError::Config(format!( + "cannot acknowledge this community's membership credential: {e}" + )) + })? + .with_id(format!("urn:uuid:{}", Uuid::new_v4())); vmc.sign(signing_secret, None) .await .map_err(|e| OpenVTCError::Config(format!("sign member VMC: {e}")))?; @@ -209,19 +235,19 @@ mod tests { const COMMUNITY: &str = "did:example:community"; /// A community-issued grant in the wire form a member receives — including - /// `credentialStatus`, which every VMC issued against a status list carries and - /// which `dtg-credentials` does not model. The fixture carries it deliberately: - /// a grant built through the local model would not exercise the case the digest - /// has to get right. + /// `credentialStatus`, which every VMC issued against a status list carries. The + /// fixture carries it deliberately: a grant built through the local model would + /// not exercise the case the digest has to get right. fn grant() -> Value { serde_json::json!({ "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + dtg_credentials::W3C_VC_V2_CONTEXT, + dtg_credentials::DTG_CONTEXT_V1 ], "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"], "id": "urn:uuid:0d7f4d2c-1b8e-4a55-9e1f-7c4a2b9d3e60", "issuer": COMMUNITY, + "issuerScope": "public", "validFrom": "2026-01-01T00:00:00Z", "credentialStatus": { "id": "https://community.example/status#7", @@ -236,7 +262,7 @@ mod tests { async fn signed_vmc() -> (Secret, Value) { let secret = Secret::generate_ed25519(None, None); - let vc = build_member_vmc(&secret, &grant()) + let vc = build_member_vmc(&secret, MEMBER, &grant()) .await .expect("build the member VMC"); (secret, vc) @@ -257,22 +283,20 @@ mod tests { "the digest must cover the grant the community sent" ); - // Digesting a round trip through the local model now produces the *same* + // Digesting a round trip through the local model produces the *same* // value. This used to be the hazard the acknowledgement guarded against: // the model had no field for `credentialStatus`, so parsing dropped it and - // a digest over the parse mismatched the one the community recomputes. As - // of `dtg-credentials` 0.9.1 (Working Draft 02) the model carries - // `credentialStatus`, so the round trip is lossless and either route - // digests to the same bytes — the divergence is closed at the library - // rather than worked around here. Parsed without the proof, which the - // digest excludes anyway. + // a digest over the parse mismatched the one the community recomputes. The + // model carries `credentialStatus` (and `issuerScope`), so the round trip + // is lossless and either route digests to the same bytes. Parsed without + // the proof, which the digest excludes anyway. let mut proofless = grant(); proofless.as_object_mut().expect("object").remove("proof"); let parsed: DTGCredential = serde_json::from_value(proofless).expect("parses"); assert_eq!( vc["credentialSubject"]["digestMultibase"], Value::String(parsed.digest_multibase().expect("digest")), - "0.9.1 models credentialStatus, so digesting the parsed grant matches the wire" + "the model is lossless, so digesting the parsed grant matches the wire" ); } @@ -304,7 +328,45 @@ mod tests { "issuer": COMMUNITY, "credentialSubject": { "id": MEMBER } }); - assert!(build_member_vmc(&secret, ¬_a_grant).await.is_err()); + assert!( + build_member_vmc(&secret, MEMBER, ¬_a_grant) + .await + .is_err() + ); + } + + /// The member acknowledges only a grant naming the persona that signs. A grant + /// for someone else — relayed, misdelivered or forged — is refused rather than + /// answered on their behalf. + #[tokio::test] + async fn a_grant_naming_someone_else_is_refused() { + let secret = Secret::generate_ed25519(None, None); + assert!( + build_member_vmc(&secret, "did:example:someone-else", &grant()) + .await + .is_err() + ); + } + + /// A pre-v1 grant — the retired context, no `issuerScope` — is not a grant this + /// client can acknowledge: the edge it would complete does not exist under the + /// current specification. + #[tokio::test] + async fn a_pre_v1_grant_is_refused() { + let secret = Secret::generate_ed25519(None, None); + let mut old = grant(); + old["@context"][1] = Value::String(crate::dtg::fixtures::RETIRED_CONTEXT.into()); + old.as_object_mut().unwrap().remove("issuerScope"); + assert!(build_member_vmc(&secret, MEMBER, &old).await.is_err()); + } + + /// The acknowledgement declares the member's own scope: `directed`, for the + /// persona DID a membership names. + #[tokio::test] + async fn the_acknowledgement_declares_the_member_scope() { + let (_secret, vc) = signed_vmc().await; + assert_eq!(vc["issuerScope"], "directed"); + assert_eq!(vc["@context"][1], dtg_credentials::DTG_CONTEXT_V1); } /// The community keys a member's VMC by its top-level `id` and refuses one that has diff --git a/openvtc-core/src/messaging.rs b/openvtc-core/src/messaging.rs index c968a4bd..1568a1ba 100644 --- a/openvtc-core/src/messaging.rs +++ b/openvtc-core/src/messaging.rs @@ -1053,6 +1053,8 @@ pub fn handle_credential_issue( return CredentialIssueOutcome::NONE; }; record.credentials.insert(kind, credential); + // A conformant replacement answers a pre-v1 one set aside on load. + record.clear_retired(kind.config_key()); // Capture the join request id *before* activating. `activate` replaces // `Pending { request_id }` with `Active`, so this is the last moment the @@ -2708,12 +2710,20 @@ mod tests { VerifiedIssuedCredential::assume_verified(credential_in_issue(m).expect("a credential")) } + /// A conformant, unsigned credential of the kind `types` names — built through + /// `dtg-credentials` so the fixture is what a v1 community actually issues. fn vc(types: &[&str], issuer: &str, subject: &str) -> serde_json::Value { - serde_json::json!({ - "type": types, - "issuer": issuer, - "credentialSubject": { "id": subject }, - }) + match types.last().copied() { + Some("MembershipCredential") => crate::dtg::fixtures::grant(issuer, subject), + Some("AuthorityCredential") => { + crate::dtg::fixtures::role_vac(issuer, subject, "member") + } + _ => serde_json::json!({ + "type": types, + "issuer": issuer, + "credentialSubject": { "id": subject }, + }), + } } /// Admission must report the join request it closed. @@ -2757,7 +2767,7 @@ mod tests { assert!(only(&acct, vtc).status.is_active()); } - /// A credential that does not admit anyone closes no join. A role VEC + /// A credential that does not admit anyone closes no join. A role VAC /// landing on an already-active membership must not re-send a reciprocal /// VMC naming a request that was closed long ago. #[test] @@ -2824,7 +2834,7 @@ mod tests { } #[test] - fn credential_issue_role_vec_stores_without_activating() { + fn credential_issue_role_vac_stores_without_activating() { let vtc = "did:webvh:example:vtc"; let persona = "did:webvh:example:persona"; let mut acct = account_with_persona(vtc, persona); @@ -2832,7 +2842,7 @@ mod tests { let m = issue( vtc, vc( - &["VerifiableCredential", "EndorsementCredential"], + &["VerifiableCredential", "AuthorityCredential"], vtc, persona, ), @@ -2842,11 +2852,27 @@ mod tests { let rec = only(&acct, vtc); assert!( !rec.status.is_active(), - "role VEC must not activate on its own" + "role VAC must not activate on its own" ); assert!(rec.credentials.contains_key(&crate::CredentialKind::Role)); } + /// The role credential retired with DTG Credentials v1 — a role + /// endorsement credential under the old context — is of no known kind now, + /// so a community still issuing it is refused rather than stored. + #[test] + fn a_pre_v1_role_endorsement_is_not_stored() { + let vtc = "did:webvh:example:vtc"; + let persona = "did:webvh:example:persona"; + let mut acct = account_with_persona(vtc, persona); + let m = issue( + vtc, + crate::dtg::fixtures::retired_role_endorsement(vtc, persona), + ); + assert!(!handle_credential_issue(&mut acct, verified(&m), vtc).changed); + assert!(only(&acct, vtc).credentials.is_empty()); + } + /// The dispatch path is purely registry-driven: every kind in /// `CredentialKind::ALL` is classified and stored by `handle_credential_issue` /// with no per-kind branching, so adding a kind to the registry is the only @@ -3079,6 +3105,7 @@ mod tests { fn unsigned_vrc(issuer: &str) -> DTGCredential { DTGCredential::new_vrc( issuer.to_string(), + dtg_credentials::IssuerScope::Pairwise, "did:webvh:example:subject".to_string(), Utc::now(), None, diff --git a/openvtc-core/src/personhood.rs b/openvtc-core/src/personhood.rs index 23a9a7d4..852e0a58 100644 --- a/openvtc-core/src/personhood.rs +++ b/openvtc-core/src/personhood.rs @@ -477,8 +477,9 @@ pub struct AssertReply { pub personhood: bool, /// The re-minted VMC, carrying `PersonhoodCredential` in its `type`. pub vmc: Value, - /// The re-minted role credential. - pub role_vec: Value, + /// The re-minted role credential — a community role VAC (wire `roleVac`, + /// `vtc/members/personhood/assert/0.1`). + pub role_vac: Value, } /// Parse a `members/personhood/assert/0.1#response`. @@ -495,7 +496,7 @@ pub fn parse_assert_reply(body: &Value) -> Result { .and_then(|v| v.as_bool()) .unwrap_or(false), vmc: payload.get("vmc").cloned().unwrap_or(Value::Null), - role_vec: payload.get("roleVec").cloned().unwrap_or(Value::Null), + role_vac: payload.get("roleVac").cloned().unwrap_or(Value::Null), }) } @@ -706,16 +707,20 @@ mod tests { let bare = json!({ "did": MEMBER, "personhood": true, - "vmc": { "type": ["VerifiableCredential", "MembershipCredential", + "vmc": { "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential", "PersonhoodCredential"] }, - "roleVec": { "type": ["VerifiableCredential", "EndorsementCredential"] }, + "roleVac": { "type": ["VerifiableCredential", "DTGCredential", "AuthorityCredential"] }, }); let reply = parse_assert_reply(&bare).expect("parse assert reply"); assert!(reply.personhood); assert_eq!(reply.did, MEMBER); assert_eq!( - reply.vmc["type"][2].as_str(), + reply.role_vac["type"][2], "AuthorityCredential", + "the role credential is read from `roleVac`" + ); + assert_eq!( + reply.vmc["type"][3].as_str(), Some("PersonhoodCredential"), "the re-minted VMC is what carries the claim" ); diff --git a/openvtc-core/src/rebuild.rs b/openvtc-core/src/rebuild.rs index 0143ba6a..10e1c307 100644 --- a/openvtc-core/src/rebuild.rs +++ b/openvtc-core/src/rebuild.rs @@ -91,6 +91,13 @@ pub enum RejectionReason { /// The unparseable value, verbatim. valid_until: String, }, + /// Not a conformant DTG Credentials v1 membership credential — typically + /// one the vault kept from before the v1 context and `issuerScope`. The + /// community has to re-issue it; it cannot be restored as it stands. + Nonconformant { + /// What `dtg-credentials` refused. + reason: String, + }, /// The credential's proof did not verify against the issuing community's /// DID document, or could not be checked. Unverified { @@ -113,6 +120,9 @@ impl RejectionReason { RejectionReason::MalformedValidity { valid_until } => { format!("its validity window is unreadable ({valid_until})") } + RejectionReason::Nonconformant { reason } => { + format!("it pre-dates DTG Credentials v1 and must be re-issued ({reason})") + } RejectionReason::Unverified { reason } => { format!("its signature could not be verified: {reason}") } @@ -257,6 +267,16 @@ pub fn membership_from_credential( validity(credential, now)?; + match crate::dtg::parse_conformant(credential) { + Ok(parsed) if parsed.type_() == dtg_credentials::DTGCredentialType::Membership => {} + Ok(_) => { + return Err(RejectionReason::Nonconformant { + reason: "not a MembershipCredential".to_string(), + }); + } + Err(reason) => return Err(RejectionReason::Nonconformant { reason }), + } + Ok(RebuiltMembership { vtc_did: issuer.to_string(), persona_did: subject.to_string(), @@ -447,9 +467,12 @@ mod tests { fn vmc(issuer: Value, subject: Option<&str>) -> Value { let mut vc = serde_json::json!({ + "@context": [dtg_credentials::W3C_VC_V2_CONTEXT, dtg_credentials::DTG_CONTEXT_V1], "id": "vmc-1", - "type": ["VerifiableCredential", "MembershipCredential"], + "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"], "issuer": issuer, + "issuerScope": "public", + "validFrom": "2026-01-01T00:00:00Z", }); if let Some(s) = subject { vc["credentialSubject"] = serde_json::json!({ "id": s }); @@ -473,16 +496,32 @@ mod tests { assert_eq!(m.credential["id"], "vmc-1"); } - /// Issuers are written both ways in this ecosystem. + /// A DTG credential names its issuer as a DID string; the object form W3C + /// also allows is not one `dtg-credentials` parses, so it is refused rather + /// than restored on a reading the library would not share. #[test] - fn an_object_issuer_is_accepted() { - let m = membership_from_credential( - &vmc(serde_json::json!({ "id": VTC }), Some(ALICE)), - &ours(), - Utc::now(), - ) - .expect("verifies"); - assert_eq!(m.vtc_did, VTC); + fn an_object_issuer_is_not_a_conformant_membership() { + assert!(matches!( + membership_from_credential( + &vmc(serde_json::json!({ "id": VTC }), Some(ALICE)), + &ours(), + Utc::now(), + ), + Err(RejectionReason::Nonconformant { .. }) + )); + } + + /// A membership credential the vault kept from before DTG Credentials v1 + /// is not restored: the community must re-issue it. + #[test] + fn a_pre_v1_membership_credential_is_rejected() { + let mut vc = vmc(VTC.into(), Some(ALICE)); + vc["@context"][1] = crate::dtg::fixtures::RETIRED_CONTEXT.into(); + vc.as_object_mut().unwrap().remove("issuerScope"); + assert!(matches!( + membership_from_credential(&vc, &ours(), Utc::now()), + Err(RejectionReason::Nonconformant { .. }) + )); } /// D18 — the check that stops a hostile VTA inventing a membership. A @@ -558,6 +597,9 @@ mod tests { RejectionReason::SubjectNotOurs { subject: BOB.to_string(), }, + RejectionReason::Nonconformant { + reason: "x".to_string(), + }, RejectionReason::Expired { valid_until: "2020-01-01T00:00:00Z".to_string(), }, diff --git a/openvtc-core/src/tasks.rs b/openvtc-core/src/tasks.rs index 0a41eb1c..41ada569 100644 --- a/openvtc-core/src/tasks.rs +++ b/openvtc-core/src/tasks.rs @@ -147,7 +147,15 @@ impl Display for TaskType { } /// Collection of in-progress tasks, indexed by task ID. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +/// +/// # A task carrying a pre-v1 VRC is dropped on load +/// +/// [`TaskType::VRCIssued`] holds the VRC itself, and a VRC stored before DTG +/// Credentials v1 no longer parses. Rather than fail the whole protected config +/// for one inbox item, such a task is dropped with a logged reason (the peer +/// can issue a fresh VRC). Any other task that fails to parse still fails the +/// load, as it always has — only the case this migration creates is tolerated. +#[derive(Clone, Debug, Default, Serialize)] pub struct Tasks { /// key: Task ID /// @@ -156,6 +164,33 @@ pub struct Tasks { pub tasks: HashMap, Task>, } +impl<'de> Deserialize<'de> for Tasks { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Raw { + #[serde(default)] + tasks: HashMap, serde_json::Value>, + } + let raw = Raw::deserialize(deserializer)?; + let mut tasks = HashMap::with_capacity(raw.tasks.len()); + for (id, value) in raw.tasks { + let carries_vrc = value.pointer("/type_/VRCIssued/vrc").is_some(); + match serde_json::from_value::(value) { + Ok(task) => { + tasks.insert(id, task); + } + Err(e) if carries_vrc => tracing::warn!( + task = %id, + reason = %e, + "dropping a stored VRC task whose credential does not conform to DTG Credentials v1" + ), + Err(e) => return Err(serde::de::Error::custom(e)), + } + } + Ok(Tasks { tasks }) + } +} + impl Tasks { /// Removes a task by ID. Returns `true` if the task was found and removed. pub fn remove(&mut self, id: &Arc) -> bool { diff --git a/openvtc-core/src/vrc.rs b/openvtc-core/src/vrc.rs index fb305781..54e32aad 100644 --- a/openvtc-core/src/vrc.rs +++ b/openvtc-core/src/vrc.rs @@ -17,21 +17,88 @@ use std::{ sync::Arc, time::SystemTime, }; -use tracing::debug; +use tracing::{debug, warn}; use uuid::Uuid; -/// Collection of VRCs, keyed by remote P-DID and then by VRC ID. +/// Collection of VRCs, keyed by the remote party's persona DID and then by VRC ID. /// /// Typically two instances are maintained: one for issued VRCs and one for received VRCs. -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +/// +/// # Stored VRCs are held to the current specification on load +/// +/// A VRC stored by an earlier build carries the retired pre-v1 DTG context and no +/// `issuerScope`, and `dtg-credentials` refuses it. Deserializing the map +/// strictly would make one such credential fail the whole protected config — a +/// crash at unlock. Instead each stored VRC is parsed on its own: a +/// non-conformant one is **dropped with a logged reason** and counted in +/// [`Vrcs::retired`], which the relationships view shows so the member knows to +/// ask the peer for a fresh one. Nothing non-conformant is kept, and nothing is +/// dropped silently. +#[derive(Serialize, Debug, Clone, Default)] pub struct Vrcs { /// Hashmap of VRCs - /// key = remote P-DID + /// key = the remote party's persona DID /// secondary key is the VRC-ID vrcs: HashMap, HashMap, Arc>>, + /// How many stored VRCs were set aside on load as non-conformant (pre-v1). + /// Persisted, so the notice survives the save that drops them. + #[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>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Raw { + #[serde(default)] + vrcs: HashMap, HashMap, serde_json::Value>>, + #[serde(default)] + retired: usize, + } + let raw = Raw::deserialize(deserializer)?; + let mut vrcs: HashMap, HashMap, Arc>> = + 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::(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 { + /// How many stored VRCs were set aside on load because they pre-date DTG + /// Credentials v1. See the type's docs. + #[must_use] + pub fn retired(&self) -> usize { + self.retired + } + + /// Forget the retired count, once the member has been told. + pub fn clear_retired(&mut self) { + self.retired = 0; + } + /// Returns an iterator over all per-relationship VRC maps. pub fn values(&self) -> Values<'_, Arc, HashMap, Arc>> { self.vrcs.values() @@ -229,7 +296,7 @@ impl VRCRequestReject { /// `DTGCredential::new_vrc` leaves `id` unset, and a credential with no /// identifier cannot be stored under one — so a peer that keys relationship /// credentials by `id` cannot make a re-issue idempotent, tell a renewal from a -/// duplicate, or reference this VRC from a witness credential's `digest`. +/// duplicate, or reference this VRC from a `witnessed/1` statement (`object.digestMultibase`). /// /// Nothing rejects a VRC for a missing `id` *today*. That is exactly what the /// reciprocal membership credential looked like, right up until a community @@ -240,14 +307,20 @@ impl VRCRequestReject { /// proof covers every member but `proof`, so one spliced in afterwards leaves a /// document whose proof no longer verifies. Building and identifying in one /// call is what stops the two being separated later. +/// +/// `issuer_scope` is the scope declared for `issuer` — pass +/// [`crate::dtg::relationship_issuer_scope`] for the identifier the relationship +/// uses: `pairwise` for a relationship DID, `directed` for a persona DID. pub fn new_identified_vrc( issuer: &str, + issuer_scope: dtg_credentials::IssuerScope, subject: &str, valid_from: chrono::DateTime, valid_until: Option>, ) -> DTGCredential { DTGCredential::new_vrc( issuer.to_string(), + issuer_scope, subject.to_string(), valid_from, valid_until, @@ -265,11 +338,14 @@ mod tests { 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}"); @@ -282,12 +358,44 @@ mod tests { #[test] fn each_issued_vrc_gets_a_fresh_identifier() { let now = chrono::Utc::now(); - let a = new_identified_vrc("did:key:zI", "did:key:zS", now, None); - let b = new_identified_vrc("did:key:zI", "did:key:zS", now, None); + 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()); } + /// A VRC stored before DTG Credentials v1 must not stop the config loading: + /// it is dropped, counted, and the conformant ones beside it survive. + #[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()))); + + // The count survives a save, so the notice outlives the drop. + 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(); From 11233c0f54cf178e0f71f6d405a72b7cd723f5eb Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Wed, 30 Sep 2026 17:18:29 +0200 Subject: [PATCH 2/9] feat(openvtc-core)!: vetting statements are vetted/1 VSCs; vetter roles are VACs Follows vta-sdk 0.57 (vetting statements are vetted/1 VSCs; role credentials are VACs). - The vetter's Vetting Statement is a StatementCredential under https://registry.trustoverip.org/dtg/vsc/vetted/1, the body at object.value, issued directed, citing the vetting/session document by taskContext and taskDigestMultibase. The desk keeps the signed session document (VettingBook::record_session_document, DeskSession::document) and a session without one cannot be attested; the applicant keeps the one it received (OpenSession::document) and refuses a statement that does not cite it (check_against_session). - Statements are claimed by predicate, never by type; manifests name the predicate IRI as statementType. - A vetter grant is a community role VAC (community_roles, role:vetter). - Stored vetter grants and held statements in a pre-v1 shape are set aside on load (VettingBook::retire_nonconformant, run by ProtectedConfig::parse) with a notice in VettingBook::retired, cleared when a replacement arrives. - openvtc-vetting-pcs: only the statementType changes; the wire fixture is regenerated, which moves its requirementsDigest. - zkp_reference_flow and its output are regenerated in the v1 shapes. Signed-off-by: Glenn Gore --- .../examples/zkp_reference_flow.output.json | 509 ++++++++++-------- openvtc-core/examples/zkp_reference_flow.rs | 135 ++--- openvtc-core/src/config/protected_config.rs | 9 +- openvtc-core/src/vetting/applicant.rs | 31 +- openvtc-core/src/vetting/book.rs | 129 ++++- openvtc-core/src/vetting/guide.rs | 2 +- openvtc-core/src/vetting/hidden.rs | 7 +- openvtc-core/src/vetting/inbound.rs | 19 +- openvtc-core/src/vetting/tests.rs | 81 +-- openvtc-core/src/vetting/vetter.rs | 79 ++- openvtc-core/src/vetting/wire.rs | 17 +- .../tests/hidden_vetting_onboarding.rs | 6 +- openvtc-core/tests/vetting_e2e.rs | 37 +- .../examples/hidden_vetting.rs | 6 +- openvtc-vetting-pcs/src/meta.rs | 2 +- openvtc-vetting-pcs/tests/end_to_end.rs | 4 +- .../tests/fixtures/submission.json | 12 +- openvtc-vetting-pcs/tests/wire_fixture.rs | 6 +- 18 files changed, 687 insertions(+), 404 deletions(-) diff --git a/openvtc-core/examples/zkp_reference_flow.output.json b/openvtc-core/examples/zkp_reference_flow.output.json index 51cf87c0..1e64a4a9 100644 --- a/openvtc-core/examples/zkp_reference_flow.output.json +++ b/openvtc-core/examples/zkp_reference_flow.output.json @@ -102,10 +102,10 @@ "requiredClaims": [ "name.legal" ], - "statementType": "https://firstperson.network/endorsements/identity-vetting/0.1", + "statementType": "https://registry.trustoverip.org/dtg/vsc/vetted/1", "version": "0.1" }, - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "tokenKeyTvk": "zHsug8oA2yLzp1uMbhTpbLc5iBn9fSeDtXAC37Egsr5LCYetkR64gdybgcvACnusdkXdV3A4anhHA4N73AdohW9Dg4mMzPXxTStQcGW61P5vkgPW5zUN17XG6KenTq8Hj88WT7ExTjyCtErqYAgHaDBGmU2eucfai68AFMNccb4kWAbpLy7Y8b1MpxXv8jcFFPPWhXwvp3iVtnv3GLupwq12vJigYcnt1BPcSZiPLcDWsSkhfAVsxUazpFtzgTSVpfz5PvoJxtsV6Psor9tPDipNusgrDy937VTjcY71UePVvEskfQzHcWGTMr9kSLi5rRSbRg6zxWEha3cde3ubRUdby7c7DRjkRptovGekbmaHmiiQQedHAKzL6AKr8P2jUebVSd7ZUqdtUQjPpfQDmURwauTR1xzyJ1iFFHGBZEkQdpth1gyJ5nQaw8AoEb6S7H3FwQRadYHT", "tokenLabel": "token/2026-09", "vetterDids": [ @@ -129,312 +129,342 @@ { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX" }, - "id": "urn:uuid:c4895727-e534-4664-8d0f-ddd068374305", + "id": "urn:uuid:153b00cf-f38c-4c4e-81f4-6384dfe57f28", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:46Z", + "created": "2026-09-30T14:53:13Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z2A6d1fJWWXuGdfcZafmig4GXvqrqgUjHxtjb4tZcqjPKMfuKXYTHBQYb6sPdC2RUGwGgpPbs1SrR3UM2JwwXf54w", + "proofValue": "z2s8VqaJMYhHrV3vYonajjv2jY7LskZGBY2HGqAxTyAA22GosFys8Whc3CgWecC9wa4T9qhatNfRD4QAEKMounUKg", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:46Z", - "validUntil": "2027-09-23T09:29:46Z" + "validFrom": "2026-09-30T14:52:13Z", + "validUntil": "2027-09-30T14:53:13Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH" }, - "id": "urn:uuid:f4587ae5-2b8b-47ac-a905-de036f58e363", + "id": "urn:uuid:b90184ed-1c69-4502-bedd-f89c5c731ecc", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:47Z", + "created": "2026-09-30T14:53:13Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z26povpgQnTYxvNs6ZJyXypYh32u4pCyhcXzWJYjwv2TWuCa65B7pk2bwQwrqd27TJBi3mbr5AmWeCwSxaVjo3q6v", + "proofValue": "z4crQavEkBjbWdMX3mtf3hU8C4Ra3mpNzcBzBqpDTwwAA6v7MLfJF5Y2CmMx3YUHKs7nNqis37Qk48G7eaw4zvPhh", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:47Z", - "validUntil": "2027-09-23T09:29:47Z" + "validFrom": "2026-09-30T14:52:13Z", + "validUntil": "2027-09-30T14:53:13Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6MkvRXNYcE7MMduynWTgeKbDaT1iijDSC8pZqXZc8rHPrf2" }, - "id": "urn:uuid:b64f65f7-71d9-438e-b78f-5f15b0ac2170", + "id": "urn:uuid:9cebea31-bffc-49a3-96f1-9247845defc0", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:47Z", + "created": "2026-09-30T14:53:13Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z2QMVPQN89cFMTyjdEBUc3FVwAgktHoNWU8WezqC4UoBxWgst7HTPD89WG2tretGWbBXzajF7WvAYyDkXM8ADTKnZ", + "proofValue": "z4i4AQojBFvRS5AiL66Q78PniDm62dyPLNsESsCkv2EnY7QYmFgL539Zpbh14GFzgfq5cubd8Bx6mJtkeRqdAr5fZ", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:47Z", - "validUntil": "2027-09-23T09:29:47Z" + "validFrom": "2026-09-30T14:52:13Z", + "validUntil": "2027-09-30T14:53:13Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6Mkt6316e2PN3mZdB6N9CrzomJYUd1s5yBZi1XYHmwT9TUP" }, - "id": "urn:uuid:f2c04a78-ac9b-43cb-bb28-455759c339e9", + "id": "urn:uuid:a393930c-a099-465b-bd22-57d254ee4b15", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:47Z", + "created": "2026-09-30T14:53:14Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z5vjqAVnuXuXJVjCVXAGY2xbMNizj5P9e6EooX4e5YNFYQkru9HCiXx7fASn7AFFkze6v5MSTuH7wSUCi8ivYbpnY", + "proofValue": "z2FxAmY5FzdEjMdV7GZRDbhayVigzSiLiQm5rAiQNt3FnZPRzWCRygC85Yner1erXapMFiREkqWBLND4CEdrLMGse", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:47Z", - "validUntil": "2027-09-23T09:29:47Z" + "validFrom": "2026-09-30T14:52:14Z", + "validUntil": "2027-09-30T14:53:14Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6MkmtWtY63GQVBrpMyRJWEzsnxfsGkemu6CtMDwGTv4RYj2" }, - "id": "urn:uuid:a3564854-e558-4790-832e-ff56277868d0", + "id": "urn:uuid:9064c518-96bd-4653-b796-90894b1e2ce6", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:48Z", + "created": "2026-09-30T14:53:14Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z43vR4ffBm3sSwnekiR4adXjbyJ7ZiaVaPirTAUR2kBoZtbNhN4p14EJcGShSj6A5eiG2Wcr1E3YGMm3Taacj6TBR", + "proofValue": "z5rTzU5XUoXnB7vjMud9LfHrbY19HgtCPWWZaCG2Uxx4vN31x48PzJgfSqmKmYfohG6nTv4RXXkVFn99N6rkPxrMB", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:48Z", - "validUntil": "2027-09-23T09:29:48Z" + "validFrom": "2026-09-30T14:52:14Z", + "validUntil": "2027-09-30T14:53:14Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6Mkon22vwz9JoNpGDxCrGZRgeNFTdRTwXYYN3fvAhA3K19x" }, - "id": "urn:uuid:85cc443f-309f-4dc4-b312-f89359248f4a", + "id": "urn:uuid:b5fea947-ede5-41ad-a2f7-417867936c39", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:48Z", + "created": "2026-09-30T14:53:14Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z4KrHwoWd4zUD1nmz3pkCC1qhKBbAy9gBjQxbwLYEApG11rs1RUNorZBHWLM9Qu8hkK3grdmmkGArDLpPnESCY8zu", + "proofValue": "z3N2vdwiwfFQ759wcYHMqimGHjt3zaLoCZGrb8Cv84ZcKUhPD42CWx78TiyQ2ge4hS5f9nu6S7ratYWkgrGVmkLhQ", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:48Z", - "validUntil": "2027-09-23T09:29:48Z" + "validFrom": "2026-09-30T14:52:14Z", + "validUntil": "2027-09-30T14:53:14Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6MkvDqGT54cXesYGvABpF1UapVNwjCqRcafi4Px6Thv5T3Z" }, - "id": "urn:uuid:abef2430-bd76-4e76-a0b1-8ef5e16f51d2", + "id": "urn:uuid:1fc6c49d-3754-4230-918e-6187e2a8ce31", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:48Z", + "created": "2026-09-30T14:53:15Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z2EdHrZekpWpqYsmZrj54GdHs7kMvNBTDS5jpssf3nQRTKsh9dnrgojGisX2kdBBMxCN47KGUp29ToVb5JBcFSDjq", + "proofValue": "z33agDJrwdCqfcxM2NBvybc38e6iVHxxdzUF2woueVBTJoQy5n5JVcj95uKN33regujjfKyf5ZC65nAYgueGA6mSz", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:48Z", - "validUntil": "2027-09-23T09:29:48Z" + "validFrom": "2026-09-30T14:52:15Z", + "validUntil": "2027-09-30T14:53:15Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6Mkfmm57fsb6VL7zVusP8zeA9SYkCKdvUhby2G7Yh8vvQ1P" }, - "id": "urn:uuid:01c71b5a-ed8f-488d-8ab9-78d73f4ff857", + "id": "urn:uuid:63bf92ad-2d8e-495f-a740-01c554eb44d9", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:49Z", + "created": "2026-09-30T14:53:15Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z2aoDV5dBhXBWbtdiJEzb9Jrqma9Jruq4KdHRC4JzB6y13U44HSsmEdUPTZ7nrgcndTd6Rm2CdEnFx58q1nq4spUG", + "proofValue": "z4SUFgCCBDgHkN6yKPbAyXKtz7CUVXRyT9TwTKRPakwcwfSdVeredMaXB8ZRCo3jMP3KUmo3dEDq51WJ2iUFNg69T", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:49Z", - "validUntil": "2027-09-23T09:29:49Z" + "validFrom": "2026-09-30T14:52:15Z", + "validUntil": "2027-09-30T14:53:15Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6MkwVDfCg9LbbY6xjH3EZk8YSFQZujV5Y4y1ZWeER9tDiN3" }, - "id": "urn:uuid:1ee1a09c-4d7c-4282-8ff2-ac86b6dab342", + "id": "urn:uuid:14bda9df-8064-43ef-a9e6-401970a8286e", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:49Z", + "created": "2026-09-30T14:53:16Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z2kpL9xNMeohky8iBFkvEgJ4HiD4aynTeUdBnaM68zR3eVFM2sHUBtC4K32BSyn4BhtD9e6rFVqPSakFAw9J55b8d", + "proofValue": "zrzSvtrpwB3FmieneTCkpqXQEFTPK1N8PwVZquxsLyqVcb29FaMSb7PxuEbF1Go5EfQbPpLNDkr5GvTGXa1yRUMu", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:49Z", - "validUntil": "2027-09-23T09:29:49Z" + "validFrom": "2026-09-30T14:52:16Z", + "validUntil": "2027-09-30T14:53:16Z" }, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "vetter", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:vetter" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6Mkj1MDZKcfx9AX5CeXHdysiGkRLzBbALyFuShD6wNeY1E3" }, - "id": "urn:uuid:de296f92-f37b-4ee8-b11f-c4d15ea9d05b", + "id": "urn:uuid:4541391b-f035-4db4-8edd-ada27ffca3ee", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:49Z", + "created": "2026-09-30T14:53:16Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z2Fr9ct8aaVEwa2h1JfVZzMX1PMA3ymkgX6BF5e4bhP4Biy4cdZWmhPFMpbFPhVNKq7n9mKbzd1vHGXPgeV5vmq5n", + "proofValue": "z4rEcGe3GK18mtBFMimKTzSqX9Dvf4XUE9HZPemR9GUK52afW15cixGBiPXuZbQBhdwBpQivSStewKQF6WpxWdaiE", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:49Z", - "validUntil": "2027-09-23T09:29:49Z" + "validFrom": "2026-09-30T14:52:16Z", + "validUntil": "2027-09-30T14:53:16Z" } ], "issuedBy": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" @@ -727,7 +757,7 @@ "presentationDefinition": { "credentials": [] }, - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "vetting": { "acceptedMethods": [ "inPerson", @@ -768,45 +798,46 @@ "requiredClaims": [ "name.legal" ], - "statementType": "https://firstperson.network/endorsements/identity-vetting/0.1", + "statementType": "https://registry.trustoverip.org/dtg/vsc/vetted/1", "version": "0.1" } }, "step05_application": { "joinDid": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc", "note": "The commitment salt stays local; the identity commitment below is computed over it.", - "pcsIdentifier": "z5xmV25sLNmZb96UQAtADGWsFtnSaaFwfSREZSpri8N4YX7xs8BkLPfi1fY2hcZN83J" + "pcsIdentifier": "z73uURXHeExZ5Zfck3VPCt6SEYNLVEyEvUXuGLp7ws97Z7UkCiZ3m6PNPRRb9a7sVom" }, "step06_sessions": { "ofEligible": 10, "sessions": [ { "hiddenAttestation": { - "attestation": "z97vNDK2Tow6ZYf5HrwCej668tjnGakeF8DkU2xCHAyYtYfh12HZko5bbUXTWqKJKveyc8ioHbuNM9NVgFUcWTyYuLSu5YbpC4G8JjQLhkf1k9JxiiUv8CvHCSDEJJ9h5EKXUCGikYc3XhXy8vTsMLf361cQ3FZtrQKKdupSQ8e7HTbHPwEmGBhi28gPjSE5n6jvZqgzDo4p9WjsJGM3w2ehaXab5vorQZ5VXeBsXT6ZnFjVG4jaW36tEJSpdoR9jjMGzFjsZN4waG2ZyMJPEs9pVXvectxrahPPS8615xQVrBtXQEcQyny9d7sHjSVnasvnwg4xQkTmTB5gABX5", + "attestation": "zAM7kNvqyJxcCMk24MjiwNyf3j8kotvzcJxi9VKX4P3whMYLnSyGYavxuvmQRjTHjM7jqHaJsi6xHkvQoo84JLaZvkcHDESKUws2QJZMRvbUnGvDEiqAj9euymgaa2qtPLU4tspKjQhtJdURCwTgaz41TCkq2Cvj28R4syi9eXBHDiQjCJpF1Km8AxjZ8BdQ4CUoHeCEnTXPau3K7aKG8rukFGRLUNhjeuVjM5UG3UCQvi23o1PKNnnH3vrfeQR9DUNw9s9Pbbhj7a6YmsLYVSjvd41UXzxuLJFCBY7YKk6Re1eSb1KpJTZ8Db1GamPBq8nuhSg9W2teSvVVr7aG", "meta": { - "cardDigestMultibase": "zQmdDCA5hZ36FmheRBfxonvqtP1mBswttPeWCgdCWjtfyNR", + "cardDigestMultibase": "zQmXuEF7CJTwnf9s21ZSyhdXCagAv2edxXhNozv2nM14LwJ", "claimsVerified": [ "name.legal" ], "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "declaredRelationship": "none", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, "method": "inPerson", - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "tokenLabel": "token/2026-09", "tokenSerial": "zkHRSmnTz9TCJwGC5uFygjdwzQB64qyEC9xAzq162iR2", - "validFrom": "2026-09-23", - "validUntil": "2027-03-22" + "validFrom": "2026-09-30", + "validUntil": "2027-03-29" }, "tokenLabel": "token/2026-09", "tokenSerial": "zkHRSmnTz9TCJwGC5uFygjdwzQB64qyEC9xAzq162iR2", "tokenShown": "zrGWJWBJ6jkFoCPPBK4G3irokLSrFHBbvgzEnSUx64SqPi1TdkdzATK6ouhGPxV3N39VUdhUzbeXjCkZ94nRziuS7kbm8tHHAeJYKHQGQHgmofxVJ4FjHZw93ixydQXpNjB1" }, "hiddenAttestationBytes": 1138, - "matchCode": "GK6J-J37T", - "namedPathStatementEndorsement": { - "cardDigestMultibase": "zQmdDCA5hZ36FmheRBfxonvqtP1mBswttPeWCgdCWjtfyNR", + "matchCode": "QTKH-Q9WC", + "namedPathStatementNote": "On the named path the vetter signs this as the `object.value` of a `vetted/1` statement credential (VSC) addressed to the applicant, citing the session by `taskContext` and `taskDigestMultibase`. On the hidden path it is never signed and never issued — a signed statement carries the vetter's DID, which is the thing being withheld. Its facts go into the attestation instead.", + "namedPathStatementValue": { + "cardDigestMultibase": "zQmXuEF7CJTwnf9s21ZSyhdXCagAv2edxXhNozv2nM14LwJ", "claimsVerified": [ "name.legal" ], @@ -815,17 +846,15 @@ "documentClasses": [ "passport" ], - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, - "method": "inPerson", - "type": "https://firstperson.network/endorsements/identity-vetting/0.1" + "method": "inPerson" }, - "namedPathStatementNote": "On the named path the vetter signs this endorsement into a statement credential addressed to the applicant. On the hidden path it is never signed and never issued — a signed statement carries the vetter's DID, which is the thing being withheld. Its facts go into the attestation instead.", "vetterDid": "did:key:z6MkvRXNYcE7MMduynWTgeKbDaT1iijDSC8pZqXZc8rHPrf2", "vettingCard": { "audience": "did:key:z6MkvRXNYcE7MMduynWTgeKbDaT1iijDSC8pZqXZc8rHPrf2", "cardVersion": 1, - "challenge": "v82gZkimP1VjWGOl8PWs-FbRkdX4opAcGDQ5aKOUD68", + "challenge": "w36SRZbeeDrNAQsd-cgRGYwjEiGHy6R5x3eX1KNsO4k", "claims": [ { "provenance": "selfAsserted", @@ -833,18 +862,18 @@ "value": "Bob Example" } ], - "commitmentSalt": "cMkWSpSWvamHVQaRnJaGal8y9mUemg3pfYDoakrIJJs", + "commitmentSalt": "KzYbnlkA6x_yJ9qaWFttLSOFl68U6WvSlJqWrFzuxh8", "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "domain": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "expiresAt": "2026-09-23T09:44:50.376515Z", - "id": "urn:uuid:025bf624-8784-4837-a362-454738003741", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", - "issuedAt": "2026-09-23T09:29:50.379557Z", + "expiresAt": "2026-09-30T15:08:16.928691Z", + "id": "urn:uuid:4bbd9125-9681-4e38-925f-8764773d8ba6", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", + "issuedAt": "2026-09-30T14:53:16.931796Z", "proof": { - "created": "2026-09-23T09:29:50Z", + "created": "2026-09-30T14:53:16Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z54gW4rk4BVSxZmLuFcyiRodcuzTFECmFwh4pQqHJ76Y3EQuBhxXqkzvNr8cp76DuWri5biHR78e6RQozKSgRCHTD", + "proofValue": "z3kCRNJD6MYcwhbjj1BEtT4mHryR5Nt23pqt77NZKYYdG5NevudgFHsGjqhExuiKD6mWKxX7uM3W1WCPNJZibTruB", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc#z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc" }, @@ -858,31 +887,32 @@ }, { "hiddenAttestation": { - "attestation": "zAGU1xy4yCUhL69hNtUU8vW28c4buxJ7bU1Cr4ksNjhSE84gc9fGZeAJ3kT8fJBkvg4fodr7qF1WXuRRKcET9px93KvA9A49Yaz8ejnv6QXJCRpF4zNGNvcGM9GKVgfiEbm2bSTfvwgGjocuJkQMFREPc8Phq58CGTjgCDQdry8YcEXTG4FabKDMmn1pijRBkmqeR3wdStW5AaEhSVHWcoyNHLTKDhy2F5ARg47D3PBJeCEHFuMbNvyCvRbfdSASpGSVsHtnpvzVd2LxtYdLr1wZtgzHR3YnuoasRUM4rZsUMGJ8s5SbhyEtuT9M7uENyWEJfSkdRziM6NCWqQPj", + "attestation": "z9EaG6EzkTrEWkoMuB49CwSSpANdFFKAN2fwfUCDgtavP8a5vdNtgRP4HBfnkrC3JrC7QicpCxaiedWE8VUwoqXHJP4hfRszaNopCtnVYkdPmFLzwyM4TovD9MpUXYVt2wAunx1KbnpD7U6Lu3WD7dGaLEjXF4idZuzfSzhmaqt7xccsfeZtYhRSxinpARsjoEqYPC1UyYhacCubqSqnwETxZhgrwHgbaaTPR4p5TyEmfpWcPVjkv7zUkC6H4cdg3c33g48makzv2kTSTrTG49hn3MZqyAx84j5QTkrQpzXAFcvKncdyx8ASmhSW2cccEdVhvNsY2WX2M42cUKWg", "meta": { - "cardDigestMultibase": "zQmVNrMtxR6k4UcdT5eG8nLkc1scF3qaNGudFn6A1aHBmoo", + "cardDigestMultibase": "zQmWS13eXyfzPparF2s668cwJ9VjSR4Xmib87XSGrbykXCX", "claimsVerified": [ "name.legal" ], "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "declaredRelationship": "none", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, "method": "video", - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "tokenLabel": "token/2026-09", "tokenSerial": "z6R8SM4p3gN7sHgv59vnLyMMEesfUUJkYj3oaZjZbCWb5", - "validFrom": "2026-09-23", - "validUntil": "2027-03-22" + "validFrom": "2026-09-30", + "validUntil": "2027-03-29" }, "tokenLabel": "token/2026-09", "tokenSerial": "z6R8SM4p3gN7sHgv59vnLyMMEesfUUJkYj3oaZjZbCWb5", "tokenShown": "zzdbgmiyMj8vfF4CJbeXqDzxQto9Pohfc1bfJnP6yRPAk7qzGYa6S6816WTkqwvAiCbiJLQY4RGJXRSKm1iUu4XPDmTEsAxGH9Efiqrn4TWDfowdjPqoQYe6w1QmoJnCvMBv" }, "hiddenAttestationBytes": 1137, - "matchCode": "H5KK-WV9C", - "namedPathStatementEndorsement": { - "cardDigestMultibase": "zQmVNrMtxR6k4UcdT5eG8nLkc1scF3qaNGudFn6A1aHBmoo", + "matchCode": "BETA-YJXV", + "namedPathStatementNote": "On the named path the vetter signs this as the `object.value` of a `vetted/1` statement credential (VSC) addressed to the applicant, citing the session by `taskContext` and `taskDigestMultibase`. On the hidden path it is never signed and never issued — a signed statement carries the vetter's DID, which is the thing being withheld. Its facts go into the attestation instead.", + "namedPathStatementValue": { + "cardDigestMultibase": "zQmWS13eXyfzPparF2s668cwJ9VjSR4Xmib87XSGrbykXCX", "claimsVerified": [ "name.legal" ], @@ -891,17 +921,15 @@ "documentClasses": [ "nationalId" ], - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, - "method": "video", - "type": "https://firstperson.network/endorsements/identity-vetting/0.1" + "method": "video" }, - "namedPathStatementNote": "On the named path the vetter signs this endorsement into a statement credential addressed to the applicant. On the hidden path it is never signed and never issued — a signed statement carries the vetter's DID, which is the thing being withheld. Its facts go into the attestation instead.", "vetterDid": "did:key:z6Mkon22vwz9JoNpGDxCrGZRgeNFTdRTwXYYN3fvAhA3K19x", "vettingCard": { "audience": "did:key:z6Mkon22vwz9JoNpGDxCrGZRgeNFTdRTwXYYN3fvAhA3K19x", "cardVersion": 1, - "challenge": "XHmPiQj7Ree6R9D4UWLlWQuqjj-IlGNi60zPDbrWtkg", + "challenge": "r8UnzN9Hv71kFik0qm51-8EA8N2alMNwjL7RXpUwP98", "claims": [ { "provenance": "selfAsserted", @@ -909,18 +937,18 @@ "value": "Bob Example" } ], - "commitmentSalt": "cMkWSpSWvamHVQaRnJaGal8y9mUemg3pfYDoakrIJJs", + "commitmentSalt": "KzYbnlkA6x_yJ9qaWFttLSOFl68U6WvSlJqWrFzuxh8", "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "domain": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "expiresAt": "2026-09-23T09:44:50.635483Z", - "id": "urn:uuid:3cdfd018-ff98-4d5f-9fd8-90ccbce05334", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", - "issuedAt": "2026-09-23T09:29:50.638272Z", + "expiresAt": "2026-09-30T15:08:17.193574Z", + "id": "urn:uuid:4468f571-0289-4157-92a6-039b844a41ca", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", + "issuedAt": "2026-09-30T14:53:17.196571Z", "proof": { - "created": "2026-09-23T09:29:50Z", + "created": "2026-09-30T14:53:17Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z4XVZzADHbhXiCvFnbWEy3VSuKYm9aZQTySXyEpWKVypbVmbu7ZAqWRT2RJ2Xp7mmzbrewAdjxbVaFEywLnLwKsdG", + "proofValue": "zWZMRdqk9sP7CKXdNfG8a5DqiHyuJakvMKs2xHBpKiK6mKXXWrYyMoFYC6dToirHt9SCgX22ATw4Yuoyo83xPStW", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc#z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc" }, @@ -934,31 +962,32 @@ }, { "hiddenAttestation": { - "attestation": "zBi9ZnTLCqtcy3shcYL1gUNevwKwZnU5kfe16FYyt8KTqqJr7U4SkVbet8oTgZdaURvpP9Fw2tZxDhRYqbVPTg9nxiM4ueLTKEJqeMbPWNTjgrWfvdGWYofTYhzq6RFufX7T7e7vm9vAZzosv6e5HiwHjxUMmsRiwSjMz54NCHk6cjqxZU6RuxMYddamCgu49FRwwdYGuyCfTipj2sP1CXPPeLug2K84addFWhPEyXSJgg2KA2CJja8RjMX54RFm5LYGTMX57NyEWHSwaip3Pg1YNeBtPFePkCuNjPZyFCeGFvUH3WdtCHmzM7Ben69sQUHhQvPmi3EvRH6b6FVU", + "attestation": "zBNpdrduHF6WJB6vE9BWpWDznfntpfNixbbSHxDyLTJo26wwv1K9bMR8S84npbUdb6hDf42nTmHeyATdi3v5KKZAs11v77qY8ktW1nfznzDA8xpv1rhKd7UHZMxrSXubSQBhxb6FgneuBV9Z7Fc6c83SrXH7n7BuPq5KfnN8s1X89bF4SsF67SVmrEpWTUZD3gDFWxNDDZFyGqhnZTmLMF7M8mBkarXAWyxSmd9rEu1w5T4Wuzo46WH6tPqhq4fLQ6mLTuwJyUptu1HLpxfshiJ5D4Kn4BHurydm1vqmZLJyY5WM2zpeihggAEqsgA9Q3rFEQZVzRexmPe4FP7AS", "meta": { - "cardDigestMultibase": "zQmerqsEtbpLsZ2TQXvWSWDDNv3zu9aDsxnpiAycVhfhTgY", + "cardDigestMultibase": "zQmdFKuwh6yaoQLutBAWDmkXrkYt6hYkJYrsrEagp9hgcRd", "claimsVerified": [ "name.legal" ], "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "declaredRelationship": "none", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, "method": "inPerson", - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "tokenLabel": "token/2026-09", "tokenSerial": "z9N22VhviJWtQ4iFU7ASn9WypRhQqe7bM4ZVhBeVh6Ddm", - "validFrom": "2026-09-23", - "validUntil": "2027-03-22" + "validFrom": "2026-09-30", + "validUntil": "2027-03-29" }, "tokenLabel": "token/2026-09", "tokenSerial": "z9N22VhviJWtQ4iFU7ASn9WypRhQqe7bM4ZVhBeVh6Ddm", "tokenShown": "zoYhieh7vARjHYffqavRb4siv5MQdjKv2PCjkNPk8Jf8h2orMUChkNPBhyEQj7C8vLCX4VwbAyuA7jyhDEu3NkVFqNVsrdENpguDEUhTwCg2xnF4dS4fNqiLN6qjnCEWpWev" }, "hiddenAttestationBytes": 1140, - "matchCode": "G84G-G3QJ", - "namedPathStatementEndorsement": { - "cardDigestMultibase": "zQmerqsEtbpLsZ2TQXvWSWDDNv3zu9aDsxnpiAycVhfhTgY", + "matchCode": "5J93-PTZT", + "namedPathStatementNote": "On the named path the vetter signs this as the `object.value` of a `vetted/1` statement credential (VSC) addressed to the applicant, citing the session by `taskContext` and `taskDigestMultibase`. On the hidden path it is never signed and never issued — a signed statement carries the vetter's DID, which is the thing being withheld. Its facts go into the attestation instead.", + "namedPathStatementValue": { + "cardDigestMultibase": "zQmdFKuwh6yaoQLutBAWDmkXrkYt6hYkJYrsrEagp9hgcRd", "claimsVerified": [ "name.legal" ], @@ -967,17 +996,15 @@ "documentClasses": [ "passport" ], - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, - "method": "inPerson", - "type": "https://firstperson.network/endorsements/identity-vetting/0.1" + "method": "inPerson" }, - "namedPathStatementNote": "On the named path the vetter signs this endorsement into a statement credential addressed to the applicant. On the hidden path it is never signed and never issued — a signed statement carries the vetter's DID, which is the thing being withheld. Its facts go into the attestation instead.", "vetterDid": "did:key:z6Mkfmm57fsb6VL7zVusP8zeA9SYkCKdvUhby2G7Yh8vvQ1P", "vettingCard": { "audience": "did:key:z6Mkfmm57fsb6VL7zVusP8zeA9SYkCKdvUhby2G7Yh8vvQ1P", "cardVersion": 1, - "challenge": "PdMY4gmPaNyY_AhwF9kzCcLi2EU77LI678lVX3MTnnk", + "challenge": "MAt0lJbf-zSwf5KdASZl1_Vk-Sl3tIOM5KuDQLWmR40", "claims": [ { "provenance": "selfAsserted", @@ -985,18 +1012,18 @@ "value": "Bob Example" } ], - "commitmentSalt": "cMkWSpSWvamHVQaRnJaGal8y9mUemg3pfYDoakrIJJs", + "commitmentSalt": "KzYbnlkA6x_yJ9qaWFttLSOFl68U6WvSlJqWrFzuxh8", "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "domain": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "expiresAt": "2026-09-23T09:44:50.897575Z", - "id": "urn:uuid:ab14e50e-a122-4b9d-ba1b-b5218457c139", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", - "issuedAt": "2026-09-23T09:29:50.900458Z", + "expiresAt": "2026-09-30T15:08:17.454077Z", + "id": "urn:uuid:61aef00b-b02e-4176-9dca-faddb5e479a9", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", + "issuedAt": "2026-09-30T14:53:17.456987Z", "proof": { - "created": "2026-09-23T09:29:50Z", + "created": "2026-09-30T14:53:17Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z3gAjMBPqK9tFyw7LmjMoy93PJxU5amHbCJkSk5k6zbeW3v5FWVG8FMY4TjSrLaFN8yShTnfhR7kmpse1wZ1r8BRD", + "proofValue": "z2DT4xcYaMbb1yzkYJC6zfmxtVMsLpfJzQRSkuaVpAShYkymR266WY7y8BKs7zu4XrrooJgtVZLe5kKipCNHfxhHL", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc#z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc" }, @@ -1018,27 +1045,27 @@ "extensions": { "hiddenVetting": { "challenge": "a961aa3e63df4d15c9ad565feed87c46", - "id": "z5xmV25sLNmZb96UQAtADGWsFtnSaaFwfSREZSpri8N4YX7xs8BkLPfi1fY2hcZN83J", - "idBinding": "62d561b2c3ae7ade660d3fa409edcd9bc6cfc2f9294ed7b3de67ec7c6f68465f", + "id": "z73uURXHeExZ5Zfck3VPCt6SEYNLVEyEvUXuGLp7ws97Z7UkCiZ3m6PNPRRb9a7sVom", + "idBinding": "c1de105d128e71a93331d6ba8fdcfdee23d6f4c9c3f80c77d104f07395cd6173", "joinDid": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc", - "proof": "z23nVxAWUpMWC1W6bP3bwz5d27PmHTChM12VPS9ofQT4zH8SJEygfEyj5D3ZrdbDSAqcDknqwWRVk9kFVYtseKyDL1FhYfsCEck9mcPEQrvift9se7hTL1TNZpDPYznKJWFDuByFoUw33x4JRpHFvvN2BMD27qEVLbqYd6wvVcYKsWomZv76QteUHzRjnuT6E37fcXZdKgBtDYjasQ2tqEz5ricY6DCmCYUVS5DqAr4VnXT6J4Sj6T7LnK6WNtD5ybJGBVCnGTe59XrNoMjp3YmETo62vcAMw18a9QoCXmrQcy6ZRQTywoEQyn8LtTh9r6rQHCyyNX5GfHESbSkyY5pjDeEGDitkhv6CUipvRbuiASJfDXeHcJxSAeniat5QJGZHRJ53ZYU6VLrgv82YybuvUPcUweJmYBxmVUyd7K4pd3r9WiWJzDRueWSBc2SJ4RfHihQR6QtZo4CXCsWYfZ5Ne7Bj8tvBpYGGkB5WhgRCUx1QozXxufaGtrLVPHWrmcjViw2pTc4LzCncR4e8MnC8b82tw7Skrng2wzZuPwBrtvdAaHyNTxmwPKx6tP2mzAQzz3ew5BzMr8VrDSSSFVM8niFYtg8TAgxguEBs1Ma1qSxJhbtPfw7Ve3G5xxpJkU5xJVQUg7kSrb1gnMzRWWxEfKYANPfUkKjVH1CLS3T6pd7ZCZK52rToULNqdzuaWHCgGH3X9r3cuquVekn8RSBnoxScJuNNgEBrdPG8n6wAQJUcaYf2DpMkX8bGvBYJqjjomcM8icKRGsGbHJzz6drCFYxhfdx46wm88vhaTU42rCPYzdUEgVQg2aS8ytkpdJFL5ZjL9kYhe4KohQ1txchfFf3fesQmwkbNhFR3fdM5EBVSNQUoUwbTHThwXeRnzmefUe4P2gveiov6Zqu4hpkdHJwuxn1CPxGEkQeaDRmnwamHYaJjEGg1hMRip6FtSRvRXxsEQY1Vrqq5kLqqrRJeZ2gJgnnZtSsuhU7m9PNcW68Bt6oWq3L8c6B1z5jaHoJpeDLSc7oF8FvKn7DQaDy2H16s3PtVfygZbQgaEV24HTYKiEt8Di2fD7WJCXYDuLmfkn7chdSWoTcDwWSDWY2JU9t2nMAGYUWsEK6iscEmv3wjGhLf6jrU1wUvNGPND5RgfMp4DkroiNaCNrpPNEq15p9zZ2KiHiwvsy6G61rQPFXXZa7ECDC6ADnjYiz9RPmmUwR57DExuPWCUuL1ovRheMEsSg6ocqJ5ydo8cGcbBwrC2J4qVyi68tHMcCQvgoR2rbpcCPteihADYYghA", + "proof": "z23nVxAWUpMWMHd3xHZcA4zUEULALfMuqoWiGn3rgGj1oaeQG8CSMkjsFdUHrhn5PE5uGs1gA1dzwDxMaoPAmgQyN22Myx5jZAQbrRsMn1RYRzKWZiF1wjj4J8MaQvFCW1WovrktjjctVbEcTSpcNTdKghXPRAqEMP6nPZDPBT7ARMS3quqJkEeiJF6NL2AGvnj8vMUcWe7Z3CCx1b9UwFvVmwr6CVDnWr9q9Eb3mcQtfkRNW4rPoVfGMkiWD4K8pjyb8KDmMiDvsMqN3Si2dHyYZ7f6M4Eeg4WJgbvsbHDZBuyeCRo77AzfCPSReJ6qcvtncwkL8PjEYKgGN3VMhKFZQV5RGoN7eqWZNimeFxZYwrdBaaCn9J72mGSmuEy3qA7dwkCMomrFEsXe9MZyc7Tyivp4qcvw22WW22tDugKBaEveNLrnw8XdrgvdAcyGdvyY7YGQvHsvn6J7sWumhJsmMeEHBGX9qwuJzaE7iQsBSqMExfCkPZKMxbsitsXwX59434s5ky25yUXwrUz6Fy3oDJpsLfDLCGp5a5ctA1gw2vWjib4TAnsPumnUWyLiQZxzuy1QorC9ci8aTVebCEQueYG3sASmqYG24WeSyp9mHWBFASxcKVaL7scJfLVsMsQusozdpZZsDzRA41AAtqrJPJ2PsWYyaBAPtBWm9rgGxuuMEnr4CrfWsKCAGc8rW9q5jcctNYtGMfNk9UcVY5d8YbknJU86dwgb5aiE36VRWNEMDNQbqNAbcag2U54ajbCPHc2wBwGKmrAXbiQTJdk9NwBEu1ZMKVmwgq9gLgrVMGar7QweqPXCRg3YWV9VzugttGFga2HaecoW1g6R1nvvSuaZ5DepQ5jMXimi8h85K7EB2HW5ZnHJQEsKzrrTtvbiKxzFBB6ZayuyWoQuMBA6m5hAVfSKSKEGoUKdf9ag7tcz6e4CgoxEUT4Tw8S8ZXx6Ya7CqePBCfLErEodYVLoSj4Xhzrj9Tk4gfGnGtDU1CRTJFv8CpUksmq5fx9yZ2SCHuX3J3tADE3jvDdXqEumuYgCtwwE3nhriFkuhGN8ARLiqyeRZTsU62vLCsfaKmjkCUzW7L4uNxfDx5GMkMJUxxTUd3KMmdueifyuPJgDdWAyPkpW81r9FxDMK4JR1SqJiBihusfGHUG7unBdV4ukXfFHDxGex37h4BJ8z1LPXR2633UsPcSWjuBEB72k26ycN13GkD19NQpWMn1sTyKKKs1bTVYD7eB22JW1rpNj3iyTSCKiL36n5nhE1bqRpQoz9BCgWeA6LvZg1j6T7", "statements": [ { "meta": { - "cardDigestMultibase": "zQmdDCA5hZ36FmheRBfxonvqtP1mBswttPeWCgdCWjtfyNR", + "cardDigestMultibase": "zQmXuEF7CJTwnf9s21ZSyhdXCagAv2edxXhNozv2nM14LwJ", "claimsVerified": [ "name.legal" ], "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "declaredRelationship": "none", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, "method": "inPerson", - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "tokenLabel": "token/2026-09", "tokenSerial": "zkHRSmnTz9TCJwGC5uFygjdwzQB64qyEC9xAzq162iR2", - "validFrom": "2026-09-23", - "validUntil": "2027-03-22" + "validFrom": "2026-09-30", + "validUntil": "2027-03-29" }, "token": { "label": "token/2026-09", @@ -1048,20 +1075,20 @@ }, { "meta": { - "cardDigestMultibase": "zQmVNrMtxR6k4UcdT5eG8nLkc1scF3qaNGudFn6A1aHBmoo", + "cardDigestMultibase": "zQmWS13eXyfzPparF2s668cwJ9VjSR4Xmib87XSGrbykXCX", "claimsVerified": [ "name.legal" ], "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "declaredRelationship": "none", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, "method": "video", - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "tokenLabel": "token/2026-09", "tokenSerial": "z6R8SM4p3gN7sHgv59vnLyMMEesfUUJkYj3oaZjZbCWb5", - "validFrom": "2026-09-23", - "validUntil": "2027-03-22" + "validFrom": "2026-09-30", + "validUntil": "2027-03-29" }, "token": { "label": "token/2026-09", @@ -1071,20 +1098,20 @@ }, { "meta": { - "cardDigestMultibase": "zQmerqsEtbpLsZ2TQXvWSWDDNv3zu9aDsxnpiAycVhfhTgY", + "cardDigestMultibase": "zQmdFKuwh6yaoQLutBAWDmkXrkYt6hYkJYrsrEagp9hgcRd", "claimsVerified": [ "name.legal" ], "community": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", "declaredRelationship": "none", - "identityCommitment": "zQmZYQrkASJFu1nPtPm1Ky74MTyUxkwGhFFJnyguiqbrMDV", + "identityCommitment": "zQmSernux2UrS46EcVWmiJtztz9TqsQBoKTT6Et8yJXnEK2", "livenessConfirmed": true, "method": "inPerson", - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE", + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C", "tokenLabel": "token/2026-09", "tokenSerial": "z9N22VhviJWtQ4iFU7ASn9WypRhQqe7bM4ZVhBeVh6Ddm", - "validFrom": "2026-09-23", - "validUntil": "2027-03-22" + "validFrom": "2026-09-30", + "validUntil": "2027-03-29" }, "token": { "label": "token/2026-09", @@ -1095,7 +1122,7 @@ ], "suite": "ps-ddh-bls12381" }, - "requirementsDigest": "zQmdmF2WvyXF9dgZeb2W2VxxwYeSzqN3fBykoLTkru5QUYE" + "requirementsDigest": "zQmPqZUWiAsEcuteXCYAzR7dBKo9GoEBqc4uyrNdKjTRt6C" }, "extensionsBytes": 4045, "namedStatementsPresented": 0 @@ -1113,8 +1140,8 @@ "declaredRelationship": "none", "eligible": true, "failures": [], - "id": "0f9883e462f27d3727ce14e7ca83d2f3eb12c185a6c0f5bb2b20c99002682327", - "issuer": "z5kvr9VLYbaHfCBka5bB2QJd33g4BRDTizAdXZs6f1uMgqX8S8wEiPZSrJxeXNvs1ni", + "id": "1b114ca083e795085c7245c4fff443fb1fee37f42b94d34c94a8731523b676e3", + "issuer": "z75dRS3s3DAb4G97CiYnPF4xjcnMx8jcjE29DShLrP2WWLnVJFCyhxHnUaPf48oo5gt", "method": "inPerson", "revoked": false, "verified": true @@ -1124,8 +1151,8 @@ "declaredRelationship": "none", "eligible": true, "failures": [], - "id": "5c1ad3c3bc43471b3a3d304a6e963a5dcbd996758f406c8387db88afb5919993", - "issuer": "z6RvMLFUEsH6r1RLgRb841GHTQMTCA1E5aVPttGh39paZ6jUnf5FgGYowampcJqrRQX", + "id": "1b57d96a49c97b445b1062dacd51d2a930d0e6290c1c06d2d2d5d0ad75e4a70f", + "issuer": "z5ppxTviCZZNLjm65VLp1zWA7eCQRyUSjohFNL1uADdNyebARb9z22qLGkoBGhVPqnT", "method": "video", "revoked": false, "verified": true @@ -1135,8 +1162,8 @@ "declaredRelationship": "none", "eligible": true, "failures": [], - "id": "b96ac64aa29b42a233a961d300dd1a42656c052b747f67b39d9c9fc1c9d650fb", - "issuer": "z7GxDM1BPme4jpcfLnjGRxMC5nSSvJpA3Jz1A2iZzdZWD7Go2ZjYSTLrFrETHrzC4Bg", + "id": "fa8c9b3e12ba0e1f88756e32bad8eac2da5f0e30a57e4c16da790fe3b8f33309", + "issuer": "z6UePwGMat1UBT7Q2k5aZpoTEcBpG3Xyzz8s93otpbfuT3Gdw9oDvTuXzuyeYSDwynu", "method": "inPerson", "revoked": false, "verified": true @@ -1148,49 +1175,52 @@ }, "step09_admission_credentials": { "checks": { - "grantDigestMultibase": "zQmdWPHyaLnnDPqPmTMbtstE9ZegWFwiosaNJwsPtF25ALm", + "grantDigestMultibase": "zQmXXVKVUfbdYtBDcN5rN7tcCnXMLCvp2GTdM5iS3pJwUNh", "grantProofVerifies": true, "reciprocalDigestsTheGrantAsItArrived": true, "reciprocalIssuerIsTheMember": true, "reciprocalProofVerifies": true, "reciprocalSubjectIsTheCommunity": true, - "roleVecProofVerifies": true + "roleVacProofVerifies": true }, - "memberRoleVec": { + "memberRoleVac": { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "endorsement": { - "communityDid": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", - "role": "member", - "type": "CommunityRole" + "authority": { + "actions": [ + "role:member" + ], + "maxAttenuation": 0, + "scope": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "id": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc" }, - "id": "urn:uuid:fb717b58-7544-47e6-b7c0-a563f460021e", + "id": "urn:uuid:5476bc4b-bd49-4813-88e3-9532ac7cea0d", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:51Z", + "created": "2026-09-30T14:53:18Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z4i6uYTi6JoUannSBWhYhUZzaPMis1UKajzNwdF4raEVZkN9V4doEmPU19yyw7Knercq6qmuz8UvJHYKBbheBirTT", + "proofValue": "z2AhnX2CG3B9ojYs1nWN1QVpFS6TEupQ5qnvNk4UzxWwjPRMc9Tup5B6okmBJtFskMR9N7WprVfEpyGFVGjDcGF9y", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, "type": [ "VerifiableCredential", "DTGCredential", - "EndorsementCredential" + "AuthorityCredential" ], - "validFrom": "2026-09-23T09:28:51Z", - "validUntil": "2027-09-23T09:29:51Z" + "validFrom": "2026-09-30T14:52:18Z", + "validUntil": "2027-09-30T14:53:18Z" }, "membershipGrantVmc": { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialStatus": { "id": "https://kernel-vtc.example/v1/status-lists/revocation#0", @@ -1202,13 +1232,14 @@ "credentialSubject": { "id": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc" }, - "id": "urn:uuid:d892e8ee-ad4d-443b-adb6-94996ab49208", + "id": "urn:uuid:1a41f336-f188-4975-8a82-50e849d923b2", "issuer": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p", + "issuerScope": "public", "proof": { - "created": "2026-09-23T09:29:51Z", + "created": "2026-09-30T14:53:18Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "zPg2BfYVdSdf3V3Kjk9xjS18Qqr8aEJCCJ5MXeJ9P6nVTXHd5hUEJYyqALC3eQkp9nAxfUxfA9gepfHFRLBMCEFH", + "proofValue": "z2skeHJUcKirQYwqG2TsU8RLNT4xpaauMNq3oGN2ezCVrEepQuMhTpqpVCYTcdmm7BpgxxoGkFJrUjrpG1nq2V8dv", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p#z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, @@ -1217,26 +1248,27 @@ "DTGCredential", "MembershipCredential" ], - "validFrom": "2026-09-23T09:29:51Z", - "validUntil": "2026-10-23T09:29:51Z" + "validFrom": "2026-09-30T14:53:18Z", + "validUntil": "2026-10-30T14:53:18Z" }, "note": "The pair is the membership edge: the community's grant and the member's acknowledgement, bound by a digest over the grant's wire form. Neither half names a vetter — admission carries no trace of who vetted.", "reciprocalMemberVmc": { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], "credentialSubject": { - "digestMultibase": "zQmdWPHyaLnnDPqPmTMbtstE9ZegWFwiosaNJwsPtF25ALm", + "digestMultibase": "zQmXXVKVUfbdYtBDcN5rN7tcCnXMLCvp2GTdM5iS3pJwUNh", "id": "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p" }, - "id": "urn:uuid:cba8f31e-c133-4250-8e26-b131847d5670", + "id": "urn:uuid:429ed5d1-f591-42a9-9350-2ba60030574f", "issuer": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc", + "issuerScope": "directed", "proof": { - "created": "2026-09-23T09:29:51Z", + "created": "2026-09-30T14:53:18Z", "cryptosuite": "eddsa-jcs-2022", "proofPurpose": "assertionMethod", - "proofValue": "z3oa7eYUmfQ4bGPFZQG57jQaj6k79k7LvCRdW1yKaQeQgYtYz8JdxUjb977DknXAKtTHUYY4LaFMUMp8nLXz4wuBw", + "proofValue": "z2rpfvhBE3Pksd3C4TM1RKhvxrEGXUJ2a6wkb8SqDH4tnkKbQY1Bc9pLWCxEABWABs9NW2F8zMGc59gorYpaKWaXA", "type": "DataIntegrityProof", "verificationMethod": "did:key:z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc#z6Mkn1vNVEZYu4dKzmXsX3BGuT3PGaKuCtWJ1kqxQTuuVMNc" }, @@ -1245,7 +1277,8 @@ "DTGCredential", "MembershipCredential" ], - "validFrom": "2026-09-23T09:29:51Z" + "validFrom": "2026-09-30T14:53:18Z", + "validUntil": "2026-10-30T14:53:18Z" } }, "step10_what_the_community_learns": { @@ -1277,9 +1310,9 @@ "did:key:z6Mkj1MDZKcfx9AX5CeXHdysiGkRLzBbALyFuShD6wNeY1E3" ], "tags": [ - "z5kvr9VLYbaHfCBka5bB2QJd33g4BRDTizAdXZs6f1uMgqX8S8wEiPZSrJxeXNvs1ni", - "z6RvMLFUEsH6r1RLgRb841GHTQMTCA1E5aVPttGh39paZ6jUnf5FgGYowampcJqrRQX", - "z7GxDM1BPme4jpcfLnjGRxMC5nSSvJpA3Jz1A2iZzdZWD7Go2ZjYSTLrFrETHrzC4Bg" + "z75dRS3s3DAb4G97CiYnPF4xjcnMx8jcjE29DShLrP2WWLnVJFCyhxHnUaPf48oo5gt", + "z5ppxTviCZZNLjm65VLp1zWA7eCQRyUSjohFNL1uADdNyebARb9z22qLGkoBGhVPqnT", + "z6UePwGMat1UBT7Q2k5aZpoTEcBpG3Xyzz8s93otpbfuT3Gdw9oDvTuXzuyeYSDwynu" ], "whoActuallyVetted": [ "did:key:z6MkvRXNYcE7MMduynWTgeKbDaT1iijDSC8pZqXZc8rHPrf2", @@ -1290,8 +1323,8 @@ "step11_credential_inventory": { "deliberatelyNotIssued": [ { - "credential": "Vetting statement credential (IdentityVettingEndorsement VEC)", - "why": "this is the point of the hidden path — a signed statement names its vetter. The endorsement is still built (it is where the attested facts come from) but never signed and never sent. `presentable_statements()` is 0." + "credential": "Vetting statement credential (vetted/1 VSC)", + "why": "this is the point of the hidden path — a signed statement names its vetter. The statement value is still built (it is where the attested facts come from) but never signed and never sent. `presentable_statements()` is 0." }, { "credential": "Relationship credential pair (VRC)", @@ -1302,11 +1335,11 @@ "why": "invitation-gated admission only; this community is vetting-gated" }, { - "credential": "Personhood credential (VPC / personhood VMC)", + "credential": "Personhood (the PersonhoodCredential hint on the VMC)", "why": "a separate evaluation (`personhood.rego`); this VMC carries personhood = false, as `admit` mints it" }, { - "credential": "Withdrawal credential (VWC)", + "credential": "Statement withdrawal (vtc/vetting/revoke-statement)", "why": "withdraws a named statement, which the hidden path does not produce. Hidden withdrawal is by token spend-set and class-label rotation instead" } ], @@ -1319,7 +1352,7 @@ }, { "count": 10, - "credential": "Vetter role VEC (CommunityRole)", + "credential": "Vetter role VAC (role:vetter)", "issuer": "community", "note": "the eligibility the criterion's `eligibleVetters.role` names" }, @@ -1349,7 +1382,7 @@ }, { "count": 1, - "credential": "Member role VEC (CommunityRole, role=member)", + "credential": "Member role VAC (role:member)", "issuer": "community" }, { diff --git a/openvtc-core/examples/zkp_reference_flow.rs b/openvtc-core/examples/zkp_reference_flow.rs index cee1eb62..27076846 100644 --- a/openvtc-core/examples/zkp_reference_flow.rs +++ b/openvtc-core/examples/zkp_reference_flow.rs @@ -9,7 +9,9 @@ //! credentials and the Vetting Cards carry real Data Integrity proofs, and the identity //! commitment and card digest bound into each attestation are the ones the ceremony computed. //! Admission mints the same credentials `vtc-service` mints — the community's VMC against a -//! revocation slot, the role VEC, and the member's own reciprocal VMC that closes the edge. +//! revocation slot, the member's role VAC, and the member's own reciprocal VMC that closes the +//! edge. Every one is a DTG Credentials v1 credential: the `https://registry.trustoverip.org/dtg/context/v1` +//! context, a declared `issuerScope`, exactly one concrete type. //! //! Every credential is emitted in full, so the output doubles as a test-vector set: each VC is //! self-contained and verifies against the `did:key` in its `issuer` with no network. @@ -52,8 +54,8 @@ use openvtc_vetting_pcs::{ use rand::{SeedableRng, rngs::StdRng}; use serde_json::{Value, json}; use vta_sdk::protocols::vetting::{ - COMMUNITY_ROLE_ENDORSEMENT_TYPE, IDENTITY_VETTING_ENDORSEMENT_TYPE, VETTER_ROLE, VettingMethod, - VettingRelationship, request, session, + VETTED_PREDICATE, VETTER_ROLE, VETTING_SESSION_TYPE, VettingMethod, VettingRelationship, + request, session, }; use vta_sdk::trust_task_proof::TrustTaskVmResolver; use vta_sdk::vetting::card::sign_card; @@ -75,7 +77,7 @@ fn identity(seed: u8) -> (Secret, String) { fn requirements() -> Value { json!({ "version": "0.1", - "statementType": IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": VETTED_PREDICATE, "minStatements": 3, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video", "priorAcquaintance"], @@ -89,34 +91,31 @@ fn requirements() -> Value { }) } -/// A real community role credential: a DTG endorsement credential, signed by the community. -/// The shape `vtc-service` mints in `credentials::dtg::issue_role` — `{ type: CommunityRole, -/// role, communityDid }` at `credentialSubject.endorsement`. +/// A real community role credential: a DTG Verifiable Authority Credential signed by the +/// community — `issuerScope` public, `credentialSubject.authority` `{ scope: , +/// actions: ["role:"], maxAttenuation: 0 }`, the shape `vtc/vetting/vetters/grant/0.1` +/// and `vtc/join-requests/decide/0.1` deliver. async fn role_credential(issuer: &Secret, community: &str, subject: &str, role: &str) -> Value { let now = Utc::now(); - let mut credential = DTGCredential::new_vec( + let mut credential = DTGCredential::new_community_role_vac( community.to_string(), subject.to_string(), + role, now - Duration::minutes(1), - Some(now + Duration::days(365)), - json!({ - "type": COMMUNITY_ROLE_ENDORSEMENT_TYPE, - "role": role, - "communityDid": community, - }), + now + Duration::days(365), ) + .expect("a role VAC") + .with_max_attenuation(0) + .expect("a VAC") .with_id(format!("urn:uuid:{}", uuid::Uuid::new_v4())); credential.sign(issuer, None).await.unwrap(); serde_json::to_value(&credential).unwrap() } -/// Sign a credential **document** — the body plus the members the catalog type does not model. -/// -/// A VMC issued against a status list carries `credentialStatus`, and `DTGCredential` has no -/// field for it, so signing the parsed credential would leave the status reference outside the -/// proof: a revoked credential could have it stripped without breaking the signature. This is -/// `vtc-service`'s `LocalSigner::sign_doc`, which splices `id` + `credentialStatus` onto the -/// serialised body and signs the whole document. +/// Sign a credential **document**, the way `vtc-service`'s `LocalSigner::sign_doc` does: the +/// whole serialised body — `id` and `credentialStatus` included — under one proof, so a +/// revoked credential cannot have its status reference stripped without breaking the +/// signature. async fn sign_doc(doc: &mut Value, signer: &Secret) { doc.as_object_mut().expect("a JSON object").remove("proof"); let proof = DataIntegrityProof::sign(&*doc, signer, SignOptions::new()) @@ -153,29 +152,23 @@ async fn membership_grant( slot: u32, now: DateTime, ) -> Value { + // `new_vmc` declares `issuerScope: public`, the only scope a community grant can declare. let dtg = DTGCredential::new_vmc( community.to_string(), member.to_string(), now, Some(now + Duration::days(30)), false, - ); - let mut doc = serde_json::to_value(dtg.credential()).unwrap(); - let obj = doc.as_object_mut().unwrap(); - obj.insert( - "id".into(), - json!(format!("urn:uuid:{}", uuid::Uuid::new_v4())), - ); - obj.insert( - "credentialStatus".into(), - json!({ - "id": format!("{status_list}#{slot}"), - "type": "BitstringStatusListEntry", - "statusPurpose": "revocation", - "statusListIndex": slot.to_string(), - "statusListCredential": status_list, - }), - ); + ) + .with_id(format!("urn:uuid:{}", uuid::Uuid::new_v4())) + .with_credential_status(json!({ + "id": format!("{status_list}#{slot}"), + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": slot.to_string(), + "statusListCredential": status_list, + })); + let mut doc = serde_json::to_value(&dtg).unwrap(); sign_doc(&mut doc, issuer).await; doc } @@ -267,7 +260,7 @@ async fn main() { ); role_credentials.push(credential); // The community's record of who holds the vetter role. On the VTI side this is the ACL - // and the role endorsement the credential above was written from — `pcs_issue::enrol` + // and the role grant the credential above was written from — `pcs_issue::enrol` // reads those, never a list of its own. vtc.grant(did); @@ -577,11 +570,29 @@ async fn main() { Utc::now(), ) .expect("a session opens"); + // The session document, as it goes on the wire. The named path's statement would cite + // it by `taskContext` and `taskDigestMultibase` (both REQUIRED by `vetted/1`), so the + // desk keeps it; the digest leaves the proof out, so the unsigned document digests the + // same as the signed one. + let session_document = serde_json::to_value( + openvtc_core::vetting::wire::document( + VETTING_SESSION_TYPE, + vetter_did, + &applicant_did, + session_document_id.clone(), + &session_payload, + ) + .expect("a session document"), + ) + .unwrap(); + desk.record_session_document(&request_id, session_document.clone()) + .expect("the desk keeps the session document"); let open = book .application_mut(&community, persona) .unwrap() .on_session( &session_document_id, + &session_document, vetter_did, session_payload, Utc::now(), @@ -658,22 +669,22 @@ async fn main() { Utc::now(), ) .expect("a statement draft"); - let endorsement = serde_json::to_value(&statement_draft.endorsement).unwrap(); + let vetted_value = serde_json::to_value(&statement_draft.value).unwrap(); let meta = StatementMeta { community: community.clone(), requirements_digest: digest.clone(), method: *method, claims_verified: statement_draft - .endorsement + .value .claims_verified .iter() .map(|c| c.to_string()) .collect(), - liveness_confirmed: statement_draft.endorsement.liveness_confirmed, - declared_relationship: statement_draft.endorsement.declared_relationship, - identity_commitment: statement_draft.endorsement.identity_commitment.clone(), - card_digest_multibase: statement_draft.endorsement.card_digest_multibase.clone(), + liveness_confirmed: statement_draft.value.liveness_confirmed, + declared_relationship: statement_draft.value.declared_relationship, + identity_commitment: statement_draft.value.identity_commitment.clone(), + card_digest_multibase: statement_draft.value.card_digest_multibase.clone(), valid_from: statement_draft.valid_from.date_naive(), valid_until: statement_draft.valid_until.date_naive(), token_label: String::new(), @@ -711,10 +722,11 @@ async fn main() { "vetterDid": vetter_did, "matchCode": open.match_code, "vettingCard": card, - "namedPathStatementEndorsement": endorsement, + "namedPathStatementValue": vetted_value, "namedPathStatementNote": - "On the named path the vetter signs this endorsement into a statement credential \ - addressed to the applicant. On the hidden path it is never signed and never \ + "On the named path the vetter signs this as the `object.value` of a `vetted/1` \ + statement credential (VSC) addressed to the applicant, citing the session by \ + `taskContext` and `taskDigestMultibase`. On the hidden path it is never signed and never \ issued — a signed statement carries the vetter's DID, which is the thing being \ withheld. Its facts go into the attestation instead.", "hiddenAttestation": attestation.clone(), @@ -797,7 +809,7 @@ async fn main() { // --- 9. Admission: the membership credentials --------------------------------------------- // The decision satisfied the criterion, so the community admits. This is what // `vtc-service`'s `Admit` effect mints (`ceremony::execute::issue_member_credentials`): a - // VMC against the next free revocation slot and a role VEC at the granted role, both signed + // VMC against the next free revocation slot and a role VAC at the granted role, both signed // by the community. The member then issues the other half of the edge with `openvtc-core`'s // own `build_member_vmc` — the same call the TUI makes — which digests the grant **as it // arrived**, `credentialStatus` and all. @@ -814,10 +826,11 @@ async fn main() { admitted_at, ) .await; - let member_role_vec = + let member_role_vac = role_credential(&community_secret, &community, &applicant_did, "member").await; - // The member's half. Signed by the applicant's own key, subject = the community. - let reciprocal = build_member_vmc(&applicant_secret, &grant) + // The member's half. Signed by the applicant's own key, subject = the community, declaring + // the member's own `issuerScope` (`directed`). + let reciprocal = build_member_vmc(&applicant_secret, &applicant_did, &grant) .await .expect("the grant is a membership grant this member can acknowledge"); @@ -826,11 +839,11 @@ async fn main() { "step09_admission_credentials".into(), json!({ "membershipGrantVmc": grant, - "memberRoleVec": member_role_vec, + "memberRoleVac": member_role_vac, "reciprocalMemberVmc": reciprocal, "checks": { "grantProofVerifies": proof_verifies(&grant, &community_secret), - "roleVecProofVerifies": proof_verifies(&member_role_vec, &community_secret), + "roleVacProofVerifies": proof_verifies(&member_role_vac, &community_secret), "reciprocalProofVerifies": proof_verifies(&reciprocal, &applicant_secret), "reciprocalSubjectIsTheCommunity": reciprocal["credentialSubject"]["id"] == json!(community), @@ -847,7 +860,7 @@ async fn main() { ); let admission_json = - serde_json::to_string(&json!([grant, member_role_vec, reciprocal])).unwrap(); + serde_json::to_string(&json!([grant, member_role_vac, reciprocal])).unwrap(); report.insert( "step10_what_the_community_learns".into(), json!({ @@ -877,7 +890,7 @@ async fn main() { "issuedHere": [ { "credential": "Vetting Card (VDS)", "issuer": "applicant", "count": VETTING.len(), "note": "one per session, signed by the applicant's key and verified by the vetter" }, - { "credential": "Vetter role VEC (CommunityRole)", "issuer": "community", "count": VETTERS, + { "credential": "Vetter role VAC (role:vetter)", "issuer": "community", "count": VETTERS, "note": "the eligibility the criterion's `eligibleVetters.role` names" }, { "credential": "PCS root credential", "issuer": "community (as PCS helper)", "count": VETTERS, "note": "not a W3C VC — a blind PS credential on the class label `vetter/`" }, @@ -887,14 +900,14 @@ async fn main() { "note": "stands in for the named path's signed vetting statement" }, { "credential": "Membership grant (VMC)", "issuer": "community", "count": 1, "note": "against revocation slot 0, the shape `issue_member_credentials` mints" }, - { "credential": "Member role VEC (CommunityRole, role=member)", "issuer": "community", "count": 1 }, + { "credential": "Member role VAC (role:member)", "issuer": "community", "count": 1 }, { "credential": "Reciprocal member VMC", "issuer": "the new member", "count": 1, "note": "closes the membership edge; digests the grant's wire form" }, ], "deliberatelyNotIssued": [ - { "credential": "Vetting statement credential (IdentityVettingEndorsement VEC)", + { "credential": "Vetting statement credential (vetted/1 VSC)", "why": "this is the point of the hidden path — a signed statement names its vetter. \ - The endorsement is still built (it is where the attested facts come from) \ + The statement value is still built (it is where the attested facts come from) \ but never signed and never sent. `presentable_statements()` is 0." }, { "credential": "Relationship credential pair (VRC)", "why": "not part of admission in any path — VRCs are the peer relationship layer, and \ @@ -903,10 +916,10 @@ async fn main() { VRC names both ends." }, { "credential": "Invitation credential (VIC)", "why": "invitation-gated admission only; this community is vetting-gated" }, - { "credential": "Personhood credential (VPC / personhood VMC)", + { "credential": "Personhood (the PersonhoodCredential hint on the VMC)", "why": "a separate evaluation (`personhood.rego`); this VMC carries personhood = false, \ as `admit` mints it" }, - { "credential": "Withdrawal credential (VWC)", + { "credential": "Statement withdrawal (vtc/vetting/revoke-statement)", "why": "withdraws a named statement, which the hidden path does not produce. Hidden \ withdrawal is by token spend-set and class-label rotation instead" }, ], diff --git a/openvtc-core/src/config/protected_config.rs b/openvtc-core/src/config/protected_config.rs index a3725877..540e2375 100644 --- a/openvtc-core/src/config/protected_config.rs +++ b/openvtc-core/src/config/protected_config.rs @@ -408,8 +408,15 @@ impl ProtectedConfig { /// the post-decrypt half of [`load`](Self::load), split out so the /// deserializer can be exercised directly (e.g. by fuzz harnesses) without a /// keyring or real keys. + /// + /// Credentials stored by an earlier build that do not conform to DTG + /// Credentials v1 are set aside here, each with a logged reason and a + /// notice the UI shows (see [`crate::dtg`]); relationship credentials and + /// tasks do the same as they deserialize ([`Vrcs`]). pub fn parse(plaintext: &[u8]) -> Result { - Ok(serde_json::from_slice(plaintext)?) + let mut config: ProtectedConfig = serde_json::from_slice(plaintext)?; + config.vetting.retire_nonconformant(); + Ok(config) } pub fn get_seed( diff --git a/openvtc-core/src/vetting/applicant.rs b/openvtc-core/src/vetting/applicant.rs index 2e62ee5c..37e376d9 100644 --- a/openvtc-core/src/vetting/applicant.rs +++ b/openvtc-core/src/vetting/applicant.rs @@ -338,7 +338,7 @@ pub enum RequestState { /// The vetter's handle. request_id: String, /// The open session. - session: OpenSession, + session: Box, /// The card we sent, once we have. #[serde(default, skip_serializing_if = "Option::is_none")] card: Option, @@ -389,6 +389,12 @@ pub struct OpenSession { pub expires_at: DateTime, /// The code both people read aloud. pub match_code: String, + /// The signed `vetting/session` document as it arrived. A statement for + /// this session must cite it — `taskContext` its `id`, `taskDigestMultibase` + /// its task digest — or it is not the statement for this session. + /// `None` on a session recorded before statements carried the digest. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub document: Option>, } /// The card we sent into a session. @@ -848,9 +854,13 @@ impl Application { /// /// A malformed or expired session, one for another community, or one for /// a request this vetter never accepted. + /// + /// `document` is the signed session document exactly as received; it is + /// kept, because the statement that closes the session cites its digest. pub fn on_session( &mut self, session_document_id: &str, + document: &Value, vetter: &str, body: session::v0_1::Payload, now: DateTime, @@ -890,10 +900,11 @@ impl Application { .collect(), expires_at: body.expires_at, match_code: vetting_match_code(session_document_id), + document: Some(Box::new(document.clone())), }; request.state = RequestState::Session { request_id: body.request_id.as_str().to_string(), - session: session.clone(), + session: Box::new(session.clone()), card: None, }; request.updated_at = now; @@ -905,7 +916,7 @@ impl Application { pub fn session(&self, session_id: &str) -> Option<(&str, &OpenSession)> { self.requests.iter().find_map(|r| match &r.state { RequestState::Session { session, .. } if session.id == session_id => { - Some((r.vetter.as_str(), session)) + Some((r.vetter.as_str(), &**session)) } _ => None, }) @@ -1156,7 +1167,7 @@ impl Application { if verified.subject() != self.join_did { return Err(ApplicantError::Binding("subject")); } - let endorsement = verified.endorsement(); + let endorsement = verified.value(); if endorsement.community != self.community { return Err(ApplicantError::Binding("community")); } @@ -1173,11 +1184,21 @@ impl Application { }) .ok_or(ApplicantError::NoMatchingRequest)?; let RequestState::Session { - request_id, card, .. + request_id, + card, + session, } = &request.state else { unreachable!("matched a session above"); }; + // `vetted/1` REQUIRES the statement to cite the session by digest as + // well as by id: an `id` locates the exchange, only the digest binds + // the statement to the document we were actually shown. + let document = session + .document + .as_ref() + .ok_or(ApplicantError::Binding("taskDigestMultibase"))?; + verified.check_against_session(document)?; let card = card .as_ref() .ok_or(ApplicantError::WrongState("take a statement before a card"))?; diff --git a/openvtc-core/src/vetting/book.rs b/openvtc-core/src/vetting/book.rs index 9214021b..e1f2401a 100644 --- a/openvtc-core/src/vetting/book.rs +++ b/openvtc-core/src/vetting/book.rs @@ -456,6 +456,11 @@ pub struct VettingBook { /// enrolled under*, which must not move beneath a credential we already hold. #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] pub hidden_published: std::collections::BTreeMap, + /// What [`Self::retire_nonconformant`] set aside, one sentence each, for the + /// Vetting page. Persisted so the notice survives the save that drops the + /// credentials, and cleared by the operator ([`Self::clear_retired`]). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub retired: Vec, /// Questions put to communities and not yet answered. Memory only — see /// [`super::queries`]. #[serde(skip)] @@ -472,6 +477,76 @@ pub struct VettingBook { } impl VettingBook { + /// Set aside every stored vetting credential that does not conform to DTG + /// Credentials v1, and say so in [`Self::retired`]. Run once on load + /// (`ProtectedConfig::parse`). + /// + /// Two stores hold credentials: + /// + /// - **vetter grants** — the community role credential we present to + /// applicants. A pre-v1 one (a role endorsement credential) is refused by + /// every applicant under the current specification, so presenting it only + /// produces `notEligible` at the far end. Dropped; the community has to + /// grant the role again, as a VAC. + /// - **held statements** — the Vetting Statements an application collected. + /// A pre-v1 statement is not a `vetted/1` VSC and a community will not + /// count it. Dropped; the application asks for vetting again. + /// + /// What we *issued* as a vetter is kept: it records an id and a digest for + /// withdrawal, not a credential. + /// + /// Returns how many were set aside. + pub fn retire_nonconformant(&mut self) -> usize { + let mut notes = Vec::new(); + self.vetter_grants.retain(|grant| { + match crate::dtg::nonconformance(&grant.credential) { + None => true, + Some(reason) => { + tracing::warn!( + community = %grant.community, + %reason, + "dropping a stored vetter role credential that does not conform to DTG Credentials v1" + ); + notes.push(format!( + "Your vetter role credential from {} pre-dates DTG Credentials v1 and \ + was set aside. Ask the community to grant the vetter role again.", + grant.community + )); + false + } + } + }); + for application in &mut self.applications { + let community = application.community.clone(); + application.statements.retain(|held| { + match crate::dtg::nonconformance(&held.credential) { + None => true, + Some(reason) => { + tracing::warn!( + %community, + vetter = %held.vetter, + %reason, + "dropping a stored vetting statement that does not conform to DTG Credentials v1" + ); + notes.push(format!( + "A vetting statement from {} for {community} pre-dates DTG \ + Credentials v1 and was set aside. Ask to be vetted again.", + held.vetter + )); + false + } + } + }); + } + let count = notes.len(); + for note in notes { + if !self.retired.contains(¬e) { + self.retired.push(note); + } + } + count + } + /// Our hidden-vetting engine for `community` and `persona`, if one has been enrolled. #[must_use] pub fn hidden_vetter(&self, community: &str, persona: PersonaId) -> Option<&HiddenVetterState> { @@ -600,6 +675,7 @@ impl VettingBook { && self.vetter_grants.is_empty() && self.communities.is_empty() && self.vetter_profiles.is_empty() + && self.retired.is_empty() && self.extra.is_empty() } @@ -611,13 +687,22 @@ impl VettingBook { if existing.credential == grant.credential { return false; } + let community = grant.community.clone(); *existing = grant; + self.clear_retired_for(&community); } else { + self.clear_retired_for(&grant.community.clone()); self.vetter_grants.push(grant); } true } + /// Drop the [`Self::retired`] notices about `community` once a conformant + /// replacement from it has been stored. + pub fn clear_retired_for(&mut self, community: &str) { + self.retired.retain(|note| !note.contains(community)); + } + /// `persona`'s live vetter grant from `community`. #[must_use] pub fn vetter_grant( @@ -1175,7 +1260,7 @@ mod tests { ) -> manifest::v0_2::Response { let requirements: VettingRequirements = serde_json::from_value(serde_json::json!({ "version": "0.1", - "statementType": vta_sdk::protocols::vetting::IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": vta_sdk::protocols::vetting::VETTED_PREDICATE, "minStatements": 1, "acceptedMethods": ["inPerson"], "requiredClaims": ["name.legal"], @@ -1445,4 +1530,46 @@ mod tests { true ); } + + /// A pre-v1 vetter grant is set aside on load with a notice, a conformant + /// one stays, and a conformant replacement clears the notice. + #[test] + fn a_pre_v1_vetter_grant_is_set_aside_with_a_notice() { + let community = "did:example:kernel"; + let grant = |credential: serde_json::Value| VetterGrant { + community: community.to_string(), + persona: PersonaId::new(), + credential_id: None, + valid_until: None, + received_at: Utc::now(), + credential, + }; + let mut book = VettingBook::default(); + book.vetter_grants + .push(grant(crate::dtg::fixtures::retired_role_endorsement( + community, + "did:example:m", + ))); + book.vetter_grants + .push(grant(crate::dtg::fixtures::role_vac( + "did:example:other", + "did:example:m", + "vetter", + ))); + assert_eq!(book.retire_nonconformant(), 1); + assert_eq!(book.vetter_grants.len(), 1); + assert_eq!(book.retired.len(), 1); + assert!(book.retired[0].contains(community)); + assert!(!book.is_empty()); + + book.keep_vetter_grant(grant(crate::dtg::fixtures::role_vac( + community, + "did:example:m", + "vetter", + ))); + assert!( + book.retired.is_empty(), + "the replacement answers the notice" + ); + } } diff --git a/openvtc-core/src/vetting/guide.rs b/openvtc-core/src/vetting/guide.rs index c2ddc4f8..6fa08004 100644 --- a/openvtc-core/src/vetting/guide.rs +++ b/openvtc-core/src/vetting/guide.rs @@ -145,7 +145,7 @@ mod tests { fn requirements() -> VettingRequirements { serde_json::from_value(serde_json::json!({ "version": "0.1", - "statementType": vta_sdk::protocols::vetting::IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": vta_sdk::protocols::vetting::VETTED_PREDICATE, "minStatements": 2, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video", "priorAcquaintance"], diff --git a/openvtc-core/src/vetting/hidden.rs b/openvtc-core/src/vetting/hidden.rs index a24c467e..9722b5d8 100644 --- a/openvtc-core/src/vetting/hidden.rs +++ b/openvtc-core/src/vetting/hidden.rs @@ -406,7 +406,7 @@ pub fn statement_meta( community: &str, requirements_digest: &str, ) -> StatementMeta { - let e = &draft.endorsement; + let e = &draft.value; StatementMeta { community: community.to_string(), requirements_digest: requirements_digest.to_string(), @@ -738,8 +738,7 @@ mod tests { "2026-09", json!({ "version": "0.1", - "statementType": - "https://firstperson.network/endorsements/identity-vetting/0.1", + "statementType": vta_sdk::protocols::vetting::VETTED_PREDICATE, "minStatements": 3, "acceptedMethods": ["inPerson", "video"], "eligibleVetters": { "role": "vetter" } @@ -753,7 +752,7 @@ mod tests { "id": "kernel-developer", "vetting": { "version": "0.1", - "statementType": "https://firstperson.network/endorsements/identity-vetting/0.1", + "statementType": vta_sdk::protocols::vetting::VETTED_PREDICATE, "minStatements": 3, "acceptedMethods": ["inPerson", "video"], "eligibleVetters": { "role": "vetter" }, diff --git a/openvtc-core/src/vetting/inbound.rs b/openvtc-core/src/vetting/inbound.rs index 054b437c..3dccc331 100644 --- a/openvtc-core/src/vetting/inbound.rs +++ b/openvtc-core/src/vetting/inbound.rs @@ -43,7 +43,7 @@ use vta_sdk::protocols::vetting::{ }; use vta_sdk::trust_task_proof::TrustTaskVmResolver; use vta_sdk::vetting::eligibility::{ - EligibilityExpectations, community_role, verify_eligibility_vp, + EligibilityExpectations, community_roles, verify_eligibility_vp, }; use super::applicant::{GrantStatus, VetterEligibility}; @@ -909,7 +909,15 @@ async fn session( warn!(%sender, "vetting session for a community we are not applying to"); return Handled::default(); }; - match application.on_session(&opened.document.id, sender, opened.payload, ctx.now) { + // The body is the signed document as it arrived: the statement that + // closes this session cites its task digest, so it is kept verbatim. + match application.on_session( + &opened.document.id, + &message.body, + sender, + opened.payload, + ctx.now, + ) { Ok(session) => Handled { changed: true, notice: Some(Notice::SessionOpened { @@ -1006,8 +1014,8 @@ async fn statement( // every `issue` as a signed document, the credential under `payload`. let issued = crate::messaging::credential_in_issue(message); if let Some(credential) = issued.as_ref() - && let Some((community, role)) = community_role(credential) - && role_matches(&role, VETTER_ROLE) + && let Some((community, roles)) = community_roles(credential) + && roles.iter().any(|role| role_matches(role, VETTER_ROLE)) { // The delivery first: signed by the community that sent it, under // `authentication`. A grant that arrives any other way is not the @@ -1060,6 +1068,9 @@ async fn statement( .await { Ok(held) => { + let community = application.community.clone(); + book.retired + .retain(|note| !(note.contains(&community) && note.contains(sender))); return Some(Handled { changed: true, notice: Some(Notice::StatementReceived { diff --git a/openvtc-core/src/vetting/tests.rs b/openvtc-core/src/vetting/tests.rs index 7ebe19c4..6914a7a9 100644 --- a/openvtc-core/src/vetting/tests.rs +++ b/openvtc-core/src/vetting/tests.rs @@ -15,11 +15,10 @@ use trust_tasks_rs::TrustTask; use uuid::Uuid; use vta_sdk::protocols::join_requests::{JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE, manifest}; use vta_sdk::protocols::vetting::{ - COMMUNITY_ROLE_ENDORSEMENT_TYPE, IDENTITY_VETTING_ENDORSEMENT_TYPE, VETTER_ROLE, - VETTING_DECLINE_TYPE, VETTING_REQUEST_ERR_INVALID_TICKET, VETTING_REQUEST_ERR_NOT_ELIGIBLE, - VETTING_REQUEST_TYPE, VETTING_SESSION_TYPE, VETTING_VETTER_PROFILE_ERR_NOT_ELIGIBLE, - VETTING_VETTER_RESEND_ERR_NOT_GRANTED, VettingMethod, VettingRelationship, VettingRequirements, - decline, request, revoke_statement, session, vetters, + VETTED_PREDICATE, VETTER_ROLE, VETTING_DECLINE_TYPE, VETTING_REQUEST_ERR_INVALID_TICKET, + VETTING_REQUEST_ERR_NOT_ELIGIBLE, VETTING_REQUEST_TYPE, VETTING_SESSION_TYPE, + VETTING_VETTER_PROFILE_ERR_NOT_ELIGIBLE, VETTING_VETTER_RESEND_ERR_NOT_GRANTED, VettingMethod, + VettingRelationship, VettingRequirements, decline, request, revoke_statement, session, vetters, }; use vta_sdk::trust_task_proof::TrustTaskVmResolver; use vta_sdk::vetting::card::sign_card; @@ -108,21 +107,30 @@ fn the_community_is_its_key() { assert_eq!(did(&secret(COMMUNITY_SEED)), COMMUNITY); } -/// A community role credential, signed by `issuer`. -async fn role_credential(issuer: &Secret, community: &str, subject: &str, role: &str) -> Value { +/// A community role credential — a VAC conferring `role:` in +/// `community`'s scope, `issuerScope` public, `maxAttenuation` 0 — signed by +/// `issuer`. `issuer` other than `community` is a forgery: authority in a scope +/// the signer does not govern. +fn role_vac(issuer: &Secret, community: &str, subject: &str, role: &str) -> DTGCredential { let now = Utc::now(); - let mut credential = DTGCredential::new_vec( + DTGCredential::new_vac( did(issuer), + dtg_credentials::IssuerScope::Public, subject.to_string(), + community.to_string(), + vec![vta_sdk::protocols::vetting::role_action(role)], now - chrono::Duration::minutes(1), - Some(now + chrono::Duration::days(365)), - json!({ - "type": COMMUNITY_ROLE_ENDORSEMENT_TYPE, - "role": role, - "communityDid": community, - }), + now + chrono::Duration::days(365), ) - .with_id(wire::new_id()); + .unwrap() + .with_max_attenuation(0) + .unwrap() + .with_id(wire::new_id()) +} + +/// A community role credential, signed by `issuer`. +async fn role_credential(issuer: &Secret, community: &str, subject: &str, role: &str) -> Value { + let mut credential = role_vac(issuer, community, subject, role); credential.sign(issuer, None).await.unwrap(); serde_json::to_value(&credential).unwrap() } @@ -209,6 +217,13 @@ impl Party { } async fn receive(&mut self, message: &Message, sender: &str) -> Handled { + self.try_receive(message, sender) + .await + .expect("a vetting message is claimed") + } + + /// [`Self::receive`] for a message the vetting handler may not claim at all. + async fn try_receive(&mut self, message: &Message, sender: &str) -> Option { let resolver = TrustTaskVmResolver::did_key_only(); let did_resolver = did_resolver().await; // As the dispatcher does off the loop: a delivered credential is @@ -223,9 +238,7 @@ impl Party { recipient: Some((self.persona, &self.did)), now: Utc::now(), }; - handle(&mut self.book, &ctx, &mut self.seen, message, sender) - .await - .expect("a vetting message is claimed") + handle(&mut self.book, &ctx, &mut self.seen, message, sender).await } /// [`Self::receive`], with the community answer already checked off the @@ -270,7 +283,7 @@ async fn signed(mut document: TrustTask, signer: &Secret) -> Message { fn requirements() -> VettingRequirements { serde_json::from_value(json!({ "version": "0.1", - "statementType": IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": VETTED_PREDICATE, "minStatements": 1, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video"], @@ -538,6 +551,13 @@ async fn in_session(applicant: &mut Party, vetter: &mut Party) -> (String, Trust &body, ) .unwrap(); + // The statement cites this document by task digest, which leaves out the + // proof — so the unsigned document digests the same as the signed one the + // applicant receives. + vetter + .book + .record_session_document(&request_id, serde_json::to_value(&doc).unwrap()) + .unwrap(); (request_id, doc) } @@ -918,11 +938,13 @@ async fn a_member_the_community_has_not_named_cannot_vet() { async fn a_vetter_grant_is_kept_only_from_its_community() { let mut member = Party::new(5).member_of(COMMUNITY); let impostor = secret(0xC1); + // Authority in a scope the signer does not govern is not a community role + // credential at all, so the vetting handler does not even claim it. let forged = role_credential(&impostor, COMMUNITY, &member.did.clone(), VETTER_ROLE).await; let handled = member - .receive(&delivery(&forged, &impostor).await, &did(&impostor)) + .try_receive(&delivery(&forged, &impostor).await, &did(&impostor)) .await; - assert!(handled.notice.is_none() && member.book.vetter_grants.is_empty()); + assert!(handled.is_none_or(|h| h.notice.is_none()) && member.book.vetter_grants.is_empty()); let for_someone_else = role_credential( &secret(COMMUNITY_SEED), @@ -1614,22 +1636,9 @@ async fn a_manifest_answers_whoever_asked_and_brings_the_communitys_branding() { } /// A community role credential carrying a status list entry, signed by -/// `issuer`. `DTGCredential` does not model `credentialStatus`, so the -/// credential is signed as JSON. +/// `issuer`. Signed as JSON, as a community's own signer does. async fn role_credential_with_status(issuer: &Secret, subject: &str) -> Value { - let now = Utc::now(); - let credential = DTGCredential::new_vec( - did(issuer), - subject.to_string(), - now - Duration::minutes(1), - Some(now + Duration::days(365)), - json!({ - "type": COMMUNITY_ROLE_ENDORSEMENT_TYPE, - "role": VETTER_ROLE, - "communityDid": COMMUNITY, - }), - ) - .with_id(wire::new_id()); + let credential = role_vac(issuer, COMMUNITY, subject, VETTER_ROLE); let mut value = serde_json::to_value(&credential).unwrap(); value.as_object_mut().unwrap().remove("proof"); value["credentialStatus"] = json!({ diff --git a/openvtc-core/src/vetting/vetter.rs b/openvtc-core/src/vetting/vetter.rs index 454602cb..04d569b1 100644 --- a/openvtc-core/src/vetting/vetter.rs +++ b/openvtc-core/src/vetting/vetter.rs @@ -21,11 +21,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; use vta_sdk::protocols::vetting::{ - CheckShape, ClaimType, IDENTITY_VETTING_ENDORSEMENT_TYPE, IdentityVettingEndorsement, - ShapeError, VETTING_REQUEST_ERR_CAPACITY, VETTING_REQUEST_ERR_DECLINED, - VETTING_REQUEST_ERR_METHOD_UNAVAILABLE, VETTING_REQUEST_ERR_NOT_ELIGIBLE, VettingDocumentation, - VettingMethod, VettingRelationship, check_request, decline, documentation, request, - revoke_statement, session, + CheckShape, ClaimType, ShapeError, VETTING_REQUEST_ERR_CAPACITY, VETTING_REQUEST_ERR_DECLINED, + VETTING_REQUEST_ERR_METHOD_UNAVAILABLE, VETTING_REQUEST_ERR_NOT_ELIGIBLE, VettedObjectValue, + VettingDocumentation, VettingMethod, VettingRelationship, check_request, decline, + documentation, request, revoke_statement, session, }; use vta_sdk::trust_task_proof::TrustTaskVmResolver; use vta_sdk::vetting::VettingError; @@ -52,6 +51,10 @@ pub enum VetterError { /// Building or verifying an artifact failed. #[error(transparent)] Vetting(#[from] VettingError), + /// The session's signed document was not kept, so a statement cannot cite + /// it (`taskDigestMultibase`). Open the session again. + #[error("this session predates statement citations — open the session again")] + SessionDocumentMissing, /// Liveness is required for this method (D10). #[error("confirm the match code with the person before attesting")] LivenessNotConfirmed, @@ -169,6 +172,16 @@ pub struct DeskSession { pub expires_at: DateTime, /// The code both people read aloud. pub match_code: String, + /// The signed `vetting/session` document exactly as we sent it + /// ([`VettingBook::record_session_document`]). + /// + /// The Vetting Statement cites it by `taskContext` (its `id`) **and** + /// `taskDigestMultibase` (its task digest), both REQUIRED by the + /// `vetted/1` predicate profile, so a statement cannot be drafted without + /// it. `None` on a session opened by a build before the profile required + /// the digest; such a session has to be opened again. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub document: Option>, } /// A verified card. The claims and the card itself are forgotten after @@ -513,12 +526,40 @@ impl VettingBook { optional_claims: optional_claims.clone(), expires_at: body.expires_at, match_code: vetting_match_code(session_document_id), + document: None, }, }; entry.updated_at = now; Ok(body) } + /// Keep the signed `vetting/session` document for `request_id`, exactly as + /// it goes on the wire. Call it after signing and before sending: the + /// statement cites this document by its task digest, which covers + /// everything but the top-level `proof`, so a re-built copy would not do. + /// + /// # Errors + /// + /// No such request, no session open on it, or a document whose `id` is not + /// the session's. + pub fn record_session_document( + &mut self, + request_id: &str, + document: Value, + ) -> Result<(), VetterError> { + let entry = self + .desk_entry_mut(request_id) + .ok_or(VetterError::NoSuchRequest)?; + let DeskState::Session { session } = &mut entry.state else { + return Err(VetterError::WrongState("record its session document")); + }; + if document.get("id").and_then(Value::as_str) != Some(session.id.as_str()) { + return Err(VetterError::WrongState("record another session's document")); + } + session.document = Some(Box::new(document)); + Ok(()) + } + /// The applicant's card for session `session_id` /// (`vetting/session#response`). Verified against everything the session /// bound it to before the vetter sees it. @@ -585,12 +626,17 @@ impl VettingBook { Ok(entry) } - /// The statement to sign for `request_id`, from the vetter's checklist. + /// The statement to sign for `request_id`, from the vetter's checklist: a + /// `vetted/1` VSC whose `object.value` is the checklist, issued under the + /// vetter's member DID declaring + /// [`MEMBER_IDENTIFIER_SCOPE`](crate::dtg::MEMBER_IDENTIFIER_SCOPE) + /// (`directed`, which the profile's minimum requires), and citing the + /// session document it answers. /// /// # Errors /// - /// No card yet, a checklist that does not support the statement, or a - /// malformed endorsement. + /// No card yet, a session whose signed document was not kept, a checklist + /// that does not support the statement, or a malformed value. pub fn statement_draft( &self, request_id: &str, @@ -640,8 +686,12 @@ impl VettingBook { .iter() .map(|c| ClaimType::try_from(c.as_str()).map_err(|e| schema(&e))) .collect::, ShapeError>>()?; - let endorsement = IdentityVettingEndorsement { - endorsement_type: IDENTITY_VETTING_ENDORSEMENT_TYPE.into(), + let session_document = session + .document + .as_deref() + .cloned() + .ok_or(VetterError::SessionDocumentMissing)?; + let value = VettedObjectValue { community: entry.community.clone(), method: attestation.method, document_classes, @@ -652,15 +702,16 @@ impl VettingBook { declared_relationship: attestation.declared_relationship, attestation_text_digest: attestation.attestation_text_digest, }; - endorsement.check_shape()?; + value.check_shape()?; Ok(StatementDraft { id: format!("urn:uuid:{}", Uuid::new_v4()), issuer: vetter_did.to_string(), + issuer_scope: crate::dtg::MEMBER_IDENTIFIER_SCOPE, subject: entry.applicant.clone(), - endorsement, + value, valid_from: now, valid_until: now + self.policy.statement_validity(), - task_context: session.id.clone(), + session: session_document, }) } @@ -690,7 +741,7 @@ impl VettingBook { applicant: entry.applicant.clone(), community: entry.community.clone(), persona: entry.persona, - method: verified.endorsement().method, + method: verified.value().method, issued_at: now, valid_until: verified.valid_until(), withdrawal: None, diff --git a/openvtc-core/src/vetting/wire.rs b/openvtc-core/src/vetting/wire.rs index 7f0fd57f..b9274cca 100644 --- a/openvtc-core/src/vetting/wire.rs +++ b/openvtc-core/src/vetting/wire.rs @@ -664,14 +664,19 @@ pub fn delivered_statement(body: &Value) -> Option<&Value> { } /// The identity-vetting statement at `credential_response.credential` in an -/// issue payload, if that is what it holds. +/// issue payload, if that is what it holds: a statement credential whose +/// `credentialSubject.predicate` is the registry's `vetted/1` predicate. +/// +/// Classified by predicate, never by a type string — a VSC's meaning is its +/// predicate. This only claims the delivery for the vetting handler; the +/// statement is parsed and verified in full by `verify_statement`. #[must_use] pub fn statement_in(payload: &Value) -> Option<&Value> { let credential = payload.pointer("/credential_response/credential")?; (credential - .pointer("/credentialSubject/endorsement/type") + .pointer("/credentialSubject/predicate") .and_then(Value::as_str) - == Some(vta_sdk::protocols::vetting::IDENTITY_VETTING_ENDORSEMENT_TYPE)) + == Some(vta_sdk::protocols::vetting::VETTED_PREDICATE)) .then_some(credential) } @@ -933,9 +938,9 @@ pub(crate) mod tests { fn a_statement() -> Value { json!({ - "credentialSubject": { "endorsement": { - "type": vta_sdk::protocols::vetting::IDENTITY_VETTING_ENDORSEMENT_TYPE - } } + "credentialSubject": { + "predicate": vta_sdk::protocols::vetting::VETTED_PREDICATE + } }) } diff --git a/openvtc-core/tests/hidden_vetting_onboarding.rs b/openvtc-core/tests/hidden_vetting_onboarding.rs index 9ef1dfff..91d43636 100644 --- a/openvtc-core/tests/hidden_vetting_onboarding.rs +++ b/openvtc-core/tests/hidden_vetting_onboarding.rs @@ -22,9 +22,7 @@ use openvtc_vetting_pcs::{ }; use rand::{SeedableRng, rngs::StdRng}; use serde_json::{Value, json}; -use vta_sdk::protocols::vetting::{ - IDENTITY_VETTING_ENDORSEMENT_TYPE, VettingMethod, VettingRelationship, -}; +use vta_sdk::protocols::vetting::{VETTED_PREDICATE, VettingMethod, VettingRelationship}; const COMMUNITY: &str = "did:webvh:QmVtcScid:kernel-vtc.example"; const PERIOD: &str = "2026-09"; @@ -33,7 +31,7 @@ const JOIN_DID: &str = "did:webvh:QmVtcScid:bob-kernel"; fn requirements() -> Value { json!({ "version": "0.1", - "statementType": IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": VETTED_PREDICATE, "minStatements": 2, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video"], diff --git a/openvtc-core/tests/vetting_e2e.rs b/openvtc-core/tests/vetting_e2e.rs index 55556dc1..f39f9bdf 100644 --- a/openvtc-core/tests/vetting_e2e.rs +++ b/openvtc-core/tests/vetting_e2e.rs @@ -52,9 +52,9 @@ use tokio::sync::mpsc; use uuid::Uuid; use vta_sdk::protocols::join_requests::{JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE, manifest}; use vta_sdk::protocols::vetting::{ - COMMUNITY_ROLE_ENDORSEMENT_TYPE, IDENTITY_VETTING_ENDORSEMENT_TYPE, VETTER_ROLE, - VETTING_REQUEST_RESPONSE_TYPE, VETTING_REQUEST_TYPE, VETTING_SESSION_RESPONSE_TYPE, - VETTING_SESSION_TYPE, VettingMethod, VettingRelationship, VettingRequirements, session, + VETTED_PREDICATE, VETTER_ROLE, VETTING_REQUEST_RESPONSE_TYPE, VETTING_REQUEST_TYPE, + VETTING_SESSION_RESPONSE_TYPE, VETTING_SESSION_TYPE, VettingMethod, VettingRelationship, + VettingRequirements, session, }; use vta_sdk::trust_task_proof::TrustTaskVmResolver; use vta_sdk::vetting::card::sign_card; @@ -200,7 +200,7 @@ impl Party { fn requirements() -> VettingRequirements { serde_json::from_value(json!({ "version": "0.1", - "statementType": IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": VETTED_PREDICATE, "minStatements": 1, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video"], @@ -262,18 +262,24 @@ async fn manifest_reply(community: &str, to: &str, signer: &Secret) -> Message { /// The community's `vetter` role credential for `subject`. async fn role_credential(issuer: &Secret, community: &str, subject: &str) -> Value { - let now = Utc::now(); - let mut credential = DTGCredential::new_vec( + // A community-issued VAC: `role:vetter` in the community's own scope, + // `issuerScope` public, not further attenuable. + assert_eq!( did_of(issuer), + community, + "a community grants roles in itself" + ); + let now = Utc::now(); + let mut credential = DTGCredential::new_community_role_vac( + community.to_string(), subject.to_string(), + VETTER_ROLE, now - chrono::Duration::minutes(1), - Some(now + chrono::Duration::days(365)), - json!({ - "type": COMMUNITY_ROLE_ENDORSEMENT_TYPE, - "role": VETTER_ROLE, - "communityDid": community, - }), + now + chrono::Duration::days(365), ) + .expect("a role VAC") + .with_max_attenuation(0) + .expect("a VAC") .with_id(wire::new_id()); credential.sign(issuer, None).await.expect("sign the grant"); serde_json::to_value(&credential).expect("credential json") @@ -470,6 +476,13 @@ async fn the_vetting_ceremony_completes_over_the_wire() { ) .expect("session document"); wire::sign(&mut document, &bob.secret).await.expect("sign"); + // Kept as sent: the statement cites it by `taskDigestMultibase`. + bob.book + .record_session_document( + &request_id, + serde_json::to_value(&document).expect("session json"), + ) + .expect("record the session document"); let session_doc = document.clone(); let session_msg = wire::to_message(&document).expect("session message"); diff --git a/openvtc-vetting-pcs/examples/hidden_vetting.rs b/openvtc-vetting-pcs/examples/hidden_vetting.rs index b4c34fd3..818a97a2 100644 --- a/openvtc-vetting-pcs/examples/hidden_vetting.rs +++ b/openvtc-vetting-pcs/examples/hidden_vetting.rs @@ -20,9 +20,7 @@ use openvtc_vetting_pcs::{ }; use rand::{SeedableRng, rngs::StdRng}; use serde_json::json; -use vta_sdk::protocols::vetting::{ - IDENTITY_VETTING_ENDORSEMENT_TYPE, VettingMethod, VettingRelationship, -}; +use vta_sdk::protocols::vetting::{VETTED_PREDICATE, VettingMethod, VettingRelationship}; const COMMUNITY: &str = "did:example:kernel-vtc"; const APPLICANT: &str = "did:example:bob"; @@ -35,7 +33,7 @@ const VETTING: [usize; 3] = [2, 5, 7]; fn requirements() -> serde_json::Value { json!({ "version": "0.1", - "statementType": IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": VETTED_PREDICATE, "minStatements": 3, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video"], diff --git a/openvtc-vetting-pcs/src/meta.rs b/openvtc-vetting-pcs/src/meta.rs index 58a8a88e..99f03e31 100644 --- a/openvtc-vetting-pcs/src/meta.rs +++ b/openvtc-vetting-pcs/src/meta.rs @@ -10,7 +10,7 @@ use vta_sdk::protocols::vetting::{VettingMethod, VettingRelationship}; use crate::ProtoError; /// The public metadata of one hidden statement: the fields of the V0 `IdentityVetting` -/// endorsement that the VTC counts, minus any identifier of the vetter. Sent beside the +/// statement that the VTC counts, minus any identifier of the vetter. Sent beside the /// attestation; bound into its `ctx_j`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/openvtc-vetting-pcs/tests/end_to_end.rs b/openvtc-vetting-pcs/tests/end_to_end.rs index 711b0765..5408d00a 100644 --- a/openvtc-vetting-pcs/tests/end_to_end.rs +++ b/openvtc-vetting-pcs/tests/end_to_end.rs @@ -15,7 +15,7 @@ use predicate_credential_system::Error as PcsError; use rand::{SeedableRng, rngs::StdRng}; use serde_json::json; use vta_sdk::{ - protocols::vetting::{IDENTITY_VETTING_ENDORSEMENT_TYPE, VettingMethod, VettingRelationship}, + protocols::vetting::{VETTED_PREDICATE, VettingMethod, VettingRelationship}, vetting::requirements::Need, }; @@ -29,7 +29,7 @@ fn now() -> DateTime { fn requirements() -> serde_json::Value { json!({ "version": "0.1", - "statementType": IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": VETTED_PREDICATE, "minStatements": 2, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video", "priorAcquaintance"], diff --git a/openvtc-vetting-pcs/tests/fixtures/submission.json b/openvtc-vetting-pcs/tests/fixtures/submission.json index ad9ed1fe..e00f0961 100644 --- a/openvtc-vetting-pcs/tests/fixtures/submission.json +++ b/openvtc-vetting-pcs/tests/fixtures/submission.json @@ -9,9 +9,9 @@ "hiddenVetting": { "challenge": "2101bb65d957c15019d8b586a344eccc", "id": "z7Maq7iaYFvoymP89sz5H3Yj5ZWRb61NMc7U7HB7QEuBpyHmLojjss1Gp1q2C8MxAMS", - "idBinding": "34663f054fb5604c99f946827d4e4248fa7ecd32f98d17dbc5e002e1b9b4bc9b", + "idBinding": "5433844cc1c272ef59bdb6fc40af6cf8f9df863167e2ab4f30924622f65d4d43", "joinDid": "did:example:bob-kernel", - "proof": "z3Xef1JY2x1tDTCQu3iRfrEBKWKCxkKHcQCAmBK47kad85z7MpYWgxypfXSt3Gd81X7FBXmMF1cryU3q6R7mkxdRzqS1wqYbFvy1yQsigvsThMyTroDm1YfYt16P9CjQjG3mGhaTLLHo9FnAFogCuMCWYX1gePidZ7d7yHuJJKtW3WU7h8KzUf65NfUFQxZK7yd9cMDF5sJaqxqRh7AXNbh4oSUf88LxWnLjq5bqF8fjNVh4LAsbcFPZVDS4vQgjtBPwFuUQUXWw9e3AsxVidYMuaDNaKRHFf5443WbRFHZGgjEBqrcL2SoPEfd9AnSVhNGqEmeyteVx1a7jhuA7Wpn4uzjGpszYjN42FgxL2cSYbobQcDC4JBt2gtyRoF6cVVDwG7X45qurKtzQs4RSnNwA5Vs64UFBzz9pgPqvnhq4ZpdFQYJ5RWS5TgSqYnZhUobPDU1PqqoeHvKA7iZk7CHbdQF935LFQiSE3FdvwwHzgoxDieJXDzYAcT7Bsx8JghikJh2tghTxDBKaYh7NGtQvZcnZjZoS4xFbZgzu6rYF1vAFgcA2vwsmZbdJQCkNRvXqaGh2rCLa1uijrRE29ZYUBKMfRub4uLvo6j4DpCJwAZxedNQHF5FH1DXunDUfJ6gLjBBdWf5vriJTMs2mVduTbk5B43VVPceT9KRbNhxN8aMZo4QctZaFEnZ9DFcqmSQLyiaBAghMFLy1VA8iCiE3CPi6S4TBy9QUSvghpMihwQKXof9gNHupdTTFZpGGHDp129fdjgJyjauHo4Hgt9Q1qKDVAVBWaNTSe8SymcKwocjaYyy7ptRB34Apc4eKx9gQm8dPpJgB5RXeQiXdD2jK7bSzYzwnv85Nj34CY3NkEX67Rdrhb4k8TdoVveMM8RvzQC6mPGwNeJkwUW56HtDAU8R2DxHQ7H3mEBGdNx5nHHkUAHNoSs3ZoCggRBajiV", + "proof": "z3Xef1JY2x1tDTCQu3iRfrEBKWKCxkKHcQCAmBK47kad85z7MpYWgxypfXSt3Gd81X7FBXmMF1cryU3q6R7mkxdRzqS1wqYbFvy1yQsigvsThMyTroDm1YfYt16P9CjQjG3mGhaTLLHo9FnAFogCuMCWYX1gePidZ7d7yHuJJKtW3WU7h8KzUf65NfUFQxZK7yd9cMDF5sJaqxqRh7AXNbh4oSUf88LxWnLjq5bqF8fjNVh4LAsbcFPZVDS4u3Y6dYugcB8DZkrcn3sdR2vGChxZnwUuAB22uVi952vxi5pn25Lrw2YZU55xZG6G4FEd9DMQ1gahScLgF8eLNGrG1pC9VssbkHR16ZC7eqtsVuj8VmKMiVhwr9VcMKW8xgDzVKFS71M4dDuvaWDoDmdPAAKJKGds7JE3t9dSD1XEuxAoCXmEhhdoSRYHvX2UMtuSebzQLBuEWEA8PKzGPfBho4fikF4EZsAVEzshrFDtjypueJrGNMNAWKLZqXzGRbPiqd5jy7fkQoQr7zD2hkaNYuzs6LF1fehPXv25tYk2zo4cK9kUkCAoC19g2oWmMWjQDq71ooN3RjjbduykkWiUtAiBYXeZPRBWPbdWx4xdeiqw7j3LTNtvyjzW1rKrD4LfPTN1irKQkjU4hhVZgQVQu2fT5dDt8RpNdDE9HxFnVKZ4A9dLP7ZCXdapTAyRBoU5eDr8FgHQ3VSfxzVqvFshLZXZibroHQDsZaj4mS2duhDdkGHDLLwB2Gujdn9a5hnrxbzpmrm9w68pQQqXmfM37dFABYKxYu8KB3AQxQCCg5pd4TsC7CjhMg1GPNid5seYvviFc3ac58rox4ZqHizZWLBKLwWk77Fn2FdUqcK423puGzzP6uSJfNnW9gQufS3sVGw6XcUgv1PZ3GngyPAELZwdEjzSAkoSPu5rmc7khEraBKCTSRW1FyswjnEHaD4cgH", "statements": [ { "meta": { @@ -24,7 +24,7 @@ "identityCommitment": "zCommitmentOfThisApplication", "livenessConfirmed": true, "method": "inPerson", - "requirementsDigest": "zQmWLpaPx2M8kj8vYGChQPWeL7gkYqxHVGfhzoaCRHnmYeX", + "requirementsDigest": "zQmd5MPCDVbwQyjk1aENTUFTcNCEjEGqGzNbTkmxLS5pQEJ", "tokenLabel": "token/2026-09", "tokenSerial": "z4cExPgjXik8gjzspYn3UQBSceCNMxvyf3WsVXi8gg2r6", "validFrom": "2026-09-20", @@ -47,7 +47,7 @@ "identityCommitment": "zCommitmentOfThisApplication", "livenessConfirmed": true, "method": "video", - "requirementsDigest": "zQmWLpaPx2M8kj8vYGChQPWeL7gkYqxHVGfhzoaCRHnmYeX", + "requirementsDigest": "zQmd5MPCDVbwQyjk1aENTUFTcNCEjEGqGzNbTkmxLS5pQEJ", "tokenLabel": "token/2026-09", "tokenSerial": "zAMfD5zM58iQen9r6iy3Gjr1fMwyQRwCKvrzCdgND6CrK", "validFrom": "2026-09-20", @@ -94,9 +94,9 @@ "requiredClaims": [ "name.legal" ], - "statementType": "https://firstperson.network/endorsements/identity-vetting/0.1", + "statementType": "https://registry.trustoverip.org/dtg/vsc/vetted/1", "version": "0.1" }, - "requirementsDigest": "zQmWLpaPx2M8kj8vYGChQPWeL7gkYqxHVGfhzoaCRHnmYeX", + "requirementsDigest": "zQmd5MPCDVbwQyjk1aENTUFTcNCEjEGqGzNbTkmxLS5pQEJ", "tvk": "zPpoQ2paPusHjJgDp7dnNHT3QdZP6cY39HmZQfNxYoj7jLFCy9qbmPRskAQhbzdARo6pdnvXcUuYFFmEX7SH6seLxhbCpoAGcr3BQAqBUHFCFfdGtxV99Daw6BTGUEtDB2nSdvTZFaVgco94SjbDcuFxAj5NTD6qQaAmH8UNRQ4hnVgM1eZp2p4hMi5XqEsfs2NRbS7EhXt8cBEax7y1SoQibejEtkgFyS1WZRBTEPh4Ghr65zQky3yz3TZS84zh4b4WZHJXZptqbTWFyLUzG6P8Q5HsyDvc5yQKikn5TpWZ2uPhg77qNn8fCHNViHsyQVbQkDaCFRg77nhr1AzuhtEjoiSR7G8RmNrJsuMfoihm6b4P13CXiwCEt5Tami6roD2EznQmtm359t8tFPwp9SUdGxUuufEmWvkiopDEZ18dCSVHe96P6hZqkRKyjkxGuZ6o18cpeX1x" } diff --git a/openvtc-vetting-pcs/tests/wire_fixture.rs b/openvtc-vetting-pcs/tests/wire_fixture.rs index b4fb3398..1afe4e1b 100644 --- a/openvtc-vetting-pcs/tests/wire_fixture.rs +++ b/openvtc-vetting-pcs/tests/wire_fixture.rs @@ -15,9 +15,7 @@ use openvtc_vetting_pcs::{ use predicate_credential_system::serialization::{to_bytes, to_multibase}; use rand::{SeedableRng, rngs::StdRng}; use serde_json::json; -use vta_sdk::protocols::vetting::{ - IDENTITY_VETTING_ENDORSEMENT_TYPE, VettingMethod, VettingRelationship, -}; +use vta_sdk::protocols::vetting::{VETTED_PREDICATE, VettingMethod, VettingRelationship}; const COMMUNITY: &str = "did:example:kernel-vtc"; const PERIOD: &str = "2026-09"; @@ -25,7 +23,7 @@ const PERIOD: &str = "2026-09"; fn requirements() -> serde_json::Value { json!({ "version": "0.1", - "statementType": IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": VETTED_PREDICATE, "minStatements": 2, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video", "priorAcquaintance"], From 5a617045aa292c1e6a9a694cee895ceb5aabc4ea Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Wed, 30 Sep 2026 17:18:29 +0200 Subject: [PATCH 3/9] feat(openvtc)!: show role VACs, statements and set-aside credentials - VRCs are issued declaring the scope of the identifier the relationship uses (pairwise relationship DID, directed persona DID). - The vetter signs the vetting/session document before sending it, and the desk keeps that copy for the statement to cite. - Communities: the role credential shows the roles it confers, and a credential set aside on load as pre-v1 is shown as a warning asking for it to be re-issued. - Credentials: held credentials are described from their content ("Role: vetter", a statement by predicate), and set-aside VRCs are counted. - Vetting: set-aside grants and statements are listed on the page. - Test fixtures are v1 credentials; an object-form VIC issuer is refused. Signed-off-by: Glenn Gore --- .../src/state_handler/community_actions.rs | 3 ++ openvtc/src/state_handler/inbox_actions.rs | 26 ++++++--- openvtc/src/state_handler/join_flow.rs | 19 ++++--- .../src/state_handler/main_page/content.rs | 17 +++++- openvtc/src/state_handler/main_page/mod.rs | 53 +++++++++++++++++-- openvtc/src/state_handler/message_dispatch.rs | 10 +++- openvtc/src/state_handler/vetting_actions.rs | 43 +++++++++++++-- .../main/components/communities_panel.rs | 14 ++++- .../main/components/credentials_panel.rs | 11 ++++ .../ui/pages/main/components/vetting_panel.rs | 7 +++ openvtc/src/ui/pages/main/mod.rs | 2 + 11 files changed, 178 insertions(+), 27 deletions(-) diff --git a/openvtc/src/state_handler/community_actions.rs b/openvtc/src/state_handler/community_actions.rs index 0622f7e0..9f444535 100644 --- a/openvtc/src/state_handler/community_actions.rs +++ b/openvtc/src/state_handler/community_actions.rs @@ -234,6 +234,9 @@ impl CommunityOutcome { && let Some(c) = config.account.membership_mut(&self.vtc_did, self.persona) { c.member_vmc = Some(vmc); + c.clear_retired( + openvtc_core::config::account::RetiredCredential::MEMBER_ACKNOWLEDGEMENT, + ); save.mark_dirty(); } // Only the *send* succeeded. A DIDComm `Ok` means the frame was diff --git a/openvtc/src/state_handler/inbox_actions.rs b/openvtc/src/state_handler/inbox_actions.rs index 105d68cb..98bb1005 100644 --- a/openvtc/src/state_handler/inbox_actions.rs +++ b/openvtc/src/state_handler/inbox_actions.rs @@ -784,7 +784,7 @@ async fn prepare_accept_vrc_request( // publish to the community trust graph, so a persona-attributed issuer // correlated every relationship a persona had, indefinitely, from a // credential that outlives the handshake that produced it. The subject was - // already the peer's R-DID; only the issuer half was persona-attributed, + // already the peer's relationship DID; only the issuer half was persona-attributed, // which meant the pairwise channel led straight back to the persona. // // This could not be fixed here until the VTC stopped requiring it: its @@ -798,18 +798,32 @@ async fn prepare_accept_vrc_request( // unset, and a credential with no identifier cannot be stored under one — // so a peer that keys relationship credentials by `id` cannot make a // re-issue idempotent, tell a renewal from a duplicate, or reference this - // VRC from a witness credential. Nothing rejects a VRC for it *today*, + // VRC from a `witnessed/1` statement. Nothing rejects a VRC for it *today*, // which is exactly what the reciprocal VMC looked like right up until a // community started keying on it. // // Before signing, necessarily: a Data Integrity proof covers every member // but `proof`, so an id spliced in afterwards leaves a document whose // proof no longer verifies. - let mut vrc = openvtc_core::vrc::new_identified_vrc(&our_r_did, &their_r_did, valid_from, None); + // + // It declares the scope of that identifier (DTG Credentials `issuerScope`): + // `pairwise` for a relationship DID minted for this one peer, `directed` + // when the relationship uses the persona DID — the legible graph a + // community declaring `relationshipIdentifierDefault: attributed` asks for. + // See `openvtc_core::dtg::relationship_issuer_scope`. + // // `our_did` is the persona DID for a relationship established without a - // dedicated R-DID, so sign as whichever identity it actually is — the same - // discriminator `listener_id_for_did` routes sends on. - if config.is_persona_did(&our_r_did) { + // dedicated relationship DID, so sign as whichever identity it actually is — + // the same discriminator `listener_id_for_did` routes sends on. + let issuer_is_persona = config.is_persona_did(&our_r_did); + let mut vrc = openvtc_core::vrc::new_identified_vrc( + &our_r_did, + openvtc_core::dtg::relationship_issuer_scope(issuer_is_persona), + &their_r_did, + valid_from, + None, + ); + if issuer_is_persona { let persona_keys = config.get_persona_keys(tdk).await?; vrc.sign(&persona_keys.signing.secret, None).await?; } else { diff --git a/openvtc/src/state_handler/join_flow.rs b/openvtc/src/state_handler/join_flow.rs index 734bba63..c64f3033 100644 --- a/openvtc/src/state_handler/join_flow.rs +++ b/openvtc/src/state_handler/join_flow.rs @@ -3935,13 +3935,15 @@ mod tests { // contexts and the `DTGCredential` base type. A fixture that omits // them is not a VIC a community could have issued. "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1" + openvtc_core::join::W3C_VC_V2_CONTEXT, + openvtc_core::join::DTG_CONTEXT ], "id": id, "type": ["VerifiableCredential", "DTGCredential", "InvitationCredential"], "issuer": COMMUNITY, + "issuerScope": "public", "credentialSubject": { "id": "did:webvh:example.com:alice" }, + "validFrom": "2026-01-01T00:00:00Z", "validUntil": "2099-01-01T00:00:00Z", "credentialStatus": { "type": "BitstringStatusListEntry" }, "proof": { "type": "DataIntegrityProof" } @@ -4158,15 +4160,16 @@ mod tests { assert_eq!(state.join.invitation_issuer.as_deref(), Some(COMMUNITY)); } - /// `issuer` also has an object form; both must yield the community DID, or - /// the prefill silently stops working for half the issuers out there. + /// A DTG credential names its issuer as a DID string. The W3C object form + /// is not one `dtg-credentials` parses, so a pasted invitation carrying it + /// is refused rather than half-read — and leaves no issuer behind. #[test] - fn an_object_form_issuer_is_recorded_too() { + fn an_object_form_issuer_is_refused() { let mut state = State::default(); let mut vic = pasteable_vic("urn:uuid:one"); vic["issuer"] = json!({ "id": COMMUNITY, "name": "Example Community" }); paste_verified(&mut state, &vic.to_string(), None); - assert_eq!(state.join.invitation_issuer.as_deref(), Some(COMMUNITY)); + assert_eq!(state.join.invitation_issuer, None); } /// A rejected paste must not leave an issuer behind: the entry page would @@ -4272,7 +4275,7 @@ mod vetting_tests { fn manifest(vets: bool) -> manifest::v0_2::Response { let requirements = serde_json::from_value(serde_json::json!({ "version": "0.1", - "statementType": vta_sdk::protocols::vetting::IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": vta_sdk::protocols::vetting::VETTED_PREDICATE, "minStatements": 2, "acceptedMethods": ["inPerson", "video"], "requiredClaims": ["name.legal"], @@ -4485,7 +4488,7 @@ mod vetting_tests { fn requirements(invitation: Option<&str>) -> VettingRequirements { let mut value = serde_json::json!({ "version": "0.1", - "statementType": vta_sdk::protocols::vetting::IDENTITY_VETTING_ENDORSEMENT_TYPE, + "statementType": vta_sdk::protocols::vetting::VETTED_PREDICATE, "minStatements": 2, "acceptedMethods": ["inPerson"], "eligibleVetters": { "role": "vetter" } diff --git a/openvtc/src/state_handler/main_page/content.rs b/openvtc/src/state_handler/main_page/content.rs index c78f6ba8..bb7fe1aa 100644 --- a/openvtc/src/state_handler/main_page/content.rs +++ b/openvtc/src/state_handler/main_page/content.rs @@ -637,8 +637,14 @@ pub struct CommunitySummary { pub request_id: String, /// Whether the membership credential (VMC) has been received + stored. pub has_membership_credential: bool, - /// Whether the role endorsement credential (VEC) has been received. + /// Whether the role credential (a community VAC) has been received. pub has_role_credential: bool, + /// The roles that credential confers (`role:` actions), for display. + pub role_names: Vec, + /// One line per stored credential set aside on load because it pre-dates + /// DTG Credentials v1 ([`openvtc_core::config::account::RetiredCredential`]). + /// Shown as a warning with what to do about it. + pub retired_credentials: Vec, /// The accent colour the community publishes in its manifest's branding. pub accent: Option<(u8, u8, u8)>, /// Why the membership ended, for a `Rejected`/`Removed` row (issue #240). @@ -2050,6 +2056,9 @@ pub struct VettingState { /// than naming one, so that what they asked for is not itself a /// distinguishing detail. pub event_offers: Arc<[EventOffer]>, + /// Stored vetting credentials set aside on load because they pre-date DTG + /// Credentials v1 (`VettingBook::retired`), one sentence each. + pub retired: Arc<[String]>, } /// One (event, tier) a community we vet for is offering. @@ -3011,7 +3020,7 @@ pub struct CredentialsState { pub received: Arc<[VrcSummary]>, /// VRCs we issued. `Arc<[…]>` for cheap per-frame clones. pub issued: Arc<[VrcSummary]>, - /// Membership (VMC) + role (VEC) credentials issued to us by the VTCs we've + /// Membership (VMC) + role (VAC) credentials issued to us by the VTCs we've /// joined, one or two entries per community (reuses [`VrcSummary`]). /// `Arc<[…]>` for cheap per-frame clones. pub membership: Arc<[VrcSummary]>, @@ -3023,6 +3032,10 @@ pub struct CredentialsState { pub mode: CredentialsMode, /// Transient status message pub status_message: Option, + /// Stored VRCs set aside on load because they pre-date DTG Credentials v1 + /// ([`openvtc_core::vrc::Vrcs::retired`]), across every store. Shown as a + /// warning so the member knows to ask those peers for fresh ones. + pub retired_vrcs: usize, /// When `Some(vrc_id)`, removal of that credential is awaiting `y`/`n` /// confirmation (armed from the detail view). Mirrors the Communities/ /// VTA-DID confirm pattern (R25). diff --git a/openvtc/src/state_handler/main_page/mod.rs b/openvtc/src/state_handler/main_page/mod.rs index 76c3e5a7..36b175bc 100644 --- a/openvtc/src/state_handler/main_page/mod.rs +++ b/openvtc/src/state_handler/main_page/mod.rs @@ -404,6 +404,13 @@ impl MainPageState { ) .into(); self.content_panel.credentials.membership = collect_membership_creds(config).into(); + self.content_panel.credentials.retired_vrcs = config.private.vrcs_received.retired() + + config.private.vrcs_issued.retired() + + config + .account + .memberships() + .map(|m| m.vrcs_received.retired() + m.vrcs_issued.retired()) + .sum::(); // Sync settings self.content_panel.settings.friendly_name = config.public.friendly_name.clone(); @@ -674,6 +681,30 @@ impl MainPageState { has_role_credential: c .credentials .contains_key(&openvtc_core::CredentialKind::Role), + role_names: c + .credentials + .get(&openvtc_core::CredentialKind::Role) + .and_then(|vc| openvtc_core::dtg::parse_conformant(vc).ok()) + .and_then(|vac| openvtc_core::dtg::community_roles(&vac)) + .unwrap_or_default() + .into_iter() + .map(|r| sanitize_display(&r, 64)) + .collect(), + retired_credentials: c + .retired_credentials + .iter() + .map(|r| { + let what = match r.kind.as_str() { + "Membership" => "membership credential", + "Role" => "role credential", + _ => "membership acknowledgement", + }; + format!( + "Your {what} pre-dates DTG Credentials v1 and was set aside — \ + ask the community to re-issue it (renew your membership)." + ) + }) + .collect(), decision: community_decision_summary(c), }); } @@ -974,7 +1005,9 @@ fn collect_membership_creds(config: &Config) -> Vec { .map(|n| sanitize_display(n, 256)), validity, status, - kind: Some(kind.config_key().to_string()), + // What it asserts, read from the credential itself: "Membership", + // "Role: vetter", or for a statement its predicate. + kind: Some(sanitize_display(&openvtc_core::dtg::describe(vc), 128)), subject_is_self: config.is_persona_did(&subject), valid_from, valid_until, @@ -1445,6 +1478,7 @@ mod tests { member_vmc: None, decision: None, relationship_identifier_default: None, + retired_credentials: Vec::new(), extra: serde_json::Map::new(), vtc_did: vtc_did.to_string(), display_name: display_name.map(str::to_owned), @@ -1883,9 +1917,13 @@ mod tests { /// so an unsigned credential cannot be stored. fn signed_vrc(issuer: &str, subject: &str) -> Arc { let json = serde_json::json!({ - "@context": ["https://www.w3.org/ns/credentials/v2"], + "@context": [ + dtg_credentials::W3C_VC_V2_CONTEXT, + dtg_credentials::DTG_CONTEXT_V1 + ], "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"], "issuer": issuer, + "issuerScope": "pairwise", "validFrom": "2024-06-18T10:00:00Z", "credentialSubject": { "id": subject }, "proof": { @@ -2502,6 +2540,7 @@ mod tests { let valid_from = Utc.with_ymd_and_hms(2024, 1, 2, 3, 4, 5).unwrap(); let vrc = DTGCredential::new_vrc( "did:test:issuer".to_string(), + dtg_credentials::IssuerScope::Pairwise, "did:test:subject".to_string(), valid_from, Some(Utc.with_ymd_and_hms(2025, 6, 7, 8, 9, 10).unwrap()), @@ -2519,11 +2558,15 @@ mod tests { #[test] fn test_raw_credential_value_matches_eager_output() { let vc = serde_json::json!({ - "@context": ["https://www.w3.org/ns/credentials/v2"], - "type": ["VerifiableCredential", "MembershipCredential"], + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://registry.trustoverip.org/dtg/context/v1" + ], + "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"], "issuer": "did:test:vtc", + "issuerScope": "public", "validFrom": "2024-01-01T00:00:00Z", - "credentialSubject": { "id": "did:test:member", "role": "member" } + "credentialSubject": { "id": "did:test:member" } }); let eager = serde_json::to_string_pretty(&vc).unwrap(); diff --git a/openvtc/src/state_handler/message_dispatch.rs b/openvtc/src/state_handler/message_dispatch.rs index 7b4f0d9a..d678704e 100644 --- a/openvtc/src/state_handler/message_dispatch.rs +++ b/openvtc/src/state_handler/message_dispatch.rs @@ -1062,6 +1062,7 @@ async fn process_inbound( // re-send it without minting a different credential. if let Some(record) = config.account.membership_mut(&from_did, persona_id) { record.member_vmc = Some(vmc); + record.clear_retired(openvtc_core::config::account::RetiredCredential::MEMBER_ACKNOWLEDGEMENT); } info!( vtc = %from_did, @@ -1311,6 +1312,7 @@ async fn process_inbound( Ok((_, vmc)) => { if let Some(record) = config.account.membership_mut(&from_did, persona_id) { record.member_vmc = Some(vmc); + record.clear_retired(openvtc_core::config::account::RetiredCredential::MEMBER_ACKNOWLEDGEMENT); } info!( vtc = %from_did, @@ -1855,10 +1857,14 @@ mod tests { async fn membership_vmc(vtc: &str, key: &Secret) -> serde_json::Value { let mut vc = serde_json::json!({ - "@context": ["https://www.w3.org/ns/credentials/v2"], + "@context": [ + dtg_credentials::W3C_VC_V2_CONTEXT, + dtg_credentials::DTG_CONTEXT_V1 + ], "id": format!("urn:uuid:{}", uuid::Uuid::new_v4()), - "type": ["VerifiableCredential", "MembershipCredential"], + "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"], "issuer": vtc, + "issuerScope": "public", "validFrom": "2026-01-01T00:00:00Z", "validUntil": "2099-01-01T00:00:00Z", "credentialSubject": { "id": PERSONA }, diff --git a/openvtc/src/state_handler/vetting_actions.rs b/openvtc/src/state_handler/vetting_actions.rs index 51870de3..7452b07a 100644 --- a/openvtc/src/state_handler/vetting_actions.rs +++ b/openvtc/src/state_handler/vetting_actions.rs @@ -198,6 +198,11 @@ pub(crate) fn sync(vetting: &mut VettingState, config: &Config) { } } vetting.event_offers = offers.into(); + vetting.retired = book + .retired + .iter() + .map(|note| sanitize_display(note, 320)) + .collect(); // The desk's header. Built from `vetter_standing`, which — alone among the // vetter-side reads — keeps lapsed grants, so a vetter who has quietly @@ -2483,6 +2488,34 @@ async fn open_session(ctx: &mut ActionCtx<'_>, request_id: &str, method_index: u Ok(d) => d, Err(e) => return abandon(ctx, "Could not open the session", e), }; + // Signed here rather than in `sign_and_send`, because the desk keeps the + // document exactly as it goes out: the Vetting Statement that closes this + // session cites it by `taskDigestMultibase`, which `vetted/1` requires. + // Signed and kept before `persist`, so the saved desk holds it. + let mut document = document; + let keys = match ctx + .config + .get_persona_keys_for(entry.persona, ctx.tdk) + .await + { + Ok(keys) => keys, + Err(e) => return abandon(ctx, "Could not send the session", e), + }; + if let Err(e) = wire::sign(&mut document, &keys.authentication.secret).await { + return abandon(ctx, "Could not send the session", e); + } + let recorded = serde_json::to_value(&document) + .map_err(|e| e.to_string()) + .and_then(|value| { + ctx.config + .private + .vetting + .record_session_document(request_id, value) + .map_err(|e| e.to_string()) + }); + if let Err(e) = recorded { + return abandon(ctx, "Could not send the session", e); + } let code = ctx .config .private @@ -2505,9 +2538,13 @@ async fn open_session(ctx: &mut ActionCtx<'_>, request_id: &str, method_index: u let sent = Sent::Session { request_id: request_id.to_string(), }; - if let Err(e) = sign_and_send(ctx, entry.persona, document, sent).await { - abandon(ctx, "Could not send the session", e); - } + let message = match wire::to_message(&document) { + Ok(message) => message, + Err(e) => return abandon(ctx, "Could not send the session", e), + }; + let from = document.issuer.clone().unwrap_or_default(); + let to = document.recipient.clone().unwrap_or_default(); + spawn_send(ctx, message, &from, &to, sent); } /// Attest the hidden way: no statement, no signature, nothing that names this vetter. diff --git a/openvtc/src/ui/pages/main/components/communities_panel.rs b/openvtc/src/ui/pages/main/components/communities_panel.rs index b8484c9e..16013acb 100644 --- a/openvtc/src/ui/pages/main/components/communities_panel.rs +++ b/openvtc/src/ui/pages/main/components/communities_panel.rs @@ -250,9 +250,19 @@ pub fn render( } else { "—" }, - if c.has_role_credential { "✓" } else { "—" }, + match (c.has_role_credential, c.role_names.is_empty()) { + (false, _) => "—".to_string(), + (true, true) => "✓".to_string(), + (true, false) => format!("✓ ({})", c.role_names.join(", ")), + }, ), )); + // Credentials set aside on load because they pre-date DTG + // Credentials v1: say what went and what to do, rather than + // showing the membership as if nothing were missing. + for note in &c.retired_credentials { + lines.push(Line::from(format!("⚠ {note}")).fg(COLOR_ORANGE)); + } // Why a Rejected/Removed membership ended, as the community stated it // (issue #240). The reason always renders — "no reason given" when // none travelled — so a terminal row never silently omits its cause; @@ -729,6 +739,8 @@ mod key_hint_tests { request_id: String::new(), has_membership_credential: false, has_role_credential: false, + role_names: Vec::new(), + retired_credentials: Vec::new(), accent: None, decision: None, } diff --git a/openvtc/src/ui/pages/main/components/credentials_panel.rs b/openvtc/src/ui/pages/main/components/credentials_panel.rs index 6bb9d064..470113a8 100644 --- a/openvtc/src/ui/pages/main/components/credentials_panel.rs +++ b/openvtc/src/ui/pages/main/components/credentials_panel.rs @@ -50,6 +50,17 @@ fn render_list(state: &CredentialsState) -> Vec> { super::status::push_status(&mut lines, msg, ""); lines.push(Line::from("")); } + if state.retired_vrcs > 0 { + lines.push( + Line::from(format!( + "⚠ {} stored relationship credential(s) pre-date DTG Credentials v1 and were \ + set aside — request fresh ones from those relationships.", + state.retired_vrcs + )) + .fg(COLOR_ORANGE), + ); + lines.push(Line::from("")); + } let active_list = match state.selected_tab { CredentialTab::Received => &state.received, diff --git a/openvtc/src/ui/pages/main/components/vetting_panel.rs b/openvtc/src/ui/pages/main/components/vetting_panel.rs index f08e4001..0e22952e 100644 --- a/openvtc/src/ui/pages/main/components/vetting_panel.rs +++ b/openvtc/src/ui/pages/main/components/vetting_panel.rs @@ -180,6 +180,13 @@ pub fn render(v: &VettingState) -> Vec> { super::status::push_status(&mut lines, message, ""); lines.push(Line::from("")); } + // Credentials set aside on load because they pre-date DTG Credentials v1. + for note in v.retired.iter() { + lines.push(Line::from(format!("⚠ {note}")).fg(COLOR_ORANGE)); + } + if !v.retired.is_empty() { + lines.push(Line::from("")); + } match &v.mode { VettingMode::List => match v.tab { diff --git a/openvtc/src/ui/pages/main/mod.rs b/openvtc/src/ui/pages/main/mod.rs index b806be33..39294a1d 100644 --- a/openvtc/src/ui/pages/main/mod.rs +++ b/openvtc/src/ui/pages/main/mod.rs @@ -3760,6 +3760,8 @@ mod key_handler_tests { request_id: String::new(), has_membership_credential: false, has_role_credential: false, + role_names: Vec::new(), + retired_credentials: Vec::new(), accent: None, decision: None, } From 913177be53a02967b2ace3ba9b7eb557296db1bd Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Wed, 30 Sep 2026 17:19:22 +0200 Subject: [PATCH 4/9] docs: describe the DTG Credentials v1 shapes and the stored-state rule - vetting-process, vetting-hidden-vetters-pcs, hidden-vetting-live-run: the Vetting Statement is a vetted/1 VSC citing its session by id and digest, the vetter role a community VAC, the manifest statementType the predicate IRI, and pre-v1 grants and statements are set aside on load. - relationships-vrcs: the scope a VRC declares, derived from the identifier the relationship uses, and the v1 sample VRC. - openvtc-config-data-structure: Vrcs::retired and per-VRC loading. - Retired R-DID / P-DID / M-DID / C-DID terms replaced where touched. - CHANGELOG: the v1 migration by flow, and the stored-state behaviour. Signed-off-by: Glenn Gore --- CHANGELOG.md | 75 ++++++++ README.md | 8 +- docs/design/hidden-vetting-live-run.md | 2 +- docs/design/vetting-hidden-vetters-pcs.md | 16 +- docs/design/vetting-process.md | 207 +++++++++++++--------- docs/design/vta-authoritative-state.md | 2 +- docs/openvtc-config-data-structure.md | 7 +- docs/relationships-vrcs.md | 94 +++++++--- 8 files changed, 289 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f5c7b8..8012c34c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,81 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed +- **Every DTG credential conforms to DTG Credentials v1 and the DTG VSC + predicate registry.** *Breaking, on the wire and in stored state.* Every + credential this client issues, receives, verifies, stores or shows now + carries `@context` `["https://www.w3.org/ns/credentials/v2", + "https://registry.trustoverip.org/dtg/context/v1"]`, a top-level + `issuerScope`, and exactly one concrete type; the pre-v1 + `firstperson.network/credentials/dtg/v1` context is refused with no alias. + Built on `dtg-credentials` 0.12 (unreleased; carried as 0.11 by the patch + block), `vta-sdk` 0.57, `vta-service` 0.46 and `trust-tasks-rs` / + `trust-tasks-capability-client` 0.24.8, all of which change together — a + 0.12 verifier refuses the old shapes and an old verifier refuses the new + ones, so upgrade the community and its members together. By flow: + - **Invitations (VIC)** are validated by parsing through `dtg-credentials`: + the v1 context second, `issuerScope: public`, one concrete type. + `join::DTG_CONTEXT` is now `dtg_credentials::DTG_CONTEXT_V1`. An + invitation naming its issuer in the W3C object form is refused, because + `dtg-credentials` models `issuer` as a DID string. + - **Membership.** The community's grant declares `public`; the member's + acknowledgement is built with `new_member_vmc_for` — which refuses a grant + that does not name the signing persona — and declares **`directed`** + (`dtg::MEMBER_IDENTIFIER_SCOPE`): the persona DID a membership names is + recognised by the community, the members it relates to and vets, and + possibly other communities, so `pairwise` would not stay true. + `build_member_vmc` takes the member DID. + - **Role credentials are community VACs**, not role endorsements: + `AuthorityCredential`, `authority.scope` the community DID, `actions` + `["role:"]`, `maxAttenuation: 0`. `CredentialKind::Role` classifies + one only when it is a community role grant (`dtg::community_roles`), and + `CredentialKind::from_credential` classifies only conformant credentials. + The Communities view shows the role names (`role ✓ (vetter)`), and the + credential views describe a credential from its content ("Role: vetter", + "Statement: vetted (identity vetting)"). `personhood::AssertReply::role_vec` + is `role_vac`, read from `roleVac`. + - **Vetting Statements are `vetted/1` VSCs** + (`https://registry.trustoverip.org/dtg/vsc/vetted/1`), with the body at + `credentialSubject.object.value`, citing the `vetting/session` document by + `taskContext` **and** `taskDigestMultibase`, issued `directed`. The vetter + desk keeps the signed session document it sent + (`VettingBook::record_session_document`) and the applicant the one it + received, and the applicant refuses a statement that does not cite its + session by both. Statements are recognised by predicate, never by type; the + manifest `statementType` is the predicate IRI. The hidden-vetting wire + fixture is regenerated for the new `statementType`, which changes its + `requirementsDigest`. + - **Relationships (VRC)** declare the scope of the identifier the + relationship uses (`dtg::relationship_issuer_scope`): a relationship DID + (the `pairwise` default) declares `pairwise`; the persona DID (seeded by a + community's `relationshipIdentifierDefault: attributed`) declares + `directed`. `vrc::new_identified_vrc` takes the scope. + - **Statements the client shows** are summarised by predicate and object + (`dtg::describe_statement`); a predicate it does not know is shown as its + IRI, and `dtg::core_accept_list` (the four core predicates) fails closed on + anything else. + +- **Credentials stored in a pre-v1 shape are set aside on load, explicitly.** + None can be rewritten — each proof covers the old bytes and only the issuer + can sign new ones — and none would verify anywhere now, so each is dropped + with a `warn!` naming what and why, never kept and never allowed to fail the + config load: + - a membership's VMC, role credential or own acknowledgement goes to + `CommunityRecord::retired_credentials` (persisted), and the Communities view + shows a warning asking the member to have the community re-issue it + (renewing the membership re-issues the VMC and role VAC); a conformant + replacement clears it; + - a stored VRC, anywhere, is dropped and counted (`Vrcs::retired`), and the + Credentials panel says how many; a `VRCIssued` inbox task carrying one is + dropped; + - a vetter role grant or a held Vetting Statement is dropped with a sentence + on the Vetting page (`VettingBook::retired`), cleared when a replacement + arrives; + - a rebuild from the VTA vault rejects a pre-v1 membership credential as + `RejectionReason::Nonconformant` instead of restoring it; + - a session opened by an older build has no stored session document, so + attesting it says to open the session again. + - **`git-ns/repo/create` moves to `0.3`, and the new-repository form can name owners.** A namespace admin's `repo.create` is implied by `ns.admin`, and in `0.2` that let them make themselves owner — and so forge admin — of any diff --git a/README.md b/README.md index e720a3cc..9cbbe4ec 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ openvtc --version ## Decentralised Identity -The OpenVTC tool uses the `did:webvh` to create your **Persona DID (P-DID)**. It enhances the existing `did:web` method, providing: +The OpenVTC tool uses the `did:webvh` to create your **persona DID**. It enhances the existing `did:web` method, providing: - Portability with a self-certifying identifier (SCID), allowing you to move to a different domain. @@ -100,7 +100,7 @@ _Sample did:webvh identifier:_ ## Decentralised Communication -OpenVTC seamlessly integrates with DIDComm-compatible mediators for secure, private communication using your **Persona DID (P-DID)** or **Relationship DID (R-DID)**. +OpenVTC seamlessly integrates with DIDComm-compatible mediators for secure, private communication using your **persona DID** or a per-relationship **relationship DID**. DIDComm mediators handle message routing and storage while preserving privacy through end-to-end encryption. Messages are structured in multiple "envelope" layers providing: @@ -182,8 +182,8 @@ An encrypted configuration stored inside the public configuration file, containi - List of known contacts with their Persona DIDs and Alias. - List of known relationships with their: - - Remote and local Relationship DIDs (R-DIDs) - - Remote and local Persona DIDs (P-DIDs) + - Remote and local relationship DIDs + - Remote and local persona DIDs - Relationship aliases - Verifiable Relationship Credentials (VRCs) diff --git a/docs/design/hidden-vetting-live-run.md b/docs/design/hidden-vetting-live-run.md index 6a9b024f..bb644b0e 100644 --- a/docs/design/hidden-vetting-live-run.md +++ b/docs/design/hidden-vetting-live-run.md @@ -52,7 +52,7 @@ curl -sS -X POST http://localhost:8200/v1/schemas/accepts \ "claims": [ { "path": ["givenName"] } ] } ] }, "vetting": { "version": "0.1", - "statementType": "https://firstperson.network/endorsements/identity-vetting/0.1", + "statementType": "https://registry.trustoverip.org/dtg/vsc/vetted/1", "minStatements": 3, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video"], diff --git a/docs/design/vetting-hidden-vetters-pcs.md b/docs/design/vetting-hidden-vetters-pcs.md index 0e890d63..5b72c497 100644 --- a/docs/design/vetting-hidden-vetters-pcs.md +++ b/docs/design/vetting-hidden-vetters-pcs.md @@ -59,7 +59,7 @@ hands back* and *what the VTC verifies* change. | Today (V0, named) | Hidden mode (PCS) | |---|---| -| The vetter signs a VEC with `issuer` = its member DID (D7, `vta-sdk/src/vetting/statement.rs`) | The vetter returns a PCS **attestation** plus public statement metadata. No DID of the vetter appears | +| The vetter signs a Vetting Statement — a VSC (`StatementCredential`, predicate `https://registry.trustoverip.org/dtg/vsc/vetted/1`) — with `issuer` = its member DID (D7, `vta-sdk/src/vetting/statement.rs`) | The vetter returns a PCS **attestation** plus public statement metadata. No DID of the vetter appears | | The VTC checks the issuer's member row and `vetter` grant (`vtc-service/src/vetting/mod.rs:371`) | The VTC checks the proof. `φ` ∈ AllowList{`vetter`} under this epoch's `hvk` proves an eligible vetter | | `StatementFacts.vetter` = the issuer DID, deduplicated in `requirements::evaluate` (`vta-sdk/src/vetting/requirements.rs:215`) | `StatementFacts.vetter` = the tag `T_j` (multibase). The same function deduplicates, unchanged | | `VettingFacts.statements[].issuer` = DID | `issuer` = the tag. **`join.rego` is untouched** | @@ -148,7 +148,7 @@ anonymous channel. `persona-to-membership-design.md` §4.4 recorded exactly this ### 4.5 Replay -Attestations are standing endorsements. `ctx_j` pins them to one community, one requirements +Attestations are standing statements. `ctx_j` pins them to one community, one requirements version and one validity window. `ctx_0` pins the proof to the VTC's single-use challenge. The VTC also records the tag set per `id`, which is needed for `requestMore` and supplement anyway, because the same vetter keeps the same tag across resubmissions. @@ -429,7 +429,7 @@ Bob (applicant) Vetter_j (pairwise DID) VTC ◀── attestation(id, ctx_j = metadata) + metadata … repeat until k prove(id, usk, atts, ctx_0 = challenge ‖ joinDid-binding) - submit {vp(no VECs), vettingProof} ─────────────────────▶ verify_proof (AllowList{vetter}, epoch e|e-1) + submit {vp(no VSCs), vettingProof} ─────────────────────▶ verify_proof (AllowList{vetter}, epoch e|e-1) → VettingFacts{issuer = T_j …} → join.rego (unchanged) → VMC ``` @@ -481,7 +481,7 @@ Bob (applicant) Vetter_j (pairwise DID) VTC **Correctness of the design:** 1. **Never mix named and hidden vetting within one criterion.** A vetter could give Bob both a - VEC and a PCS attestation. The VTC cannot tell they come from the same person, so that vetter + named Vetting Statement (VSC) and a PCS attestation. The VTC cannot tell they come from the same person, so that vetter would count twice. A criterion is either `mode: named` or `mode: hidden`. 2. **PCS self-exclusion gives no protection here.** It compares keys, and the applicant's `usk` is fresh for every application. A vetter who applies under a second persona can attest for @@ -736,21 +736,21 @@ It does not, and the reason is structural: the hidden path rejoins the named one `vetting::vetting_facts`. `join::orchestrate` calls it, gets the same `VettingFacts` shape with a tag where each vetter's DID would be, and everything downstream — `requirements::evaluate`, `join.rego`, `EffectPlan::Admit`, `ceremony::execute::issue_member_credentials` — is untouched. -So a live hidden admission already mints the VMC against a revocation slot, the role VEC at the +So a live hidden admission already mints the VMC against a revocation slot, the role VAC at the granted role, and solicits the member's reciprocal VMC, exactly as a named one does. Nothing in the hidden branch had to be taught about credentials. The reference example (`openvtc-core/examples/zkp_reference_flow.rs`) now mints the same three in-process, and asserts each proof verifies, that the reciprocal's subject is the community, and that its `digestMultibase` matches a digest of the grant **as it arrived** — the wire-form rule -`DTGCredential::new_member_vmc` exists to enforce. It also asserts no vetter DID appears in any +`DTGCredential::new_member_vmc_for` exists to enforce. It also asserts no vetter DID appears in any of the three. Deliberately **not** issued on this path, and each for its own reason: | credential | why not | | --- | --- | -| Vetting statement VEC (`IdentityVettingEndorsement`) | it names its issuer. This is the whole point: the endorsement is still built — it is where the attested facts come from — but never signed and never sent. `presentable_statements()` returns 0. | +| Vetting Statement VSC (`vetted/1`, `VettedObjectValue`) | it names its issuer. This is the whole point: the statement value (`object.value`) is still built — it is where the attested facts come from — but never signed and never sent. `presentable_statements()` returns 0. | | VRC pair | not part of admission on any path; VRCs are the peer relationship layer, and D8 does not require one for V0 membership. Worth stating plainly: a community that required a VRC pair **with its vetters** could not run hidden vetting at all, because a VRC names both ends. | | VIC | invitation-gated admission only. | | Personhood (VPC / `personhood: true`) | a separate evaluation; `admit` mints the VMC with `personhood = false`. | @@ -806,7 +806,7 @@ the community's own records. count twice in one proof (§13 C2). Across a rotation the same member re-enrols under the new label with the *same* identifier, which the test pins. - **The grant check is the community's own**, not a list of the minting half's: `vetter_eligible` - — membership row, not removed, joined before the grant, live role endorsement — the same + — membership row, not removed, joined before the grant, live `role:vetter` VAC — the same function the named path calls. - **The drip is capped by the issuer.** This closed a real hole: `TokenIssuer::issue` checked the label, the tick and every opening proof, and never checked *how many* requests were in the diff --git a/docs/design/vetting-process.md b/docs/design/vetting-process.md index fada0c9b..dfe6bf5d 100644 --- a/docs/design/vetting-process.md +++ b/docs/design/vetting-process.md @@ -80,7 +80,7 @@ this design are marked ⚠. | Kernel today | Detail | OpenVTC vetting | |---|---|---| | **Identity** = PGP key | Key held by the developer; 2-year expiry recommended | Persona DID (`did:webvh`) with keys in the VTA, pre-rotation, witnesses | -| **Vouch** = key signature | Signer has "met you personally… OR worked with you for some period", video OK | **Vetting Statement** (VEC) issued by an eligible vetter (§9.4) | +| **Vouch** = key signature | Signer has "met you personally… OR worked with you for some period", video OK | **Vetting Statement** (VSC) issued by an eligible vetter (§9.4) | | **Account gate** | Key signed by ≥2 existing kernel.org account holders (2015 AMA said 3) + MAINTAINERS listing or admin exception; helpdesk reviews by hand | `minStatements` from distinct eligible vetters; policy decides automatically, `refer` for edge cases (§10) | | **Keyring gate** (`pgpkeys.git`) | ≥1 in-repo signature and a trust path to Linus (reported max 5 hops) | Vetter eligibility tiers + **vetting depth** from founding anchors (§10.3) | | **Key-signing parties**, `ksmap` | ⚠ Public YAML of names + coordinates; in-person bias; geographic exclusion | Opt-in **vetter directory** with coarse region only; tickets/QR at events; video method (§7, §8) | @@ -119,14 +119,15 @@ model, and this spec keeps its terms: | **Vetting Requirements** | Manifest criterion extension | VTC | public | New field on `vtc/join-requests/manifest` (§6) | | **Vetting Ticket** | Short code / QR payload (not a credential) | Vetter | applicant | New (§8) | | **Vetting Card** | VDS, profile of the **r-card** | Applicant's join DID | one vetter | New VDS profile; signs the existing unsigned r-card render (§9.2) | -| **Vetting Statement** | **VEC** (`EndorsementCredential`) with registered endorsement type `IdentityVetting` | Vetter's member DID | applicant's join DID | New endorsement type; no new DTG credential type (§9.4, D1) | -| **Vetter role** | Role **VEC** (`CommunityRole: vetter`) | VTC | vetter's member DID | Existing mechanism, new role (§10.3) | +| **Vetting Statement** | **VSC** (`StatementCredential`) under the registered predicate `https://registry.trustoverip.org/dtg/vsc/vetted/1` | Vetter's member DID | applicant's join DID | Registered predicate; no new DTG credential type (§9.4, D1) | +| **Vetter role** | Community-issued role **VAC** (`AuthorityCredential`, action `role:vetter`) | VTC (community DID) | vetter's member DID | Existing mechanism, new role (§10.3) | | **Admission bundle** | VP in `vtc/join-requests/submit/0.2` | Applicant | VTC | Existing task (§10.1) | -| **Membership** | VMC grant + member acknowledgement, role VEC | VTC / member | — | Existing | +| **Membership** | VMC grant (`issuerScope: public`) + member acknowledgement VMC (`directed`, `digestMultibase` over the grant), role VAC | VTC / member | — | Existing | -Terms follow DTG Credentials Working Draft 02. Avoid the retired -M-DID/C-DID/R-DID/P-DID terms. "Face" means a persona **profile**, and -"world" means a **facet**. +Terms follow DTG Credentials v1 and the DTG VSC predicate registry. Use the +WD02+ identifier-role terms — persona DID, member DID, community DID, +relationship DID (pairwise identifier) — not the retired abbreviations. +"Face" means a persona **profile**, and "world" means a **facet**. ### 3.3 Which DIDs are used @@ -177,7 +178,7 @@ sequenceDiagram Note over A: sign the card with the join persona key (D19) A-->>V: #response signed Vetting Card (VDS) Note over V: Human check — person ↔ document ↔ card - Note over V: confirm the attestation, sign the VEC with the member persona key (D19) + Note over V: confirm the attestation, sign the VSC with the member persona key (D19) V->>A: credential-exchange/issue/0.1 {Vetting Statement} A->>AV: vault store (purpose: vetting, community) end @@ -189,7 +190,7 @@ sequenceDiagram A->>C: vtc/join-requests/submit/0.2 {VP: [VIC?] + statements} C->>C: verify → VerifiedFacts → join.rego (vetting module) → verdict alt allow - C-->>A: VMC grant + role VEC → reciprocal VMC → git-trust/grant + C-->>A: VMC grant + role VAC → reciprocal VMC → git-trust/grant else requestMore C-->>A: needs (e.g. "1 more in-person statement") else refer @@ -292,10 +293,10 @@ community demands a passport check and walk away having disclosed nothing. "id": "kernel-developer", "description": "Two existing kernel vetters must confirm who you are. At least one must meet you in person.", "presentationDefinition": { "credentials": [ { "id": "vetting", "format": "ldp_vc", "multiple": true, - "meta": { "type_values": [["EndorsementCredential"]] } } ] }, + "meta": { "type_values": [["StatementCredential"]] } } ] }, "vetting": { "version": "0.1", - "statementType": "https://firstperson.network/endorsements/identity-vetting/0.1", + "statementType": "https://registry.trustoverip.org/dtg/vsc/vetted/1", "minStatements": 2, "minByMethod": { "inPerson": 1 }, "acceptedMethods": ["inPerson", "video", "priorAcquaintance"], @@ -319,7 +320,9 @@ community demands a passport check and walk away having disclosed nothing. ``` The DCQL shape above is illustrative. It must follow what -`vta_sdk::vp::select_credentials` accepts (VTI#437). +`vta_sdk::vp::select_credentials` accepts (VTI#437). It selects every +`StatementCredential`; the verifier then classifies each one by its +`credentialSubject.predicate`, never by type. **Every number here is community policy** (D15). The protocol has no default statement count, method floor or age limit; the values above are an example @@ -328,14 +331,14 @@ each vetter decides what they accept (D16). | Field | Meaning | |---|---| -| `statementType` | Endorsement `typeUri` registered via `vtc/endorsement-types/register/0.1`, with its `claimSchema` (§9.4) | +| `statementType` | Predicate IRI of the Vetting Statement in the DTG VSC predicate registry, `https://registry.trustoverip.org/dtg/vsc/vetted/1`; its profile fixes the `object.value` body (§9.4) | | `minStatements` | Distinct eligible vetters, counted by **member record**, not DID (§10.2). Set by each community; no protocol default (D15) | | `minByMethod` | Per-method floors (e.g. at least one `inPerson`) | | `acceptedMethods` | Methods that count at all. `priorAcquaintance` encodes the kernel's written "worked with you" standard | | `acceptedDocumentClasses` | **Optional, absent by default.** When absent, each vetter decides what documentation they accept (D16). A community that later needs a floor can set it; statements outside it then don't count | | `requiredClaims` | Claim types (from the VTA claim-type registry) the Vetting Card must carry and the vetter must mark verified | | `maxStatementAge` | A statement older than this at submit does not count | -| `eligibleVetters` | How eligibility is proven; V0 = holds the community's `vetter` role VEC (§10.3) | +| `eligibleVetters` | How eligibility is proven; V0 = holds the community's `role:vetter` VAC (§10.3) | | `independence` | Caps on declared vetter↔applicant relationships; require the identity commitment to match across statements (§9.3) | | `invitation` | `required` / `optional` / `none` — whether a VIC must also be in the VP | | `decisionSla` | What the applicant's client uses in place of the hardcoded 7-day pending expiry (D16) | @@ -428,9 +431,14 @@ In OpenVTC: ### 7.1 Confirming a vetter is eligible before spending effort The vetter's `vetting/request` `#response` carries an **eligibility VP**: the -vetter's community-issued VMC plus the `CommunityRole: vetter` VEC, bound to -the `requestId` as the challenge. The applicant's client verifies: -- the issuer is the community DID; +vetter's community-issued VMC plus the community's `role:vetter` VAC +(`authority` = `{ scope: , actions: ["role:vetter"], +maxAttenuation: 0 }`), bound to the `requestId` as the challenge. The +applicant's client verifies (`vta_sdk::vetting::eligibility::verify_eligibility_vp`): +- the issuer is the community DID, and the VAC's `authority.scope` is that + same DID with no parent; +- both declare `issuerScope: public`; a non-public scope or any attenuation + is refused; - the subject is the vetter DID; - neither credential has been revoked. @@ -595,7 +603,12 @@ the code, and the vetter's statement records `livenessConfirmed`. **`#response`** (applicant → vetter): the signed **Vetting Card** (§9.2). The session request's `id` is the value the statement later carries as -`taskContext`, mirroring `witness/session/0.1`. +`taskContext`, mirroring `witness/session/0.1`, and its task digest (JCS over +the document minus its top-level proof, SHA-256 multihash, base58btc) is the +statement's `taskDigestMultibase`. The vetted/1 profile requires both, so the +vetter's desk keeps the signed session document it sent +(`VettingBook::record_session_document`, `DeskSession::document`) and the +applicant keeps the one it received (`OpenSession::document`). ### 9.2 Vetting Card — a VDS @@ -716,83 +729,104 @@ document details. The card itself is retained only per the vetter's `cardRetention` (default: delete 7 days after the statement is issued; keep only `cardDigestMultibase`). -### 9.4 Vetting Statement — a VEC - -**Why a VEC** (D1). DTG WD02 defines a VEC as the credential that "enables -one party to a DTG edge trust relationship to issue verifiable assertions -about the counterparty", with an `endorsement` object whose "structure and -fields [are] determined by community policy". A VTC already has an -endorsement-type registry with `claimSchema`. The two other candidates don't -fit: -- a **VWC**'s digest must point at an edge credential; +### 9.4 Vetting Statement — a VSC + +**Why a VSC** (D1). DTG Credentials v1 defines the Verifiable Statement +Credential (`StatementCredential`) as an assertion by its issuer about a +subject, whose `credentialSubject.predicate` is an IRI from the DTG VSC +predicate registry and whose `object` is shaped by that predicate's profile. +The registry's `https://registry.trustoverip.org/dtg/vsc/vetted/1` predicate +is exactly an identity-vetting assertion. So the statement is a VSC under a +registered predicate — again no new DTG credential type. The two other +candidates don't fit: +- a **VWC** (a `witnessed/1` statement) attests that its subject issued another credential, named by digest; - an **IDVC** is defined as issued by a commercial IDVP. A new DTG type would need coordination with the cred task force (governance consideration 9) for no gain. -The VEC definition presumes an edge. The accepted `vetting/request` + session -exchange is that relationship. A VRC pair is **not required** in V0 (D8); a -community may require one (`vetting.requireVrc`) if it wants the edges for -later graph analysis (§10.5). +The statement rests on a relationship: the accepted `vetting/request` + +session exchange, which the vetted/1 profile makes explicit by requiring the +statement to cite the session document. A VRC pair is **not required** in V0 +(D8); a community may require one (`vetting.requireVrc`) if it wants the edges +for later graph analysis (§10.5). ```json { "@context": ["https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/dtg/v1"], - "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"], + "https://registry.trustoverip.org/dtg/context/v1"], + "type": ["VerifiableCredential", "DTGCredential", "StatementCredential"], + "id": "urn:uuid:5d2f7b1e-9c4a-4e8b-a3f6-0b7d2e19c845", "issuer": "did:webvh:…:carol-kernel", + "issuerScope": "directed", "validFrom": "2026-09-20T10:14:00Z", "validUntil": "2027-01-18T10:14:00Z", - "taskContext": "urn:uuid:", + "taskContext": "urn:uuid:8b0e2c4a-5d7f-4e61-9a3b-2f6c1d9e7a10", + "taskDigestMultibase": "zQm…", "credentialSubject": { "id": "did:webvh:…:alice-kernel", - "endorsement": { - "type": "https://firstperson.network/endorsements/identity-vetting/0.1", - "community": "did:webvh:…:kernel-vtc", - "method": "video", - "documentClasses": ["passport"], - "claimsVerified": ["name.legal"], - "livenessConfirmed": true, - "identityCommitment": "z…", - "cardDigestMultibase": "z…", - "declaredRelationship": "communityColleague", - "attestationTextDigest": "z…" + "predicate": "https://registry.trustoverip.org/dtg/vsc/vetted/1", + "object": { + "value": { + "community": "did:webvh:…:kernel-vtc", + "method": "video", + "documentClasses": ["passport"], + "claimsVerified": ["name.legal"], + "livenessConfirmed": true, + "identityCommitment": "z…", + "cardDigestMultibase": "z…", + "declaredRelationship": "communityColleague", + "attestationTextDigest": "z…" + } } }, "proof": { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "…": "…" } } ``` -The `taskContext` shape must match WD02 §taskContext; the value above is -illustrative. +`taskContext` is the `vetting/session` document's `id`, and +`taskDigestMultibase` is that document's task digest (§9.1); the vetted/1 +profile requires both. The `object.value` body has no `type` member — the +predicate says what it is — and its digests and commitment are multibase +base58btc (`z…`). | Field | Rule | |---|---| | `issuer` | Vetter's member DID in `community` | +| `issuerScope` | At least `directed` (vetted/1). OpenVTC's vetter declares `directed`: the issuer is the member's persona DID | | `credentialSubject.id` | The applicant's `joinDid` | | `validUntil` | = `validFrom` + the community's `maxStatementAge`. Statements are *pre-admission evidence*, not a lifelong credential | -| `endorsement.community` | Scopes the statement. Another community MUST NOT count it without explicit recognition policy (VTI-REG "Recognition MUST NOT be transitive") | -| `endorsement.documentClasses` | What the vetter relied on, from their own accepted list (D16); `[]` with `priorAcquaintance` | +| `taskContext`, `taskDigestMultibase` | Required. Cite the session the statement came from, by id and by digest | +| `object.value.community` | Scopes the statement. Another community MUST NOT count it without explicit recognition policy (VTI-REG "Recognition MUST NOT be transitive") | +| `object.value.documentClasses` | What the vetter relied on, from their own accepted list (D16); `[]` with `priorAcquaintance` | | `cardDigestMultibase` | Digest of the card **in the exact transmitted form** (VTI-MEM-021; `dtg_credentials::digest_multibase_json`). Used for dispute and audit. The VTC never needs the card | -| `attestationTextDigest` | Digest of the governance text the vetter saw, so the text version is provable | +| `attestationTextDigest` | Optional. Digest of the governance text the vetter saw, so the text version is provable | | `credentialStatus` | Absent in V0; revocation goes to the VTC (§9.6, D18). Added in V1 with VTA-hosted status lists | The vetter's client signs the statement as the vetter's member persona DID (`vta_sdk::vetting::statement::sign_statement`, D19) once the vetter confirms the attestation text. That confirmation is the V0 gate; see §11.3. +`sign_statement` takes a `StatementDraft { id, issuer, issuer_scope, subject, +value: VettedObjectValue, valid_from, valid_until, session }`, where `session` +is the signed `vetting/session` document the desk kept, and derives +`taskContext` and `taskDigestMultibase` from it. Delivery uses the existing `credential-exchange/issue/0.1`. The applicant's -client verifies the statement (proof, subject, community, commitment equals -its own, card digest equals what it sent) and stores it in the VTA vault -with `purpose: "vetting"`, tagged with the community. +client verifies the statement (`verify_statement`, then +`VerifiedVettingStatement::check_against_session` against the session +document it received: proof, subject, community, commitment equals its own, +card digest equals what it sent, and the session cited by both id and +digest — a statement that does not cite its session is refused) and stores +it in the VTA vault with `purpose: "vetting"`, tagged with the community. -**Registration.** The VTC registers the type once via -`vtc/endorsement-types/register/0.1` with a `claimSchema` covering -`endorsement`. +**Registration.** The predicate and its `object.value` profile are registered +once, in the DTG VSC predicate registry (`vetted/1`), not per community. A +community names it in its manifest as `vetting.statementType`. **Implementation note.** The VTC's issuance route `vtc/endorsements/issue` (Admin/Issuer only, community-issued) is **not** used. Statements are member-issued by the vetter's own client, using -`DTGCredential::new_vec` from `dtg-credentials`. +`DTGCredential::new_vetted_vsc` from `dtg-credentials` (through +`sign_statement`). ### 9.5 Decline @@ -871,22 +905,25 @@ It sends the VP via `submit_join_request` (`openvtc-core/src/join.rs`): ### 10.2 Verification → facts The VTC's ceremony pipeline (`vtc-service/src/join/orchestrate.rs`, -`ceremony/*`) is extended. For each `EndorsementCredential` whose -`endorsement.type` is a registered vetting type, the VTC checks: +`ceremony/*`) is extended. For each `StatementCredential` whose +`credentialSubject.predicate` is the criterion's `statementType` (statements +are classified by predicate, never by type), the VTC checks: 1. The VP proof is valid, holder = `joinDid`, and challenge + audience are correct (already done). -2. The VEC proof is valid; the issuer DID resolves (webvh log verified). -3. `endorsement` validates against the registered `claimSchema`. +2. The VSC proof is valid; the issuer DID resolves (webvh log verified). +3. The credential conforms to DTG Credentials v1 and `object.value` to the + vetted/1 profile: `issuerScope` at least `directed`, `taskContext` and + `taskDigestMultibase` present. 4. `credentialSubject.id` = VP holder. -5. `endorsement.community` = this VTC. +5. `object.value.community` = this VTC. 6. `validFrom ≤ now ≤ validUntil` and age ≤ `maxStatementAge`. 7. Not revoked: no revocation notice for this statement id (§9.6). From V1, also the `credentialStatus` entry, within the freshness window. 8. **Issuer is an eligible vetter.** This check is new. The issuer DID resolves to a member record that: - is `active`; - - held a valid `vetter` role VEC **at `validFrom`**; + - held a valid `role:vetter` VAC **at `validFrom`**; - still holds it **now**, or policy allows post-issuance lapse (`eligibleAtIssuanceOnly`). @@ -947,8 +984,8 @@ Facts (`input.evidence.vetting`), as built (`vtc-service/src/vetting/mod.rs`): ### 10.3 Vetter eligibility — the `vetter` role -Eligibility is **materialised** as a role VEC (`CommunityRole: vetter`), -issued and revoked by the VTC (D5). This gives three things: +Eligibility is **materialised** as a community-issued role VAC (action +`role:vetter`), issued and revoked by the VTC (D5). This gives three things: - applicants can verify a vetter offline (§7.1); - the VTC can verify "eligible at issuance" from its own records; - the kernel's "trust path to Linus ≤ 5 hops" becomes a policy input rather @@ -956,13 +993,23 @@ issued and revoked by the VTC (D5). This gives three things: *As built:* - **Issuing.** An admin grants the role with `vtc/vetting/vetters/grant/0.1` - (`POST /v1/vetting/vetters`). The credential is an `EndorsementCredential` - with endorsement `{type: "CommunityRole", role: "vetter", communityDid}`, - a `validUntil`, and a status-list `credentialStatus`. Leaving the community + (`POST /v1/vetting/vetters`). The credential is a VAC: `type` includes + `AuthorityCredential`, `issuer` = the community DID, `issuerScope: public`, + `credentialSubject.authority` = `{ scope: , actions: + ["role:vetter"], maxAttenuation: 0 }`, a `validUntil`, and a status-list + `credentialStatus`. It is built with `dtg-credentials` + `new_community_role_vac(community, member, "vetter", from, until)` and + `.with_max_attenuation(0)`; `vta_sdk::vetting::eligibility::community_roles` + reads (community, roles) back from it (`VETTER_ROLE_ACTION`, + `role_action`, `role_of_action`). Leaving the community revokes it. Running `vetter_eligibility.rego` automatically is V1. - **Holding.** The member's client keeps the credential apart from their ordinary community role credential. It refuses requests (`notEligible`) and - offers no tickets for a community without a live grant. + offers no tickets for a community without a live grant. A grant or held + statement an older build stored in a pre-v1 shape is set aside on load + (`VettingBook::retire_nonconformant`), with a logged reason, and listed in + `VettingBook::retired` so the Vetting page can say so; the vetter asks for + the grant again. - **Presenting.** Every acceptance carries the credential as `eligibilityVp`, with `nonce` = the request document `id` and `domain` = the applicant's `joinDid`. The applicant verifies it and shows the result; the check is @@ -1109,7 +1156,7 @@ Verdict handling, reusing the existing `Verdict` enum: | Verdict | VTC effect | Applicant sees | |---|---|---| -| `allow` | `EffectPlan::Admit`: VMC grant + role VEC; solicit reciprocal VMC; record evidence; write lineage (§10.5); optional `git-trust/grant` | "Admitted" | +| `allow` | `EffectPlan::Admit`: VMC grant + role VAC; solicit reciprocal VMC; record evidence; write lineage (§10.5); optional `git-trust/grant` | "Admitted" | | `requestMore` | Request stays `deferred` with `needs` | Checklist updated with what's missing, and why | | `refer` | Request queued for moderators; moderators notified (missing today — §14) | "Under human review — expected by " | | `deny` | Rejected with a reason code | Reason in plain language + governance link | @@ -1324,7 +1371,7 @@ Following the existing patterns in with expiry). *As built, in part:* the **Membership** tab now lists the `vetter` role - credential beside the VMC and role VEC from the same community, with its + credential beside the VMC and role VAC from the same community, with its validity window and its raw JSON — it is stored apart from them (§10.3) but is a credential from that community like the others. Held *statements* still have no tab of their own. @@ -1509,7 +1556,7 @@ modern-signature subgraph only (O7). | Repo | Change | Phase | |---|---|---| | `trustoverip/dtgwg-trust-tasks-tf` | New specs: `vetting/request/0.1`, `vetting/session/0.1`, `vetting/decline/0.1`, `vtc/vetting/revoke-statement/0.1`, `vetting/tickets/{issue,list,revoke}/0.1`, `vtc/vetters/list/0.1`, `vtc/vetting/concern/0.1`; `vtc/join-requests/manifest/0.2` (`vetting` object, `requirementsDigest`); ceremony `vetting/identity-vetting/0.1` | V0 (manifest, request, session, decline, revoke-statement, ceremony); V1 (rest) | -| `trustoverip/dtgwg-cred-spec` | Profile note: `IdentityVetting` endorsement for VEC; note the vetting exchange as the edge the VEC definition presumes; cite salted commitment re #38 | V0 | +| `trustoverip/dtgwg-cred-spec` / DTG VSC predicate registry | The `vetted/1` predicate and its `object.value` profile (session cited by `taskContext` + `taskDigestMultibase`, `issuerScope` at least `directed`); note the vetting exchange as the relationship the statement rests on; cite salted commitment re #38 | V0 | | `trustoverip/dtgwg-vds-spec` | First VDS profile: Vetting Card (r-card profile; signing, binding, commitment) | V0 draft | | VTI `vta-sdk` (`vetting` feature) | Card and statement signing and verification, commitment, requirements evaluation, match code — client-side signing (D19) | V0 | | VTI `vta-service` / `vta-policy` | Step-up on statement signing and revocation; vault `purpose: vetting`; V1: ticket store + inbound gate, push events, VTA-hosted status lists shared across issuers (§9.6) | V0 / V1 | @@ -1517,14 +1564,14 @@ modern-signature subgraph only (O7). | VTI `vta-sdk` | Protocol types for the new tasks; manifest 0.2 types | V0 | | `OpenVTC/openvtc` | `openvtc-core::vetting` (tickets V0, requests, sessions, match code domain, card verify, statement verify/store, requirement checker); first call sites for `presentation.rs`; `MainMenu::Vetting`; Inbox `TaskType`s; Credentials tab; join-flow branch; replace fixed 7-day pending expiry with `decisionSla`; remove the `kernel.org/maintainers` stubs | V0 | | `OpenVTC/verifiable-git-infrastructure` | None required — `git-trust/grant` exists; wire the VTC admit effect to it for the kernel instance | V0 config | -| `OpenVTC/wiki` | Replace the "Two-VRC vouching — target design" text; retire C-DID/M-DID terms | V1 | +| `OpenVTC/wiki` | Replace the "Two-VRC vouching — target design" text; use the WD02+ terms community DID and member DID throughout | V1 | ### 14.2 Phases - **V0 — walking skeleton.** Everything marked V0 above. - **Covers:** discovery via manifest only; out-of-band vetter contact (agent name / DID / QR); client-side tickets; session + signed card + - VEC; submit; `allow` / `requestMore` / `refer` decisions. + VSC; submit; `allow` / `requestMore` / `refer` decisions. - **Excludes:** directory, push, cascade review, PGP bridge. - **Target: 5 Oct 2026** (Prague, ahead of the Kernel Maintainer Summit on 8 Oct), full V0 scope. That is about 3½ weeks from this draft, so the @@ -1574,15 +1621,15 @@ modern-signature subgraph only (O7). | # | Decision | Proposal | |---|---|---| -| D1 | Statement credential type | **VEC** with a registered `IdentityVetting` endorsement type. No new DTG credential type | +| D1 | Statement credential type | **VSC** (`StatementCredential`) under the registered predicate `https://registry.trustoverip.org/dtg/vsc/vetted/1`. No new DTG credential type | | D2 | Card format | **Signed r-card profile (VDS)**, its claims released from the join persona's face by persona disclosure (preview shown, then present), signed as the join persona DID (D19) | | D3 | Does the VTC see card/PII? | **No, by default.** Statements + salted commitment only | | D4 | Anti-spam default | **Ticket required**; introductions and `open` are vetter opt-ins | -| D5 | Vetter eligibility | **Materialised as a VTC-issued `vetter` role VEC**, driven by `vetter_eligibility.rego` plus manual grants | +| D5 | Vetter eligibility | **Materialised as a VTC-issued `role:vetter` VAC**, driven by `vetter_eligibility.rego` plus manual grants | | D6 | When evidence is evaluated | **Once, at admission**, recorded; later expiry does not affect membership; revocation → review, not removal | | D7 | Vetter identity on statements | **Vetter's member DID** (accountable within the community) | | D8 | VRC required between vetter and applicant? | **No** in V0; community option `requireVrc` | -| D9 | Statement scope | **One community** (`endorsement.community`); cross-community reuse only through explicit recognition policy | +| D9 | Statement scope | **One community** (`object.value.community`); cross-community reuse only through explicit recognition policy | | D10 | Liveness | **Match code required** for every method except `priorAcquaintance` with no session. Recommendation: require it there too | | D11 | Document data | **Never transmitted or retained**; the vetter looks, attests, forgets | | D12 | Client requirement checker | **Advisory only**; VTC verdict is authoritative | @@ -1616,9 +1663,9 @@ modern-signature subgraph only (O7). vetted is admitted? Closure and accountability vs applicant privacy. - **O9 — Spec home for peer tasks.** Top-level `vetting/*` (peer-to-peer, not community-hosted — proposed) vs `vtc/vetting/*`. -- **O10 — VEC edge wording.** Whether the cred task force accepts a - vetting exchange, rather than a VRC, as the "edge" the VEC definition - presumes. +- **O10 — Relationship wording.** Whether the cred task force accepts a + vetting exchange (cited by `taskContext` + `taskDigestMultibase`), rather + than a VRC, as the relationship a `vetted/1` statement rests on. - **O11 — Kernel instance numbers.** The kernel VTC still has to pick its own `minStatements`, `minByMethod` and `maxStatementAge` for 5 Oct. diff --git a/docs/design/vta-authoritative-state.md b/docs/design/vta-authoritative-state.md index 6a0b3b72..e7f8d91e 100644 --- a/docs/design/vta-authoritative-state.md +++ b/docs/design/vta-authoritative-state.md @@ -75,7 +75,7 @@ against `vta-sdk` 0.25.1. | VICs (invitations) | **VTA** — credential vault | unchanged | | Account model — persona list, labels, mediator refs | OpenVTC only | **→ VTA** | | Community memberships — status, sub-context, VMC, request ids | OpenVTC only | **→ VTA** | -| Relationships (R-DIDs, `did:peer`) | OpenVTC only | **→ VTA** | +| Relationships (relationship DIDs, `did:peer`) | OpenVTC only | **→ VTA** | | VRCs issued / received | OpenVTC only | **→ VTA credential vault** | | Contacts (DID + your alias) | OpenVTC only | **→ VTA** (D22) | | Agent-name cache | OpenVTC only | **→ VTA** as a *stale* hint (D22) | diff --git a/docs/openvtc-config-data-structure.md b/docs/openvtc-config-data-structure.md index 7fe97a45..e8bc6164 100644 --- a/docs/openvtc-config-data-structure.md +++ b/docs/openvtc-config-data-structure.md @@ -293,7 +293,12 @@ and **vrcs_received** (credentials you have received from others). | Field | Type | Description | | ------ | ------------------------------------------------------------- | -------------------------------------------------------------- | -| `vrcs` | `HashMap, HashMap, Arc>>` | Map of remote P-DID to VRC credentials (nested map by VRC ID). | +| `vrcs` | `HashMap, HashMap, Arc>>` | Map of the remote party's persona DID to VRC credentials (nested map by VRC ID). | +| `retired` | `usize` (omitted when 0) | How many stored VRCs were set aside on load because they pre-date DTG Credentials v1 (the retired pre-v1 context, no `issuerScope`). Each is dropped with a logged reason; the count drives a notice in the Credentials panel. | + +Every stored VRC is parsed on its own when the config loads, so one VRC in a +pre-v1 shape is dropped rather than failing the whole protected config. The +same applies to a `VRCIssued` task carrying one. **VRC Structure:** diff --git a/docs/relationships-vrcs.md b/docs/relationships-vrcs.md index 3174d852..9f87655a 100644 --- a/docs/relationships-vrcs.md +++ b/docs/relationships-vrcs.md @@ -8,31 +8,37 @@ The OpenVTC tool enables you to establish relationships with other DIDs (e.g., peers, coworkers, or community members) and communicate privately through the DIDComm protocol. -Each relationship gets its own **Relationship DID (R-DID)** by default — a pairwise identifier used only with that one contact. You can choose to use your **Persona DID (P-DID)** instead; see [Choosing your identifier](#choosing-your-identifier) for what that costs. +Each relationship gets its own **relationship DID** by default — a pairwise identifier used only with that one contact. You can choose to use your **persona DID** instead; see [Choosing your identifier](#choosing-your-identifier) for what that costs. + +DTG Credentials no longer types an identifier by the role it plays (the retired +"R-DID" and "P-DID" labels named a role and a correlation width in one token). +What an identifier carries instead is a declared **correlation scope** — +`pairwise`, `directed` or `public` — stated in the `issuerScope` of every +credential issued under it. See [The scope a VRC declares](#the-scope-a-vrc-declares). Once a relationship is established, you can request a **Verifiable Relationship Credential (VRC)**, a peer-to-peer credential that attests to verifiable trust relationships between personhood credential holders. -_The diagram below illustrates the typical flow of establishing a relationship and requesting a VRC. An R-DID is generated to enable private channel communication between parties._ +_The diagram below illustrates the typical flow of establishing a relationship and requesting a VRC. A relationship DID is generated to enable private channel communication between parties._ ```mermaid sequenceDiagram autonumber box transparent Establish Relationship - actor req as Requestor (P-DID) - actor res as Respondent (P-DID) + actor req as Requestor (persona DID) + actor res as Respondent (persona DID) end req->>res: Send Relationship Request - Note over req, res: Requestor generates R-DID and
sets Respondent's alias + Note over req, res: Requestor generates a relationship DID and
sets Respondent's alias res->>req: Accept Relationship Request - Note over res, req: Respondent generates R-DID and
sets Requestor's alias + Note over res, req: Respondent generates a relationship DID and
sets Requestor's alias req->>res: Finalize Relationship Request Note over req, res: Updates status to "Established" - create actor rdid-req as Requestor (R-DID) + create actor rdid-req as Requestor (relationship DID) req->>rdid-req: Switch to VRC flow - Note over req, rdid-req: Actors uses R-DID for private channel communication - create actor rdid-res as Respondent (R-DID) + Note over req, rdid-req: Actors use the relationship DID for private channel communication + create actor rdid-res as Respondent (relationship DID) rdid-req->>rdid-res: Request VRC rdid-res->>rdid-req: Issue VRC @@ -41,6 +47,7 @@ sequenceDiagram ## Table of Contents - [Choosing your identifier](#choosing-your-identifier) + - [The scope a VRC declares](#the-scope-a-vrc-declares) - [Establish Relationship](#establish-relationship) - [1. Send Relationship Request (Requestor)](#1-send-relationship-request-requestor) - [2. Accept Relationship Request (Respondent)](#2-accept-relationship-request-respondent) @@ -55,9 +62,12 @@ sequenceDiagram ## Choosing your identifier Every relationship is established under one of two identifiers, chosen when you -send the request or accept an incoming one. The default is the pairwise R-DID. +send the request or accept an incoming one. The default is the pairwise +relationship DID — unless the community you are working in declares +`relationshipIdentifierDefault: attributed`, in which case the form starts on +the persona DID. Either way the choice stays yours per relationship. -| | **Pairwise R-DID** (default) | **Your persona DID** | +| | **Pairwise relationship DID** (default) | **Your persona DID** | | --- | --- | --- | | What the contact sees | A fresh `did:peer` used only with them | Your published `did:webvh` | | Linkable to your other relationships | No — each contact sees a different identifier | Yes — every contact sees the same string | @@ -72,13 +82,13 @@ The reason to choose the persona DID anyway is recognition — a contact who already knows your published DID or agent name can verify it is really you without a separate introduction. -Once a relationship holds an R-DID, subsequent messages always use it. There is -no fallback to the persona DID mid-relationship. +Once a relationship holds a relationship DID, subsequent messages always use it. +There is no fallback to the persona DID mid-relationship. **Note:** The three handshake messages (request, accept, finalise) are routed between persona DIDs regardless of this choice, because the mediator has to -route them before a pairwise channel exists. The R-DID takes over once the -relationship is established, and the VRC is issued under it — so the durable +route them before a pairwise channel exists. The relationship DID takes over once +the relationship is established, and the VRC is issued under it — so the durable credential, which is the artifact that would otherwise correlate you across every relationship you hold, names only the pairwise identifier. @@ -86,6 +96,26 @@ The handshake DIDs are observed once, by the mediator, at establishment. The credential lasts as long as either party keeps it. That asymmetry is why the issuer field mattered more than the routing does. +### The scope a VRC declares + +Every VRC OpenVTC issues declares, in `issuerScope`, the correlation scope of +**its own issuer's** identifier — the identifier this relationship uses on your +side. It is derived from that identifier, not from a setting, so it cannot +disagree with what the relationship actually does: + +| Your identifier in the relationship | Seeded by the community's `relationshipIdentifierDefault` | `issuerScope` | +| --- | --- | --- | +| A relationship DID minted for this one contact | `pairwise` (or undeclared) | `pairwise` | +| Your persona DID | `attributed` | `directed` | + +A persona DID is never `pairwise`: every contact who relates to the persona sees +the same identifier, which is the legible graph an `attributed` community asks +for, and DTG Credentials names that `directed` — known to a set of +counterparties you chose. The declaration is about the issuer only; your +contact's VRC back to you declares their own scope. + +The code is `openvtc_core::dtg::relationship_issuer_scope`. + ## Establish Relationship Follow these steps to establish a relationship with another Persona DID. @@ -99,10 +129,10 @@ In the Relationships panel, start a new request and fill in: | DID | The respondent's persona DID, or an agent name such as `example.com/@bob` | | Alias | A local name for this relationship | | Reason | Why you are asking | -| Contact you as | `Pairwise R-DID` (default) or `Your persona DID` — press Space to change | +| Contact you as | `Pairwise relationship DID` (default) or `Your persona DID` — press Space to change | The last field is the choice described in [Choosing your identifier](#choosing-your-identifier). -It defaults to minting a fresh R-DID for this contact. +It defaults to minting a fresh relationship DID for this contact. **Note:** Initiating a relationship request automatically adds the respondent to your Contacts list. @@ -130,7 +160,7 @@ For more details, see the [CLI documentation](./openvtc-tool-commands.md#openvtc 2. Accept it with either identifier: - - `a` — accept with a pairwise R-DID (the default, private path) + - `a` — accept with a pairwise relationship DID (the default, private path) - `p` — accept as your persona DID See [Choosing your identifier](#choosing-your-identifier) for the trade-off. @@ -171,8 +201,8 @@ Refer to the sample response below: ```bash ✅ Relationship successfully established did:webvh:Qmbea... - Remote: P-DID(did:webvh:Qmbea...) r-did(did:peer:2.Vz6Mkkop...) - Local: P-DID(did:webvh:QmQzm...) r-did(did:peer:2.Vz6Mkgt...) + Remote: persona(did:webvh:Qmbea...) relationship(did:peer:2.Vz6Mkkop...) + Local: persona(did:webvh:QmQzm...) relationship(did:peer:2.Vz6Mkgt...) Task Id: 020bb98e-5460-4d42-b369-bf4a65b4909c Type: Relationship request finalized ``` @@ -180,9 +210,10 @@ Task Id: 020bb98e-5460-4d42-b369-bf4a65b4909c Type: Relationship request finaliz A VRC is a peer-to-peer credential attesting to a verifiable trust relationship between two parties (coworkers, peers, community members). It names each side by -the identifier that side uses in *this* relationship — the pairwise R-DID by -default, or the persona DID if that was chosen when the relationship was -established. +the identifier that side uses in *this* relationship — the pairwise +relationship DID by default, or the persona DID if that was chosen when the +relationship was established — and declares that identifier's `issuerScope` +([The scope a VRC declares](#the-scope-a-vrc-declares)). ### Prerequisite @@ -231,21 +262,24 @@ openvtc vrcs request The tool generates and issues the VRC to the requestor, storing a record in your private configuration. -Refer to the sample VRC below: +Refer to the sample VRC below — the DTG Credentials v1 shape: the v1 context +second, a declared `issuerScope`, and exactly one concrete type: ```bash Issued VRC { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://firstperson.network/credentials/relationship/v1" + "https://registry.trustoverip.org/dtg/context/v1" ], + "id": "urn:uuid:6f1c2b0e-9a4d-4c8e-b7a1-2d5e8f3c9b10", "type": [ "VerifiableCredential", "DTGCredential", "RelationshipCredential" ], "issuer": "did:peer:2.Vz6Mkgt...", + "issuerScope": "pairwise", "validFrom": "2025-12-02T08:58:43Z", "validUntil": "2026-12-02T00:00:00Z", "credentialSubject": { @@ -255,14 +289,20 @@ Issued VRC "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "created": "2025-12-02T08:58:43Z", - "verificationMethod": "did:webvh:QmQzm...#key-1", + "verificationMethod": "did:peer:2.Vz6Mkgt...#key-1", "proofPurpose": "assertionMethod", "proofValue": "zAXERK8RVBH..." } } ``` -For more details, see the [draft specification](https://github.com/trustoverip/dtgwg-cred-tf/blob/14-revised-vrc-spec---v02/dtg.md#core-structure) of the VRC. +For more details, see the [DTG Credentials specification](https://github.com/trustoverip/dtgwg-cred-spec) (§VRC and *Correlation Scope*). + +**VRCs stored by an older build.** A VRC stored before OpenVTC moved to DTG +Credentials v1 carries the retired pre-v1 context and no `issuerScope`, and no +current verifier accepts it. On load such a VRC is dropped with a logged reason +rather than kept or allowed to fail the config, and the Credentials panel says +how many were set aside — request a fresh VRC from those relationships. ### 3. Claim and Store VRC (Requestor) From 6ab24856d27afc2d6b7016e2a864aef9008686b1 Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Wed, 30 Sep 2026 18:49:05 +0200 Subject: [PATCH 5/9] chore(deps)!: dtg-credentials 0.12, vta-sdk 0.57, trust-tasks 0.25, affinidi-messaging 0.31 Replaces the local-path patch with the released crates: - dtg-credentials 0.12 - vta-sdk / vta-service 0.57 / 0.46 (OpenVTC/verifiable-trust-infrastructure#1859) - the trust-tasks-* family at 0.25 - affinidi-tdk 0.21, affinidi-messaging-sdk 0.31 and -test-mediator 0.15 (affinidi/affinidi-tdk-rs#908) Cargo.lock follows once VTI publishes. Signed-off-by: Glenn Gore --- Cargo.toml | 10 +++++----- openvtc-core/Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed52b0a3..ce9ca147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ openvtc-core = { version = "0.3", path = "openvtc-core", default-features = fals # The requirement reads "0.11" only because the unreleased 0.12 is still # versioned 0.11.0 in its worktree and the patch block at the bottom has to # match it. Raise it to "0.12" when 0.12 is published, as the patch comes out. -dtg-credentials = "0.11" +dtg-credentials = "0.12" aes-gcm = "0.11" # The one direct requirement in this workspace that was behind its latest @@ -85,7 +85,7 @@ argon2 = "0.6" # # 0.18 -> 0.20 follows `vta-sdk` 0.53 -> 0.56 below, the release that declares # `affinidi-tdk ^0.20`. -affinidi-tdk = "0.20" +affinidi-tdk = "0.21" affinidi-data-integrity = "0.7" # Decodes a community's Bitstring Status List. Already in the graph through # `vta-sdk`, whose status reader cannot verify a list signed with a proof set; @@ -130,7 +130,7 @@ affinidi-messaging-core = "0.1.11" # geometric reconnect cascade behind `#231`–`#234`; and 0.19.4 (#694) was the # `live_stream_next*` read guard that blocked `stop_websocket` for the remainder # of a 10s poll window. -affinidi-messaging-sdk = "0.30.0" +affinidi-messaging-sdk = "0.31.0" # `StreamExt::next` on the `BoxStream` returned by `MessagingService::subscribe`. futures-util = "0.3" # Agent names (DID shortcuts). `agent-names` carries the parse / @@ -385,8 +385,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # decide / personhood / renew / rotate responses, the vetting specs carry # `vetted/1` statements and role VACs, and `vtc/endorsements/issue` gains # `predicateNotIssuable`. -trust-tasks-rs = { version = "0.24.8", features = ["git-ns"] } -trust-tasks-capability-client = "0.24.8" +trust-tasks-rs = { version = "0.25.0", features = ["git-ns"] } +trust-tasks-capability-client = "0.25.0" tui-input = "0.15" url = "2.5" uuid = { version = "1.23", features = ["v4", "fast-rng", "serde"] } diff --git a/openvtc-core/Cargo.toml b/openvtc-core/Cargo.toml index 209ded53..c13935eb 100644 --- a/openvtc-core/Cargo.toml +++ b/openvtc-core/Cargo.toml @@ -119,7 +119,7 @@ criterion = { version = "0.8", features = ["html_reports"] } # # 0.14.0 is the line built on the same `vta-sdk` 0.56 the root manifest now # pins, so the dev graph keeps one copy of it too. -affinidi-messaging-test-mediator = "0.14.0" +affinidi-messaging-test-mediator = "0.15.0" # In-process MockVta for the bootstrap e2e (VTI#406/#427 test-support seams). # `vta-service` is the VTA server crate; it carries the credential-vault # lifecycle tasks (receive/query/archive/delete/restore/purge) + the From 42bbb6986f58c39d4527071b25c792dae999b4cd Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Wed, 30 Sep 2026 23:21:12 +0200 Subject: [PATCH 6/9] chore(deps): vta-sdk 0.58 / vta-service 0.47 vta-sdk 0.57.0 shipped before the DTG v1 migration, so the release that carries it (OpenVTC/verifiable-trust-infrastructure#1859) is 0.58.0. Signed-off-by: Glenn Gore --- Cargo.toml | 2 +- openvtc-core/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ce9ca147..d5702916 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -550,7 +550,7 @@ uuid = { version = "1.23", features = ["v4", "fast-rng", "serde"] } # `openvtc-core/Cargo.toml` for why that pin stays at 0.43.0 rather than # following `vta-sdk` here to 0.56 — 0.44.0 is broken independent of what # `vta-sdk` line it is paired with. -vta-sdk = { version = "0.57.0", features = [ +vta-sdk = { version = "0.58.0", features = [ "session", "client", # The SAME per-platform credential-store registration pnm-cli uses diff --git a/openvtc-core/Cargo.toml b/openvtc-core/Cargo.toml index c13935eb..8f09fb68 100644 --- a/openvtc-core/Cargo.toml +++ b/openvtc-core/Cargo.toml @@ -167,7 +167,7 @@ affinidi-messaging-test-mediator = "0.15.0" # is the check: one row means the family agrees. # # **0.46.0** is the family on `vta-sdk` 0.57 (DTG Credentials v1). -vta-service = { version = "0.46.0", default-features = false, features = ["test-support", "rest", "didcomm"] } +vta-service = { version = "0.47.0", default-features = false, features = ["test-support", "rest", "didcomm"] } base64 = { workspace = true } ed25519-dalek-bip32 = { workspace = true } secrecy = { workspace = true } From 57946e43dd9506c2dfc6be108174f1f7778849f9 Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Thu, 1 Oct 2026 02:40:23 +0200 Subject: [PATCH 7/9] chore(deps): Cargo.lock on the released crates dtg-credentials 0.12, vta-sdk 0.58, vta-service 0.47, trust-tasks 0.25, affinidi-tdk 0.21 and affinidi-messaging-sdk 0.31, all from crates.io. The only second copies come through did-git-sign 0.5.2 (vta-sdk 0.56), which clears with the did-git-sign release on vta-sdk 0.58. Signed-off-by: Glenn Gore --- Cargo.lock | 381 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 246 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28ddeab8..caf85fc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,9 +365,9 @@ dependencies = [ [[package]] name = "affinidi-messaging-mediator" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc895867941c62257b94041d6a5448abd7417befe425b2f110885369e1bf5a08" +checksum = "f4ea82654dac00d7e588c2c1a3a6878f5e8b1ef2baa4e4bfcf5ac2566503ded9" dependencies = [ "affinidi-crypto", "affinidi-data-integrity", @@ -377,7 +377,7 @@ dependencies = [ "affinidi-messaging-didcomm", "affinidi-messaging-mediator-common", "affinidi-messaging-mediator-config", - "affinidi-messaging-sdk", + "affinidi-messaging-sdk 0.31.0", "affinidi-rate-limit", "affinidi-secrets-resolver", "affinidi-task-utils", @@ -422,17 +422,17 @@ dependencies = [ "tower-http 0.7.1", "tracing", "tracing-subscriber", - "trust-tasks-proof", - "trust-tasks-rs", + "trust-tasks-proof 0.25.1", + "trust-tasks-rs 0.25.1", "url", "uuid", ] [[package]] name = "affinidi-messaging-mediator-common" -version = "0.17.0" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b09ffca7115cb04967d8cdad8955cc4cc13a17a7bc4e9acf0503dd5ecdf39f" +checksum = "0c8328f85f660b409a3c8f0d1e374de7a26c46072077cf7c36c2121f632417fe" dependencies = [ "aes-gcm 0.11.1", "affinidi-net-guard", @@ -485,6 +485,42 @@ name = "affinidi-messaging-sdk" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "017fb9f300ca4be022e5ff9ab7be5ea893d99c8d93fe2b670d2cd71ef1d96174" +dependencies = [ + "affinidi-crypto", + "affinidi-did-authentication", + "affinidi-did-common", + "affinidi-encoding", + "affinidi-messaging-core", + "affinidi-messaging-didcomm", + "affinidi-messaging-mediator-common", + "affinidi-secrets-resolver", + "affinidi-task-utils", + "affinidi-tdk-common", + "ahash", + "async-trait", + "base64 0.23.1", + "chrono", + "futures-util", + "rand 0.10.3", + "reqwest", + "rustls", + "serde", + "serde_json", + "sha256", + "thiserror 2.0.21", + "tokio", + "tokio-tungstenite 0.30.0", + "tracing", + "trust-tasks-proof 0.24.3", + "trust-tasks-rs 0.24.3", + "uuid", +] + +[[package]] +name = "affinidi-messaging-sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e473fbd27e6f3fecd641ce90959cb68f78ac78060841aa80c3a56d1d8a3b24a8" dependencies = [ "affinidi-crypto", "affinidi-did-authentication", @@ -514,24 +550,24 @@ dependencies = [ "tokio", "tokio-tungstenite 0.30.0", "tracing", - "trust-tasks-proof", - "trust-tasks-rs", + "trust-tasks-proof 0.25.1", + "trust-tasks-rs 0.25.1", "uuid", ] [[package]] name = "affinidi-messaging-test-mediator" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942d541a28a4b09e67b5fd47d20336eefabd1f51bb6aa0924e29448c333c6563" +checksum = "99ad42b92ee2b0cc992e835e7f51b7010fbe9c785ce7059dcc07b53c07e4d56d" dependencies = [ "affinidi-did-resolver-cache-sdk", "affinidi-messaging-didcomm", "affinidi-messaging-mediator", "affinidi-messaging-mediator-common", - "affinidi-messaging-sdk", + "affinidi-messaging-sdk 0.31.0", "affinidi-secrets-resolver", - "affinidi-tdk", + "affinidi-tdk 0.21.0", "async-trait", "ring", "rustls", @@ -723,7 +759,32 @@ dependencies = [ "affinidi-did-resolver-cache-sdk", "affinidi-meeting-place", "affinidi-messaging-didcomm", - "affinidi-messaging-sdk", + "affinidi-messaging-sdk 0.30.0", + "affinidi-secrets-resolver", + "affinidi-tdk-common", + "clap", + "rustls", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "affinidi-tdk" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c98ff504dcb69de6e4696f89e3c9fd926ac0e0b551a88b97dace09a810e975f8" +dependencies = [ + "affinidi-crypto", + "affinidi-data-integrity", + "affinidi-did-authentication", + "affinidi-did-common", + "affinidi-did-resolver-cache-sdk", + "affinidi-meeting-place", + "affinidi-messaging-didcomm", + "affinidi-messaging-sdk 0.31.0", "affinidi-secrets-resolver", "affinidi-tdk-common", "affinidi-tsp", @@ -1507,17 +1568,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "bip39" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" -dependencies = [ - "bitcoin_hashes", - "serde", - "unicode-normalization", -] - [[package]] name = "bip39" version = "3.0.0" @@ -2961,7 +3011,7 @@ dependencies = [ "tracing", "tracing-subscriber", "vgi-core", - "vta-sdk", + "vta-sdk 0.56.0", "windows-native-keyring-store", "zeroize", ] @@ -3107,27 +3157,9 @@ dependencies = [ [[package]] name = "dtg-credentials" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa51b03528a22ad07cd0f812a99fae834b7867c205943a40ad90bb8eb62edb08" -dependencies = [ - "affinidi-data-integrity", - "affinidi-secrets-resolver", - "chrono", - "multibase", - "serde", - "serde_json", - "serde_json_canonicalizer", - "sha2 0.11.0", - "thiserror 2.0.21", - "tracing", -] - -[[package]] -name = "dtg-credentials" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b54c22a4b29b0f89c767b03a9cca10832fcb6bd667da3caffdb1d86d801796" +checksum = "d014b5e72e83b5eb89ca4a79ecaa7bcda11f2e502a8531c36c0492058dcb502f" dependencies = [ "affinidi-data-integrity", "affinidi-secrets-resolver", @@ -3139,6 +3171,7 @@ dependencies = [ "sha2 0.11.0", "thiserror 2.0.21", "tracing", + "unicode-normalization", ] [[package]] @@ -6298,7 +6331,7 @@ version = "0.3.1" dependencies = [ "aes-gcm 0.11.1", "affinidi-data-integrity", - "affinidi-tdk", + "affinidi-tdk 0.21.0", "anyhow", "apple-native-keyring-store", "arboard", @@ -6314,7 +6347,7 @@ dependencies = [ "did-git-sign", "didwebvh-rs", "dirs", - "dtg-credentials 0.9.1", + "dtg-credentials", "ed25519-dalek-bip32", "hex", "keyring-core", @@ -6347,7 +6380,7 @@ dependencies = [ "tui-input", "url", "uuid", - "vta-sdk", + "vta-sdk 0.58.0", "windows-native-keyring-store", "x25519-dalek 2.0.1", "zeroize", @@ -6363,25 +6396,25 @@ dependencies = [ "affinidi-did-web", "affinidi-messaging-core", "affinidi-messaging-delivery", - "affinidi-messaging-sdk", + "affinidi-messaging-sdk 0.31.0", "affinidi-messaging-test-mediator", "affinidi-net-guard", "affinidi-status-list", - "affinidi-tdk", + "affinidi-tdk 0.21.0", "agent-names", "anyhow", "arbitrary", "argon2 0.6.0", "async-trait", "base64 0.23.1", - "bip39 3.0.0", + "bip39", "byteorder", "card-backend-pcsc", "chrono", "criterion", "didwebvh-rs", "dirs", - "dtg-credentials 0.9.1", + "dtg-credentials", "ed25519-dalek-bip32", "futures-util", "hex", @@ -6408,10 +6441,10 @@ dependencies = [ "tracing", "tracing-subscriber", "trust-tasks-capability-client", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", "url", "uuid", - "vta-sdk", + "vta-sdk 0.58.0", "vta-service", "wiremock", "x25519-dalek 2.0.1", @@ -6436,7 +6469,7 @@ dependencies = [ "serde_json_canonicalizer", "sha2 0.11.0", "thiserror 2.0.21", - "vta-sdk", + "vta-sdk 0.58.0", ] [[package]] @@ -9405,23 +9438,23 @@ dependencies = [ [[package]] name = "trust-tasks-capability-client" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b8d3b32476e204a63a08d181412e8a3f4919f30e956102de69c4d96673e8f7" +checksum = "158592b8aeaf38ac2dbaf40dad593b00416755df557ac572c8efc407424c1361" dependencies = [ "chrono", "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", "uuid", ] [[package]] name = "trust-tasks-didcomm" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b33348067a18f6bcd085e2613f867dd7a44250ee5c29dae1cc1303028645dfa3" +checksum = "1d92f324d7ebebff0d6e8e2f906906e848dc14e3dbed90632800330bb43c523e" dependencies = [ "affinidi-messaging-didcomm", "base64 0.22.1", @@ -9429,15 +9462,15 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", "uuid", ] [[package]] name = "trust-tasks-https" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348b082c748199f6baf5f746540bdc821a033991eca9e4aff783722d888d80d" +checksum = "dc7f146b1db678b1febf668318051178b9622d46f4326fcaffe24f1fbaaf3d1d" dependencies = [ "axum", "chrono", @@ -9448,7 +9481,7 @@ dependencies = [ "thiserror 2.0.21", "tokio", "tower", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", "uuid", ] @@ -9467,7 +9500,25 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs", + "trust-tasks-rs 0.24.3", +] + +[[package]] +name = "trust-tasks-proof" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efcf0056dbe94aa71815d782d7672b1dfcf9ec79c6554a23ac702db95b9ce4b9" +dependencies = [ + "affinidi-crypto", + "affinidi-data-integrity", + "affinidi-did-common", + "affinidi-did-resolver-cache-sdk", + "affinidi-encoding", + "async-trait", + "serde", + "serde_json", + "thiserror 2.0.21", + "trust-tasks-rs 0.25.1", ] [[package]] @@ -9486,18 +9537,34 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "trust-tasks-rs" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd57748a7e6a2ee80bc7efe8460007050d53540be25fdb40995eb63e1da5d73e" +dependencies = [ + "async-trait", + "chrono", + "jsonschema 0.46.10", + "regress", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.21", +] + [[package]] name = "trust-tasks-tsp" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dd6238475f91f828cb1a0ccf300f68483c84ad765fcdd60e933281f2da0e0e" +checksum = "9f626fe4d797ef685542ef75485214d842c300249f11810cb62c6192ecaf23f3" dependencies = [ "affinidi-tsp", "chrono", "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", ] [[package]] @@ -9892,9 +9959,9 @@ dependencies = [ [[package]] name = "vta-audit" -version = "0.3.26" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072a48308d40ee6ed0c67daf31ff25deb6a01e25377e8c041541a523fef2985" +checksum = "a2631b985ddb54f0c24b2390a43e79b50dabd2e52be64285635cf2fe0254bf6b" dependencies = [ "async-trait", "chrono", @@ -9905,15 +9972,15 @@ dependencies = [ "tokio", "tracing", "uuid", - "vta-sdk", + "vta-sdk 0.58.0", "vti-common", ] [[package]] name = "vta-backup" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a53dfeb94a1f820b76d54c122ffd0128706742b643b2d8872116b78d62432ba" +checksum = "44106acd499c598e9b658e532eaf9b9676aff7dfed59f21507a5d4e07bac7456" dependencies = [ "aes-gcm 0.11.1", "argon2 0.6.0", @@ -9932,7 +9999,7 @@ dependencies = [ "vta-config", "vta-keys", "vta-keyspaces", - "vta-sdk", + "vta-sdk 0.58.0", "vta-support", "vti-common", "zeroize", @@ -9940,9 +10007,9 @@ dependencies = [ [[package]] name = "vta-cli-common" -version = "0.26.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8913eacd5a9576bbff78ffdba799d8c2f4448a2bf591388f25061c46505211" +checksum = "b3050130fba832807542f91b798a64094961379ef9240647f8e66b696884b4c8" dependencies = [ "aes-gcm 0.11.1", "affinidi-crypto", @@ -9961,8 +10028,8 @@ dependencies = [ "sha2 0.11.0", "thiserror 2.0.21", "tokio", - "trust-tasks-rs", - "vta-sdk", + "trust-tasks-rs 0.25.1", + "vta-sdk 0.58.0", "vtc-client", "vti-common", "x25519-dalek 3.0.0", @@ -9970,9 +10037,9 @@ dependencies = [ [[package]] name = "vta-config" -version = "0.8.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256ce2dab072d52002bffd5d64409c8ecb6c9f5bfe1ecfc5efa88ca5045e5d0a" +checksum = "9f282b44b2b588a7ffbd929ab07bff07521cce3e88256712b7af9e22e7a82815" dependencies = [ "ipnetwork", "serde", @@ -9981,22 +10048,22 @@ dependencies = [ "sha2 0.11.0", "toml", "tracing", - "vta-sdk", + "vta-sdk 0.58.0", "vti-common", "vti-secrets", ] [[package]] name = "vta-keys" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd49be74b3b0bb7c086297eb4dda332cbe7485416e6898d20c5e8d0b3efeda3" +checksum = "ba93431cceac0786743f807470365ed4d690f5909747688b67ec9e7d7ad139eb" dependencies = [ "aes-gcm 0.11.1", "affinidi-crypto", - "affinidi-tdk", + "affinidi-tdk 0.21.0", "base64 0.23.1", - "bip39 2.2.2", + "bip39", "chrono", "ed25519-dalek 3.0.0", "hex", @@ -10013,7 +10080,7 @@ dependencies = [ "uuid", "vta-config", "vta-keyspaces", - "vta-sdk", + "vta-sdk 0.58.0", "vti-common", "vti-secrets", "x25519-dalek 3.0.0", @@ -10022,18 +10089,18 @@ dependencies = [ [[package]] name = "vta-keyspaces" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46c77874e46178750e6e84c21ba30a46323b6dce9df9a441d3b4e0e799ee399" +checksum = "0320d7fe991e110bf266846061dbee563c84135b7652f52d021c0504792d5034" dependencies = [ "vti-common", ] [[package]] name = "vta-persona" -version = "0.8.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6a0375666254e6533c7f0c0a9e31a47d6d0926ed5b1b1e064fd3958033fd50" +checksum = "bab1f7b6b6e3a44a3b50e9be6c965888a81140750e75810060e2ad1c53fc36d4" dependencies = [ "async-trait", "chrono", @@ -10051,9 +10118,9 @@ dependencies = [ [[package]] name = "vta-policy" -version = "0.3.25" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f15a1ed0b3fa79d1141cda1dfc4b2e9c44217e91775306642ce9ad8d1cc5b6f" +checksum = "04251bf48d9c8da3b14a71d7c782a759634f06f58f547ea2369c49042db890d7" dependencies = [ "hex", "regorus", @@ -10063,7 +10130,7 @@ dependencies = [ "tracing", "vta-config", "vta-keyspaces", - "vta-sdk", + "vta-sdk 0.58.0", "vti-common", ] @@ -10081,14 +10148,58 @@ dependencies = [ "affinidi-messaging-core", "affinidi-messaging-delivery", "affinidi-messaging-didcomm", - "affinidi-messaging-sdk", "affinidi-openid4vci", "affinidi-openid4vp", "affinidi-secrets-resolver", - "affinidi-status-list", - "affinidi-tdk", + "affinidi-tdk 0.20.0", "affinidi-vc", "agent-names", + "async-trait", + "base64 0.23.1", + "chrono", + "ciborium", + "curve25519-dalek 5.0.0", + "dirs", + "ed25519-dalek 3.0.0", + "futures-util", + "getrandom 0.4.3", + "hpke", + "multibase", + "reqwest", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror 2.0.21", + "tokio", + "tracing", + "trust-tasks-rs 0.24.3", + "url", + "uuid", + "x25519-dalek 3.0.0", + "zeroize", +] + +[[package]] +name = "vta-sdk" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe20a380910b57ef98ba11362a2d09ae0190962d0815165d3247979f27a8edaf" +dependencies = [ + "affinidi-crypto", + "affinidi-data-integrity", + "affinidi-did-common", + "affinidi-did-resolver-cache-sdk", + "affinidi-did-resolver-traits", + "affinidi-messaging-core", + "affinidi-messaging-delivery", + "affinidi-messaging-didcomm", + "affinidi-messaging-sdk 0.31.0", + "affinidi-openid4vci", + "affinidi-openid4vp", + "affinidi-secrets-resolver", + "affinidi-status-list", + "affinidi-tdk 0.21.0", + "affinidi-vc", "apple-native-keyring-store", "async-trait", "base64 0.23.1", @@ -10097,7 +10208,7 @@ dependencies = [ "curve25519-dalek 5.0.0", "dbus-secret-service-keyring-store", "dirs", - "dtg-credentials 0.11.0", + "dtg-credentials", "ed25519-dalek 3.0.0", "futures-util", "getrandom 0.4.3", @@ -10112,7 +10223,7 @@ dependencies = [ "thiserror 2.0.21", "tokio", "tracing", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", "trust-tasks-tsp", "url", "utoipa", @@ -10124,9 +10235,9 @@ dependencies = [ [[package]] name = "vta-service" -version = "0.45.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e9dc755d90c779e0b76c78d15754d31727b077f5636a8e6c53492d654161b" +checksum = "3195e1abd257f9b0fdfca2bd1842cd9c4fdc411ce68ab71c81b382f8dbc12667" dependencies = [ "aes-gcm 0.11.1", "affinidi-crypto", @@ -10142,21 +10253,21 @@ dependencies = [ "affinidi-sd-jwt-vc", "affinidi-secrets-resolver", "affinidi-status-list", - "affinidi-tdk", + "affinidi-tdk 0.21.0", "affinidi-tdk-common", "argon2 0.6.0", "async-trait", "axum", "axum-extra", "base64 0.23.1", - "bip39 2.2.2", + "bip39", "chrono", "ciborium", "clap", "dialoguer", "didwebvh-rs", "dirs", - "dtg-credentials 0.11.0", + "dtg-credentials", "ed25519-dalek 3.0.0", "fjall", "futures-util", @@ -10191,8 +10302,8 @@ dependencies = [ "tracing-subscriber", "trust-tasks-didcomm", "trust-tasks-https", - "trust-tasks-proof", - "trust-tasks-rs", + "trust-tasks-proof 0.25.1", + "trust-tasks-rs 0.25.1", "trust-tasks-tsp", "url", "urlencoding", @@ -10207,7 +10318,7 @@ dependencies = [ "vta-keyspaces", "vta-persona", "vta-policy", - "vta-sdk", + "vta-sdk 0.58.0", "vta-support", "vta-sweepers", "vta-vault", @@ -10225,9 +10336,9 @@ dependencies = [ [[package]] name = "vta-support" -version = "0.5.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c87ecc8f43e12c149f8c103791517a655b011b0b4cb04d972681d69fde1c8d3" +checksum = "8e38143ca0c895232bd284bf1867709e91d38f13223303cb2b291bff384dc38e" dependencies = [ "aes-gcm 0.11.1", "base64 0.23.1", @@ -10244,16 +10355,16 @@ dependencies = [ "tracing", "vta-config", "vta-keyspaces", - "vta-sdk", + "vta-sdk 0.58.0", "vti-common", "zeroize", ] [[package]] name = "vta-sweepers" -version = "0.3.16" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69813959b1b6b91efcaa23233d570a32b3ec7ff890be043566147f584cb3be79" +checksum = "45a22664452296463da5cb9e4c27df2ae0f159e025ed744d12d3f671ecacdde4" dependencies = [ "chrono", "serde_json", @@ -10266,9 +10377,9 @@ dependencies = [ [[package]] name = "vta-vault" -version = "0.6.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945155ca651d1df7a0899c7d60bbf16678dd4ed532d61c635b92db248f2c692b" +checksum = "db40745becce152f62b420769f90911387ae5c8a2dd5e4f32130725da6b89dc0" dependencies = [ "affinidi-crypto", "affinidi-data-integrity", @@ -10291,28 +10402,28 @@ dependencies = [ "tracing", "uuid", "vta-keyspaces", - "vta-sdk", + "vta-sdk 0.58.0", "vti-common", "x509-parser 0.18.1", ] [[package]] name = "vta-webvh" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ce43cc46cec71983ef37273d67430cfb14b048bc76a352a0c9428c576db541" +checksum = "11efcd87d829f1273071a406f525ed91a811a7b354aeb1b9cd00262dd8e74950" dependencies = [ "serde_json", "vta-keyspaces", - "vta-sdk", + "vta-sdk 0.58.0", "vti-common", ] [[package]] name = "vtc-client" -version = "0.8.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca90846684705011f7f065fd1f279791a63f70a1fd2c4be1e44d3a92841d32c" +checksum = "51054397ad3730f0aedd48016d6155f7a8ee02c73f4fe4500deb797cb9b1ec15" dependencies = [ "base64 0.23.1", "chrono", @@ -10321,16 +10432,16 @@ dependencies = [ "serde_json", "sha2 0.11.0", "thiserror 2.0.21", - "trust-tasks-rs", - "vta-sdk", + "trust-tasks-rs 0.25.1", + "vta-sdk 0.58.0", "vti-rooms", ] [[package]] name = "vti-common" -version = "0.29.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900b5af229ee4d29d45f2034ace8acb4ab24b7d42e30a61af2ffabb1036d75f6" +checksum = "7de80be8606913a7ef7512cd73b82254c8228fde14a19dd14a99dba587245e5a" dependencies = [ "aes-gcm 0.11.1", "affinidi-data-integrity", @@ -10338,7 +10449,7 @@ dependencies = [ "affinidi-messaging-core", "affinidi-messaging-delivery", "affinidi-secrets-resolver", - "affinidi-tdk", + "affinidi-tdk 0.21.0", "async-trait", "axum", "axum-extra", @@ -10367,21 +10478,21 @@ dependencies = [ "tower_governor", "tracing", "trust-tasks-capability-client", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", "url", "utoipa", "utoipa-axum", "uuid", - "vta-sdk", + "vta-sdk 0.58.0", "webauthn-rs", "zeroize", ] [[package]] name = "vti-rooms" -version = "0.2.22" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21fccc2cfe1121df37e1e8975b3039b085f730fa4fad6ca551b54127106cebe5" +checksum = "ab98aa2fa199ef30366e841808ed0ffa644e63eb23e37c5890e7c2b9803b5da5" dependencies = [ "async-trait", "base64 0.23.1", @@ -10400,22 +10511,22 @@ dependencies = [ "sha2 0.11.0", "thiserror 2.0.21", "tls_codec", - "trust-tasks-rs", + "trust-tasks-rs 0.25.1", "vti-common", ] [[package]] name = "vti-rooms-dtg" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42961cd07f7f7e2f5d8f2b77031d4177286e84d4deee1ac68fb9f36c95b1e09a" +checksum = "ce17ba91b450d0d365a94ab4b06c39e83c5fd48f891772f2a8782042d66d5df9" dependencies = [ "affinidi-data-integrity", "affinidi-secrets-resolver", "async-trait", "base64 0.23.1", "chrono", - "dtg-credentials 0.11.0", + "dtg-credentials", "serde_json", "tracing", "vti-common", @@ -10424,9 +10535,9 @@ dependencies = [ [[package]] name = "vti-secrets" -version = "0.4.8" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f599fbb84fc2545cfd5c54cc4051f58a258ce6d5ecbb9ebe65179fd23c2ab24c" +checksum = "e725b8f67b77b96995406b7c40b1186c1c8c0ae43000c71f58a3794aa33589eb" dependencies = [ "hex", "serde", From 2aa1ed6eedfd285d9d1e1555a5c7ae678a6fa366 Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Thu, 1 Oct 2026 03:07:54 +0200 Subject: [PATCH 8/9] fix(openvtc-core): resolve the dtg module doc links; take yoke-derive 0.8.4 - The outer doc comment on `pub mod dtg` made rustdoc resolve the module's inner docs from the crate root, where its own items are not in scope. dtg.rs carries its own //! docs, so the outer comment goes. - book.rs linked Self::clear_retired; the method is clear_retired_for. - yoke-derive 0.8.3 is yanked (cargo-deny). Signed-off-by: Glenn Gore --- Cargo.lock | 6 +++--- openvtc-core/src/lib.rs | 2 -- openvtc-core/src/vetting/book.rs | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index caf85fc8..7888b2cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2728,7 +2728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 3.0.6", + "syn 1.0.109", ] [[package]] @@ -11444,9 +11444,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", diff --git a/openvtc-core/src/lib.rs b/openvtc-core/src/lib.rs index 197d0278..c90a0fd3 100644 --- a/openvtc-core/src/lib.rs +++ b/openvtc-core/src/lib.rs @@ -30,8 +30,6 @@ pub mod devices; pub mod diagnostics; pub mod didcomm; pub mod display; -/// DTG Credentials conformance: issuer scopes, the conformance check stored and -/// received credentials are held to, and predicate-based statement display. pub mod dtg; pub mod errors; pub mod git_ns; diff --git a/openvtc-core/src/vetting/book.rs b/openvtc-core/src/vetting/book.rs index e1f2401a..f2f1ba11 100644 --- a/openvtc-core/src/vetting/book.rs +++ b/openvtc-core/src/vetting/book.rs @@ -458,7 +458,7 @@ pub struct VettingBook { pub hidden_published: std::collections::BTreeMap, /// What [`Self::retire_nonconformant`] set aside, one sentence each, for the /// Vetting page. Persisted so the notice survives the save that drops the - /// credentials, and cleared by the operator ([`Self::clear_retired`]). + /// credentials, and cleared by the operator ([`Self::clear_retired_for`]). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub retired: Vec, /// Questions put to communities and not yet answered. Memory only — see From 8ec0328bb9d149210bf5dddd127dcdc4bdbf8f1c Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Thu, 1 Oct 2026 03:53:26 +0200 Subject: [PATCH 9/9] chore(deps): did-git-sign 0.6 VGI 0.6.0 is on vta-sdk 0.58, affinidi-tdk 0.21 and trust-tasks 0.25, so the second registry copies of vta-sdk, trust-tasks-rs, affinidi-tdk and affinidi-messaging-sdk under did-git-sign 0.5.2 are gone. Signed-off-by: Glenn Gore --- Cargo.lock | 242 ++++++++++----------------------------------- Cargo.toml | 6 +- openvtc/Cargo.toml | 2 +- 3 files changed, 55 insertions(+), 195 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7888b2cc..49f19a31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,7 +377,7 @@ dependencies = [ "affinidi-messaging-didcomm", "affinidi-messaging-mediator-common", "affinidi-messaging-mediator-config", - "affinidi-messaging-sdk 0.31.0", + "affinidi-messaging-sdk", "affinidi-rate-limit", "affinidi-secrets-resolver", "affinidi-task-utils", @@ -422,8 +422,8 @@ dependencies = [ "tower-http 0.7.1", "tracing", "tracing-subscriber", - "trust-tasks-proof 0.25.1", - "trust-tasks-rs 0.25.1", + "trust-tasks-proof", + "trust-tasks-rs", "url", "uuid", ] @@ -480,42 +480,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "affinidi-messaging-sdk" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "017fb9f300ca4be022e5ff9ab7be5ea893d99c8d93fe2b670d2cd71ef1d96174" -dependencies = [ - "affinidi-crypto", - "affinidi-did-authentication", - "affinidi-did-common", - "affinidi-encoding", - "affinidi-messaging-core", - "affinidi-messaging-didcomm", - "affinidi-messaging-mediator-common", - "affinidi-secrets-resolver", - "affinidi-task-utils", - "affinidi-tdk-common", - "ahash", - "async-trait", - "base64 0.23.1", - "chrono", - "futures-util", - "rand 0.10.3", - "reqwest", - "rustls", - "serde", - "serde_json", - "sha256", - "thiserror 2.0.21", - "tokio", - "tokio-tungstenite 0.30.0", - "tracing", - "trust-tasks-proof 0.24.3", - "trust-tasks-rs 0.24.3", - "uuid", -] - [[package]] name = "affinidi-messaging-sdk" version = "0.31.0" @@ -550,8 +514,8 @@ dependencies = [ "tokio", "tokio-tungstenite 0.30.0", "tracing", - "trust-tasks-proof 0.25.1", - "trust-tasks-rs 0.25.1", + "trust-tasks-proof", + "trust-tasks-rs", "uuid", ] @@ -565,9 +529,9 @@ dependencies = [ "affinidi-messaging-didcomm", "affinidi-messaging-mediator", "affinidi-messaging-mediator-common", - "affinidi-messaging-sdk 0.31.0", + "affinidi-messaging-sdk", "affinidi-secrets-resolver", - "affinidi-tdk 0.21.0", + "affinidi-tdk", "async-trait", "ring", "rustls", @@ -746,31 +710,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "affinidi-tdk" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d48efd0ebd00b2609309755184dce82650f842e61583687cdb4fe45108e7678" -dependencies = [ - "affinidi-crypto", - "affinidi-data-integrity", - "affinidi-did-authentication", - "affinidi-did-common", - "affinidi-did-resolver-cache-sdk", - "affinidi-meeting-place", - "affinidi-messaging-didcomm", - "affinidi-messaging-sdk 0.30.0", - "affinidi-secrets-resolver", - "affinidi-tdk-common", - "clap", - "rustls", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", -] - [[package]] name = "affinidi-tdk" version = "0.21.0" @@ -784,7 +723,7 @@ dependencies = [ "affinidi-did-resolver-cache-sdk", "affinidi-meeting-place", "affinidi-messaging-didcomm", - "affinidi-messaging-sdk 0.31.0", + "affinidi-messaging-sdk", "affinidi-secrets-resolver", "affinidi-tdk-common", "affinidi-tsp", @@ -2728,7 +2667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 3.0.6", ] [[package]] @@ -2987,9 +2926,9 @@ dependencies = [ [[package]] name = "did-git-sign" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbf4f796b968ccf43d0864f14ef373bfd236ec09c32e7da289e604f6afa9985" +checksum = "4cfa022eea0bd35072691800f80cbf3b490a4ba0134262ae8e9e3e044e951c5f" dependencies = [ "anyhow", "apple-native-keyring-store", @@ -3010,8 +2949,9 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "url", "vgi-core", - "vta-sdk 0.56.0", + "vta-sdk", "windows-native-keyring-store", "zeroize", ] @@ -6331,7 +6271,7 @@ version = "0.3.1" dependencies = [ "aes-gcm 0.11.1", "affinidi-data-integrity", - "affinidi-tdk 0.21.0", + "affinidi-tdk", "anyhow", "apple-native-keyring-store", "arboard", @@ -6380,7 +6320,7 @@ dependencies = [ "tui-input", "url", "uuid", - "vta-sdk 0.58.0", + "vta-sdk", "windows-native-keyring-store", "x25519-dalek 2.0.1", "zeroize", @@ -6396,11 +6336,11 @@ dependencies = [ "affinidi-did-web", "affinidi-messaging-core", "affinidi-messaging-delivery", - "affinidi-messaging-sdk 0.31.0", + "affinidi-messaging-sdk", "affinidi-messaging-test-mediator", "affinidi-net-guard", "affinidi-status-list", - "affinidi-tdk 0.21.0", + "affinidi-tdk", "agent-names", "anyhow", "arbitrary", @@ -6441,10 +6381,10 @@ dependencies = [ "tracing", "tracing-subscriber", "trust-tasks-capability-client", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", "url", "uuid", - "vta-sdk 0.58.0", + "vta-sdk", "vta-service", "wiremock", "x25519-dalek 2.0.1", @@ -6469,7 +6409,7 @@ dependencies = [ "serde_json_canonicalizer", "sha2 0.11.0", "thiserror 2.0.21", - "vta-sdk 0.58.0", + "vta-sdk", ] [[package]] @@ -9446,7 +9386,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", "uuid", ] @@ -9462,7 +9402,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", "uuid", ] @@ -9481,28 +9421,10 @@ dependencies = [ "thiserror 2.0.21", "tokio", "tower", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", "uuid", ] -[[package]] -name = "trust-tasks-proof" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6565a2fa15a568b86aba0317226c9cd9e13a37377bc77c762335363b6edf14f" -dependencies = [ - "affinidi-crypto", - "affinidi-data-integrity", - "affinidi-did-common", - "affinidi-did-resolver-cache-sdk", - "affinidi-encoding", - "async-trait", - "serde", - "serde_json", - "thiserror 2.0.21", - "trust-tasks-rs 0.24.3", -] - [[package]] name = "trust-tasks-proof" version = "0.25.1" @@ -9518,23 +9440,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs 0.25.1", -] - -[[package]] -name = "trust-tasks-rs" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcecb4f138b7833f0e81515272f6a9b7fdc1f94fe7f8d680c1ebcf23c6ba9b2f" -dependencies = [ - "async-trait", - "chrono", - "jsonschema 0.46.10", - "regress", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.21", + "trust-tasks-rs", ] [[package]] @@ -9564,7 +9470,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.21", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", ] [[package]] @@ -9928,9 +9834,9 @@ dependencies = [ [[package]] name = "vgi-core" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2823159ce23c587607006cb2d6af800e9d1a2a5bebc96d77f16af24eef4870b8" +checksum = "e1d5013a26a3831c027812cc0189cccefeabe53f3f9af46c7a9aa586bc102600" dependencies = [ "anyhow", "base64 0.23.1", @@ -9972,7 +9878,7 @@ dependencies = [ "tokio", "tracing", "uuid", - "vta-sdk 0.58.0", + "vta-sdk", "vti-common", ] @@ -9999,7 +9905,7 @@ dependencies = [ "vta-config", "vta-keys", "vta-keyspaces", - "vta-sdk 0.58.0", + "vta-sdk", "vta-support", "vti-common", "zeroize", @@ -10028,8 +9934,8 @@ dependencies = [ "sha2 0.11.0", "thiserror 2.0.21", "tokio", - "trust-tasks-rs 0.25.1", - "vta-sdk 0.58.0", + "trust-tasks-rs", + "vta-sdk", "vtc-client", "vti-common", "x25519-dalek 3.0.0", @@ -10048,7 +9954,7 @@ dependencies = [ "sha2 0.11.0", "toml", "tracing", - "vta-sdk 0.58.0", + "vta-sdk", "vti-common", "vti-secrets", ] @@ -10061,7 +9967,7 @@ checksum = "ba93431cceac0786743f807470365ed4d690f5909747688b67ec9e7d7ad139eb" dependencies = [ "aes-gcm 0.11.1", "affinidi-crypto", - "affinidi-tdk 0.21.0", + "affinidi-tdk", "base64 0.23.1", "bip39", "chrono", @@ -10080,7 +9986,7 @@ dependencies = [ "uuid", "vta-config", "vta-keyspaces", - "vta-sdk 0.58.0", + "vta-sdk", "vti-common", "vti-secrets", "x25519-dalek 3.0.0", @@ -10130,55 +10036,10 @@ dependencies = [ "tracing", "vta-config", "vta-keyspaces", - "vta-sdk 0.58.0", + "vta-sdk", "vti-common", ] -[[package]] -name = "vta-sdk" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9d67c68164ae9397dcabd8c6c61ed7ecdc37b1ed513e8526829ef7d2dbdc8a" -dependencies = [ - "affinidi-crypto", - "affinidi-data-integrity", - "affinidi-did-common", - "affinidi-did-resolver-cache-sdk", - "affinidi-did-resolver-traits", - "affinidi-messaging-core", - "affinidi-messaging-delivery", - "affinidi-messaging-didcomm", - "affinidi-openid4vci", - "affinidi-openid4vp", - "affinidi-secrets-resolver", - "affinidi-tdk 0.20.0", - "affinidi-vc", - "agent-names", - "async-trait", - "base64 0.23.1", - "chrono", - "ciborium", - "curve25519-dalek 5.0.0", - "dirs", - "ed25519-dalek 3.0.0", - "futures-util", - "getrandom 0.4.3", - "hpke", - "multibase", - "reqwest", - "serde", - "serde_json", - "sha2 0.11.0", - "thiserror 2.0.21", - "tokio", - "tracing", - "trust-tasks-rs 0.24.3", - "url", - "uuid", - "x25519-dalek 3.0.0", - "zeroize", -] - [[package]] name = "vta-sdk" version = "0.58.0" @@ -10193,13 +10054,14 @@ dependencies = [ "affinidi-messaging-core", "affinidi-messaging-delivery", "affinidi-messaging-didcomm", - "affinidi-messaging-sdk 0.31.0", + "affinidi-messaging-sdk", "affinidi-openid4vci", "affinidi-openid4vp", "affinidi-secrets-resolver", "affinidi-status-list", - "affinidi-tdk 0.21.0", + "affinidi-tdk", "affinidi-vc", + "agent-names", "apple-native-keyring-store", "async-trait", "base64 0.23.1", @@ -10223,7 +10085,7 @@ dependencies = [ "thiserror 2.0.21", "tokio", "tracing", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", "trust-tasks-tsp", "url", "utoipa", @@ -10253,7 +10115,7 @@ dependencies = [ "affinidi-sd-jwt-vc", "affinidi-secrets-resolver", "affinidi-status-list", - "affinidi-tdk 0.21.0", + "affinidi-tdk", "affinidi-tdk-common", "argon2 0.6.0", "async-trait", @@ -10302,8 +10164,8 @@ dependencies = [ "tracing-subscriber", "trust-tasks-didcomm", "trust-tasks-https", - "trust-tasks-proof 0.25.1", - "trust-tasks-rs 0.25.1", + "trust-tasks-proof", + "trust-tasks-rs", "trust-tasks-tsp", "url", "urlencoding", @@ -10318,7 +10180,7 @@ dependencies = [ "vta-keyspaces", "vta-persona", "vta-policy", - "vta-sdk 0.58.0", + "vta-sdk", "vta-support", "vta-sweepers", "vta-vault", @@ -10355,7 +10217,7 @@ dependencies = [ "tracing", "vta-config", "vta-keyspaces", - "vta-sdk 0.58.0", + "vta-sdk", "vti-common", "zeroize", ] @@ -10402,7 +10264,7 @@ dependencies = [ "tracing", "uuid", "vta-keyspaces", - "vta-sdk 0.58.0", + "vta-sdk", "vti-common", "x509-parser 0.18.1", ] @@ -10415,7 +10277,7 @@ checksum = "11efcd87d829f1273071a406f525ed91a811a7b354aeb1b9cd00262dd8e74950" dependencies = [ "serde_json", "vta-keyspaces", - "vta-sdk 0.58.0", + "vta-sdk", "vti-common", ] @@ -10432,8 +10294,8 @@ dependencies = [ "serde_json", "sha2 0.11.0", "thiserror 2.0.21", - "trust-tasks-rs 0.25.1", - "vta-sdk 0.58.0", + "trust-tasks-rs", + "vta-sdk", "vti-rooms", ] @@ -10449,7 +10311,7 @@ dependencies = [ "affinidi-messaging-core", "affinidi-messaging-delivery", "affinidi-secrets-resolver", - "affinidi-tdk 0.21.0", + "affinidi-tdk", "async-trait", "axum", "axum-extra", @@ -10478,12 +10340,12 @@ dependencies = [ "tower_governor", "tracing", "trust-tasks-capability-client", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", "url", "utoipa", "utoipa-axum", "uuid", - "vta-sdk 0.58.0", + "vta-sdk", "webauthn-rs", "zeroize", ] @@ -10511,7 +10373,7 @@ dependencies = [ "sha2 0.11.0", "thiserror 2.0.21", "tls_codec", - "trust-tasks-rs 0.25.1", + "trust-tasks-rs", "vti-common", ] diff --git a/Cargo.toml b/Cargo.toml index d5702916..96168f5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -541,10 +541,8 @@ uuid = { version = "1.23", features = ["v4", "fast-rng", "serde"] } # `issuer_scope`, `value`), role credentials are community VACs # (`eligibility::community_roles`, `role_action`), and `role_vec` is `role_vac`. # -# `did-git-sign` 0.5.2 still requires `vta-sdk ^0.56`, so until VGI releases on -# 0.57 the graph holds a second, registry `vta-sdk` beneath it. It builds — -# `did_git_sign::{config, init}` passes no sdk type across — but `cargo tree -d` -# shows it, and it closes only with a VGI release. +# `did-git-sign` 0.6.0 (VGI 0.6.0) is on `vta-sdk` 0.58, `affinidi-tdk` 0.21 and +# `trust-tasks` 0.25, so the graph holds one copy of each again. # # `vta-service` is a separate matter this cycle: see the `vta-service` note in # `openvtc-core/Cargo.toml` for why that pin stays at 0.43.0 rather than diff --git a/openvtc/Cargo.toml b/openvtc/Cargo.toml index 6b098469..95f9af35 100644 --- a/openvtc/Cargo.toml +++ b/openvtc/Cargo.toml @@ -92,7 +92,7 @@ anyhow.workspace = true # `COMMIT_MSG_HOOK_VERSION` (verifiable-git-infrastructure #72), which the Repos # panel's signing-health row reads rather than re-implementing the hook's # version marker. Same `vta-sdk` 0.51 line as 0.4.13. -did-git-sign = "0.5.2" +did-git-sign = "0.6" base64.workspace = true byteorder.workspace = true chrono.workspace = true