<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gernard Cerma</title>
    <description>The latest articles on DEV Community by Gernard Cerma (@aristech).</description>
    <link>https://dev.to/aristech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866106%2F0b13b3f7-b0f0-4b85-87ce-b556191668fd.jpeg</url>
      <title>DEV Community: Gernard Cerma</title>
      <link>https://dev.to/aristech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aristech"/>
    <language>en</language>
    <item>
      <title>QRVA: A protocol for cryptographic verification of physical QR codes — design decisions and open questions</title>
      <dc:creator>Gernard Cerma</dc:creator>
      <pubDate>Mon, 13 Apr 2026 20:48:28 +0000</pubDate>
      <link>https://dev.to/aristech/qrva-a-protocol-for-cryptographic-verification-of-physical-qr-codes-design-decisions-and-open-396f</link>
      <guid>https://dev.to/aristech/qrva-a-protocol-for-cryptographic-verification-of-physical-qr-codes-design-decisions-and-open-396f</guid>
      <description>&lt;p&gt;Physical QR codes have no trust layer.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This has been underexploited until recently. It is no longer underexploited.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QRVA&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;The reference implementation is open: &lt;a href="https://github.com/qrauth-io/qrauth" rel="noopener noreferrer"&gt;github.com/qrauth-io/qrauth&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;QR code fraud is a physical-world attack that bypasses every existing digital trust signal&lt;/li&gt;
&lt;li&gt;QRVA proposes: ECDSA-P256 signing per issuer, geospatial binding, ephemeral server-generated visual proof, heuristic anti-proxy detection, and WebAuthn passkeys for returning users&lt;/li&gt;
&lt;li&gt;We chose P256 over Ed25519 because of WebAuthn authenticator compatibility and FIPS requirements — not because it's the better curve in isolation&lt;/li&gt;
&lt;li&gt;Geospatial binding catches displaced clones but GPS accuracy limits make it useless below ~15m radius in urban environments&lt;/li&gt;
&lt;li&gt;WebAuthn is the only tier that is cryptographically unphishable — but only for returning users who've enrolled a passkey&lt;/li&gt;
&lt;li&gt;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&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Threat model
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  T1 — Physical sticker overlay
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Why existing mitigations fail:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  T2 — Legitimate domain, fraudulent QR registration
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why existing mitigations fail:&lt;/strong&gt; Nothing in a standard QR payload identifies the issuer or the authorized deployment location. Any entity can generate a QR pointing to any URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  T3 — Static page cloning
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why existing mitigations fail:&lt;/strong&gt; Client-side rendered pages are trivially cloneable. Visual trust indicators generated client-side can be reproduced.&lt;/p&gt;

&lt;h3&gt;
  
  
  T4 — Real-time MitM proxy
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  T5 — Geospatial spoofing
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  T6 — Tenant key compromise
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Protocol response
&lt;/h2&gt;

&lt;p&gt;QRVA addresses the attack classes through four successive tiers. Each tier builds on the previous. All tiers are active simultaneously.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Defeats&lt;/th&gt;
&lt;th&gt;Automated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ECDSA-P256 signing + issuer registry&lt;/td&gt;
&lt;td&gt;T1, T2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Server-generated ephemeral visual proof&lt;/td&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Anti-proxy heuristics&lt;/td&gt;
&lt;td&gt;T4 (partially)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;WebAuthn passkey (origin-bound)&lt;/td&gt;
&lt;td&gt;T4, all known phishing&lt;/td&gt;
&lt;td&gt;After opt-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Why ECDSA-P256 and not the alternatives
&lt;/h2&gt;

