Skip to content

feat!: conform to the v1 context — issuerScope, StatementCredential, predicate acceptance - #33

Merged
stormer78 merged 3 commits into
mainfrom
feat/vsc-issuer-scope-context
Sep 30, 2026
Merged

stormer78 merged 3 commits into
mainfrom
feat/vsc-issuer-scope-context

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

This brings the crate into line with the current DTG Credentials Core Specification (Working Draft 0.6.0) and the frozen https://registry.trustoverip.org/dtg/context/v1 context. It is a clean break: a 0.12 verifier refuses every credential a 0.11 issuer emits, and the reverse. The spec treats credentials issued before the Implementers Draft as non-conformant, so there is no legacy alias.

It is a draft until the VTI and OpenVTC migrations build and pass against it. The release PR (0.12.0) follows once they do.

What changes

  • Context: https://registry.trustoverip.org/dtg/context/v1 replaces firstperson.network/credentials/dtg/v1.
  • issuerScope is required on every credential, as IssuerScope { Pairwise, Directed, Public }.
    • Every constructor takes it.
    • The community-issued VMC is always public.
    • new_member_vmc_for refuses a grant that isn't public.
  • StatementCredential (VSC) carries predicate and object, where the object is one of id, digestMultibase or value.
    • Profile helpers: new_endorses_vsc, new_witnessed_vsc (replaces new_vwc_for_session, with subject = issuer of the witnessed credential), new_vetted_vsc and new_presented_vsc.
    • Constants for the registry IRIs.
    • Each profile's object kind, taskContext requirement and minimum issuerScope are enforced.
  • PredicateAcceptList is a fail-closed verifier helper. It matches predicates byte-exactly and loads from the registry's accept-list.json, filtered by status.
  • VAC: maxAttenuation was missing; spec-conformant VACs failed to parse without it. It is now parsed and enforced along the chain.
    • new_community_role_vac builds a role grant as authority.actions: ["role:<name>"], which replaces CommunityRole endorsements.
  • Removed:
    • EndorsementCredential, WitnessCredential and RCardCredential. The r-card is a verifiable data structure, not a DTG credential.
    • new_vec, new_vwc*, new_rcard, the deprecated new_member_vmc and new_delegate_vdc, and Default for DTGCommon.

The CHANGELOG has a before/after migration table covering every call site change.

Related

Checks

  • cargo fmt --check is clean.
  • cargo clippy --all-targets --all-features -- -D warnings is clean, also with --no-default-features.
  • cargo doc -D warnings is clean.
  • cargo test --all-features: 212 passed.

…predicate acceptance (0.12.0)

DTG Core Credentials (Working Draft 0.6.0) moves every credential onto the frozen
https://registry.trustoverip.org/dtg/context/v1, makes issuerScope REQUIRED, and
replaces the EndorsementCredential and WitnessCredential subtypes with one
StatementCredential whose meaning is its predicate. Credentials issued before the
Implementers Draft are not conformant, so this is a clean wire break with no
legacy alias.

- Context: the W3C context first and DTG_CONTEXT_V1 second, exact strings; the
  firstperson.network context is refused. `type` must hold VerifiableCredential,
  DTGCredential and exactly one concrete subtype (PersonhoodCredential only as a
  hint on a VMC). The WD01 `digest` alias is gone.
- IssuerScope { Pairwise, Directed, Public }: REQUIRED top-level member, taken by
  every constructor; the community-issued VMC is always `public`, enforced at
  parse and in validate().
- StatementCredential: CredentialSubjectStatement { id, predicate, object,
  witnessContext? }, StatementObject { Id, DigestMultibase, Value }, predicate IRI
  validation (absolute, no CURIEs, NFC). new_vsc plus profile constructors for
  endorses/1, witnessed/1 (ports new_vwc_for_session, reading the subject and
  digest from the edge credential), vetted/1 and presented/1; core profile
  constraints (object kind, taskContext + taskDigestMultibase, minimum
  issuerScope) enforced at parse, validate and sign; subject-object checks
  witnesses_issuance_of / witnesses_presentation_of.
- PredicateAcceptList: fail-closed exact-match acceptance, from IRIs or from the
  registry's accept-list.json with a status floor, applying entry constraints.
