keyring-wallet — the Keyring phone app
Repo: github.com/berkmancenter/keyring-wallet — the app; its core packages live in the keyring-bifold submodule. Developed at the Applied Social Media Lab of Harvard’s Berkman Klein Center for Internet & Society. Apache-2.0. iOS and Android, React Native 0.81.
Every other client in this wiki assumes a computer: the OpenVTC TUI wants a terminal, the VTA Wallet wants Chrome, pnm wants a shell. Keyring is the phone. It began life outside the OpenVTC ecosystem altogether, as a fork of the OpenWallet Foundation’s Bifold wallet (itself the framework under British Columbia’s BC Wallet), carrying the Berkman team’s own contribution to decentralized trust: peer-to-peer Verifiable Relationship Credentials exchanged between two people who met, witnessed by a third party, with the phone’s Secure Enclave or StrongBox vouching that a real device and a real person signed. The team drafted the first DTG credential specification with the ToIP working group (dtg-credential-spec), and the witnessed exchange Keyring ships is the one that spec describes. Since July 2026 the project has turned toward the OpenVTC stack deliberately, as its plan puts it, “contributor, not consumer”: it recast its exchange onto Trust Tasks and TSP, linked the phone to a cloud Verifiable Trust Agent, and — in a burst of about 1,200 commits across the two repos in September and early October — became the second client after the TUI to carry the full peer identity vetting ceremony, both seats, on a device a person already owns.
The posture matters as much as the code. Keyring is not one of the eighteen repositories in the VTI-Eucalyptus tag set; it is an independent downstream that pins upstream by commit, measures it in a lab it can rebuild, and files numbered findings back. Several OpenVTC and VTI fixes in September and October cite “Keyring VTI-NN” in their titles. That feedback loop — a second, independent implementation testing the first — is the thing Trust Task specifications formally need to leave draft status, and Keyring’s plan says so explicitly.
What It Does
The witnessed relationship exchange — the flagship. Two phones connect, each issues the other a VRC under a pairwise relationship DID (the Relationship Credential Exchange protocol, RCE v3; did:peer R-DIDs, R-Card profile data with an optional photo), and an optional witness server attests that the exchange happened in one session by issuing a Verifiable Witness Credential to each side without seeing their private claims (witnessed-vrc-exchange, witness-credential). “Secure Exchange” adds hardware attestation: a P-256 key in the iOS Secure Enclave (App Attest) or Android StrongBox/TEE signs the credential after Face ID, fingerprint or device passcode, and the attestation chain is embedded as W3C evidence and verified natively on the receiving phone — one OS prompt per exchange since 0.2.0. Locality adds co-presence: the phone advertises over BLE as a peripheral, the witness’s sensor writes a nonce and reads back a transcript signed by the same attestation key, and the proof rides inside the VWC (Android proven live on devices, iOS type-checked only, both shipped behind the witness’s opt-in). Credentials are VC 2.0 with eddsa-rdfc-2022 Data Integrity proofs; everything else a Bifold wallet does — AnonCreds, W3C VCs, Indy VDR, OpenID4VC — is still there through Credo-TS.
Three transports side by side. The legacy exchange rides DIDComm v1 over an Aries-style mediator; since 0.2.0 the same documents can ride DIDComm v2 (Credo’s unreleased v2 from PR #2704, consumed as a pinned 0.7.1-pr-2704 snapshot) and TSP (trust-spanning-protocol, Rev 3 since 2026-09-21) as a third carriage, with HPKE-Auth on @noble so no WebCrypto is needed on Hermes. The team’s noble HPKE backend was contributed upstream and became the TSP wire layer of @openvtc/vti-tsp-js 0.2.0 at Cypress. The phone↔VTA leg and the phone↔community leg are both Trust Task documents in the published tsp/0.1 or DIDComm binding envelopes, signed with eddsa-jcs-2022 document proofs. Keyring does not depend on @openvtc/pnm-core — it treats that package as a reference and implements the VTA client in its own @bifold/trust-tasks and @bifold/core (see keyring-bifold) — but it does take the TSP wire layer from @openvtc/vti-tsp-js, vendored as a prebuilt 0.3.0 (Rev 3) tarball from the browser plugin’s monorepo with a one-file Hermes patch, because 0.3.x is not yet on npm.
Keyring as a PNM: the phone over a cloud agent
In the vocabulary of vta-topology, Keyring is a Personal Network Manager: a thin surface over an always-on cloud VTA — on the VTA Farm or self-hosted — that holds the person’s keys and answers while the phone sleeps. The phone’s own identity is a software key in the wallet; everything community-facing is a persona the agent mints and whose keys the phone borrows into memory only while it uses them. The screens never say “VTA” or show a DID: the agent is “your agent”, identities are “the code the community sees for you”, and every DID sits behind “Show the code”.
- Link. Scan the agent’s enrollment QR or paste its address; the phone signs in with a temporary key, is granted a seat on the agent’s access list, and swaps onto a long-lived key — a reducer-driven state machine (
notLinked → confirming → submitting → awaitingGrant → linking → linked, withoffline,reconnecting,revokedandgoneas sub-states) so leaving and returning to the screen shows the same step. Since 10-01 the Farm’s automatic connection QR ({vta_did, callback_url}) collapses the whole thing into one scan; the callback is treated as a credential and onlyic3.dev/firstperson.devhosts are allowed. - Own. “Create my agent” (09-25 → 10-01) lets a person with no computer create an agent on a hosting service’s website and claim it from the phone: the phone shows the owner code for the Farm’s “Admin DID” box, then connects. Owner acts — adding a backup device, removing one — always ask for Face ID again.
- Devices. A new phone is a new device (
device/register0.2): a devices list with names and last-seen, a one-time offer after a new phone links (keep or remove the old ones), two phones finishing a link between themselves (09-28), heartbeats, and a wipe that the agent refuses at login from then on. - Several agents. A row of agent chips across the top of Your agent, each with its own count of waiting requests, “Add” at the end; a Wallet card says which agent holds it, cards of an unlinked agent are hidden, and the app looks at the other agents’ inboxes when it comes to the foreground.
- Approvals and consent. The phone is the agent’s approver: a pushed consent request shows the match code and what approving would do, and is answered with a signed decision; the agent’s
requireStepUpis answered from the phone with a DID-signedauth/step-up/approve-response; “Ask me before…” (10-04) sets consent rules from the phone for tasks the phone itself never sends (contexts, keys, ACL changes), lists rules set elsewhere read-only, and warns when one would trap the phone behind its own approval. - Push. Signal-style contentless wake pushes from OpenVTC’s push gateway, one wake channel per agent, no Firebase initialized before the person opts in, a notification the phone composes itself (generic text while locked). The gateway image is deployed by this repo’s own workflow for its test stack.
Joining a community from the phone
“I want to join a community” starts from a scanned or pasted keyring://vti/community link (or a vetter’s ticket, or an invitation), reads the community’s join manifest (0.2 and 0.3 — a 0.3 criterion states its own admission, automatic or review, and the screen says a way in admits only when it is automatic), makes the person’s identity for that community with one Face ID, and then offers every way in: an invitation from the admin, vetting by a member, a credential the community asks for, or agreement alone. “I was invited” hands the admin the identity to invite (an invitation names one DID, and there is no invitation by reference yet) and redeems the console’s QR or DIDComm offer; the join itself presents the invitation, the vetting statements or both, and on allow keeps the membership card, role cards and any vetter grant as Wallet cards. Membership is read back the same way the community means it: removal notices, status lists, requestAlreadyOpen, withdraw and supplement, leaving. A community on another mediator is asked over DIDComm from the first message (a TSP Rev 3 relationship did not cross two mediators until the fix in VTI 0.41 / messaging-sdk 0.26.27 — Keyring’s VTI-41 — and the Farm’s mediator must still be told to admit the relayed hop).
The vetting flow, on a phone
This is the part the TUI and Keyring now share end to end, and the reason the two were demonstrated side by side for the kernel community in Prague. The ceremony is the one peer-identity-vetting describes: a ticket gates the vetter, a Vetting Card carries the applicant’s face and legal name, a spoken match code binds the person in the room to the client that controls the join DID, and a Vetting Statement is what the community counts.
One screen, Vetting, seats the person according to what their persona holds: a live CommunityRole: vetter grant puts them at the vetting desk (its own title, a differently colored header); otherwise it is their application. Each step has one filled button, and each step is one screen.
The applicant. Start my application reads the requirements (“This community needs 2 vetters to confirm your name — 1 in person”), then asks for a face photo and the legal name as on the ID (“the name on your profile may be different” — the card comes from a profile face, as in the TUI). Ask a vetter: scan or paste the vetter’s ticket; a ticket for another community is refused with a warning that using it would show your identity there. The request goes to the vetter as a signed vetting/request/0.1 carrying the ticket; the acceptance comes back with the vetter’s eligibility presentation, which the phone verifies against the manifest’s eligibleVetters.role before telling the person the statement will count. When the vetter opens the session both phones show the match code (“Read this code aloud — both screens must show the same one”); the applicant confirms it matches, the Vetting Card is sent inside the session’s #response, and the statement arrives as a signed Trust Task document, checked against the card and the session digest before it is kept. A checklist counts what still is needed by method, discounts statements whose vetter’s grant did not cover the moment of signing (checked against the community’s status list), caps statements from declared relationships at what the community counts, and lights Apply when the criteria are met.
The vetter’s desk. New ticket cuts an eight-character Crockford code with a QR and a link (“Read this code to the person, or let them scan or paste this link”), good for a stated number of uses and a time. Requests arrive under Requests; Open session sends vetting/session/0.1 and shows the match code; Codes match waits for the card; Their card shows the face, the name and the identity code the community sees — the same code the applicant’s phone shows under “Your identity in this community”, so the two can be compared; “I checked them and their ID — attest” issues the statement, and Statement issued shows the confirmed name. A vetter can publish a vetter profile so applicants can find them in the community’s directory, and a desk whose grant was revoked still opens, with the reason it will not sign. Tickets are client-local in V0, exactly as the reference client keeps them.
What the statement is. Until 09-30 a Keyring vetter wrote the WD02 IdentityVetting endorsement; the community’s own in-person identity check is kept as evidence rather than as a vetter’s statement. Since the DTG Credentials v1 cutover upstream (VTI 0.47 and OpenVTC e165bd55, which refuse the old shape with no dual acceptance), Keyring reads both shapes always and writes the one the community counts: the vetted/1 StatementCredential (statement-credential) with @context exactly [VCDM 2.0, registry v1], taskContext and taskDigestMultibase binding it to the session document, and only the registry’s vetted/1 members in the subject. Since 10-01 it writes v1 by default where a community asks for it; a developer flag forces either.
Judged by upstream, not by itself. Three times Keyring’s producer and Keyring’s consumer agreed with each other and not with upstream (the card digest, the statement, the eligibility VP). The rule since 09-25: every document Keyring signs or checks is judged in CI by upstream’s own Rust (card-verify, built from vta-sdk at the VTI pin, plus a second clone at vta-service-v0.47.0 so VTI’s SDK judges the v1 statement), and an interop harness drives Keyring and the real OpenVTC TUI in lockstep in both directions — Keyring vetter / TUI applicant and the reverse — reading assertions from the TUI’s state and the community’s records. The pinned TUI is v0.4.0; the gate’s test community lives on the Farm.
How it compares with the TUI. Same wire, same ceremony, same V0 limits (client-local tickets, no zero-knowledge k-of-n, no automated document check). What differs is the medium: the phone’s camera scans the ticket and the agent’s QR, Face ID stands in for the TUI’s key handling, the two seats are one screen rather than two pages, identities are never shown as DIDs, and the applicant’s identity is minted before the ticket because the desk checks the ticket against it — “the same order as the OpenVTC TUI: the community, its requirements, the identity it will know you as, then the application,” as the join screen’s own comment says.
The feedback loop with upstream
docs/VTI_UPSTREAM_FINDINGS.md (v1.44, 2026-09-24) numbers every defect Keyring measured against a VTI stack it can rebuild — forty-three VTI-NN findings, each with the command, the observation, the upstream source line and the “era” of the stack it was measured on (eras A through H, plus F and F′ for the Farm). None was filed as an issue; they reach upstream through the document, and upstream answered it as a published remediation plan (their KR-NN is Keyring’s VTI-NN): of thirty-six, twenty-three shipped, four already fixed, three answered in docs, four declined or re-scoped. The cited fixes include the mediator’s WebSocket refusal of phones (tdk-rs #831), the per-sender queue cap that let non-collecting members silence a community (#828), consent pushed only to did:key approvers (VTI #1579), the requestMore request that could never be closed (withdraw/0.1, supplement/0.1), and VTI-42: a VTC’s DIDComm router that had never heard of verbs its own service dispatched, resolved by making the binding envelope the only DIDComm carriage on both services — which is why OpenVTC #372 is titled “send every VTC-bound Trust Task in the DIDComm binding envelope (Keyring VTI-42)“. Later rounds reached VTI-61 (OpenVTC #415, one relationship store for TSP invites and replies), VTI-13 (join criteria that state their own admission, VTI #1907) and the proof-set round (VTI #1868). docs/UPSTREAM_ISSUES.md is the one-page list of what is still open; docs/VTI_CONFORMANCE_INVENTORY.md is the table of every document produced or accepted and which upstream code judges it.
Components
| Path | Role |
|---|---|
app/ | The Keyring experience: theme, onboarding (agent slide, notifications), push/ (wake registration, gateway, OS permission watch), demo-profiles/ (starter, trading-card, approver — reference apps built on the core; the shipped build carries none) |
bifold/ | Git submodule → keyring-bifold at a pinned SHA (37f34dc7 on 10-07); every wallet PR that needs new core lands as a chore/pin-bifold-* pointer bump |
e2e/ | Appium two-device journeys: VRC exchange on emulators and real phones, witnessed, locality, DIDComm v2, TSP, migration, and the gated OpenVTC journeys (agent home, link, invite, vetting, join); the release gate’s evidence lives in docs/gate-evidence/ |
scripts/openvtc/ | PINS.json and setup-external / sync-external (clone upstream at pinned SHAs, report drift); local-vti-stack/up.sh (the lab: VTA, VTC, mediator, DID host, Redis); card-verify (Rust, judges cards and statements with vta-sdk); Farm health and VTC-admin tooling |
tsp-reference/ | The runnable reference ladder (ref-00 … ref-20-local-vetting) that backs every technical claim in the plans, with frozen fixtures |
docs/plans/ | Living plans with dated review companions: openvtc-integration-plan.md (Rev 25), keyring-on-the-vta-farm.md and its community_vetting, own_agent, tsp_rev3 subtasks, locality-plan.md, push-notifications-plan.md, vsc-migration-plan.md, openvtc-interop-harness-plan.md, ui-ux-improvements-plan.md, release-flow-plan.md |
deploy/, devops/ | The team’s own mediator and push-gateway deployments |
Implemented vs planned
Shipped (0.2.0 line, TestFlight and Play internal): VRC and witnessed exchange as Trust Tasks over DIDComm v1, v2 and TSP; hardware attestation and one-prompt signing; BLE locality (Android proven on devices); R-Card photos and editable multi-profile; agent link, own-agent claim, Farm automatic connection, devices, two-phone linking, several agents, approvals, step-up, “Ask me before…”, push wake; join by invitation, by vetting or open, manifest 0.3; the full vetting ceremony both seats with vetted/1 statements; DTG v1 reading throughout; credential-exchange/query answered from the wallet; the interop harness Phase 0 and both-direction TUI runs.
Planned or partial: the VSC migration of the witness credential to the dtg:witnessed predicate profile (proposal; the predicate vocabulary does not yet exist upstream); hidden vetting and the PCS challenge (vtc/vetting/hidden/*, pcs-challenge/0.1, vetters/event-mode/0.1 — pinned from trust-tasks 0.27.7 as fixtures only, no client code); the message-protection client contract for the mediator’s acceptance / refusal / expiry rules; VTA-to-VTA carriage of the relationship exchange (a counterparty’s agent answering while their phone sleeps); tickets held on the VTA (vetting/tickets/*, V1); iOS locality verified on a device; the interop harness as a nightly Farm job; store release (the app is alpha, not for production).
Recent Development
Releases: v0.1.0-alpha.1 (2026-05-24), v0.1.0-alpha.2 (2026-06-28), 0.2.0 unreleased — staging builds 205 → 228 through staging-v0.2.0-* tags, shipped to TestFlight internal testers and the Play internal track. 1,276 commits since the public import on 2026-04-15; 697 of them in September.
- 10-07 — pins at Eucalyptus day.
VTI-Eucalyptuswas tagged upstream the same day Keyring moved its pins tovta-service-v0.55.0(822ff78a, “the tag the Farm runs”;vtc-service0.11.58; v0.56.0 named as the next, additive step), affinidi-tdk-rsc0a69f1e(mediator 0.37.0, messaging-sdk 0.33.0, tdk 0.23.0 — what 0.55.0’s lockfile builds against), OpenVTCv0.4.0(the TUI the gate uses; the VTI-67may_claimfix is main-only) and trust-tasks-tfv0.27.7(#342, #343, #345), which brings the hidden-vetting specs in as citable fixtures. Also: onboarding URL disclaimer (#339), push watcher fixes (#324, #330, #332), e2e for the 236 gate (#320–#334). - 10-01 → 10-06 — living with an agent. Automatic Farm connection (bifold #247), Requests screen and member notices (#260, #261), “join another” and rejoin after removal (#262, #267), approval rules “Ask me before…” (#285), approvers follow a key swap (#287), community store per identity (#289), devices named and “devices, not phones” (#307, #308), scan adds another agent and agent chips (#303, 10-06), join shows the identity the community sees (#305, #319), approve asks the owner (#317), a distinct vetter header (#315), one address path and hidden cards of unlinked agents (#339, #348). Wallet side: onboarding agent and notifications slides (#300, #312), push without Firebase before opt-in (#290), the Keyring Firebase project (#289), approvals fixtures generator (#309), card-verify at v0.47 (#271, 09-30).
- 09-28 → 10-01 — DTG v1, devices, push, step-up. Reading
vetted/1,witnessed/1and v1 role VACs (bifold #231–#235, 09-30), writingvetted/1behind a flag then per community (#240, #242) and by default where asked (10-01); statement bound to the session document (#238); devices screen and presence (#174–#176, 09-27/28), two-phone linking (#194), the step-up answered from the phone (#200), consent requests showing the match code (#199), pushset-wake(#196) and the wallet’s push registration and gateway deploy (#241, #244, #278); VTC-admin lab tooling for the test community (#251–#256); release 228 / 229 (release/dtg-v1-auto-229, 10-01). Prague (OSS Summit Europe, 10-01) was the stated target for all of it. - 09-23 → 09-26 — proving it against upstream. My Agent redesign (#115), the e2e Farm harness (#122), VTI conformance vectors and signed fixtures (#162, #163), the VTI conformance inventory (#172) and card-verify (#146, #169, #173, #176), the interop harness against the real OpenVTC TUI (#182, both directions green 09-25), the vetting ticket URL scheme (#165), own-agent plan and decisions (#186–#192), community admin inviting guide (#195), the release gate (#220), release 225 and the 226 pin series. Findings reached v1.44.
- 09-21 → 09-22 — the Prague consolidation lands.
prague-1,prague-2,prague-farm-membership(#67, #69, #76), any VTA with any community (#78), join as a profile (#83), the UI/UX pass (#74, #80), cross-mediator DIDComm (#81), names not DIDs (#92), the phone↔VTA leg on TSP Rev 3 (bifold, 09-21); findings VTI-41, -42, -43 filed the same day; staging moved to internal testing (#60). Upstream answered the whole findings document on 09-22 and fixed VTI-37 to -43 on 09-23. - 09-15 → 09-19 (in bifold) — the client is born. Address a VTA or VTC by DID through the mediator (09-15); the phone as manager of its own VTA and join by invitation as a VTA-held persona (09-16); the ticket link and match code “as upstream defines them” (09-17); “vet a member from the phone — both sides of the vetting guide in Keyring” (09-18); vetter profiles and grant revocation (09-19).
- 09-08 → 09-18 — Trust Tasks and the three carriages. The VRC/witness exchange recast onto Trust Tasks with TSP carriage (#20, #27), the wallet answering
credential-exchange/query(#31), BLE locality on Android then iOS (#21, #38), R-Card photos (#30), reference-app demos (#34), credo-ts 0.7 with DIDComm v2 and TSP over v2 (#54, 09-17), the VSC migration plan (#55); the Farm plan (#26), SDK packaging plan (#24), VTA-carriage plan (#19). - August — the ladder and the gate. VC 2.0 with Data Integrity proofs (#17, 08-05);
tsp-referencePhases A–C run (TSP mechanics with no infrastructure, noble HPKE on Hermes, the task layer between two Credo agents); the 08-17 gate (“investigation complete, implementation can start”) the day Cypress shipped with Keyring’s HPKE insidevti-tsp-js; the witnessed exchange recast rehearsed on devices (08-20); release-flow plan (#22). - July — the OpenVTC integration plan begins (Rev 2, 07-22, after an upstream audit): TSP as the direction, Trust Tasks as the operation layer, three packages (
tsp-core,credo-tsp-adapter, the RN assembly), coexistence of DIDComm v1, v2 and TSP as a stated requirement. - 05-24 / 06-28 — the alphas. VRC exchange, Secure Exchange attestation and witness verification; then device-passcode fallback and the submodule’s rename to
keyring-bifold.
See also: keyring-bifold, peer-identity-vetting, openvtc, verifiable-trust-agent, vta-topology, vtafarm, witnessed-vrc-exchange, relationship-credential, witness-credential, statement-credential, membership-credential, verifiable-trust-community, verifiable-trust-infrastructure, trust-spanning-protocol, didcomm, dtg-credential-spec, persona-credential, first-person-network, coordinated-releases