Proof-of-Personhood Without Biometrics: The IRLid Protocol
With Reddit rolling out mandatory human verification and World ID expanding its iris-scanning Orb into Discord, Shopify, and beyond, proof-of-personhood is suddenly a mainstream problem. Most proposed solutions fall into two camps: behavioural analysis (invisible, low-trust) or biometrics (high-trust, high-privacy cost). This writeup describes a third approach — anchoring proof of personhood to a physical real-world interaction between two devices, with no biometric capture and no backend server.
1. The Problem
The core question is: how do you establish that an account is controlled by a unique human, without learning anything else about that human?
Behavioural analysis (reCAPTCHA, Cloudflare Turnstile, Arkose) answers a weaker question: "does this session look like a human?" It fails against sophisticated bots, leaks behavioural telemetry, and produces a probability score rather than a guarantee. Biometric approaches (World ID's iris scan, Face ID) answer a stronger question but require a trusted hardware device, a central database of biometric proofs, and an implicit trust relationship with whoever operates that database.
The specific threat model IRLid addresses: can two parties produce a jointly-signed, time-stamped, location-bound proof that they were physically co-present, using only their existing devices and a QR code exchange? No proprietary hardware. No biometric capture. No always-online server.
2. Protocol Overview
IRLid is a two-scan handshake between two devices. Each device generates a local ECDSA P-256 keypair (via the Web Crypto API) on first use. The private key never leaves the device. The handshake proceeds as follows:
Step 1 — HELLO (Offer)
Person A opens IRLid. The application acquires A's GPS position and generates a signed HELLO object:
{
"v": 2,
"type": "hello",
"pub": { /* A's ECDSA P-256 public key as JWK */ },
"nonce": 3847261049,
"ts": 1743750000,
"offer": {
"payload": {
"v": 1,
"type": "offerPayload",
"lat": 51.509865,
"lon": -0.118092,
"acc": 8.3,
"ts": 1743750000,
"nonce": 3847261049
},
"hash": "base64url(SHA-256(canonical(offerPayload)))",
"sig": "base64url(ECDSA_P256_sign(hash, A_privKey))"
}
}
This object is base64url-encoded and embedded in a URL, which is rendered as a QR code for B to scan.
Step 2 — ACCEPT (Response)
B scans A's QR. B's device first verifies the HELLO: it checks the offer hash, verifies A's signature, and confirms the timestamp is within the 90-second tolerance window. If the HELLO is valid, B's device acquires its own GPS position and constructs a signed response that binds to both the HELLO hash and the signed offer hash:
{
"v": 2,
"type": "response",
"payload": {
"v": 1,
"type": "payload",
"helloHash": "base64url(SHA-256(HELLO_bytes))",
"offerHash": "base64url(SHA-256(offerPayload_bytes))",
"lat": 51.509871,
"lon": -0.118085,
"acc": 6.1,
"ts": 1743750042,
"nonce": 2918476305
},
"hash": "base64url(SHA-256(payload_bytes))",
"sig": "base64url(ECDSA_P256_sign(hash, B_privKey))",
"pub": { /* B's ECDSA P-256 public key as JWK */ }
}
B's response is shown as a QR for A to scan.
Step 3 — Combined Receipt
A scans B's response. A's device verifies B's signature and payload, then checks mutual tolerances:
- Time delta: |ts_A − ts_B| ≤ 90 seconds
- Distance: haversine(GPS_A, GPS_B) ≤ 12 metres
- HELLO binding: B's
helloHashandofferHashmatch the original HELLO - Signature validity: both ECDSA signatures verify against their respective public keys
If all checks pass, a combined receipt is produced:
{
"v": 2,
"type": "combined",
"tol": { "dist_m": 12, "ts_s": 90 },
"hello": { /* original signed HELLO from A */ },
"a": { /* A's signed response */ },
"b": { /* B's signed response */ }
}
This combined receipt is itself base64url-encoded and embedded in a URL, rendered as a QR. Anyone with a QR scanner can open the receipt and verify all signatures client-side — no server call required.
3. Cryptographic Primitives
| Primitive | Choice | Rationale |
|---|---|---|
| Key algorithm | ECDSA P-256 | Native WebCrypto support across all modern browsers; hardware-backed on devices with a secure enclave |
| Hash | SHA-256 | Standard; used for both payload hashing and signature input |
| Key storage | localStorage (JWK) | Current approach — see limitations below |
| Encoding | base64url (RFC 4648 §5) | URL-safe, no padding issues in QR/hash fragments |
| Distance | Haversine formula | Sufficient precision at ≤12m scale |
4. Security Properties
What the protocol provides
- Co-presence guarantee (conditional): Both parties must have been at the same GPS location (±12m) within the same 90-second window. A bot cannot satisfy this without physical hardware at the location.
- Replay resistance: Each HELLO contains a fresh nonce and a timestamp. Responses bind to both the HELLO hash and the signed offer hash. A replayed HELLO will fail the timestamp check after 90 seconds.
- Cryptographic binding: B's response is bound to A's specific signed offer — not just any HELLO. A HELLO cannot be substituted mid-handshake without invalidating B's response.
- No biometric data: The receipt proves physical co-presence without capturing any biometric. The verifier learns that two keypairs were at the same place at the same time — nothing else.
- No backend: The entire protocol runs in the browser. Verification is fully client-side.
- Public verifiability: Any party with the combined receipt URL can verify all signatures without trusting a third party.
Known limitations and attack surface
GPS spoofing. The location data comes from navigator.geolocation, which on desktop is IP-based and on mobile uses GNSS/WiFi. A motivated attacker can spoof GPS coordinates at the OS or app layer. The protocol does not have a mechanism to verify that GPS readings are genuine. On mobile, hardware attestation (SafetyNet / DeviceCheck) could be added as a future layer, but is not currently present.
Key storage. Private keys are stored in localStorage as JWK. This means they are accessible to any JavaScript running on the same origin, and are not protected by a secure enclave at rest. A device compromise means key compromise. Using the WebCrypto extractable: false flag for the private key material (storing only the public JWK, and regenerating from an imported private key) would improve this — but the non-extractable key could not then be backed up. The current tradeoff is usability over key confinement.
Social collusion. Two colluding parties could manufacture a receipt by simply meeting and scanning. The protocol makes no attempt to prevent this — its goal is to prove physical co-presence, not to assert that the meeting was meaningful or authorised. How a relying party uses receipts (e.g. requiring receipts from multiple independent parties, or from parties with established trust graphs) is an application-layer concern.
Sybil resistance is partial. A single person with two phones can produce a receipt. The protocol reduces bot-scale Sybil attacks (you cannot script thousands of receipts without physically moving hardware) but does not prevent a human with multiple devices.
No identity binding. The keypairs are device-local and ephemeral. A receipt proves that two keypairs were co-present — it does not prove that a specific human controls those keypairs, unless that binding is established by a higher-level system.
No key revocation. There is currently no mechanism to revoke a key or invalidate receipts issued by a compromised keypair.
5. Comparison to Alternatives
| Approach | Biometric data | Backend required | Hardware required | Bot resistance |
|---|---|---|---|---|
| reCAPTCHA / Turnstile | No | Yes | No | Probabilistic |
| World ID (iris scan) | Yes (iris code) | Yes | Yes (Orb) | Strong |
| Passkeys (FIDO2) | Optionally (Face ID / fingerprint) | Partial | No | Moderate (device-bound) |
| Government ID | Yes | Yes | No | Strong (but invasive) |
| IRLid | No | No | No | Moderate (physical presence required) |
IRLid occupies the gap between "no friction, low trust" (CAPTCHA) and "high friction, high privacy cost" (iris scan / government ID). It is not a drop-in replacement for either — its value is in contexts where physical co-presence is meaningful: in-person event check-ins, peer-to-peer trust bootstrapping, local community verification, and hybrid systems where receipts accumulate into a trust graph over time.
6. Open Questions
A few areas where input from the security community would be genuinely useful:
- Is there a practical way to attest GPS integrity without proprietary hardware attestation?
- Should the private key be made non-extractable (accepting no backup) to improve key confinement?
- What does a reasonable revocation mechanism look like for a serverless protocol?
- How should a relying party weight a receipt vs. a chain of receipts when evaluating a trust claim?
7. Try It
The application runs entirely in your browser. Open IRLid on one device, scan with a second device, and the receipt is produced locally. No account required. The source is visible in the browser.
Feedback and critique welcome via the contact page.