← The Kibitz Engine · deep dive
How Kibitz proves that the party on the other end of this encrypted connection really is who they claim — with no server in the loop and a credential that can't be replayed.
This is the distinctive piece of Kibitz's identity layer. The cryptographic ingredients are standard (an RS256-signed OIDC token, SHA-256, the WebRTC DTLS handshake); the composition — stapling the identity token to the live transport certificate and verifying it peer-to-peer — is the part we built. Internally it's the third identity tier, hence L3.
Companion docs: verification.md (all admission methods, §4.1 is the OIDC entry), threat-model.md (what's in/out of scope).
Plain "Sign in with Google" (OIDC) gives you a token that means "Google attests this is [email protected]." That's necessary but, in our setting, not sufficient — for two reasons:
So the open question is: given a signed "I am alice" token arriving over a peer connection, how does each browser — with no server — know the person operating this specific connection is alice, and not someone replaying her token?
Cert-binding answers it by tying the token to the one thing unique to a live connection: its DTLS certificate fingerprint.
At sign-in you put a hash of your connection's cert fingerprint inside the identity token; every peer re-derives that hash from the cert it actually handshook with and checks it matches — so the token is only valid for whoever holds this connection's certificate.
Every WebRTC peer connection is secured by a DTLS certificate. Its fingerprint (a hash of the
cert's public key material) uniquely identifies that encrypted channel, and — crucially — each
side can read the fingerprint of the certificate the other side presented
(RTCDtlsTransport.getRemoteCertificates(), surfaced via safetyCode.ts). That readable,
per-connection value is the anchor we bind the identity to.
Read your own connection's pinned cert fingerprint fp.
Compute the binding nonce:
nonce = base64url( SHA-256( canonicalFingerprint(fp) + "|" + roomId ) )
(canonicalFingerprint = trimmed, lowercase colon-hex, so both sides hash identical bytes;
roomId salts it — see §5. Implemented in nonceForFingerprint(fp, roomId).)
Run the OIDC sign-in passing that nonce in the provider's standard nonce field. The
provider (Google, or our email-code backend) echoes
the nonce into the signed token — it can't be altered without breaking the signature.
Broadcast the signed token to peers over the data channel.
The prover never reveals a secret. The nonce isn't secret either — it's a commitment to a cert the prover controls.
For a token received from a peer whose connection presented cert fingerprint remoteFp:
verifyIdToken): RS256 signature against the provider's published JWKS
keys (algorithm pinned, never read from the token header), plus iss / aud / exp and
email_verified === true.bindingMatches): recompute base64url(SHA-256(canonicalFingerprint(remoteFp) + "|" + roomId)) and require it to equal the token's nonce.{ email, name, … }); else → reject with a reason.Composed in verifyPeerIdentity (and routed by provider in verifyPeerMulti). No Kibitz server
participates — each browser fetches the provider's public keys and checks locally.
Without step 2, a valid signature only proves the token is genuine — not who is presenting it. The binding turns the bearer token into a connection-bound one:
SHA-256(yourFp + "|" + room) ≠ the
nonce minted for alice's cert. The token is refused. There is no way to present alice's token
over a connection alice doesn't control.So a verified peer isn't "someone who showed a valid alice token" — it's "the entity that, at sign-in, controlled the private key of the certificate securing this exact end-to-end-encrypted channel, and that entity is alice."
The nonce input includes the room id (… + "|" + roomId). A binding minted in room A therefore
won't verify in room B even against the same cert, so a token captured in one room can't be carried
into another. Both peers derive the salt from the normalized room id, so two participants whose
URLs differ only by casing still agree.
Kibitz already lets two people compare an emoji safety code derived from their cert
fingerprints to rule out a man-in-the-middle (safetyCode.ts). Cert-binding reuses the same
fingerprints, so "this is really alice" and "there's no MITM on this channel" collapse into a
single guarantee: a passing binding is a passing SAS, checked automatically by the crypto instead
of by humans reading emoji aloud.
require rooms are online-only by design.alg:"none", HS* with the public key as an HMAC
secret, ES*) is rejected — the verifier pins the algorithm and never derives it from the token.The same check runs at three moments (all client-side, no server):
| Moment | Caller | Effect |
|---|---|---|
| Admission (verified-only rooms) | the authority's makeGateVerify → verifyPeerMulti |
a joiner is rostered only if their token verifies + binds; an unverified peer is never added |
| Per-peer badge + mutual pre-share | useCall.getIdentity → verifyPeerMulti (poll) |
drives the ✓ badge and the verified-roster "hold content until everyone's verified" gate |
| Self-verify | useCall.signInIdentity → selfVerify |
lights your own badge only from a real verify, never the raw payload |
| Piece | File |
|---|---|
| Nonce derivation + binding check | src/core/oidcBinding.ts (nonceForFingerprint, bindingMatches, canonicalFingerprint) |
| OIDC token verify (RS256, alg-pinned, JWKS) | src/core/oidcVerify.ts |
Compose: signature + email_verified + binding |
src/core/identity.ts (verifyPeerIdentity, verifyPeerMulti) |
| Reading the live cert fingerprints | src/core/safetyCode.ts |
| Drivers (admission / badge / self) | src/widget/Widget.tsx (makeGateVerify), src/react/useCall.ts (getIdentity, signInIdentity) |
Status: built and shipped (the L3 identity work). bindsFingerprint: true on the OIDC and
email-code methods.