DEV Community

Gernard Cerma
Gernard Cerma

Posted on

QRVA: A protocol for cryptographic verification of physical QR codes — design decisions and open questions

Physical QR codes have no trust layer.

When your phone decodes a QR sticker on a parking meter, it reads a URL and opens it. That's the entire security model. The camera has no way to verify that the code was placed by the claimed authority — or that it wasn't placed by someone with a label printer and a freshly registered lookalike domain.

This has been underexploited until recently. It is no longer underexploited.

NYC's Department of Transportation issued an official consumer alert in June 2025 after fraudulent QR stickers appeared on ParkNYC meters city-wide. Austin PD confirmed 29 compromised pay stations. In Southend-on-Sea, councils manually removed approximately 100 fake stickers from parking signage. In every case the same thing happened: existing mitigations failed silently. Safe Browsing had nothing to flag. HTTPS verified the attacker's server correctly. The codes looked legitimate because nothing in a QR code identifies who placed it.

QRVA is a protocol we've been building to fix this at the infrastructure layer. This post covers the threat model, every design decision (including the ones we argued about), and — critically — the six areas we believe are still broken. We're publishing before a formal security audit because the protocol needs scrutiny at the design stage, not after it hardens.

The reference implementation is open: github.com/qrauth-io/qrauth


TL;DR

  • QR code fraud is a physical-world attack that bypasses every existing digital trust signal
  • QRVA proposes: ECDSA-P256 signing per issuer, geospatial binding, ephemeral server-generated visual proof, heuristic anti-proxy detection, and WebAuthn passkeys for returning users
  • We chose P256 over Ed25519 because of WebAuthn authenticator compatibility and FIPS requirements — not because it's the better curve in isolation
  • Geospatial binding catches displaced clones but GPS accuracy limits make it useless below ~15m radius in urban environments
  • WebAuthn is the only tier that is cryptographically unphishable — but only for returning users who've enrolled a passkey
  • Six open problems we haven't solved: adaptive anti-proxy detection, Merkle-backed transparency log, tenant identity bootstrapping, sub-second revocation, geolocation API trust boundary, and radius selection tooling

If you find a flaw in the signing model or the anti-proxy heuristics, we want to know. Open an issue or drop a comment here.


1. Threat model

Six attack classes, ordered by sophistication. The first two are the dominant real-world attacks. The remainder are currently theoretical but become relevant once the lower tiers are defended.

T1 — Physical sticker overlay

The attacker prints a QR code sticker pointing to a lookalike domain (parkng-thessaloniki.gr, paybyphone-nyc.com) and places it over or adjacent to the legitimate code. No technical capability required beyond a label printer and physical access.

Why existing mitigations fail: Safe Browsing blocklists require days to propagate; freshly registered phishing domains have no reputation yet. HTTPS is irrelevant — the attacker's server has a valid certificate for their domain. Visual inspection cannot distinguish a printed QR from a genuine one.

T2 — Legitimate domain, fraudulent QR registration

The attacker generates their own signing keys, creates a QR pointing to a convincing endpoint, and places it physically. Without a registry of legitimate issuers and their authorized locations, nothing distinguishes this from a genuine code.

Why existing mitigations fail: Nothing in a standard QR payload identifies the issuer or the authorized deployment location. Any entity can generate a QR pointing to any URL.

T3 — Static page cloning

The attacker scrapes the legitimate verification page and hosts an identical copy on their domain. The victim scans a fraudulent code, lands on the clone, sees convincing trust indicators.

Why existing mitigations fail: Client-side rendered pages are trivially cloneable. Visual trust indicators generated client-side can be reproduced.

T4 — Real-time MitM proxy

Rather than cloning the page, the attacker proxies all requests to the legitimate verification endpoint. Responses are genuinely fresh — defeating static clone detection — but the proxy introduces detectable artifacts: an additional TLS hop, measurable latency overhead, and a fingerprint mismatch.

T5 — Geospatial spoofing

Against a protocol that performs geospatial binding, an attacker who knows the registered coordinates can instruct the victim's browser to report those exact coordinates. The browser Geolocation API is user-controllable and does not authenticate GPS data.

