fix(consent): derive the approver's match code from the digest, not its encoding - #113
Merged
Conversation
…ts encoding
Trust Tasks 0.4 moved `payloadDigest` to the shared `DigestMultibase` type — a
multibase-encoded multihash — and landed it errata-style, **in place**, on
`task-consent/{request,decision,granted}/0.1`. The type URI did not move. No
version check anywhere can detect this; the encoding is the only signal.
The approver screen sliced the first six characters of that string as the code
the human matches across two devices. Under base58btc every SHA-256 digest
begins `zQm` — the multibase marker plus the `0x12 0x20` multihash prefix,
identical for every digest ever produced:
zQmcdLJ… zQmRTnb… zQmb7oR… zQmbu6r… ← four different payloads
So half the code would have been a format marker. ~17.6 bits where the human
believes they are comparing ~35, and it still *looks* like six random
characters — which is what makes it dangerous rather than merely wasteful. An
attacker searching offline for a payload that renders the same code would face
~195k candidates instead of ~60 billion.
This is the one check on that screen that a hostile device cannot defeat. Every
other check assumes the device is honest; only a comparison the human performs
across two independent screens moves the check somewhere the device cannot
reach. Quietly narrowing it to three characters would have left the ceremony
looking intact.
`matchCodeFromDigest` decodes the multibase, strips the multihash prefix and
hex-renders the leading bytes. Because the digest is still SHA-256 this
reproduces exactly the six characters this surface showed when the wire carried
bare hex, so the migration is invisible to the human and the two repositories
can cut over without stranding whatever the operator already has in front of
them. It agrees character-for-character with the VTA's
`vta_mobile_core::consent::match_code_from_digest`; the fixture
`zQmSK9pGKFnmc77…` → `3b0c7f` is asserted in both, and is the contract between
them.
A digest that will not decode now yields no code at all: the request renders an
explicit "this wallet cannot read it, do not approve", the type-to-confirm input
does not appear, and approval is blocked. The comparison *is* the control, so
with nothing to compare there is nothing to downgrade to. A stale hex digest
from a version-skewed agent fails here rather than at the executor, after the
human has already agreed.
The case-insensitive compare stays and is now correct rather than accidentally
so — the derived code is hex. It forgives the keyboard without widening the
match, which lowercasing base58 would have done.
Also decodes the other bases the framework's `^[zumbfF]` pattern admits, so a
conforming digest from a non-VTA executor is not refused, and reuses the
base58btc decoder already in `trust-tasks/canonical.ts` rather than adding a
second one. An unrecognised prefix is refused rather than guessed at — not
inferring the base from context is the entire point of multibase.
Wire-compatible in the other direction: `payloadDigest` is still echoed verbatim
into the decision, which the VTA's own round-trip test requires.
Dependencies updated in the same pass (everything in range; `npm outdated` is
now empty). vite 8.2 dropped esbuild and `vite-plugin-top-level-await@1.6.0`
requires it without declaring it, so the extension build died with
`Cannot find module 'esbuild'` — added as an explicit devDependency. The MV3
invariant was re-checked by hand across that bump: `dist/background.js` is still
a single bundle with no dynamic `import()`.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78
force-pushed
the
feat/digest-multibase
branch
from
August 9, 2026 20:51
0e55c6a to
6452b2c
Compare
stormer78
added a commit
that referenced
this pull request
Aug 9, 2026
Cuts the three changes sitting under `[Unreleased]` since 0.2.0: the `digestMultibase` decoding and byte-derived approver match code (#113), the canonical `auth/authenticate/0.1` type for VTA DIDComm auth, and the consent-gated task detection that keys on `details.reason` rather than a top-level code that never matched. Minor rather than patch: `trust-tasks/digest.ts` adds public API (`matchCodeFromDigest`, `decodeDigestMultibase`, `decodeMultibase`, `MATCH_CODE_LEN`, `DigestMultibaseError`). Nothing in the TypeScript surface was removed or narrowed, so no BREAKING section — but the release is not drop-in, and the changelog says so under Migration rather than leaving a consumer to find out from a mismatched code on an approval screen. The reason it is not drop-in is that `payloadDigest` changed encoding with no type-URI version to signal it — Trust Tasks 0.4 re-pinned the task-consent specifications errata-style, in place. So this release pairs with verifiable-trust-infrastructure#911 and the two have to move together. Unlike the 0.1.3 authcrypt change there is deliberately no dual-accept fallback to stage behind: the digest is what the approver signs, and accepting both encodings would mean accepting two different digests for one payload — the exact substitution the digest exists to prevent. Both skew directions fail closed, which is what bounds this to an upgrade inconvenience rather than a security event. A 0.3.0 wallet against a pre-0.4 VTA refuses the bare-hex digest and blocks approval with an explicit message. A 0.2.0 wallet against a 0.4 VTA displays `zQmSK9…` where the requesting screen displays `3b0c7f`, so destructive approvals become impossible while non-destructive ones still complete. Nothing is silently mis-approved either way. Installed extensions need a rebuild — there is no store auto-update path in this repository. Dependents moved to `^0.3.0` in the same commit so the workspace keeps resolving to the local package rather than the published 0.2.0. Not published here. `npm publish` remains a manual step after this merges. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pairs with OpenVTC/verifiable-trust-infrastructure#911 — must land together
Trust Tasks 0.4 moved
payloadDigestto the sharedDigestMultibasetype and landed it errata-style, in place ontask-consent/{request,decision,granted}/0.1. The type URI did not move, so no version check on either side detects this — the encoding is the only signal.The digest is what the approver signs and what the executor re-derives. A mismatched pair produces an approval that is given, accepted by the human, and then silently never takes effect. This should not merge before #911, and #911 should not merge without it.
What was wrong
The approver screen took the first six characters of
payloadDigestas the code the human matches across two devices. Under base58btc every SHA-256 digest beginszQm— the multibase marker plus the0x12 0x20multihash prefix, constant for every digest ever produced:Half the code would have been a format marker: ~17.6 bits where the human believes they are comparing ~35, while still looking like six random characters. That is the one check on this screen a hostile device cannot defeat — every other check assumes an honest device; only a comparison the human performs across two independent screens moves the check somewhere the device cannot reach.
What this does
matchCodeFromDigest(new,packages/core/src/trust-tasks/digest.ts) decodes the multibase, strips the multihash prefix, and hex-renders the leading bytes. Because the digest is still SHA-256, this reproduces exactly the six characters this surface showed when the wire carried bare hex — so the migration is invisible to the operator and neither repo strands what the other already has on screen.It agrees character-for-character with
vta_mobile_core::consent::match_code_from_digestin #911. The fixturezQmSK9pGKFnmc77pqyNAPJyPKt8rMqctngfg3vwuMArwGYZ→3b0c7fis asserted in both repositories and is the contract between them.^[zumbfF]pattern admits, so a conforming digest from a non-VTA executor is not refused. An unrecognised prefix is refused rather than guessed at.trust-tasks/canonical.ts.payloadDigestis still echoed verbatim into the decision, which #911's round-trip test requires.9 new tests in
packages/core/tests/trust-tasks.digest.mjs, including the shared fixture, thehex(digest)[..6]continuity property, bare-hex refusal, and 64 payloads → 64 distinct codes.Dependency refresh (same PR, by request)
Everything in range;
npm outdatedis now empty. react 19.2.8, vite 8.2.1,@scure/base2.3.0,@noble/*2.3.0,@types/chrome0.2.5, lucide-react 1.31.0, and others.vite-plugin-top-level-await@1.6.0(already latest)requires it without declaring it — the extension build fails withCannot find module 'esbuild'. Addedesbuildas an explicit extension devDependency. That dependency is now load-bearing for the build; it can come back out if the plugin ever declares it properly.The MV3 invariant was re-checked by hand across the vite bump:
dist/background.jsis still a single 88.52 kB bundle with no dynamicimport().Two notes for #911's author
match_code_from_digestand rewiresparse_task_consent_requestto it. That section is the one a reviewer needs in order to realise this repo has to move.trust-tasks-rs0.4.0, but 0.4.1 and 0.5.0 have since shipped. I checked — they add the ceremony envelope member and the/ceremony/namespace, not a further digest change. This PR is unaffected either way.Pre-merge checklist (vti-stack-development-guide §9)