Skip to content

feat!: a VAC is not a bearer credential; remove audience - #21

Merged
stormer78 merged 1 commit into
mainfrom
feat/vac-presenter-binding
Sep 9, 2026
Merged

stormer78 merged 1 commit into
mainfrom
feat/vac-presenter-binding

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Closes the credential-library half of
dtgwg-trust-tasks-tf#414.

The hole

authority::verify_chain took a presenter and used it for exactly one thing: comparing
it against the leaf's OPTIONAL audience. So a leaf with no audience was accepted
from anybody. A chain names what may be done, not who is doing it, and without a
presenter rule a captured presentation is a bearer token — every verifier a distribution
point for the authority it was just shown.

Both known consumers had already found this and written the missing rule by hand, each with
a comment explaining why they had to:

  • vti-rooms-dtg/src/lib.rs"verify_chain takes presenter but uses it for one
    thing: the audience check… Which makes it ours, and it is not optional: without it a
    presentation is a bearer token, and anyone who observes one inherits everything it
    confers."
  • vti-rooms-dtg/src/nomination.rs — the same paragraph again, for nominations.

Two copies of a security check is one place for it to be forgotten. It moves here.

The rule

verify_chain now requires the leaf to grant to presenter, refusing with the new
AuthorityError::NotThePresenter. This is
dtgwg-cred-spec#41 verbatim: a
verifier MUST NOT accept a party as holding the authority a VAC confers unless that party
demonstrates control of the verification method associated with the presented VAC's
credentialSubject.id.

Only the leaf's subject demonstrates anything. The links above it are not present and
are asked for nothing — requiring otherwise would defeat attenuation, whose whole purpose
is that the party who attenuated is not in the loop when its agent acts.

presenter is a precondition, not an input. It must be an identifier whose key control
the caller already established for that request — a DID the transport authenticated, or one
a signature over the request proved. Passing a value read out of the request body reduces
this to a string comparison an attacker picks both sides of. Said in the module docs and on
the parameter.

Removing audience

Once the presenter must be the subject, an audience can only name that same subject
(adding nothing) or name someone else (satisfiable by no presentation). PR #41 removes it
for exactly that reason; this removes it rather than keeping it as a weaker second check —
gone from AuthorityGrant, from attenuate/attenuate_from_json, and from the verifier
with WrongAudience.

The field was also being read two incompatible ways, which is what brought this forward
rather than waiting for #41 to merge. This library compared audience to the presenter;
trusttasks.org/spec/rooms/keys/present/0.1 described it as the party the presentation is
for"a host's identifier, normally". An implementation following the registry minted
leaves this verifier refused every time; omitting it skipped the check entirely. Broken one
way, unprotected the other.

The destination question the registry was reaching for is real. It is answered one layer up,
by the trust task document's recipient member, which its proof covers and which
SPEC.md §4.8.2 already requires whenever a proof is present. Naming an intended verifier
inside the credential would restrict where a presentation may be made, which is the task's
question and not the credential's.

Migrating

  • Drop the trailing argument from attenuate / attenuate_from_json.
    • Passing the agent's DID? It was already redundant with subject.
    • Passing a host's or verifier's identifier? That binding belongs on the request
      document now.
  • Callers that compare the leaf's subject to the presenter themselves can delete that check.

Deliberately not in this PR

delegation::verify_chain takes no presenter at all, so the VDC's Invocation Binding
— normative in merged WD02, same rule, same reasoning — is unimplemented. It is the same
class of gap and deserves the same fix, but the VDC has no audience and so is not part of
this issue. Filing separately.