T6 — Tenant key compromise

If an attacker compromises a tenant's ECDSA signing key, they can issue arbitrarily many QR codes that verify as legitimate. This is the highest-severity attack class — analogous to a compromised TLS certificate — and the one with the slowest response path.


2. Protocol response

QRVA addresses the attack classes through four successive tiers. Each tier builds on the previous. All tiers are active simultaneously.

Tier Mechanism Defeats Automated
1 ECDSA-P256 signing + issuer registry T1, T2 Yes
2 Server-generated ephemeral visual proof T3 Yes
3 Anti-proxy heuristics T4 (partially) Yes
4 WebAuthn passkey (origin-bound) T4, all known phishing After opt-in

T5 (geospatial spoofing) is addressed partially by cross-referencing browser Geolocation against IP geolocation. A meaningful discrepancy contributes negatively to the trust score. This is heuristic, not cryptographic.

T6 (key compromise) is addressed by KMS-backed key isolation, a transparency log, and a revocation endpoint. The propagation window is currently 5 minutes. This is not good enough for high-value deployments and is discussed in section 6.


3. Why ECDSA-P256 and not the alternatives

This was the most contested internal decision. The two serious candidates were ECDSA-P256 and Ed25519.

The case for Ed25519 is strong. It is faster, has a simpler implementation surface, eliminates the nonce dependency that makes naive ECDSA implementations dangerous, and has no known patent issues. In a fresh system with no compatibility constraints, Ed25519 would be the default choice.

QRVA has compatibility constraints.

WebAuthn authenticator key material. QRVA's Tier 4 uses WebAuthn passkeys. The FIDO2 specification mandates P256 (COSE algorithm -7) as the required algorithm for Level 1 authenticators. Ed25519 (COSE algorithm -8) is optional and unsupported in several hardware authenticators — particularly older Android FIDO2 implementations and some YubiKey models. Using P256 throughout the stack means a single key type to audit, a single curve implementation to trust, and no algorithm negotiation surface.

KMS availability. Tenant keys are managed in AWS KMS or HashiCorp Vault. P256 has near-universal KMS support. AWS KMS added Ed25519 in late 2023 but it remains unavailable in some regions and absent from several enterprise KMS providers.

FIPS compliance. P256 is specified in FIPS 186-4. Ed25519 is not FIPS-approved. For US government and regulated-industry tenants this is a procurement requirement, not a preference.

The nonce problem. We implement deterministic ECDSA per RFC 6979, which derives the nonce from the private key and message hash. This eliminates the nonce management risk — the catastrophic failure mode of standard ECDSA — while retaining P256.

Why not P384? Security level of P256 is 128 bits against classical computers — sufficient against all known classical attacks. P384 provides 192 bits and produces 96-byte compact signatures versus 64 bytes for P256. QR codes have finite payload capacity. Higher density QRs are harder to scan in degraded conditions: dirt, damage, low light, camera angle. The 32-byte difference is meaningful at scale.


4. Why geospatial binding — and what it doesn't solve

Geospatial binding addresses T2: it is not enough to verify that a QR was signed by a legitimate tenant. The code must have been registered at the location where it's being scanned.

The registration model: when a tenant generates a QR, they supply GPS coordinates and an accuracy radius in meters. This tuple is bound into the signed payload. At scan time, the verifier requests the scanner's location via the browser Geolocation API and computes the Haversine distance against the registered centroid. Distance greater than the registered radius fails verification or degrades the trust score.

