← back to the cover

VeryGoodWallet: the identity wallet I wanted to build years ago

The story and technology behind a passkey-native, pure-TypeScript identity wallet, an issuer, and two verifiers. The setting is fictional, but the protocols and cryptography are real. You don't need a background in zero-knowledge proofs to follow along. If you have one, there's plenty of technical detail here to dig into.

Where the idea came from

I have been thinking about versions of this project for a long time. The idea goes back to what first excited me about decentralized identity and to the conversations that eventually led my two co-founders and me to start Trinsic.

We met through the early decentralized identity community, at a conference where people were building and promoting technologies such as AnonCreds and the work happening around the Sovrin Foundation. The three of us shared a belief that people should have more control over how their identity information was held and used.

The wallet was the part I couldn't stop thinking about. I loved the idea that someone could hold trusted credentials, decide when to use them, and prove only what was needed instead of handing over an entire identity document.

That idea shaped our earliest work at Trinsic and stayed with me throughout the five years I spent there. We were able to explore important parts of it using the technologies available at the time, but several pieces that now feel essential were not ready yet.

Passkeys did not exist. Key management was difficult. Credential issuance and presentation protocols were still developing. Browser-based cryptography came with significant practical constraints. Building the experience I imagined meant asking users and developers to carry much more complexity than I wanted.

This particular question stayed with me: what should a private, user-controlled identity wallet actually feel like?

VeryGoodWallet is my way of returning to that question. It explores what the wallet vision that first excited me can look like with the tools available today.

The pieces have caught up

Passkeys offer a practical foundation for wallet key management. OpenID for Verifiable Credential Issuance and OpenID for Verifiable Presentations provide much better plumbing between issuers, wallets, and verifiers. W3C Verifiable Credentials provide a recognizable document model for signed claims, and BBS signatures give us a strong cryptographic foundation for selective disclosure and freshly randomized presentations.

VeryGoodWallet is a passkey-native system implemented end to end in pure TypeScript. The wallet derives its keys from a passkey and constructs every holder-side proof directly in the browser. The same cryptographic implementation verifies those proofs in Cloudflare Workers. There is no WASM, native cryptographic library, circuit compiler, or wallet backend involved in the holder's cryptography.

The individual pieces come from different parts of the identity, web, and cryptography communities. What interested me was whether they could now be pulled together into one coherent, browser-native experience. The rest of this article looks at that system in three layers: the privacy properties it provides, the standards and protocols that connect its participants, and the browser-native infrastructure that makes the wallet practical.

A credential, up close

A verifiable credential is a signed set of claims. An issuer, such as a DMV, university, or employer, signs those claims and gives the credential to you. Later, you show it to a verifier, who checks the signature with the issuer's public key. The verifier does not need to call the issuer, so the issuer never learns where you use the credential. The format is a W3C standard, and the demo's Utopia driver's license is a W3C Verifiable Credential.

With a wallet and a signed credential in it, two problems appear immediately, and they shape everything that follows. With a typical digital signature:

  • You have to show everything. The signature covers the whole document, so removing one field breaks it. Proving that you are over 18 means sharing your name, address, and exact birthday too, which is the same problem you have with a physical ID card.
  • Presentations can be linked. If the same signature is shown in two places, the bytes match. Two verifiers can compare them and connect their records, even if you shared different fields with each one. The issuer can recognize its own signature too.

The question, then, is whether you can prove that an issuer signed your credential without showing the whole thing or giving verifiers something they can compare later. That is the question the rest of this article answers.

The anatomy of a credential

Before the how, it helps to look closely at the what. Below is the license the demo DMV issues. It is a real signed document produced by the demo's own stack, with only the long strings shortened. The highlights are the parts the rest of the article keeps returning to.

figure 1 · a signed credential, verbatim

The license as the wallet holds it

