feat: assert personhood to a community over Trust Tasks - #257
Merged
Conversation
A community that vets its members can mark them as people; the claim lands as a `PersonhoodCredential` type on the member's VMC, which is what DTG Credentials means by a PHC. `openvtc` had no way to take part — personhood appeared only in README prose, and the VTC's verbs were reachable over REST alone, which a client that talks to communities over DIDComm and TSP and holds no bearer token cannot use. The two member-facing verbs now ride the same Trust Task document path as the join ceremony: DIDComm wraps the document in an authcrypt envelope, TSP carries it bare, and the sender is cryptographically proven either way. `Route` groups the transport plumbing both verbs need — six same-typed DID strings passed loose is one transposed pair away from addressing the wrong party. ## The spoken match code `challengeId` is a UUID: fine on a wire, hopeless read aloud. `match_code` derives eight Crockford base32 characters from it (no I/L/O/U, so nothing in one is confusable when spoken) — the thing two people in a room actually say to each other. It is derived, never transferred: anyone holding the challenge id computes the same characters, anyone who does not cannot. Nothing on the VTC checks it, because it proves nothing `proof.challenge` does not already prove. It exists so a human can tell that the ceremony their client is answering is the one the administrator in front of them just started. `match_code_agrees_with_the_vtc` pins the vector `5CY1-GZEE`, and the VTC pins the same one. Nothing else would catch the two derivations drifting: a changed domain tag or alphabet on either side still yields eight plausible characters that simply never match, which reads to the people in the room as "wrong ceremony" rather than "your software disagrees with itself". ## The challenge is written twice, deliberately `assert/0.1` says `proof.challenge` must be the paired `challengeId`, and that this is what "stops one captured and replayed into another". In W3C Data Integrity that holds because the proof options are canonicalised with the document, so `challenge` is signed. `affinidi_data_integrity` has no `challenge` proof option, and the VTC verifies over the presentation with the whole `proof` block removed. A value written only there is therefore **not covered by the signature** — swapping it on a captured presentation would go unnoticed. So `build_presentation` writes the challenge to `proof.challenge` (what the spec names) and to top-level `nonce` (what the signature covers), and the VTC requires both to agree. `the_signed_nonce_is_covered_by_the_proof` pins that tampering with `nonce` breaks verification — which is the whole reason it carries the challenge, and the reason neither copy can be dropped as redundant. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…e disagreement The two verbs could be sent but not understood: replies arrived as `#response` documents that nothing parsed, so a member never learned the challenge id to sign against. `parse_challenge_reply` and `parse_assert_reply` normalise both reply shapes the VTC produces — the dispatcher's `#response` document with members under `payload`, and a hand-built bare body — the same way `messaging` already does, which also makes them transport-agnostic: a TSP frame carries the response document raw. Routing needed no change; `OPENVTC_CATCH_ALL_PATTERN` already admits `https://trusttasks.org/spec/vtc/.*`. The challenge parser compares the community's own copy of the match code against the one derived locally and treats a disagreement as an error. The two are computed by different implementations, and a drift is otherwise invisible: both sides still show eight plausible characters, so the people in the room conclude they have the wrong ceremony rather than that the software disagrees with itself. A community that sends no copy at all is fine — the code is derived locally, so an older VTC costs nothing. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
|
🛡️ AI Agentic Security Code Review — all clear. We checked this change and found nothing to report. Keep shipping secure code! Note: for major, breaking, or feature-introducing changes, you can always request an in-depth review from the security team. |
The library surface had no way in. `p` on an Active membership asks the community for a challenge; the reply lands as the panel's live challenge and shows the match code; `P` answers it. The match code gets its own line in the panel's emphasis colour rather than being folded into the status text — it is the thing a member reads aloud, and a code that has to be picked out of a sentence is a code that gets misread. An expired challenge renders as expired rather than disappearing. A member who has just been read a code, looking at a panel that never mentions one, cannot tell "it lapsed" from "it never arrived". Key hints are gated exactly as the handler gates the keys, which is this file's existing discipline: `p` on Active rows only, `P` only while a live challenge is in hand. Both would otherwise be dead keys — the defect that gating was written to remove. The assertion targets the membership the *challenge* names, not the highlighted row. Answering against whichever row happens to be selected is exactly the confusion the stored `vtc_did` exists to prevent. It presents that membership's own credentials and lets the community's policy choose: a client-side filter would have to guess at a policy it cannot read, and guessing wrong withholds the very credential that would have satisfied it. Scoping to the one membership also keeps one community's credentials from being disclosed to another. Two supporting changes: - `Verb` gained the two personhood sends, so `CommunityOutcome`'s `leaving: bool` became a `Performed` enum. A boolean cannot say which of four things happened, and the arms that got it wrong would have told the member the wrong thing went wrong rather than failed. - The three out-params of `process_inbound_message` became `InboundEffects`. The live challenge belongs to `State`, not the account, so it is reported up rather than persisted — a single-use nonce with a ten-minute life has no business surviving a restart. 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.
The member side of the VTC's personhood ceremony. Pairs with OpenVTC/verifiable-trust-infrastructure#1085 (merged) and its follow-up, which routes the two verbs over messaging — before that they were REST-only, so a client that talks to communities over DIDComm and TSP and holds no bearer token could not reach them at all. Personhood appeared in this repo only as README prose.
What lands
openvtc-core::personhood—request_challenge,build_presentation,assert_personhood, and parsers for both replies.Both verbs ride the same Trust Task document path as the join ceremony: DIDComm wraps the document in an authcrypt envelope, TSP carries it bare, and the sender is cryptographically proven either way. Routing needed no change —
OPENVTC_CATCH_ALL_PATTERNalready admitshttps://trusttasks.org/spec/vtc/.*.Routegroups the transport plumbing both verbs need. Six same-typed DID strings passed loose is one transposed pair away from addressing the wrong party.The spoken match code
challengeIdis a UUID: fine on a wire, hopeless read aloud.match_codederives eight Crockford base32 characters from it — noI/L/O/U, so nothing in one is confusable when spoken. This is what two people in a room actually say to each other.It is derived, never transferred: anyone holding the challenge id computes the same characters, anyone who does not cannot. Nothing on the VTC checks it, because it proves nothing
proof.challengedoes not already prove. It exists so a human can tell that the ceremony their client is answering is the one the administrator in front of them just started — a Bluetooth pairing code, not a password.match_code_agrees_with_the_vtcpins the vector5CY1-GZEE; the VTC pins the same one. Nothing else would catch the two derivations drifting — a changed domain tag or alphabet on either side still yields eight plausible characters that simply never match, which reads to the people in the room as "wrong ceremony" rather than "your software disagrees with itself".parse_challenge_replyturns a live disagreement into an error for the same reason.The challenge is written twice, deliberately
assert/0.1saysproof.challengemust be the pairedchallengeId, and that this is what "stops one captured and replayed into another". In W3C Data Integrity that holds because the proof options are canonicalised with the document, sochallengeis signed.affinidi_data_integrityhas nochallengeproof option, and the VTC verifies over the presentation with the wholeproofblock removed. A value written only there is not covered by the signature — swapping it on a captured presentation would go unnoticed.So
build_presentationwrites the challenge toproof.challenge(what the spec names) and to top-levelnonce(what the signature covers), and the VTC requires both to agree.the_signed_nonce_is_covered_by_the_proofpins that tampering withnoncebreaks verification — which is the whole reason it carries the challenge, and why neither copy can be dropped as redundant.Not in this PR
No CLI or TUI command yet — this is the library surface the command will call.
cargo test -p openvtc-core --libis green (437 tests, 9 of them new); clippy and fmt clean.