&lt;p&gt;This was the most contested internal decision. The two serious candidates were ECDSA-P256 and Ed25519.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The case for Ed25519 is strong.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;QRVA has compatibility constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebAuthn authenticator key material.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KMS availability.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FIPS compliance.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The nonce problem.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not P384?&lt;/strong&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Why geospatial binding — and what it doesn't solve
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The registration model:&lt;/strong&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Haversine distance computation (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;haversineDistanceMeters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lng1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lng2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6371000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Earth radius in meters&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;φ1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lat1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;φ2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Δφ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Δλ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lng2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lng1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Δφ&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
            &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;φ1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;φ2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Δλ&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it solves:&lt;/strong&gt; a cloned QR code displaced five blocks from its registered location fails verification at the new location. Simple code copying is caught.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't solve:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GPS accuracy constraints.&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Geolocation API is not authenticated.&lt;/em&gt; &lt;code&gt;navigator.geolocation&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Radius selection is unspecified.&lt;/em&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Where WebAuthn fits — and what it doesn't solve
&lt;/h2&gt;

&lt;p&gt;WebAuthn passkeys are origin-bound at the hardware level. A passkey created for &lt;code&gt;https://qrauth.io&lt;/code&gt; authenticates only on &lt;code&gt;https://qrauth.io&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The enrollment flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Triggered after a successful Tier 1–3 verification&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;serverChallenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Fresh from server, single-use&lt;/span&gt;
    &lt;span class="na"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QRAuth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;qrauth.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userEmail&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;pubKeyCredParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// ECDSA-P256 (required)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// Ed25519 (optional, where supported)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;authenticatorSelection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;residentKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// Discoverable credential&lt;/span&gt;
      &lt;span class="na"&gt;userVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Biometric or PIN&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Public key stored on server, scoped to this tenant's user pool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What WebAuthn solves:&lt;/strong&gt; 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).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What WebAuthn doesn't solve:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The first scan.&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The passkey proves endpoint integrity, not code legitimacy.&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cross-device enrollment.&lt;/em&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Open questions — what we want scrutinized
&lt;/h2&gt;

&lt;p&gt;These are the areas with the most residual risk. We are naming them explicitly because we want critique at the design stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Anti-proxy detection against an adaptive adversary
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;JA3/JA4 durability.&lt;/strong&gt; Tools like &lt;a href="https://github.com/lwthiker/curl-impersonate" rel="noopener noreferrer"&gt;&lt;code&gt;curl-impersonate&lt;/code&gt;&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canvas fingerprint erosion.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency threshold collapse.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Transparency log integrity
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  6.3 Tenant identity bootstrapping
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4 Key compromise response window
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.5 Geolocation API trust boundary
&lt;/h3&gt;

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

&lt;p&gt;Modern iOS and Android expose secure enclave attestation primitives. We want to know: &lt;strong&gt;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?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.6 Radius selection tooling
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;We have not designed tooling or guidance for this. Contributions welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;QRVA is our proposal. The reference implementation is live at &lt;a href="https://github.com/qrauth-io/qrauth" rel="noopener noreferrer"&gt;github.com/qrauth-io/qrauth&lt;/a&gt;. The full protocol specification is at &lt;code&gt;docs.qrauth.io/protocol&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;We want this broken before it is deployed at scale. That's the point of publishing it now.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cryptography</category>
      <category>webauthn</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Animated Cryptographic QR Codes: Killing Screenshot Attacks at the Display Layer</title>
      <dc:creator>Gernard Cerma</dc:creator>
      <pubDate>Thu, 09 Apr 2026 13:21:11 +0000</pubDate>
      <link>https://dev.to/aristech/animated-cryptographic-qr-codes-killing-screenshot-attacks-at-the-display-layer-4kf2</link>
      <guid>https://dev.to/aristech/animated-cryptographic-qr-codes-killing-screenshot-attacks-at-the-display-layer-4kf2</guid>
      <description>&lt;p&gt;What if the QR code on your screen was alive?&lt;/p&gt;

&lt;p&gt;Not alive in the "it links to a website" sense. Alive in the sense that it changes every 500 milliseconds, each frame is cryptographically signed, and a screenshot of it is worthless before you can even share it.&lt;/p&gt;

&lt;p&gt;We are building this at QRAuth, and it solves a problem that no amount of short TTLs or rate limiting can fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Screenshots Kill QR Authentication
&lt;/h2&gt;

&lt;p&gt;QR-based authentication has a vulnerability that gets hand-waved away: the screenshot attack.&lt;/p&gt;

&lt;p&gt;A user opens a login page. A QR code appears. The user screenshots it and sends it to someone else — or worse, an attacker captures it via screen share, a compromised display, or a shoulder-surfing camera. The recipient scans the screenshot and authenticates as the original user.&lt;/p&gt;

&lt;p&gt;Short time-to-live values help but do not eliminate the window. A QR code valid for 30 seconds is still valid for 30 seconds. That is plenty of time for an automated relay attack.&lt;/p&gt;

&lt;p&gt;Static QR codes displayed on physical surfaces (parking meters, restaurant tables, event posters) have an even worse version of this problem: someone sticks a fraudulent QR code over the legitimate one, and every person who scans it gets redirected to a phishing page. This is called quishing, and it is growing fast — cities like Montreal and Ottawa have already issued public warnings about fake QR codes on parking meters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Animated Cryptographic QR Codes
&lt;/h2&gt;

&lt;p&gt;Instead of displaying a static QR code that refreshes every N seconds, we render a continuous animation where each frame is a new, independently signed QR code.&lt;/p&gt;

&lt;p&gt;The protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;frame_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base_challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;frame_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HMAC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;frame_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 500ms, the display generates a new frame with an incremented counter and fresh timestamp. The HMAC binds the frame to the server secret, the original challenge, and the exact moment it was generated.&lt;/p&gt;

&lt;p&gt;When a phone scans the code, the server validates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recompute the HMAC with the server secret&lt;/li&gt;
&lt;li&gt;Check the timestamp is within the 500ms window&lt;/li&gt;
&lt;li&gt;Verify the frame_index is not stale (replay detection)&lt;/li&gt;
&lt;li&gt;Confirm the base challenge matches an active session&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A screenshot captures exactly one frame. By the time it reaches anyone else, the timestamp window has closed. A video recording captures multiple frames, but without the server secret, the attacker cannot generate the next valid frame in the sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering with Skia
&lt;/h2&gt;

&lt;p&gt;The animation cannot be a flickering mess of black and white squares. It needs to look intentional, designed, and unmistakably alive.&lt;/p&gt;

&lt;p&gt;We use Skia for rendering — the same 2D graphics engine behind Chrome, Android, and Flutter. On the web, CanvasKit (Skia compiled to WebAssembly) provides hardware-accelerated canvas rendering. On mobile, @shopify/react-native-skia gives us a declarative rendering pipeline in React Native.&lt;/p&gt;

&lt;p&gt;The key insight: QR data modules must stay binary for scanner compatibility, but everything around them is a design surface. Skia lets us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ripple modules outward from the center like a pulse&lt;/li&gt;
&lt;li&gt;Make finder patterns glow and breathe with subtle animation&lt;/li&gt;
&lt;li&gt;Shift a color wash across the quiet zone over time&lt;/li&gt;
&lt;li&gt;Add a gradient border that cycles in sync with frame rotation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this affects decodability. QR scanners read luminance thresholds at each module position, not color. The styling is cosmetic, but the effect is powerful — it is visually obvious that this QR code is alive and cannot be a printout or a sticker.&lt;/p&gt;

&lt;p&gt;Performance target: under 8ms per frame generation (ECDSA signing + QR matrix computation + Skia draw call), leaving 492ms of headroom on the 500ms rotation cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the QR: The Trust Reveal
&lt;/h2&gt;

&lt;p&gt;The animation on the display is half the story. The other half is what happens when you scan.&lt;/p&gt;

&lt;p&gt;Most verification flows end with a web page showing a green checkmark. That is forgettable. We are building a Trust Reveal — a designed, 2-3 second verification moment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Full-screen takeover on the scanning device&lt;/li&gt;
&lt;li&gt;A server-generated visual fingerprint unique to that exact scan — a crystalline pattern derived from the SHA-256 of the timestamp, device ID, location, and token&lt;/li&gt;
&lt;li&gt;A color sweep from deep red to verified green&lt;/li&gt;
&lt;li&gt;The issuer identity materializes on screen&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the frame that gets screenshotted and shared. It is also cryptographically meaningful — the visual is server-generated and provably live.&lt;/p&gt;

&lt;p&gt;For failed verification (scanning a fraudulent QR code), the experience is equally designed: full-screen red, alarm animation, "UNVERIFIED — This QR code is not cryptographically signed. Do not proceed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust-Reactive Animation
&lt;/h2&gt;

&lt;p&gt;Here is where it gets genuinely novel.&lt;/p&gt;

&lt;p&gt;The QR code's animation state reflects its security status in real time. The server pushes animation parameters via WebSocket to the display client:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server State&lt;/th&gt;
&lt;th&gt;Visual Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;Calm, slow pulse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elevated scan velocity&lt;/td&gt;
&lt;td&gt;Animation accelerates, subtle hue shift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraud flagged&lt;/td&gt;
&lt;td&gt;Visual distortion, finder patterns degrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revoked&lt;/td&gt;
&lt;td&gt;Code dissolves on screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The QR code is not just displaying a challenge. It is visually communicating the security state of that challenge to anyone looking at the screen. A calm, slowly pulsing QR code means everything is normal. A distorted, rapidly flickering one means something is wrong — and you do not need to understand cryptography to read that signal.&lt;/p&gt;

&lt;p&gt;This sits on top of the existing QRAuth architecture. The event bus already handles scan events. The fraud detection pipeline already tracks scan velocity. The animation layer is a client-side visualization of server-side state, pushed over the same WebSocket connection that handles session authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fraud Demo
&lt;/h2&gt;

&lt;p&gt;Two QR codes on a screen, side by side. They look identical to the naked eye — because every QR code looks like a random grid of black and white squares to a human.&lt;/p&gt;

&lt;p&gt;Scan QR Code A: the phone shows the Trust Reveal. Green sweep, verified, issuer identity confirmed.&lt;/p&gt;

&lt;p&gt;Scan QR Code B: the phone flashes red. "FRAUDULENT CODE DETECTED — DO NOT PROCEED."&lt;/p&gt;

&lt;p&gt;The difference was invisible. Until you scanned.&lt;/p&gt;

&lt;p&gt;That is the entire value proposition of QR verification in 15 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Quishing
&lt;/h2&gt;

&lt;p&gt;The sticker-over-QR attack works because there is no verification layer between the QR code and the user's browser. You scan, you follow the link, you are on whatever page the attacker set up.&lt;/p&gt;

&lt;p&gt;Animated cryptographic QR codes break this at two levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A physical sticker cannot animate. If the legitimate QR is pulsing and the sticker is static, the difference is immediately visible.&lt;/li&gt;
&lt;li&gt;Even if an attacker manages to display their own animated QR, the cryptographic signature will fail verification because they do not have the signing key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same trust model as SSL/HTTPS. A padlock icon in your browser means the site's certificate was issued by a trusted authority. A verified QR code means it was signed by a registered issuer. Unsigned codes are flagged — not blocked, but flagged, the same way browsers warn about HTTP sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;QRAuth is open source. The animated QR work is on our roadmap for Q3 2026, building on the existing ECDSA-P256 challenge-response protocol.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://qrauth.io" rel="noopener noreferrer"&gt;qrauth.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/QRAuth-io/qrauth" rel="noopener noreferrer"&gt;github.com/QRAuth-io/qrauth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Medium article on the broader architecture: &lt;a href="https://medium.com/@aris_86188/passwordless-login-needs-less-than-passkeys-f7351c0f9ced" rel="noopener noreferrer"&gt;Passwordless Login Needs Less Than Passkeys&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are working on anything related to QR security, authentication, or anti-fraud — we would love to hear from you. The protocol (QRVA) is open and licensed under CC BY 4.0.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>cryptography</category>
    </item>
    <item>
      <title>Passwordless Login Needs Less Than Passkeys</title>
      <dc:creator>Gernard Cerma</dc:creator>
      <pubDate>Tue, 07 Apr 2026 14:51:27 +0000</pubDate>
      <link>https://dev.to/aristech/passwordless-login-needs-less-than-passkeys-1j8i</link>
      <guid>https://dev.to/aristech/passwordless-login-needs-less-than-passkeys-1j8i</guid>
      <description>&lt;p&gt;Passwords are broken. This is not a hot take. Every security professional, every developer who has implemented auth, and every user who has clicked "Forgot Password" for the tenth time this month knows it.&lt;/p&gt;

&lt;p&gt;The industry response has been a wave of alternatives: magic links, SMS codes, TOTP authenticators, and most recently, passkeys. Each one solves part of the problem while introducing new friction. Magic links depend on email delivery speed. SMS codes are vulnerable to SIM-swap attacks. TOTP apps require users to manually type rotating six-digit codes. Passkeys tie authentication to platform keystores that don't sync well across ecosystems.&lt;/p&gt;

&lt;p&gt;The pattern is clear: every method works well in some contexts and fails in others. The answer is not to find the one perfect method. It is to build a platform that unifies multiple methods under one identity.&lt;/p&gt;

&lt;p&gt;That is what we set out to build with QRAuth. It started as QR-based authentication. It is becoming an identity verification platform where QR is the primary -- but not the only -- transport.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The flow is simple, and deliberately so.&lt;/p&gt;

&lt;p&gt;A user opens a login page. The page displays a unique QR code. The user scans it with their phone camera -- no app install, no proprietary reader. The phone asks for confirmation -- biometric, PIN, or passkey, depending on how the developer configured it. Once confirmed, the browser session is authenticated instantly via SSE. Done.&lt;/p&gt;

&lt;p&gt;If this sounds familiar, it should. WhatsApp Web, Telegram Desktop, and Signal Desktop all use a variation of this pattern. Billions of people already trust QR-based session authentication daily without thinking about it.&lt;/p&gt;

&lt;p&gt;The difference is that QRAuth makes this available as a general-purpose authentication layer, not locked inside a single app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Model
&lt;/h2&gt;

&lt;p&gt;Every QR code generated by QRAuth is cryptographically signed, single-use, and time-limited -- measured in seconds, not minutes. Each code is bound to a specific session. If nobody scans it before it expires, it is destroyed and replaced.&lt;/p&gt;

&lt;p&gt;This means there is nothing persistent to leak. Unlike a password, API key, or even a session token, the QR code is ephemeral by design. A screenshot of a QR code is useless by the time someone tries to use it.&lt;/p&gt;

&lt;p&gt;When a user scans the code, their phone does not simply confirm a session. It presents context: the requesting domain, timestamp, device information, and location. The user sees exactly what they are authorizing before they confirm. This makes QR authentication phishing-resistant in a way that most alternatives are not -- you cannot trick someone into confirming a session for evil-site.com when their phone clearly shows the requesting domain.&lt;/p&gt;

&lt;p&gt;The phone itself acts as the authentication device. Think of it as a hardware security key that everyone already carries.&lt;/p&gt;

&lt;p&gt;But a valid cryptographic challenge is only half the picture. The other half is knowing which device made the request and whether that device should be trusted. That is where the device registry comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device Trust: From "Valid Request" to "Known Device"
&lt;/h2&gt;

&lt;p&gt;There is a critical difference between verifying that a request is cryptographically valid and verifying that it comes from a device you trust. Most authentication systems stop at the first. QRAuth is building toward the second.&lt;/p&gt;

&lt;p&gt;The architecture already captures device data on every authentication event. The next layer -- currently in development -- exposes this as a user-facing device registry. Each user gets a list of their trusted devices: "iPhone Aris," "MacBook Office," "Pixel Personal." Devices can be named, reviewed, and revoked.&lt;/p&gt;

&lt;p&gt;This changes the security model fundamentally. Instead of asking "is this challenge valid?" the system asks "is this challenge valid AND does it come from a device this user has previously registered and not revoked?"&lt;/p&gt;

&lt;p&gt;The practical implications are significant. A stolen phone can be revoked from any other trusted device. A new device triggers a step-up verification before it is added to the trust list. An enterprise admin can enforce policies: only company-provisioned devices, only devices with biometric capability, only devices in specific geolocations.&lt;/p&gt;

&lt;p&gt;This is the layer that turns authentication into identity management.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question Everyone Asks: Why Not Passkeys?
&lt;/h2&gt;

&lt;p&gt;This is the first question every developer raises, and rightfully so. Passkeys are a strong technology with serious backing from Apple, Google, and Microsoft.&lt;/p&gt;

&lt;p&gt;Here is an honest comparison.&lt;/p&gt;

&lt;p&gt;Passkeys store credentials in platform keystores -- iCloud Keychain, Google Password Manager, Windows Hello. The protocol is open, and anyone can run their own WebAuthn server. But the keys live on the client side, inside platform ecosystems. This creates three practical problems.&lt;/p&gt;

&lt;p&gt;First, cross-ecosystem portability is still fragmented. A passkey created on an iPhone and stored in iCloud Keychain is not seamlessly available on a Windows workstation. The sync story is improving, but it is not solved.&lt;/p&gt;

&lt;p&gt;Second, shared and public devices are a problem. Walk into an office with shared PCs, a library with public terminals, or a hotel business center. Passkeys do not work well on devices that belong to someone else or to no one. There is no keystore to pull from.&lt;/p&gt;

&lt;p&gt;Third -- and this is the part that does not get discussed enough -- when passkeys do handle cross-device authentication, the mechanism they use is a QR code. The FIDO Alliance's own cross-device authentication flow involves scanning a QR code with your phone to bootstrap the connection. QR-based authentication is not a competitor to passkeys. It is a layer that passkeys themselves rely on.&lt;/p&gt;

&lt;p&gt;QRAuth is complementary, not competitive. The architecture includes a WebAuthn bridge that unifies QR and passkey authentication under the same identity. A developer does not choose between QR and passkeys. They offer both. The user chooses based on context. On their own laptop with a synced keystore, passkeys are the right choice. On a shared PC, a kiosk, a smart TV, or a colleague's machine, QR is faster and simpler. Either way, the identity is the same, the device trust model is the same, and the session management is the same.&lt;/p&gt;

&lt;p&gt;This is the core architectural decision: QR is a transport, not the product. The product is a unified identity verification layer that happens to start with QR because it solves the widest range of contexts today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where QR Authentication Shines
&lt;/h2&gt;

&lt;p&gt;The use case that generates the most immediate interest is shared device authentication.&lt;/p&gt;

&lt;p&gt;Picture an office with ten shared workstations. Currently, each employee either has a local account with a password (credential sprawl), uses a shared account (security nightmare), or goes through an SSO flow that still requires typing credentials on a machine they do not own.&lt;/p&gt;

&lt;p&gt;With QR authentication, the employee walks up to a workstation, scans the QR code on the login screen with their phone camera, confirms with a biometric tap, and the workstation loads their session with their permissions. No password typed on a shared keyboard. No credentials stored on the device. When they leave, the session ends.&lt;/p&gt;

&lt;p&gt;This pattern extends naturally to kiosks, point-of-sale systems, smart TVs, digital signage, conference room displays, and any environment where typing credentials is either impractical or insecure.&lt;/p&gt;

&lt;p&gt;Beyond digital authentication, QRAuth also handles physical QR verification. Any printed or displayed QR code can be cryptographically signed to prevent tampering, spoofing, or replacement. This addresses a growing problem in restaurants, parking systems, event ticketing, and product authentication where fraudulent QR codes redirect users to malicious destinations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Developer Experience
&lt;/h2&gt;

&lt;p&gt;None of the above matters if integration is painful.&lt;/p&gt;

&lt;p&gt;QRAuth ships stable SDKs for Node.js and Python today, with Go, PHP, Swift, and Kotlin on the roadmap. The target is under ten minutes from npm install to a working QR login. A generous free tier removes evaluation friction.&lt;/p&gt;

&lt;p&gt;The architecture is straightforward. The developer creates a session, receives a QR payload, renders it, and listens for a webhook or WebSocket event when the user confirms. No redirect flows, no callback URLs, no OAuth dance. The simplicity is intentional -- authentication should be infrastructure, not a project.&lt;/p&gt;

&lt;p&gt;The entire platform is open source. The SDKs, the protocol, and a reference implementation are available on GitHub. We are not asking anyone to trust a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;QRAuth is early, and we are transparent about where we are in the journey.&lt;/p&gt;

&lt;p&gt;What exists today: QR-based authentication with cryptographic verification, session management, SDKs for six languages, and a working developer experience. The core protocol is sound and battle-tested.&lt;/p&gt;

&lt;p&gt;What is in active development: the device registry with user-facing management and revocation, the WebAuthn bridge for passkey unification, and the admin control plane with session visibility and audit trails.&lt;/p&gt;

&lt;p&gt;What is on the roadmap: enterprise features (SSO, SCIM, policy engine), compliance certifications (SOC2, ISO 27001, GDPR documentation), and anomaly detection for risk-based authentication.&lt;/p&gt;

&lt;p&gt;The direction is clear. We are not building a QR login widget. We are building an identity verification platform where QR is one method -- the one that happens to work everywhere today -- with passkeys, device trust, and policy controls converging under the same architecture.&lt;/p&gt;

&lt;p&gt;The roadmap is shaped by what real developers tell us they need. We launched publicly this month and every conversation -- whether on GitHub, in developer communities, or in direct feedback -- makes the product sharper.&lt;/p&gt;

&lt;p&gt;Try the demo at &lt;a href="https://qrauth.io" rel="noopener noreferrer"&gt;qrauth.io&lt;/a&gt;. Read the docs. Break it if you can. Tell us what is missing.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