{
  "@context": [
    "https://www.w3.org/ns/credentials/v2",
    "https://w3id.org/vdl/v1",
    "https://w3id.org/vdl/aamva/v1",
    "https://verygoodwallet.com/contexts/vgw/v1"
  ],
  "type": ["VerifiableCredential", "Iso18013DriversLicenseCredential"],
  "name": "Utopia Driver's License",
  "issuer": {
    "id": "did:key:zUC74Snr3hDGwENw4db3…oJfwUKdXiq"1,
    "name": "Utopia DMV"
  },
  "validFrom": "2026-07-14T00:00:00Z"2,
  "validUntil": "2032-07-14T00:00:00Z",
  "credentialSubject"3: {
    "type": "LicensedDriver",
    "driversLicense": {
      "type": "Iso18013DriversLicense",
      "document_number": "UDL-2984-1156",
      "given_name": "Jamie",
      "family_name": "Voss",
      "birth_date": "1996-03-14"4,
      "age_over_18": true5,
      "age_over_21": true,
      "age_over_25": true,
      "issuing_authority": "UADMV",
      "issuing_country": "UA",
      "issue_date": "2026-07-14T00:00:00Z",
      "expiry_date": "2032-07-14T00:00:00Z"
    }
  },
  "credentialStatus": {
    "type": "VgwRevocationRegistryEntry",
    "revocationRegistry": "https://dmv.verygoodwallet.com/api/registry"6,
    "revocationId": "53685601770269079567…59075766165998"
  },
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "credkit-bbs-sha-2026"7,
    "verificationMethod": "did:key:zUC74Snr3hDGwENw4db3…#zUC74Snr…",
    "proofPurpose": "assertionMethod",
    "proofValue": "u2WMEhlhQjT7nv6WxLK9BfqRukmgnh5Uega…"
  }
}

1Who signed it

The issuer is a did:key, which spells the DMV's public key as an identifier. Checking the signature needs only this document and this key; the DMV is never contacted and never learns where the license is used.

2Always disclosed

The issuer and validity window are the credential's only mandatory fields. Every presentation reveals them. They are kept date-granular on purpose: a millisecond timestamp would be a unique value that follows the credential around.

3What's missing: you

There is no id here: no DID, no holder key, nothing. An identifier in the subject would surface in every presentation and link them all. What ties the license to Jamie instead is a hidden link secret, explained below.

4One date, signed twice

Signed once as readable text and again as a hidden numeric twin: day 35,136 counted from 1900. The twin never appears in the document; it is what age proofs compute against later, at any cutoff a verifier picks.

5Frozen booleans

Age flags are computed once, on issuance day, and never again. They can only answer the questions the DMV anticipated. The demo keeps them to contrast with the live range proof over the hidden twin.

6Revocation coordinates

The registry URL is shared by every DMV credential, so it identifies nobody. The revocation id is signed as another hidden twin: presentations prove it is not on the revoked list without ever showing it.

7The engraving

A W3C Data Integrity proof under credkit's experimental BBS suite. Everything cryptographic lives in the 447-byte proofValue. The strip below decodes it.

Inside proofValue: 447 bytes, decoded

tag3 B
BBS signature80 B
header96 B
issuer public key96 B
HMAC key32 B
mandatory pointerstext
twin declarationstext

The 80-byte signature itself

A: a point on BLS12-38148 B
e: a scalar32 B
  • envelope tag: a multibase u plus three tag bytes (d9 63 04) naming a credkit holder-bound base proof. This value can never be mistaken for another suite's.
  • BBS signature: 80 bytes whether it covers five messages or fifty. Here it covers 32 slots: the license's 28 canonicalized statements, the two hidden numeric twins, and the holder's blind link-secret commitment with its blinding factor.
  • header: three 32-byte hashes pinning the proof configuration, the mandatory-pointer set, and the twin declarations. Change any of them and the signature no longer verifies.
  • issuer public key: the DMV's BLS12-381 key, cross-checked against the did:key in the document.
  • HMAC key: keys a pseudorandom shuffle of statement labels, so the numbering of statements never becomes a fingerprint of its own.
  • mandatory pointers: /issuer, /validFrom, and /validUntil are the fields every future presentation must disclose.
  • twin declarations: birth_date → date1900, revocationId → frScalar: which hidden twins the signature carries and how each one is encoded. They sit as plain text in the base64 tail of proofValue.

