Status: wellknown.id's witness is live at wellknown.id/witness in the preview, and has worked on an Android phone with a real passport and ID card. A legal review and a DPIA come before it's announced. Running your own works as below, with one file to change; adopting a witness in the field, the DNS form of its config and in-person witnessing are planned.
What a witness does
- The channel. kivi opens a WebSocket (TLS) to the witness. The witness sends a fresh nonce; kivi answers with the chip's access key (from the document's MRZ) and one fresh P-256 key per credential it wants, each signing the nonce.
- The chip. The witness runs the same reader kivi does (@wellknown-id/emrtd), sending each command to the phone, which passes it over NFC. It unlocks the chip (PACE or BAC), then runs Chip or Active Authentication with its own challenge. After Chip Authentication the chip and the witness share keys the phone doesn't have, so the phone can't replay an old session or change what the chip says, and a cloned chip fails.
- The checks. The data matches the SOD, the issuing state's signature verifies against the country signing certificates it trusts (the BSI's master list, pinned to the German CSCA's fingerprint), and the chip proves it's genuine. A chip with neither Chip nor Active Authentication isn't vouched for.
- What it reads: the data page (DG1) and the security files. Never the face.
- What it issues: a batch of up to 10 single-use SD-JWT VCs of the claims it vouches for (
age_equal_or_over,nationalities,issuing_country), each bound to its own holder key, dated to the day, so a batch can't be linked. - What it keeps: nothing. The data page is in memory for the seconds a session takes. No database, and no logs of data or keys.
It proves that a genuine chip, whose data its issuing state signed, was read just now. It doesn't prove that the person holding the phone is the person in the document.
Describing itself
A witness is identified by its domain, and describes itself in its own /.well-known/id, as sites do: a witnesses entry with its name, operator, endpoint, what it vouches for, and its privacy notice, pinned by URL, version and SHA-256. Its signing keys are published as SD-JWT VC issuer metadata at /.well-known/jwt-vc-issuer. kivi fetches the notice when a holder is about to use the witness, refuses one that doesn't match its pinned hash, and asks the holder to agree to that exact version, keeping a consent record. Verifiers decide whose word they take: kivi sends a witnessed credential only to a verifier whose request names that witness in trusted_authorities (OpenID4VP 1.0).
Running one
The reference witness is apps/witness: a small Node server around the protocol in packages/wallet/src/witness.ts.
| Variable | |
|---|---|
WITNESS_ISSUER | who it is: its origin, e.g. https://witness.example |
WITNESS_KEY_FILE | where its signing key lives (made on first start, readable only by it) |
MASTER_LIST | the BSI's CSCA master list, checked against the German CSCA's pinned fingerprint |
NOTICE_FILE, NOTICE_URL | its privacy notice, and where it's published (its hash goes in the config) |
PORT | where it listens (8080) |
Its witnesses entry (name, operator, documents and claims) is in apps/witness/index.ts, set for wellknown.id's: change it to say who you are before you run your own. Put it behind TLS at its origin, with /.well-known/id, /.well-known/jwt-vc-issuer and /witness routed to it.