- VAC: authority.maxAttenuation (a spec-conformant VAC carrying it previously did
  not parse), enforced at attenuation and in verify_chain per link and per
  ancestor; new_community_role_vac for role grants as `role:<name>` actions.
- Removed: EndorsementCredential, WitnessCredential, RCardCredential and their
  constructors and subject structs; the deprecated new_member_vmc and
  new_delegate_vdc; Default for DTGCommon.
- validate() re-runs the parse checks, so a credential mutated out of shape is
  never signed.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The registry stopped tagging releases, so accept-list.json is dropping
revision and verifiers pin commit or the list digest instead. Read the
envelope leniently (build metadata is not a constraint) and keep predicate
entries strict, where an unknown member may be a constraint to fail closed on.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78

Copy link
Copy Markdown
Contributor Author

Re the registry cleanup that drops revision from accept-list.json (trustoverip/dtgwg-vsc-registry#14): 9e1531d removes revision from RegistryAcceptList. The envelope is now read leniently, since build metadata isn't a constraint; commit is what a verifier pins.

Predicate entries stay strict, so an unknown member there still fails closed. The crate therefore loads the list both before and after the cleanup, and the two PRs can land in either order.

@stormer78

Copy link
Copy Markdown
Contributor Author

Taken out of draft. OpenVTC/openvtc#397 builds and passes its full suite (1722 tests) against this branch, and OpenVTC/verifiable-trust-infrastructure#1859 is migrated onto it. The release order is:

  1. This PR and spec!: conform vetting, member and endorsement credentials to DTG VSC/VAC and the predicate registry trustoverip/dtgwg-trust-tasks-tf#691 merge and publish.
  2. VTI drops its local-path patch and releases vta-sdk.
  3. OpenVTC follows.

stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 30, 2026
…worktrees

Builds the workspace against the unreleased dtg-credentials 0.12
(OpenVTC/dtg-credentials#33, local worktree) and the trust-tasks crates
regenerated from the DTG VSC conformance specs
(trustoverip/dtgwg-trust-tasks-tf feat/dtg-vsc-conformance, local
worktree): trust-tasks-rs, -capability-client, -https, -didcomm, -tsp and
-proof, with the lockfile moved onto the patched 0.24.8 copies.

Drop this commit once both are released, and raise the workspace's
`dtg-credentials` requirement to the released 0.12 (the unreleased
worktree still carries version 0.11.0, which is why the requirement is
left at "0.11" here).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 3d84d36 into main Sep 30, 2026
7 checks passed
@stormer78
stormer78 deleted the feat/vsc-issuer-scope-context branch September 30, 2026 16:28
@stormer78 stormer78 mentioned this pull request Sep 30, 2026
stormer78 added a commit that referenced this pull request Sep 30, 2026
Cuts 0.12.0: conformance to the DTG Credentials Core Specification WD 0.6.0
and its frozen v1 context (#33).

- Cargo.toml: 0.11.0 -> 0.12.0
- CHANGELOG.md: [Unreleased] moves under [0.12.0] - 2026-09-30

No code changes.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 30, 2026
…ing 0.31

Replaces the local-path patch with the released crates:
- dtg-credentials 0.12 (OpenVTC/dtg-credentials#33)
- the trust-tasks-* family at 0.25 (trustoverip/dtgwg-trust-tasks-tf#691)
- affinidi-tdk 0.21, affinidi-messaging-sdk 0.31, -mediator-admin 0.6 and
  -test-mediator 0.15 (affinidi/affinidi-tdk-rs#908), which move to
  trust-tasks 0.25 so the graph holds one copy

Cargo.lock follows once affinidi-tdk-rs#908 publishes.
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 30, 2026
…ments, IDVCs, issuerScope (#1859)

* feat(vta-sdk)!: vetting statements are vetted/1 VSCs; role credentials are VACs

Conform the SDK's vetting artifacts to the DTG Credentials Core
Specification (v1 context, `issuerScope`) and the DTG VSC predicate
registry, following the regenerated vetting specifications
(dtgwg-trust-tasks-tf feat/dtg-vsc-conformance).

Vetting Statement (vetting/session/0.1): no longer an
EndorsementCredential. `sign_statement` builds a StatementCredential with
`new_vetted_vsc` under `https://registry.trustoverip.org/dtg/vsc/vetted/1`;
the body is `credentialSubject.object.value`, with no `type` member.
`taskContext` and `taskDigestMultibase` are both read from the
`vetting/session` document, which `StatementDraft::session` now carries
in place of `task_context`, and `StatementDraft::issuer_scope` is
`directed` or `public` (pairwise is refused by the profile).
`verify_statement` parses through `dtg-credentials`, so the v1 context,
the one-subtype rule and the profile are checked by the code that issues
them; `VerifiedVettingStatement::check_against_session` binds a statement
to the session document by id and task digest.

- `IdentityVettingEndorsement` -> `VettedObjectValue` (no `type`; digests
  and commitment must be base58btc, as the registry schema requires).
- `IDENTITY_VETTING_ENDORSEMENT_TYPE` -> `VETTED_PREDICATE`.
- `VerifiedVettingStatement::endorsement()` -> `value()`; new
  `issuer_scope()`, `task_digest_multibase()`.

Vetter role credential (vtc/vetting/vetters/grant/0.1, vetting/request/0.1):
a community-issued VAC, `issuerScope` public, `authority` { scope:
<community DID>, actions: ["role:vetter"], maxAttenuation: 0 }.
`eligibility::community_role` -> `community_roles`, returning every
`role:<name>` of a VAC the community issued in its own scope with no
parent; `verify_eligibility_vp` parses the VAC strictly and refuses a
non-public scope or an attenuation.

- `COMMUNITY_ROLE_ENDORSEMENT_TYPE` removed; new `ROLE_ACTION_PREFIX`,
  `VETTER_ROLE_ACTION`, `role_action`, `role_of_action`.
- `protocols::members::ENDORSEMENT_CREDENTIAL_TYPE` removed; new
  `AUTHORITY_CREDENTIAL_TYPE` and `STATEMENT_CREDENTIAL_TYPE`.
- `VerdictWith::role_vec` -> `role_vac` (wire `roleVac`).

vtc-client: `role_vec` -> `role_vac` on `DecideResult`, `MemberRenewal`,
`MemberRotated` and `PersonhoodRevocation`, matching the `roleVac`
rename in vtc/join-requests/decide, vtc/members/{renew,rotate} and
vtc/members/personhood/*.

vti-common: `MembershipRenewedData::role_vec_id` and
`DidRotatedData::role_vec_id` -> `role_vac_id` (wire `roleVacId`,
reading `roleVecId` on older rows). The `VecIssued` envelope keeps its
name, which SIEM rules key on; it now records role VACs and statement
VSCs, named by `credentialType`.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* feat(vta-service)!: room credentials declare issuerScope under the DTG v1 context

Adopt dtg-credentials 0.12 across the room surfaces — vta-service's
`rooms/owner/*` handlers and room oracle, vti-rooms-dtg and
vti-rooms-wasm. Every DTG credential they mint now carries the v1
context (`https://registry.trustoverip.org/dtg/context/v1`) and a
top-level `issuerScope`, chosen for what the identifier truthfully is:

- `public` where a room issues as itself — the invitation (VIC) from
  `rooms/owner/invite` and the root VAC from `rooms/owner/issue-authority`.
  The room's DID is the scope every verifier of a room credential
  already resolves.
- `directed` for every attenuation — a member narrowing a room grant to
  a presentation leaf (`room_oracle::present`, vti-rooms-wasm
  `MemberIdentity::present`). The member issues under the DID the room
  admitted: one identifier recognised by the room, its host and the
  agent, neither per-counterparty nor a public identity.

Attenuations pass `max_attenuation: None`, taking the library default
under the parent's bound.

No room code used the retired endorsement, witness or r-card types. Two
test fixtures carried the old endorsement type name as a plain string:
an SD-JWT `vct` in vta-vault (now `SkillEndorsement`) and an opaque vault
entry in vta-service's persona tests (now `StatementCredential`).

Wire change, no API change: a VIC or root VAC from `rooms/owner/*`, and a
presentation leaf from the room oracle or the browser, now carry
`issuerScope` and the v1 context, and 0.11 verifiers refuse them.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* feat(vtc-service)!: issue and verify DTG v1 credentials — role VACs, statement VSCs, IDVCs

Every DTG credential the VTC issues, verifies or documents now conforms to
the DTG Credentials Core Specification's v1 context and the DTG VSC
predicate registry, following the regenerated Trust Tasks
(dtgwg-trust-tasks-tf feat/dtg-vsc-conformance). Built on
dtg-credentials 0.12.

Common structure (credentials::ingress::classify_dtg): `@context` is
exactly `[credentials/v2, https://registry.trustoverip.org/dtg/context/v1]`
in that order (the firstperson.network context is refused, no alias),
`issuerScope` is REQUIRED, `type` carries exactly one concrete subtype
(the library's rule, so retired types are refused by name), and a
statement is held to its predicate profile. Everything the VTC mints is
built through the catalog and validated before signing; the community
always declares `issuerScope` `public`.

- VIC: `issuerScope: public`.
- VMC grant: `public` (fixed by the catalog). Member acknowledgement
  (members/inbound_vmc): any declared scope accepted; the stored grant it
  binds to must be `public`; the Working Draft 01 `digest` spelling is no
  longer read.
- Role grants (credentials::vac, was vec): a VAC from
  `new_community_role_vac` — `authority` { scope: community DID, actions:
  ["role:<VtcRole wire>"], maxAttenuation: 0 }. The vetter grant is
  `role:vetter` (vtc/vetting/vetters/grant/0.1), recorded as an
  endorsement row of type `role:vetter` so vtc/endorsements/revoke keeps
  revoking it. `CommunityRole` is gone. Cross-community recognition
  (vtc/auth/recognise/0.2) takes a role VAC + VMC, requires scope ==
  issuer and no parent, reads the role from the first `role:` action, and
  hands `cross_community_roles.rego` `input.foreign_vac` (was
  `foreign_vec`).
- Vetting Statements: counted only as `vetted/1` VSCs verified by
  vta-sdk, picked by predicate, and only under a predicate the community
  accepts (new failure `predicate-not-accepted`); eligibility reads the
  `role:vetter` grant rows.
- Witness (credentials::witness): a VWC is a `witnessed/1` VSC,
  classified by predicate, digest read from `object.digestMultibase`;
  a digest naming an edge whose issuer is not the statement's subject is
  the new binding state `subjectMismatch`. `taskContext` requirement
  derives from the predicate profile (task_context::
  requirement_for_predicate). The ceremony `Credential` fact gains
  `predicate`, and DI credentials report their concrete subtype as
  `type` instead of `DTGCredential`.
- Identity verification (credentials::idvc): a plain W3C VC,
  `["VerifiableCredential", "IdentityVerificationCredential"]`, v2
  context only, issued by the community on its revocation list. Issued
  through vtc/endorsements/issue/0.1 with the reserved typeUri
  `IdentityVerificationCredential` — no registration — and revoked like
  any row. That reuse diverges from the task's "mints a VSC" text; it is
  recorded in docs/03-vtc/personhood-and-graph.md pending a dedicated
  task. personhood.rego accepts it when issuer is the community and the
  subject the applicant; its witness rule matches the `witnessed/1`
  predicate. A workspace-authored personhood default that refuses a
  bound v1 witness, or admits an unbound one, is upgraded at boot
  (policy::default::upgrade_stale_personhood_default).
- Endorsement types → predicate accept list: a registered `typeUri` must
  be a predicate IRI (`invalidUri` otherwise); `role:vetter` and the IDVC
  type are reserved. The four core registry predicates are seeded once at
  boot (a marker keeps deletions deleted). endorsement_types::accept_list
  builds a fail-closed `PredicateAcceptList`, applied to vetting
  statements, join presentations and personhood assertions.
  vtc/endorsements/issue/0.1 mints a VSC (predicate = typeUri,
  object.value = claim, validated against the claimSchema) and refuses a
  predicate whose profile needs `taskContext` with `predicateNotIssuable`.
  Schema-registry defaults are Membership/Authority/Statement/Invitation
  plus the IDVC.
- Relationships: VRCs and VPCs must declare `issuerScope`; the Rego
  `identifier_form` is read from it (pairwise → pairwise, directed |
  public → attributed) and the raw `issuer_scope` is passed too. A
  `pairwise` VRC under the caller's membership DID is refused.
- Renames on the wire and in storage: `roleVec` → `roleVac` (join
  decide, members renew/rotate/personhood, ceremony verdicts),
  `currentRoleVecId`/`currentRoleVec` → `currentRoleVacId`/
  `currentRoleVac`, endorsement row `vecId` → `credentialId`; stored rows
  keep reading under the old names.

Also: admin-ui (vetting predicate, `role:vetter` rows, a
`holds_trusted_statement` rule condition and `statement_trusted` helper,
openapi.json + wire.ts regenerated); `cnm member endorse` help;
vti-vetting-pcs fixture statementType; pseudonym extraction reads only
`credentialSubject.pseudonym`.

Wire break with no upgrade ordering: 0.11-era credentials are refused and
existing role grants, statements and vetter grants must be re-issued.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* docs: describe the DTG v1 credential shapes the VTI issues and verifies

Rewrite the operator and design docs, and the local trust-tasks folder,
for the conformance change: role credentials are VACs (`role:<name>`,
maxAttenuation 0), Vetting Statements are `vetted/1` VSCs, witnesses are
`witnessed/1` VSCs classified by predicate, the identity-verification
credential is a plain W3C VC issued through the reserved
`IdentityVerificationCredential` typeUri (recorded as a divergence from
vtc/endorsements/issue/0.1), endorsement types are the community's
fail-closed predicate accept list, and a VRC's `issuerScope` sets the
Rego `identifier_form` (pairwise → pairwise, directed | public →
attributed). The ceremony rule-IR docs and examples gain the `predicate`
fact and `statement_trusted(p)`.

trust-tasks/: recognise/1.0 describes the role VAC + VMC instead of the
`Verifiable`-prefixed names; join-requests/accept/1.0 replaces
`MembershipAcknowledgement` with the member-issued VMC; `roleVec` →
`roleVac`; relationships 1.0 `supersededBy` now names the 0.2 versions the
Rust serves.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* fix(vta-sdk): community_roles parses a role VAC as strictly as verify_eligibility_vp

`vetting::eligibility::community_roles` read the raw JSON while
`verify_eligibility_vp` parses through dtg-credentials, so a wallet
could pick a candidate the verifier then refused as malformed. It now
parses the credential with the same strict DTG parse (v1 context, one
subtype, declared issuerScope) before reading `authority`. Signature
unchanged; credentials that are not conformant DTG VACs now yield None.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* test(vtc-service): join fixtures carry DTG v1 member VMCs and witnessed/1 statements

Member-issued VMC fixtures gain the v1 context, `DTGCredential` and
`issuerScope: directed`; the SD-JWT witness fixture is a `witnessed/1`
statement (vct `StatementCredential`, predicate + object claims); the
vetter-grant assertion checks the VAC authority. The vti-vetting-pcs
cross-repo fixture is the one OpenVTC regenerated for the vetted/1
statementType (its requirementsDigest moved with it).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* test(vtc-service): the last join fixture names StatementCredential

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* fix(vtc-service): an IDVC claim naming id is the declared claimSchemaViolation

vtc/endorsements/issue/0.1 declares that refusal as claimSchemaViolation;
it was a generic validation error.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* chore(deps)!: dtg-credentials 0.12, trust-tasks 0.25, affinidi-messaging 0.31

Replaces the local-path patch with the released crates:
- dtg-credentials 0.12 (OpenVTC/dtg-credentials#33)
- the trust-tasks-* family at 0.25 (trustoverip/dtgwg-trust-tasks-tf#691)
- affinidi-tdk 0.21, affinidi-messaging-sdk 0.31, -mediator-admin 0.6 and
  -test-mediator 0.15 (affinidi/affinidi-tdk-rs#908), which move to
  trust-tasks 0.25 so the graph holds one copy

Cargo.lock follows once affinidi-tdk-rs#908 publishes.

* chore(deps): Cargo.lock on the released crates; pnm-cli on mediator-tui 0.6

One copy each of trust-tasks-rs, trust-tasks-proof, affinidi-messaging-sdk,
affinidi-tdk, dtg-credentials and vta-sdk in the graph. mediator-tui 0.5
pulled the old affinidi-messaging-sdk 0.30.

---------

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant