Skip to content

feat(core): the wallet verifies the replies it receives - #215

Merged
stormer78 merged 1 commit into
mainfrom
feat/client-verifies-replies
Sep 9, 2026
Merged

feat(core): the wallet verifies the replies it receives#215
stormer78 merged 1 commit into
mainfrom
feat/client-verifies-replies

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

The browser half of the gap the Rust side just closed. Both services sign their responses now (OpenVTC/verifiable-trust-infrastructure#1334, #1335) and the Rust client verifies (#1341); this wallet checked nothing, so the signature was decorative here too.

Why a client verifies

A reply is bytes off a socket. Nothing else on this path establishes who produced them — the transport proves a connection, and over REST not even that beyond TLS to a host name.

Without the proof, an intermediary can rewrite an ACL listing, flip a policy decision, or answer for an agent that never spoke — and every check after it passes, because the checks after it are about shape.

Two checks, and the second is the one easy to omit

The proof must verify, and its proven signer must be the agent this channel addressed.

verifyTrustTaskProof answers only the first — its own contract says it "does NOT check framework-level bindings ... that's the caller's job". Skipping the binding turns "signed by somebody" into "signed by our agent".

The expected DID comes from the channel's own configuration (auth.service.did, vta.did, vta.vid), never from the reply's issuer — that is the claim under test.

Wired at all three channels

REST, DIDComm and TSP. The latter two already prove the sender at the envelope layer and verify the document anyway, because an envelope attests to the transport, not to what it carried: a relay that can pack for us could still hand us a body we did not get from the agent.

No staging flag, deliberately

The Rust client has one because it talks to agents that may predate response signing. This one does not: nothing here is deployed, and the house rule in CLAUDE.md is that the plugin and the VTA cut over together rather than carrying a fold that reads as a live constraint.

Error documents are exempt from the specification rather than for convenience — trust-task-error's own proof requirement is RECOMMENDED, so demanding one would make every conforming refusal unreadable.

Four test doubles now sign, and that is the fix

A double that answers unsigned models a VTA that no longer exists, so making them sign is the correct repair rather than a workaround. Two needed a real did:key identity so their proof resolves offline (a did:web stub would need the network).

The TSP double needed its signature taken after threadId is set. Signing first and mutating after produces a proof that fails to verify — which is precisely what the channel then refuses, so the failure was a good advertisement for the check working.

Verification

npm test1144 tests, 0 failures across all four workspaces. npm run lint and npm run build clean.

The browser half of the same gap. Both services sign their responses now
(OpenVTC/verifiable-trust-infrastructure#1334, #1335) and the Rust client
verifies (#1341); this wallet checked nothing, so the signature was
decorative here too.

A reply is bytes off a socket. Nothing else on this path establishes who
produced them — the transport proves a connection, and over REST not even
that beyond TLS to a host name. Without the proof an intermediary can
rewrite an ACL listing, flip a policy decision, or answer for an agent that
never spoke, and every check after it passes, because the checks after it
are about shape.

Two checks, and the second is the one easy to omit: the proof must verify,
**and its proven signer must be the agent this channel addressed**.
`verifyTrustTaskProof` answers only the first — its own contract says it
does not check framework bindings, that being the caller's job — so
skipping the binding turns "signed by somebody" into "signed by our agent".
The expected DID comes from the channel's own configuration, never from the
reply's `issuer`, which is the claim under test.

Wired at all three channels. The DIDComm and TSP arms already prove the
*sender*; they verify the document too, because an envelope attests to the
transport and not to what it carried.

No staging flag, unlike the Rust client. That one talks to agents which may
predate response signing; nothing here is deployed, and the house rule is
that this plugin and the VTA cut over together rather than carrying a fold
that reads as a live constraint.

Error documents are exempt from the specification rather than for
convenience: `trust-task-error`'s own proof requirement is RECOMMENDED, so
demanding one would make every conforming refusal unreadable.

**Four test doubles now sign, and that is the fix rather than a workaround.**
A double that answers unsigned models a VTA that no longer exists. Two
needed a real `did:key` identity to resolve offline, and the TSP double
needed its signature taken *after* `threadId` is set — signing first and
mutating after produces a proof that fails to verify, which is exactly what
the channel then refuses. That failure is a good advertisement for the
check.

1144 tests pass; lint and build clean.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 18eb734 into main Sep 9, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/client-verifies-replies branch September 9, 2026 07:54
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