Peer Identity Vetting

Why: People Are the Root of Personhood

A community that admits anyone on request is only as trustworthy as its weakest signup form. The communities this ecosystem is built for — the Linux kernel’s maintainers are the worked example — have always solved that the same way: existing members check who you are before you get in. The kernel does it with a PGP web of trust: a key signed by two or three account holders who met you, a trust path to Linus, key-signing parties. It works, and its pain points are known — a public list of names and coordinates, a published graph of who vouched for whom, in-person bias, and a strong set that shrank from 358 to 94 keys when GnuPG dropped SHA-1 signatures.

Peer identity vetting is the ecosystem’s replacement for that model. Its premise is that the human root of personhood is not a document check by a machine but admission by people who checked you — and that this can be made into evidence a community can count, under rules it publishes, without publishing who vouched for whom. The design (OpenVTC’s docs/design/vetting-process.md, still marked DRAFT v3) set itself one date: a working V0 by 2026-10-05, in Prague, ahead of the Kernel Maintainer Summit. It met it, and the result is what was shown at the Linux Plumbers Conference on 2026-10-07 — that story is told on kernel-web-of-trust; this page is about the mechanism.

The V0 Ceremony, As Shipped

An applicant who wants to join a vetting community:

  1. reads the community’s vetting requirements from its join manifest before disclosing anything — how many statements, by which methods (inPerson, video, priorAcquaintance), which claims, how fresh, how independent;
  2. chooses or mints the persona DID they will join with, and builds a signed Vetting Card — a Verifiable Data Structure profiled on the r-card, its claims released by persona disclosure from one of their faces, signed by the join persona’s own key, good for fifteen minutes and addressed to one vetter;
  3. reaches a vetter — a member the community has named — by presenting a ticket the vetter issued (an eight-character Crockford code, or a QR carrying a vetting-ticket: URI with a 256-bit secret): the anti-spam gate, since a wrong code gets no answer;
  4. sits a vetting session, in person or on video. Both screens derive the same spoken match code from the session document under the domain tag vetting-session-match/v1; reading it aloud proves the person in the room is driving the client that controls the join DID. The vetter checks the card against whatever documentation they accept (each vetter decides — the design’s D16, close to the kernel’s own unwritten practice), and issues back a Vetting Statement;
  5. repeats until the checklist fills, reviews exactly what the community will receive, and submits the statements in one presentation. The VTC verifies them, counts, evaluates governance policy, and admits, asks for more, refers to a moderator, or denies.

The Vetting Statement is a DTG Statement Credential — a StatementCredential whose credentialSubject.predicate is the DTG VSC registry predicate https://registry.trustoverip.org/dtg/vsc/vetted/1, issued by the vetter’s member DID with issuerScope at least directed, and bound to the session by [[trust-task-context-binding|taskContext and taskDigestMultibase]]: a statement that does not cite its session is refused by the applicant’s client before it is ever stored. Its body (object.value, no type member — the predicate carries the meaning) records the community, the method, the document classes relied on, the claims verified, liveness, a salted identity commitment, the digest of the card as transmitted, and the vetter’s declared relationship to the applicant. This corrects an earlier reading of the design: through September the statement was an Endorsement Credential of type IdentityVetting, and that is what the previous version of this page said. DTG Credentials v1 (VTI #1859, 2026-09-30; openvtc #397, 2026-10-01; the Keyring phone the same week) moved it onto the registered predicate, with pre-v1 statements and grants set aside on load rather than counted.

Vetters are named by a role credential, and that too changed shape: it is a DTG Authority Credential (AuthorityCredential, actions ["role:vetter"], maxAttenuation: 0 so nobody can delegate it onward), issued by the community with issuerScope: public, a status-list credentialStatus, and a validity of one year by default (one day to two). A vetter answers every vetting/request with it as an eligibilityVp — nonce the request’s id, domain the applicant’s join DID — so the applicant knows before the session that the statement will count, and checks its status list afterwards (an Unknown answer is never read as active). Grants are an admin act in the console, cnm vetting vetters grant, or the vtc/vetting/vetters/grant/0.1 Trust Task; a vetter who lost the credential asks for it again with vetters/resend; leaving the community revokes it. A community can also let policy name its vetters: the vetterEligibility Rego purpose is evaluated for every active member by a periodic sweep over status, roles, tenureDays, admittedVia (genesis / vetting / invitation / open), underReview and depth — so the kernel’s “trust path to Linus of at most five hops” becomes one line of policy over a privately recorded depth, never a computation over a published keyring.