The wallet keeps this document exactly as issued and never sends it to anyone. Everything a verifier ever sees is derived from it.

Privacy properties and the proofs behind them

The cryptography in VeryGoodWallet is easier to understand as a set of privacy rules. Reveal only what a verifier needs. Do not add a stable holder identifier. Keep separate presentations unlinkable by default. Let the holder prove a connection only when they choose to. Prove conditions without exposing the underlying values, and prove that a credential remains valid without telling the issuer where it was used. Different proof systems work together to enforce those rules.

When a presentation needs more than one statement, the wallet combines its BBS, predicate, equality, and non-revocation proofs under one Fiat–Shamir challenge. The shared transcript binds the components to one another and to one verifier request. The sections below explain what each component proves within that composite construction.

Reveal only what is needed

The proof block at the bottom of the license names the signature scheme: BBS, after Boneh, Boyen, and Shacham, whose 2004 work on group signatures it grew out of. It is now being standardized in an IETF draft. Two properties separate it from signatures the web already uses, such as ECDSA or Ed25519.

First, BBS signs a list of messages, not one blob. The wallet's processing turns the license into a list of statements, roughly one per field, and the DMV signs the list. Figure 1's license becomes 32 signed slots: 28 statements from the document, two hidden numeric twins, and the blind-committed link secret with its blinding factor. The signature is 80 bytes no matter how long the list gets.

Second, the signature itself is never shown to anyone. The wallet keeps it. What a verifier receives instead is a proof: a freshly randomized demonstration that the wallet holds a valid DMV signature over the full list. The proof reveals exactly the messages the holder chooses and nothing about the rest. The verifier learns that the hidden messages were signed, but not what they say. That is selective disclosure. The same license can reveal a birth date to one verifier and keep it hidden from another, with the wallet's consent screen controlling the choice.

For the cryptographically inclined

A BBS-derived proof is a zero-knowledge proof of knowledge of the signature: a sigma protocol with the same commit, challenge, and response steps as Schnorr identification, made non-interactive by hashing the transcript with Fiat–Shamir. The arithmetic is pairings on BLS12-381. There is no circuit compiler, no proving key, and no setup ceremony.

Bind without identifying the holder

Removing a stable holder identifier creates an obvious question: what stops someone else from presenting a stolen credential? Adding an identifier such as credentialSubject.id would solve that problem, but it would also surface in every presentation and link them all. That is why figure 1's subject has no id. The DMV deliberately issues no DID, public key, or holder property.

The wallet solves that problem with a lifelong link secret derived from its passkey. At issuance, it sends the DMV a commitment to that secret, along with proof that it knows the value behind the commitment. Following the IETF's blind BBS extension, the DMV signs the committed value into the credential without ever seeing it. It is one of the 32 slots under figure 1's signature.

From then on, every presentation proves that the presenter knows the same hidden secret, without revealing an identifier, not even a pairwise one. And because the wallet commits the same secret into every credential it collects, it can also prove that two credentials belong to the same holder when the holder chooses to prove that connection. The infrastructure section returns to how the passkey creates and protects that secret.

Stay unlinkable across verifiers

Selective disclosure controls what a verifier learns, but privacy also depends on whether two verifiers can recognize the same credential. Every BBS-derived proof uses fresh randomness, so presenting the same credential twice does not produce matching proof bytes. Because the credential contains no stable holder identifier, the proofs themselves give verifiers nothing persistent to compare.