Revocation (#39) and maxAttenuation (#40) remain unimplemented, as before.

Verified

  • cargo test — 117 passed, 0 failed
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo fmt, cargo doc --no-deps — clean
  • cargo run --example data_room — runs end to end

New tests: a chain presented by a non-subject is refused; a root presented by a
non-subject is refused (the no-attenuation case, where a bearer reading is easiest to reach
for); and the attenuating holder cannot present their own agent's chain.

`verify_chain` already took a `presenter` and used it for one thing: comparing
it against the leaf's OPTIONAL `audience`. A leaf without an `audience` was
therefore accepted from anybody, which made a captured presentation a bearer
token — a chain names what may be done, not who is doing it.

It now requires the leaf to grant to `presenter`, refusing otherwise with the
new `AuthorityError::NotThePresenter`. That is the rule dtgwg-cred-spec PR #41
states normatively, and both known consumers had already written it by hand
after hitting the gap independently — `vti-rooms-dtg`'s chain verifier and its
nomination check each call `verify_chain` and then re-compare the subject, with
a comment explaining why they must.

With that rule in place `audience` can only name the subject (adding nothing)
or somebody else (satisfiable by nobody), so it is removed rather than kept as
a weaker second check: from `AuthorityGrant`, from `attenuate` and
`attenuate_from_json`, and from the verifier with `WrongAudience`.

The field was also being read two incompatible ways, which is what brought this
forward. This library compared it to the presenter; the Trust Tasks registry
described it as the party a presentation is FOR — "a host's identifier,
normally" — so an implementation following the registry minted leaves the
verifier refused every time. The destination question is real, and it is
answered one layer up by the trust task document's `recipient`, which its
`proof` covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-cred-spec#41
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner September 9, 2026 11:41
@stormer78
stormer78 merged commit 9d0687c into main Sep 9, 2026
7 checks passed
@stormer78
stormer78 deleted the feat/vac-presenter-binding branch September 9, 2026 11:49
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 9, 2026
Completes the previous commit, which repointed `audience` at the presenter so
it would at least verify. It should not be sent at all, and neither should
`nonce`. Both are removed from `rooms/keys/present` by spec 0.2.

`audience` named the party that had to PRESENT a credential, not the one it was
addressed to — so a host DID named somebody no presenter can ever be, and the
host refused every request. That is now moot: dtg-credentials 0.8.0 removed the
property and made the real rule explicit, which the VTA was already satisfying.
The leaf grants to the DID this VTA authenticated, and a host refuses a chain
whose leaf grants to anyone else. Who may present is established, not declared.

`nonce` was written into the presentation object, which is closed and has no
member for it — so a caller supplying one got a presentation the host rejects
as malformed. It could not have been made to work: every signature in a
presentation is an ISSUER's, never the presenter's, so a challenge inside it is
unauthenticated and a replay copies it along with everything else. Freshness is
the request's, via `issuedAt` and the duplicate-execution rule keyed on
document `id`.

## A second live instance, found while doing this

`rooms/keys/backfill` passed the caller-named host as the audience in
`vta-service`, exactly as that spec's normative MUST instructed. Every backfill
this VTA attempted was refused for the same reason. What actually makes a
caller-named host safe is that the leaf grants to the VTA: naming a host
transfers no standing. It does hand that host sight of the principal's
credentials, which is a disclosure rather than an escalation, and the comment
now says so.

## Nothing binds a presentation to a host, deliberately

A chain is scoped to the ROOM. One rooted in a room confers nothing anywhere
else, and any host serving that room would honour it — a room may have more
than one host, and moving between them without reissuing credentials is the
point. Binding a request to its destination is `recipient` on the document that
carries the presentation, per SPEC.md §4.8.2, which its proof covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-trust-tasks-tf#415
Refs: OpenVTC/dtg-credentials#21
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 9, 2026
Completes the previous commit, which repointed `audience` at the presenter so
it would at least verify. It should not be sent at all, and neither should
`nonce`. Both are removed from `rooms/keys/present` by spec 0.2.

`audience` named the party that had to PRESENT a credential, not the one it was
addressed to — so a host DID named somebody no presenter can ever be, and the
host refused every request. That is now moot: dtg-credentials 0.8.0 removed the
property and made the real rule explicit, which the VTA was already satisfying.
The leaf grants to the DID this VTA authenticated, and a host refuses a chain
whose leaf grants to anyone else. Who may present is established, not declared.

`nonce` was written into the presentation object, which is closed and has no
member for it — so a caller supplying one got a presentation the host rejects
as malformed. It could not have been made to work: every signature in a
presentation is an ISSUER's, never the presenter's, so a challenge inside it is
unauthenticated and a replay copies it along with everything else. Freshness is
the request's, via `issuedAt` and the duplicate-execution rule keyed on
document `id`.

## A second live instance, found while doing this

`rooms/keys/backfill` passed the caller-named host as the audience in
`vta-service`, exactly as that spec's normative MUST instructed. Every backfill
this VTA attempted was refused for the same reason. What actually makes a
caller-named host safe is that the leaf grants to the VTA: naming a host
transfers no standing. It does hand that host sight of the principal's
credentials, which is a disclosure rather than an escalation, and the comment
now says so.

## Nothing binds a presentation to a host, deliberately

A chain is scoped to the ROOM. One rooted in a room confers nothing anywhere
else, and any host serving that room would honour it — a room may have more
than one host, and moving between them without reissuing credentials is the
point. Binding a request to its destination is `recipient` on the document that
carries the presentation, per SPEC.md §4.8.2, which its proof covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-trust-tasks-tf#415
Refs: OpenVTC/dtg-credentials#21
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 9, 2026
Completes the previous commit, which repointed `audience` at the presenter so
it would at least verify. It should not be sent at all, and neither should
`nonce`. Both are removed from `rooms/keys/present` by spec 0.2.

`audience` named the party that had to PRESENT a credential, not the one it was
addressed to — so a host DID named somebody no presenter can ever be, and the
host refused every request. That is now moot: dtg-credentials 0.8.0 removed the
property and made the real rule explicit, which the VTA was already satisfying.
The leaf grants to the DID this VTA authenticated, and a host refuses a chain
whose leaf grants to anyone else. Who may present is established, not declared.

`nonce` was written into the presentation object, which is closed and has no
member for it — so a caller supplying one got a presentation the host rejects
as malformed. It could not have been made to work: every signature in a
presentation is an ISSUER's, never the presenter's, so a challenge inside it is
unauthenticated and a replay copies it along with everything else. Freshness is
the request's, via `issuedAt` and the duplicate-execution rule keyed on
document `id`.

## A second live instance, found while doing this

`rooms/keys/backfill` passed the caller-named host as the audience in
`vta-service`, exactly as that spec's normative MUST instructed. Every backfill
this VTA attempted was refused for the same reason. What actually makes a
caller-named host safe is that the leaf grants to the VTA: naming a host
transfers no standing. It does hand that host sight of the principal's
credentials, which is a disclosure rather than an escalation, and the comment
now says so.

## Nothing binds a presentation to a host, deliberately

A chain is scoped to the ROOM. One rooted in a room confers nothing anywhere
else, and any host serving that room would honour it — a room may have more
than one host, and moving between them without reissuing credentials is the
point. Binding a request to its destination is `recipient` on the document that
carries the presentation, per SPEC.md §4.8.2, which its proof covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-trust-tasks-tf#415
Refs: OpenVTC/dtg-credentials#21
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 9, 2026
Completes the previous commit, which repointed `audience` at the presenter so
it would at least verify. It should not be sent at all, and neither should
`nonce`. Both are removed from `rooms/keys/present` by spec 0.2.

`audience` named the party that had to PRESENT a credential, not the one it was
addressed to — so a host DID named somebody no presenter can ever be, and the
host refused every request. That is now moot: dtg-credentials 0.8.0 removed the
property and made the real rule explicit, which the VTA was already satisfying.
The leaf grants to the DID this VTA authenticated, and a host refuses a chain
whose leaf grants to anyone else. Who may present is established, not declared.

`nonce` was written into the presentation object, which is closed and has no
member for it — so a caller supplying one got a presentation the host rejects
as malformed. It could not have been made to work: every signature in a
presentation is an ISSUER's, never the presenter's, so a challenge inside it is
unauthenticated and a replay copies it along with everything else. Freshness is
the request's, via `issuedAt` and the duplicate-execution rule keyed on
document `id`.

## A second live instance, found while doing this

`rooms/keys/backfill` passed the caller-named host as the audience in
`vta-service`, exactly as that spec's normative MUST instructed. Every backfill
this VTA attempted was refused for the same reason. What actually makes a
caller-named host safe is that the leaf grants to the VTA: naming a host
transfers no standing. It does hand that host sight of the principal's
credentials, which is a disclosure rather than an escalation, and the comment
now says so.

## Nothing binds a presentation to a host, deliberately

A chain is scoped to the ROOM. One rooted in a room confers nothing anywhere
else, and any host serving that room would honour it — a room may have more
than one host, and moving between them without reissuing credentials is the
point. Binding a request to its destination is `recipient` on the document that
carries the presentation, per SPEC.md §4.8.2, which its proof covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-trust-tasks-tf#415
Refs: OpenVTC/dtg-credentials#21
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 9, 2026
Completes the previous commit, which repointed `audience` at the presenter so
it would at least verify. It should not be sent at all, and neither should
`nonce`. Both are removed from `rooms/keys/present` by spec 0.2.

`audience` named the party that had to PRESENT a credential, not the one it was
addressed to — so a host DID named somebody no presenter can ever be, and the
host refused every request. That is now moot: dtg-credentials 0.8.0 removed the
property and made the real rule explicit, which the VTA was already satisfying.
The leaf grants to the DID this VTA authenticated, and a host refuses a chain
whose leaf grants to anyone else. Who may present is established, not declared.

`nonce` was written into the presentation object, which is closed and has no
member for it — so a caller supplying one got a presentation the host rejects
as malformed. It could not have been made to work: every signature in a
presentation is an ISSUER's, never the presenter's, so a challenge inside it is
unauthenticated and a replay copies it along with everything else. Freshness is
the request's, via `issuedAt` and the duplicate-execution rule keyed on
document `id`.

## A second live instance, found while doing this

`rooms/keys/backfill` passed the caller-named host as the audience in
`vta-service`, exactly as that spec's normative MUST instructed. Every backfill
this VTA attempted was refused for the same reason. What actually makes a
caller-named host safe is that the leaf grants to the VTA: naming a host
transfers no standing. It does hand that host sight of the principal's
credentials, which is a disclosure rather than an escalation, and the comment
now says so.

## Nothing binds a presentation to a host, deliberately

A chain is scoped to the ROOM. One rooted in a room confers nothing anywhere
else, and any host serving that room would honour it — a room may have more
than one host, and moving between them without reissuing credentials is the
point. Binding a request to its destination is `recipient` on the document that
carries the presentation, per SPEC.md §4.8.2, which its proof covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-trust-tasks-tf#415
Refs: OpenVTC/dtg-credentials#21
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

🔎 A manual security review is recommended before merging. Please contact the Security team for specifics and remediation guidance.

ℹ️ Detailed findings are not published on public repositories; the Security team holds the complete report.

stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 9, 2026
Completes the previous commit, which repointed `audience` at the presenter so
it would at least verify. It should not be sent at all, and neither should
`nonce`. Both are removed from `rooms/keys/present` by spec 0.2.

`audience` named the party that had to PRESENT a credential, not the one it was
addressed to — so a host DID named somebody no presenter can ever be, and the
host refused every request. That is now moot: dtg-credentials 0.8.0 removed the
property and made the real rule explicit, which the VTA was already satisfying.
The leaf grants to the DID this VTA authenticated, and a host refuses a chain
whose leaf grants to anyone else. Who may present is established, not declared.

`nonce` was written into the presentation object, which is closed and has no
member for it — so a caller supplying one got a presentation the host rejects
as malformed. It could not have been made to work: every signature in a
presentation is an ISSUER's, never the presenter's, so a challenge inside it is
unauthenticated and a replay copies it along with everything else. Freshness is
the request's, via `issuedAt` and the duplicate-execution rule keyed on
document `id`.

## A second live instance, found while doing this

`rooms/keys/backfill` passed the caller-named host as the audience in
`vta-service`, exactly as that spec's normative MUST instructed. Every backfill
this VTA attempted was refused for the same reason. What actually makes a
caller-named host safe is that the leaf grants to the VTA: naming a host
transfers no standing. It does hand that host sight of the principal's
credentials, which is a disclosure rather than an escalation, and the comment
now says so.

## Nothing binds a presentation to a host, deliberately

A chain is scoped to the ROOM. One rooted in a room confers nothing anywhere
else, and any host serving that room would honour it — a room may have more
than one host, and moving between them without reissuing credentials is the
point. Binding a request to its destination is `recipient` on the document that
carries the presentation, per SPEC.md §4.8.2, which its proof covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-trust-tasks-tf#415
Refs: OpenVTC/dtg-credentials#21
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Sep 9, 2026
…ntials 0.6 → 0.9.1 (#1356)

* fix(rooms): bind a presentation to its presenter, not to the host

`pnm-cli rooms --host-did …` could never work. Every request it made was
refused as `WrongAudience`, and omitting the flag "worked" only by
skipping the check it exists to perform — so the flag's two states were
broken and unprotected.

`audience` is not who a presentation is addressed to.
`dtg_credentials::authority::verify_chain` compares it to the PRESENTER —
"the leaf must be presentable by whoever is presenting it" — so it is
holder binding, and its whole job is to make a captured presentation
worthless to whoever captured it. Filled with the host's DID it named a
party no presenter can ever match.

The value to bind to was already named a few lines away: `RoomSigner`'s
doc says "a room request is signed by the party the presentation was
minted for". This passes exactly that. There is no unbound case left, so
the warning about one goes, and `--host-did` keeps its real job of naming
the document's recipient.

## The spec says otherwise, and that is filed separately

`rooms/keys/present/0.1` describes `audience` as "the party the
presentation is for … a host's identifier, normally". The CLI implemented
the spec faithfully; the spec and the credential library it runs on
disagree, identically in dtg-credentials 0.6 and 0.7, so it is not
version drift.

This changes the CLI to match the verifier rather than the prose, because
a presentation that cannot verify protects nobody while being wrong in
the other direction. Which side should move is a working-group question —
0.7's own notes point at upstream PR #41, "a key-control demonstration at
invocation, which removes `audience` as redundant" — and is raised there.

* fix(rooms)!: stop sending `audience` and `nonce` on rooms/keys/present

Completes the previous commit, which repointed `audience` at the presenter so
it would at least verify. It should not be sent at all, and neither should
`nonce`. Both are removed from `rooms/keys/present` by spec 0.2.

`audience` named the party that had to PRESENT a credential, not the one it was
addressed to — so a host DID named somebody no presenter can ever be, and the
host refused every request. That is now moot: dtg-credentials 0.8.0 removed the
property and made the real rule explicit, which the VTA was already satisfying.
The leaf grants to the DID this VTA authenticated, and a host refuses a chain
whose leaf grants to anyone else. Who may present is established, not declared.

`nonce` was written into the presentation object, which is closed and has no
member for it — so a caller supplying one got a presentation the host rejects
as malformed. It could not have been made to work: every signature in a
presentation is an ISSUER's, never the presenter's, so a challenge inside it is
unauthenticated and a replay copies it along with everything else. Freshness is
the request's, via `issuedAt` and the duplicate-execution rule keyed on
document `id`.

## A second live instance, found while doing this

`rooms/keys/backfill` passed the caller-named host as the audience in
`vta-service`, exactly as that spec's normative MUST instructed. Every backfill
this VTA attempted was refused for the same reason. What actually makes a
caller-named host safe is that the leaf grants to the VTA: naming a host
transfers no standing. It does hand that host sight of the principal's
credentials, which is a disclosure rather than an escalation, and the comment
now says so.

## Nothing binds a presentation to a host, deliberately

A chain is scoped to the ROOM. One rooted in a room confers nothing anywhere
else, and any host serving that room would honour it — a room may have more
than one host, and moving between them without reissuing credentials is the
point. Binding a request to its destination is `recipient` on the document that
carries the presentation, per SPEC.md §4.8.2, which its proof covers.

Refs: trustoverip/dtgwg-trust-tasks-tf#414
Refs: trustoverip/dtgwg-trust-tasks-tf#415
Refs: OpenVTC/dtg-credentials#21
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* chore(rooms)!: dtg-credentials 0.6 → 0.9.1

Three breaking releases at once. The middle one is why this is worth
doing now rather than eventually.

## 0.8 — a VAC is not a bearer credential

`verify_chain` now REQUIRES the presenter to be the leaf's subject,
refusing otherwise with `NotThePresenter`. Before, it used `presenter`
for one thing — comparing it against the leaf's OPTIONAL `audience` — so
a leaf without one was accepted from anybody and a captured presentation
was a bearer token.

**Both hand-written copies of that rule are deleted**, which is the
payoff: `vti-rooms-dtg`'s chain verifier and its nomination check had
each written it independently, after hitting the gap separately, each
with a comment explaining why they had to. Two copies of a rule is one
place for it to be forgotten, and the duplicate is the one nobody
updates.

The tests that pinned the property stay exactly where they were — a chain
presented by the wrong party is still refused, and which layer refuses it
is no longer this crate's business. Their assertions move to the
library's wording, which names both parties rather than only the rule.
`vti-rooms-wasm`'s equivalent test moves from expecting `WrongAudience`
to expecting `NotThePresenter`: same property, no field to fill in.

## 0.7 — digests, and a required expiry

`validUntil` became required on `new_vac`. `rooms/owner/issue-authority`
now REFUSES a request without one rather than defaulting: nothing about a
subject's standing is consulted when a chain is verified, so authority
that does not expire is authority nobody can withdraw by waiting, and
picking a lifetime here would put the room's most consequential number
somewhere its owner never looks.

The same rule made `Option` dead in two test fixtures — every caller
already passed a value, and "a nomination that never expires" is a state
no test can construct any more.

## 0.9.1 — no behaviour change here, and pinned deliberately

0.9.1 applies the same rule to the VDC (`delegation::verify_chain` gains
a `presenter`). This workspace does not use delegation, so it is a
drop-in.

Pinned `0.9.1` rather than `0.9` on purpose: that release is API-breaking
despite the patch version, and Cargo treats the two as compatible, so
`"0.9"` picks it up on a routine `cargo update`. Naming the floor says
which behaviour this workspace requires rather than tolerating either.

0.9.1 also adds an **Upgrading** section stating that verifiers move
before issuers — closing OpenVTC/dtg-credentials#22, which this work
raised after a browser member on 0.7 was refused by a host on 0.6 with an
error that blamed the chain.

## `vti-rooms-wasm` comes along

It takes the workspace pin, so #1347 landing put it in scope: its
`present` loses the `audience` argument it was passing, and its doc block
saying "there is no audience parameter, and that is deliberate" becomes
"there is no longer a field either" — the binding it relied on is the
library's rule now rather than a value it declined to expose.

## Verified

vti-rooms 106+3, vti-rooms-dtg 19, room-host 23+13, vti-rooms-wasm 8 —
green. `cargo clippy --workspace --all-targets -D warnings` clean, and
`vti-rooms-wasm` still checks for `wasm32-unknown-unknown`.

* fix(members): read both spellings of an acknowledgement's digest

Working Draft 02 renamed `credentialSubject.digest` to `digestMultibase`
and changed its encoding from `sha256:<hex>` to a base58btc multihash.
`vtc-service` hand-rolled the old definition and did not follow, so a
member using a current client acknowledged a grant and was told it
acknowledged a different one.

The tests caught it, being written for precisely that:

    Two implementations, one definition — this is the assertion that
    catches either side drifting.

## One definition, one implementation

The new form delegates to `dtg_credentials::digest_multibase_json`
instead of being copied here. This module already carries two digest
functions differing in encoding *and* coverage, with a doc comment
warning not to substitute one for the other; the acknowledgement binding
was quietly a third. Only the party that publishes the specification
should own the definition.

Note the trap that made this worth stating: this file's existing
`digest_multibase` is the same *encoding* over different *coverage* — it
digests the document whole, `proof` included. Reaching for it here would
compile and produce a plausible string matching nothing.

## Both forms are read, each checked its own way

A member whose client predates WD02 still sends the old spelling and
their acknowledgement must keep verifying. So whichever property arrives
is compared against the expected value computed the same way — this
widens what is *accepted*, never what counts as a *match*. `DigestForm`
keeps that pairing in one place, because a check comparing a WD02 digest
against a WD01 expectation refuses a correct acknowledgement while saying
the member acknowledged a different grant.

Multibase digests are compared as decoded bytes, never as strings: one
multihash has more than one spelling.

## Two tests for the branch that had none

The fixture builds acknowledgements with `dtg-credentials`, which emits
only the current form, so the legacy path could not be reached from it.
Both cases are hand-built: an old acknowledgement of the right grant
binds, and an old acknowledgement of a different grant is still refused.
The second is the failure a fallback invites — accepting the property and
never comparing it.

vtc-service lib: 960 passed.

---------

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.

2 participants