What the Community Publishes, and What It Counts

The join manifest is now vtc/join-requests/manifest/0.3 (VTI #1907, 2026-10-02; openvtc #412 and Keyring followed, each falling back to 0.2). Each criterion states its own admission (automatic admits, review refers to an administrator), its DCQL query and accepted issuers, its optional vetting object, and a requirementsDigest — and the criteria are listed in the order the community decides by. The manifest is served over POST /v1/trust-tasks with no session, so someone who has no relationship with the community can read that it demands a passport check and walk away having disclosed nothing; a community that would rather its answer be attributable sets public: false on join discovery, which makes the request signed, not the manifest secret. Every number in the vetting object — minStatements, minByMethod, acceptedMethods, requiredClaims, maxStatementAge, eligibleVetters.role, independence caps — is community policy. The protocol has no defaults, and the route refuses requirements no applicant could satisfy.

Predicates are a fail-closed accept list. A statement counts only under a predicate the community has registered (vtc/endorsement-types/* — the name kept from before statements were predicates). A new community accepts the registry’s four core predicates — endorses/1, witnessed/1, vetted/1, presented/1 — seeded once at first boot; a presented statement under anything else is refused, never processed as a generic statement; and a predicate cannot be withdrawn while a criterion names it or a live statement sits under it. Both are what the console’s Vetting → Requirements page writes, in sentences that say what applicants will be told.

At submit, for every statement whose predicate is the criterion’s statementType, the VTC checks the proof and the v1 shape, the accept list, that the subject is the proven holder of the presentation, that the statement is a vetter’s (it carries the commitment, card digest and declared relationship — a statement the community issued for itself carries none, and is personhood evidence, not admission evidence), that it is for this community, the method, the claims, the age, any documentation floor, and that the issuer is a current member who joined before signing and held a vetter grant recorded by the statement’s validFrom, unexpired then and not revoked now. One statement per vetter counts; the facts — each statement with its counted flag and failure codes, distinctCountedVetters, byMethod, commitmentsConsistent, independenceOk, needs — become input.evidence.vetting for the join policy, and are recorded beside the request for an administrator to read later. The default policy refers inconsistent identity commitments and exceeded relationship caps to review, asks for more with the precise shortfall (vetting:statements:1, vetting:method:inPerson:1), and otherwise admits as member or at the invited role. Neither an invitation nor a trusted credential bypasses a vetting criterion. A vetter can withdraw a statement (vtc/vetting/revoke-statement/0.1, keyed to the authenticated sender); withdrawals land in the community’s action list as a review queue, and a member admitted on a withdrawn statement reads underReview: true to the eligibility sweep.

One more statement under the same predicate matters here. Since VTI #1874 (2026-10-01) the community’s own identity check — what used to be a plain IdentityVerificationCredential — is also a vetted/1 statement, issued under the community’s DID and bound to a uniqueness pseudonym, so every identity check in the graph has one shape whoever made it. OpenVTC keeps it and offers it as personhood evidence (#408); the VTC never counts it toward admission.

Three Clients, One Protocol

Every wire type is generated from its published specification in dtgwg-trust-tasks-tf — the peer tasks vetting/request, vetting/session, vetting/decline and vetting/attestation at 0.1, and the community tasks under vtc/vetting/* (trust-tasks) — and three independent clients drive the same ceremony:

  • OpenVTC (the TUI, 0.5.0 on 2026-10-05) carries both seats. Its Vetting page has an Applications side and a Vetting desk, and since #438 each is a journey — the whole process, where you are, and why — shared by applicant and vetter. The applicant makes a face for the community without leaving the flow (#358), answers what the community asks through a disclosure, applies as a second persona (#359), sees held statements on their own tab (#423), reviews what the community will receive (#440), and chooses whether to consent to trust-registry publication (#439). The vetter can decline with an optional reason (#441), attests on the session’s method (#437), and keeps nothing about a person once the request is finished (#444). Since #433 credentials, communities and the join page carry two badges — PQC-SIGNED (post-quantum as well as classical proofs) and PCS ZKP — and say the absence in words.
  • Keyring, the Berkman Klein Center’s phone wallet, implements the same full flow in TypeScript on Credo/Bifold: a VtiVetting screen that becomes the vetter’s desk when the phone holds a grant for the community and the applicant’s side otherwise; tickets as QR or spoken code; a byte-for-byte port of the match code; the eligibility VP verified on the phone (advisory, as upstream); the statement checked against the session it received; and submission over vtc/join-requests/submit, with supplement when the community deferred. It writes the vetted/1 shape wherever the community’s manifest counts it and reads both. The 2026-09-23 → 10-07 window (staging releases 211 → 240) is almost entirely this work: vetting UX and refusal wording, personas kept in memory and restored on a new phone, DTG v1 reading and writing, manifest and submit 0.3, and a dev-flagged TSP peer leg. Hidden vetting is not on the phone yet.
  • The community’s side — the admin console (Vetting → Requirements / Vetters / Registry preview / Automatic grants / Withdrawals, gated by the vtc.vetting.manage capability, which the built-in vetting-lead role holds) and cnm vetting …, which signs the same Trust Tasks from the command line.

The PGP bootstrap is the fourth entrance, for a community that already has a web of trust. cnm vetting bootstrap-pgp --keyring … --roots <fingerprints> --max-depth N --links ./links reads a keyring, walks third-party certifications from the root keys (SHA-1 and RIPEMD-160 certifications made after GnuPG’s 2019-01-19 cut-off are not trusted; MD5 never), and names as vetters the current members whose keys are within reach — a member proves which key is theirs by clear-signing one line, openvtc-link: <their member DID>, with it. It prints a plan (grant / already granted / too far / not a member / invalid link) under --dry-run, grants on the real run, and does not keep the web of trust in sync afterwards. It names vetters once; it is not the design’s “PGP bridge” that would count a key signature as applicant evidence, which remains unbuilt.

Hidden Vetting: Proving Enough Vetters Without Naming Them

The V0 ceremony has one deliberate leak: the VTC learns which members vetted each applicant. That is accountability by design (D7) — but it is also exactly the who-vouched-for-whom graph the kernel’s keyring publishes, kept privately by the community operator instead. The design listed “k-of-n proofs over hidden vetters” as a V2 item. It was pulled forward, and shipped in Eucalyptus as an opt-in mode beside the named flow, which it does not change.

What PCS is, in a paragraph. The Predicate Credential System (predicate-credential-system, a Rust crate on arkworks over BLS12-381) lets a helper issue blind credentials to users; a credentialed user can attest for another user’s identifier; and the second user then proves in zero knowledge that they hold k attestations from pairwise-distinct credentialed users, none of whom is themself. Every public value is a tag — a key-injective PRF of the attester’s secret key and the subject’s identifier — so the same vetter attesting twice for one applicant is caught (DuplicateAttester), a vetter’s attestations for two applicants cannot be linked, and no tag can be tied to a member, even by the helper that issued their credential. An attestation discloses one thing: the class label of the credential it was made under, which is how “only vetters count” is enforced. In this ecosystem the helper is the VTC, the credentialed users are its vetters, and the proof stands where the statements stood. The applicant-to-vetter ceremony — ticket, session, match code, card, human check — is unchanged; only the artifact the vetter hands back, and what the VTC verifies, differ. The requirements line on the applicant’s screen ends: their names never reach this community.

How it is wired (openvtc docs/design/vetting-hidden-vetters-pcs.md; VTI vti-vetting-pcs; vtc-service vetting/pcs_*):

  • Keys are derived, not stored. Turning the mode on (vtc/vetting/hidden/publish/0.1, from the console or the API) derives the community’s helper key pair and token key pair by HKDF from the same master secret the credential signer uses, with the community DID in the seed, and publishes the public halves on the criterion under vetting.ext — which moves its requirementsDigest, so applicants mid-application are told their requirements changed, correctly. Nothing new is provisioned or backed up; a community that can issue a membership credential can run hidden vetting. If the signer ever changes, every enrolled vetter’s credential stops verifying and a publish is refused (signerChanged) rather than silently re-keying.
  • Epochs live in the class label, not the key. An early draft rotated the helper key per epoch; the second-pass review found that attestations under two keys cannot share one proof, so instead the helper key is long-lived and the vetter’s credential carries the period in its label — vetter/2026-10 — with the community accepting the live labels (this month’s and last month’s). A vetter enrolls once per label (vtc/vetting/vetters/pcs-root/0.1), bound for good to one PCS identifier so a re-enrollment produces the same tag and never a second vote; removal is simply not re-issuing the next label.
  • Tokens are the velocity cap. The named flow could cap how often each vetter vouched; a hidden one cannot, because counting per vetter is the thing being hidden. So every vetter’s client draws the same number of attestation tokens — Pointcheval–Sanders blind signatures on random serials — on every tick, unconditionally and on a schedule, whether or not they vetted anyone (a fetch that happened only when someone was busy would announce that they were busy). A tick is a window of time from the label’s start (tickLength, default three days; a tick not yet begun is refused tickNotYet), the community publishes the drip rate, a token is spent into each attestation and checked at submit, and an event label with a higher rate opens a conference desk for its dates — approved by an administrator who is not the one proposing it and not vetting there.
  • Freshness is the community’s. A proof verifies as often as it is submitted, so the applicant first asks the community for a single-use challenge (vtc/vetting/pcs-challenge/0.1, 15-minute lifetime, consumed on any submission that carries a proof) and binds the proof to it; the second submission of the same bytes finds no challenge.
  • The VTC counts what it always counted. The verifier turns the proof into the same StatementFacts the named path produces, with a tag where the vetter’s DID would be — so requirements::evaluate and join.rego are untouched, method floors and independence caps still apply (the per-attestation metadata travels in the clear, at day granularity), and the admin console shows three tags where three DIDs would be, with a note that the count came from a proof. Tags are masked before storage, because the one thing a future quantum adversary could do to a recorded tag is recover the vetter’s key and link every attestation they ever made; forgery, by contrast, is fixed by migrating.

What it costs. Per-vetter velocity caps, cascade review of a bad vetter’s admissions, and the ability to withdraw one statement by issuer are given up or weakened; distinct counting and self-exclusion are what the tags provide. The vetter’s own client can compute all of its tags, so anonymity holds against the VTC, other members, applicants and the mediator — not against the device or agent holding the key (today the OpenVTC client; the design’s direction is the VTA, with a step-up on attest).

Status. vetting-pcs is a non-default feature of vtc-service: a community built without it cannot verify a hidden proof and, by design, does not advertise the mode; the client needs no flag and reads the mode from the manifest. The crate is a research artifact — unaudited, with plain Fiat–Shamir (so blind signing is serialized per key), published to crates.io in the last days of September and taken by both repos at once (openvtc #396, VTI #1838). Hidden vetting runs on one criterion per community, a vetter who has not enrolled is refused before signing anything (openvtc #449), and the live labels do not advance by themselves. Keyring does not implement it. The protocol can be run with nothing around it: cargo run -p openvtc-vetting-pcs --example hidden_vetting.

What Stays Out, and Where It Stands

V0 shipped in full, and several V1 items came with it: the opt-in vetter directory (vtc/vetting/vetters/profile and list — profile, languages, methods, country and city, events; filtered and paged, callable by anyone the community can identify), the automatic-grant sweep, withdrawal notices with a review queue, and the PGP bootstrap. From V2, hidden vetting. Still out, as of Eucalyptus: VTA-side tickets and gate (tickets are client-local), push notifications, the PGP bridge as applicant evidence, cascade review, concern reports, IDVP evidence as a statement substitute, Sybil-proof uniqueness across different join DIDs, cross-community recognition of statements, portraits, and offline in-person vetting. Known limits the VTI documentation states plainly: statements are counted on the VP-submit path only; a revoked grant stops every statement its holder signed counting, including those signed while it stood (erring toward not admitting); requirementsGrace is not applied; distinct vetters are distinct member DIDs, so one person holding two memberships would count twice; and a withdrawal after admission does not yet suspend the membership it helped grant.

After admission, the kernel payoff is on community-git-namespaces: the member’s git.commit.sign right, published to the trust registry and checked by verify-trust in CI.

See also: kernel-web-of-trust, verifiable-trust-community, personhood-credential, statement-credential, authority-credential, zero-knowledge-proofs, keyring-wallet, openvtc, verifiable-trust-infrastructure, dtg-credential-spec