Unlinkability is the default, not a promise that all correlation is impossible. Values deliberately disclosed in two places can still overlap. For range and membership proofs, the wallet also checks published proof parameters before using them. That prevents a verifier from handing each visitor slightly different parameters as a hidden tag.

Prove facts, not values

Whenever you use a credential, the wallet creates a presentation for one verifier and one session. It can reveal selected fields, but it can also prove useful facts about values that remain hidden.

Range proofs prove an inequality about a hidden number. When the DMV issues a license, it encodes the birth date as a number, measured in days since 1900, and signs that hidden twin alongside the readable date. At presentation time the verifier picks a cutoff, and the wallet proves birth_date ≤ cutoff against the twin. Earlier birth dates are smaller numbers, so "at least 18" is one comparison. The Nightcap checks an 18-year cutoff; Utopia Wheels checks 25 against the very same credential. Contrast that with the license's age_over_18/21/25 flags, which were frozen at issuance: flags can only answer the questions the issuer anticipated, while a range proof takes any cutoff, live. For the demo's under-18 persona, the wallet simply cannot construct the proof. There is no way to lie with it.

Membership proofs prove that a hidden number belongs to a set the verifier publishes. The Utopia Resident Registration carries a numeric district code as a hidden twin, and a proof can show the code is one of the coastal districts without revealing which one.

For the cryptographically inclined

The range and membership proofs use the 2008 Camenisch–Chaabouni–Shelat construction. The hidden value is decomposed into base-16 digits, each proven to lie in the verifier's signed alphabet. These proofs live in the same sigma-protocol family as the BBS proof, which allows them to participate in the composite construction described above.

Link credentials only by choice

Equality proofs show that two hidden values are the same. The important case here is the link secret shared by every credential in the wallet. The equality proof joins the credential and predicate proofs in a composite presentation to establish that the credentials carry the same hidden link secret.

Presentations are unlinkable by default, but sometimes the holder wants to prove a connection. Utopia Wheels' resident-rate discount needs three facts: the customer is over 25, lives in a coastal district, and holds both credentials. You could prove that last part by disclosing a name from each credential and letting the clerk squint. This demo does it without sharing anything:

figure 2 · the resident-rate check

Two credentials, one statement about their holder

disclosed stays hidden hidden, but proven about holder binding

Utopia Driver's License

birth_date"1996-03-14"proved ≤ day 37,091
issuer"Utopia DMV"
validFrom → validUntil2026-07-14 → 2032-07-14
revocationId"53685…65998"proved unrevoked
link secretcommitted inside the signature

Utopia Resident Registration

stateFips"11"proved ∈ {11, 12, 13}
issuer"Utopia DMV"
validFrom → validUntil2026-07-14 → 2028-07-14
revocationId"48445…99756"proved unrevoked
link secretcommitted inside the signature

1The only disclosures

The issuer and validity window from each credential form the mandatory set. In this particular presentation, no other field is revealed at all.

2Over 25, proven live

Utopia Wheels computes today's cutoff: born on or before 2001-07-21 (day 37,091). The wallet then proves hidden birth date ≤ cutoff. Same hidden twin, any cutoff: the bottle shop asked for 18 with a different bound.

3Somewhere coastal

The registration's hidden district code is proven to belong to the verifier's published set {11, 12, 13} (Port Azure, Meridian Shores, and Coral Landing) without revealing which one.

4Still valid, both of them

Each credential proves its hidden revocation id still belongs to the DMV registry's current accumulator. The ids themselves never appear. The registry section below explains the machinery.

Utopia Wheels asked for three things: over 25, coastal resident, both credentials held by the same person. Every row marked hidden stays home.

The wallet combines all three checks in one composite presentation. The equality proof confirms that both credentials hide the same link secret. The verifier learns the intended results and nothing else, and it cannot discover the connection later from presentations made separately. The holder chooses when the link is provable.

The anatomy of a presentation