// Haversine distance computation (simplified)
function haversineDistanceMeters(
  lat1: number, lng1: number,
  lat2: number, lng2: number
): number {
  const R = 6371000; // Earth radius in meters
  const φ1 = lat1 * Math.PI / 180;
  const φ2 = lat2 * Math.PI / 180;
  const Δφ = (lat2 - lat1) * Math.PI / 180;
  const Δλ = (lng2 - lng1) * Math.PI / 180;
  const a = Math.sin(Δφ/2) ** 2 +
            Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
Enter fullscreen mode Exit fullscreen mode

What it solves: a cloned QR code displaced five blocks from its registered location fails verification at the new location. Simple code copying is caught.

What it doesn't solve:

GPS accuracy constraints. Consumer GPS in open sky provides ±3–10m accuracy. In urban canyons — exactly where parking meter fraud concentrates — multipath reflections degrade accuracy to ±20–50m. Indoors, GPS is unavailable entirely; the API falls back to WiFi triangulation (±15–40m) or cell towers (±100–300m). The practical minimum radius that avoids false negatives in urban environments is approximately 15m. This means the geospatial check cannot distinguish codes on opposite sides of the same city block.

The Geolocation API is not authenticated. navigator.geolocation reports coordinates but does not attest their authenticity. A user with device control can override reported coordinates. QRVA treats geolocation as a probabilistic signal, not a cryptographic guarantee.

Radius selection is unspecified. A restaurant QR on a table has a different appropriate radius than a billboard visible from 50m. Incorrect radius selection — too small causes false negatives that erode user trust; too large degrades the security property — is a systematic misconfiguration risk invisible to end users. We have not defined guidance or tooling for this yet.


5. Where WebAuthn fits — and what it doesn't solve

WebAuthn passkeys are origin-bound at the hardware level. A passkey created for https://qrauth.io authenticates only on https://qrauth.io. This is enforced by the authenticator — Secure Enclave on iOS, Titan chip on Pixel, TPM on Windows — not by JavaScript. A phishing page at any other origin physically cannot activate the passkey prompt.

This makes WebAuthn the only component of QRVA that is cryptographically unphishable. Tier 1 can be defeated if the key is compromised. Tiers 2 and 3 are heuristic. WebAuthn's origin binding is unconditional.

The enrollment flow:

// Triggered after a successful Tier 1–3 verification
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: serverChallenge,           // Fresh from server, single-use
    rp: { name: 'QRAuth', id: 'qrauth.io' },
    user: { id: userId, name: userEmail, displayName: userEmail },
    pubKeyCredParams: [
      { type: 'public-key', alg: -7 },   // ECDSA-P256 (required)
      { type: 'public-key', alg: -8 },   // Ed25519 (optional, where supported)
    ],
    authenticatorSelection: {
      residentKey: 'required',            // Discoverable credential
      userVerification: 'required',       // Biometric or PIN
    },
  },
});
// Public key stored on server, scoped to this tenant's user pool
Enter fullscreen mode Exit fullscreen mode

What WebAuthn solves: for returning users with an enrolled passkey, it provides hardware-attested confirmation of the genuine qrauth.io origin. Combined with Tier 1 signature verification, this creates a two-factor trust chain: the QR was signed by the legitimate issuer (cryptographic), and the user is on the genuine verification endpoint (hardware-attested).

What WebAuthn doesn't solve:

The first scan. A first-time user falls through to Tier 1–3. Tier 4 is only available after opt-in enrollment. In practice, most users will never enroll a passkey for a parking meter interaction. The lower tiers carry the majority of real-world traffic.

The passkey proves endpoint integrity, not code legitimacy. A passkey enrolled on qrauth.io confirms the user is on the genuine verification page. It does not confirm the QR code being verified was placed by its claimed issuer. These are different trust claims.

Cross-device enrollment. A passkey enrolled on an iPhone syncs to other Apple devices via iCloud Keychain but does not transfer to Android. Platform switching requires re-enrollment. Predictably, most users won't.


6. Open questions — what we want scrutinized

These are the areas with the most residual risk. We are naming them explicitly because we want critique at the design stage.

6.1 Anti-proxy detection against an adaptive adversary

The current model uses four heuristic signals: TLS fingerprint (JA3/JA4), round-trip latency, canvas fingerprint, and HTTP header analysis.

JA3/JA4 durability. Tools like curl-impersonate clone the TLS ClientHello of a target browser exactly. A proxy running on a common cloud provider, impersonating a Chrome fingerprint, will produce a JA3 hash indistinguishable from a legitimate Chrome client. We do not have a reliable application-layer answer for TLS fingerprint spoofing.

Canvas fingerprint erosion. Firefox's Resist Fingerprinting mode and Brave's farbling mechanism randomize canvas output per-origin. As privacy browsers gain market share, canvas fingerprint consistency as a signal degrades. An adversary operating their proxy from a privacy browser already evades this signal today.