Here is what actually leaves the wallet for that rental-counter check: the entire response body, reduced only where marked. It is worth comparing the embedded license against figure 1: the document that crosses the wire is mostly absence.

figure 3 · what actually crosses the wire

The presentation, byte for byte

{
  "@context": ["https://www.w3.org/ns/credentials/v2"],
  "type": "VerifiablePresentation"1,
  "verifiableCredential": [
    {
      "@context": [ …as issued… ],
      "type": ["VerifiableCredential", "Iso18013DriversLicenseCredential"],
      "issuer": { "id": "did:key:zUC74Snr…", "name": "Utopia DMV" }2,
      "validFrom": "2026-07-14T00:00:00Z",
      "validUntil": "2032-07-14T00:00:00Z",
      "proof": {
        "type": "DataIntegrityProof",
        "cryptosuite": "credkit-bbs-sha-2026",
        "verificationMethod": "did:key:zUC74Snr…",
        "proofValue": "u2WMHhaEAA4YAAQIDBAWAFoKCeCwvY3JlZGVud…"3
      }
    },
    { …the resident registration, reduced the same way… }
  ],
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "credkit-bbs-presentation-sha-2026",
    "proofPurpose": "authentication",
    "challenge": "LARp0Qf79jMVN_LHoiyhBw"4,
    "domain": "redirect_uri:https://rentals.verygoodwallet.com/…"5,
    "proofValue": "u2WMIhVkLKAAAAAAAAAACAAAAAAAABDCNdv9qjsXk…"6
  }
}

1Still nobody named

No holder property, DID, or key appears anywhere. Control of both credentials is proven inside the proof, never asserted beside it.

2Stripped to the mandatory

Of the license's thirty-odd signed statements, three fields survive: issuer, validFrom, and validUntil. Compare this embedded document with figure 1. Everything else is simply gone.

3A map, not a signature

155 characters telling the verifier how the revealed statements line up with signed message slots and which hidden twins exist. birth_date and date1900 are readable in the base64. The 80-byte signature itself never leaves the wallet.

4The verifier's nonce

A one-time session nonce, folded into the proof's transcript. Replay the presentation at any other session and verification fails.

5Locked to one counter

The proof's audience is this verifier's response endpoint. A presentation minted for Utopia Wheels convinces nobody else.

6Six proofs, one challenge

3,162 bytes carrying two credential proofs, a range proof, a membership proof, and two non-revocation proofs, all bound under a single merged challenge. The strip below decodes it.

Inside the presentation proofValue: 3,162 bytes

tag3 B
license proof1,072 B
registration proof848 B
range456 B
set112 B
non-revocation2 × 128 B
challenge32 B
claim records~300 B

One credential proof up close: the license's 1,072 bytes

Ā48 B
48 B
D48 B
ê, r̂₁, r̂₃3 × 32 B
m̂₁ … m̂₂₆26 × 32 B
  • credential proofs: one per credential: three freshly randomized curve points, then 32-byte response scalars consisting of ê, r̂₁, r̂₃ plus one per hidden message (26 for the license, 19 for the registration). New randomness every presentation, so no two ever share a byte.
  • predicate proofs: the range proof splits the birth-date twin into four base-16 digits proven against the cutoff (a 2008 Camenisch–Chaabouni–Shelat construction); the set proof matches the district twin against the coastal alphabet; the non-revocation proofs place each hidden revocation id in the registry's current accumulator.
  • merged challenge: one Fiat–Shamir hash over every commitment above plus the nonce, the domain, and the link-secret equality. Every sub-proof must verify against this same value, so the pieces cannot be re-mixed, replayed, or presented separately.
  • claim records: the public statement of what was proven: bound day 37,091, four digits, the set and parameter hashes, the accumulator value and epoch, and the equality. The verifier restates every one from its own policy rather than trusting the wire.

the verifier learns

  • born on or before 2001-07-21 (over 25)
  • lives in one of the three coastal districts
  • both credentials belong to the presenter
  • neither credential is revoked
  • both issued by Utopia DMV, currently valid

it never sees

  • name or license number
  • the birth date itself
  • which district, or the postal code
  • the revocation ids or the link secret
  • anything it could match at another verifier

This is not a mock-up: the presentation shown here was generated by the demo's real stack and verifies against the rental verifier's restated policy.

What two verifiers can compare

The wallet keeps an encrypted log of every presentation, and at the end of the demo it lays out what each verifier received side by side. The proofs themselves contribute nothing matchable: no holder identifier exists, and every proof is freshly randomized. Only values deliberately disclosed to both verifiers can overlap. Visit the bottle shop with a range proof, then share your name and license number only with the rental counter, and their two databases hold no common value that connects the records.

Revoke without tracking the holder

A private credential still needs a way to stop being valid. Every credential the Utopia DMV signs is enrolled in an accumulator-backed revocation registry. The credential carries a fresh revocation id as a permanently hidden signed value, shown as figure 1's credentialStatus. The wallet keeps a separate membership witness showing that the id belongs to the issuer's current unrevoked set.

When the DMV revokes a credential, the registry publishes a new epoch and public update records. Every non-revoked wallet can update its witness from the same published data. The wallet downloads the registry state as a whole, so the registry does not learn which credential is asking.

During presentation, the verifier requires a non-revocation proof against the registry state it trusts, represented by the two 128-byte segments in figure 3. That proof joins the composite presentation described above. The verifier learns exactly one fact, that the credential is still valid, and never sees the hidden revocation id or which registry entry produced the proof.

Standards and protocols

The privacy layer describes what the wallet can prove and what it keeps hidden. Standards and protocols give issuers, wallets, and verifiers a shared way to request, carry, and check those proofs.

Issuance with OID4VCI

OpenID for Verifiable Credential Issuance handles the exchange between the DMV and the wallet. The demo uses its pre-authorized code flow. The wallet's holder-binding commitment travels as a documented extension in the credential request, alongside the standard proof-of-possession JWT. OID4VCI provides the flow; blind BBS issuance gives that extension its privacy properties.

Presentation with OID4VP

OpenID for Verifiable Presentations carries requests and responses, while DCQL describes what the verifier wants. Predicate, linkage, and non-revocation requirements are small, documented extensions to that query. Requests are passed by reference to keep QR codes scannable, and responses return through direct_post.

Every proof is bound to the request's one-time nonce and the verifier's identity, so a response cannot be replayed somewhere else. In figure 3, those values appear as challenge and domain.

Credentials and Data Integrity

The credentials themselves are W3C Verifiable Credential Data Model 2.0 documents. Data Integrity supplies the proof envelope, while standard JSON-LD processing turns the document into the statements signed by BBS. Underneath that envelope, the signature core implements the CFRG BBS draft and its blind-issuance companion. The implementation is checked against the drafts' published test fixtures.

Where the experimental layer begins

The standards provide the surrounding flows and document model, but the complete proof layer in this demo is experimental. The W3C's candidate BBS suite, bbs-2023, supports selective disclosure, but not predicates, multi-credential presentations, or a link secret. This demo uses credkit-bbs-sha-2026, an experimental Data Integrity suite from credkit, a companion project of mine.

People who have worked with earlier privacy-preserving credential systems will recognize many of these goals. Some of those systems, including AnonCreds, were an important part of my own history in this field. Credkit approaches the problem through today's BBS, Verifiable Credential, OpenID4VC, passkey, and browser ecosystem.

Credkit keeps bbs-2023's document pipeline, including canonicalization, selection, and mandatory pointers, and replaces the proof layer with the composite construction described above.

It is research work, not a standard. Its identifiers are deliberately distinct, and a draft specification documents the composite construction, an area not covered by the CFRG's emerging sigma-protocols and Fiat-Shamir drafts.

Browser-native infrastructure