Latency threshold collapse. The latency heuristic assumes a minimum RTT overhead from the proxy hop. This breaks when both victim and attacker have edge-cached access to QRVA's infrastructure — for example, both terminating through Cloudflare Workers. In this configuration, proxy-added latency can fall below our detection threshold of ~50ms.

We believe robust anti-proxy detection requires CDN-layer cooperation — specifically, mutual TLS authentication that a browser can initiate but a proxy cannot relay without detection. We have not designed this.

6.2 Transparency log integrity

The current transparency log is append-only by operational convention, not by cryptographic guarantee. It works against an honest-but-curious operator; it fails against a compromised one.

Certificate Transparency (RFC 6962) solves this with a Merkle tree structure and inclusion proofs that clients can independently verify. A CT log that omits an entry cannot produce a valid inclusion proof for that entry. We referenced RFC 6962 in the QRVA spec but have not implemented Merkle inclusion proofs. The current log cannot prove to an independent verifier that a given entry has not been silently omitted.

This is a known gap. The implementation is straightforward; the deployment model for distributing Signed Tree Heads to verifiers is not yet designed.

6.3 Tenant identity bootstrapping

QRVA's security model assumes the mapping between a tenant identifier and a real-world organization is trustworthy. The spec describes a KYC verification flow but does not define its rigor.

This is analogous to the DV/EV distinction in TLS: domain validation certificates verify domain control but not organizational identity. QRVA currently only has a DV equivalent. For high-stakes deployments — government payments, healthcare navigation, regulated financial terminals — this is insufficient. The tenant identity verification process needs to be specified with the same rigor as the cryptographic components.

6.4 Key compromise response window

If a tenant's signing key is compromised, every QR code issued under that key appears legitimate until revocation propagates. Revocation propagates to the Cloudflare Workers edge through a cache TTL currently set at 5 minutes.

For payment terminals and government identity applications, 5 minutes of fraudulent verification capability after a known compromise is not acceptable. Certificate Transparency makes rogue certificate issuance publicly auditable in near-real-time — detectable within seconds. QRVA's transparency log propagation is currently batch-based, not real-time. Sub-second revocation propagation is a hard unsolved problem in the current architecture.

6.5 Geolocation API trust boundary

The browser Geolocation API is not authenticated. We cross-reference it against IP geolocation, but both signals are ultimately attacker-controllable.

Modern iOS and Android expose secure enclave attestation primitives. We want to know: can a QRVA-integrated mobile app request a location claim from the device's secure enclave that is attested by the device manufacturer and not spoofable by user-space software?

We believe this is architecturally possible on modern Apple and Google hardware but have not found a documented, stable API surface for it. If you know of one, we genuinely want to hear from you.

6.6 Radius selection tooling

This is more of a UX problem than a cryptographic one, but it has security consequences. The protocol does not prescribe a radius — tenants set it. Systematic underconfiguration (radius too large) degrades the geospatial security property silently. There is no feedback loop from scan events to radius recommendation.

We have not designed tooling or guidance for this. Contributions welcome.


Conclusion

The trust layer that physical QR codes are missing is not technically novel — it's the same primitives that solved analogous problems for TLS (certificate signing), email (DKIM/SPF), and web authentication (WebAuthn). The application to physical-world QR codes just hasn't been done yet.

QRVA is our proposal. The reference implementation is live at github.com/qrauth-io/qrauth. The full protocol specification is at docs.qrauth.io/protocol.

If you find a flaw — in the signing model, the geospatial scheme, the anti-proxy heuristics, or anything else — open an issue or leave a comment below. We are specifically interested in:

  • Whether the JA3/JA4 fingerprinting model holds against an adaptive adversary with curl-impersonate-equivalent capabilities
  • Whether authenticated location claims from device secure enclaves are accessible at the app layer
  • Whether there is a Merkle-backed transparency log deployment model that doesn't require a dedicated CT monitor infrastructure

We want this broken before it is deployed at scale. That's the point of publishing it now.

Top comments (0)