The privacy properties and protocols only become useful if the holder can manage keys and construct proofs without handing their credentials to a backend. This is where passkeys and the modern browser fit into the design.

Your passkey is the wallet

Identity wallets have traditionally relied on a long-lived secret, which often meant installing an app, opening a custodial account, backing up keys, or writing down a seed phrase. VeryGoodWallet starts with something you may already have: a synced, phishing-resistant passkey.

On supported authenticators, WebAuthn's prf extension gives the wallet the same secret material whenever you authenticate. The wallet passes that through HKDF to derive the keys it needs:

passkey PRF output
  └─ HKDF
      ├─ vault key      AES-GCM over everything stored at rest
      ├─ link secret    one secret for life, blind-committed into
      │                 every credential, no issuer ever sees it
      └─ issuance keys  one per issuer, for request freshness only

The passkey is more than a login mechanism. It is the root of the wallet's key hierarchy. The derived keys live only in memory while the wallet is unlocked, credentials are encrypted in the browser, and locking the wallet clears the key material. Recovery follows the passkey's existing synchronization model, such as iCloud Keychain or Google Password Manager. The link secret in the middle of that tree is the hidden value used for holder binding and holder-chosen credential linking.

Proofs stay in the browser

VeryGoodWallet is partly an identity experiment and partly a browser-cryptography experiment. The wallet is a static browser application: its keys are derived locally from the passkey, its credentials stay encrypted locally, and every proof in this article, including BBS, range, membership, equality, and non-revocation, is constructed on the holder's side, in the browser, in plain TypeScript. No native library, no WASM build, no SNARK runtime, and no wallet backend ever receives a private credential.

What still needs a service

The demo still has issuer and verifier services, because credential protocols require counterparties with session state, trust policy, and registries. In this demo, those services are Cloudflare Workers, but they do not perform holder-side key management or proof construction.

The cryptography is not tied to a privileged environment. Credkit runs identically in the wallet's browser tab and in a verifier's Worker, so where verification happens is an architectural choice rather than a limitation of the proof system. Registry state is public data that wallets can download and cache as a whole, allowing witness updates without asking about one credential at a time.

What is real and what remains experimental

VeryGoodWallet is a real implementation, not a clickable mockup. The issuer signs credentials. The wallet encrypts and stores them. OpenID4VCI and OpenID4VP move them between parties. The wallet constructs real zero-knowledge proofs, and the verifier checks them. Credentials can be revoked, witnesses can be updated from published registry records, and non-revocation is enforced during presentation. The specimens in figures 1 and 3 came from running that stack, not from an illustration tool.

But it is not a production wallet.

  • It is research software. Neither credkit nor this site has been independently audited. The custom proof and cryptosuite layers do not have an independent implementation or external interoperability test suite.
  • Issuer trust starts with TLS. The verifiers pin the DMV's DID from its metadata endpoint. A production system would need did:web or a trust registry.
  • Privacy still has edges. Information the holder deliberately discloses can still be compared. Credential validity windows create a small correlation surface, and registry access needs appropriate caching and distribution to reduce timing signals.
  • Losing every copy of the passkey means losing the wallet. Passkey sync is the recovery mechanism, so the wallet depends on it.

A vision I have carried with me

I built VeryGoodWallet and much of credkit over a couple of weekends, with modern coding models helping me explore, implement, test, and document the system at a pace that would have been hard to imagine when I first entered this space. The mathematics and privacy goals are not new, but the pieces have become far more accessible.

VeryGoodWallet is not the finished answer to digital identity, but it is the first time I have been able to put this particular idea together in the form I originally imagined: a wallet managed by something people already use, credentials exchanged through recognizable protocols, and privacy-preserving proofs running directly in an ordinary web environment. It is the closest I have come to the wallet I wanted to build years ago.

Everything described here is open source. The complete demo is in VeryGoodWallet, and the cryptography and draft specification are in credkit.