locqr/crypto.md

16 KiB
Raw Blame History

Encryption Scheme

The LOCQR ecosystem requires cryptographic operations across three independently developed components: the browser extension, the LOCQR server, and the companion app. These components are built on different platforms and runtimes. Any ambiguity in algorithm parameters, key encoding, or data serialisation will produce silent interoperability failures — incorrect signatures that always fail, or run keys that never match.

The trust problem

The companion app and browser extension have no pre-existing relationship. A shared secret must be established between them across an untrusted network relay, without that secret ever crossing the relay in recoverable form.

The QR code solves the bootstrapping problem: a network attacker cannot intercept what is displayed on screen. But the QR code is not a cryptographic primitive — it is the channel that makes MITM structurally impossible during key exchange. The cryptography rides on top of that property: the QR carries a cryptographic commitment and a signed token, not keys.

The backend server is the sole trust anchor. Its secret key material never leaves a controlled environment. Its corresponding public keys are compiled into both the extension and the companion at build time and cannot be substituted at runtime. All legitimacy within the system traces back to the backend's ability to sign, and the clients' ability to verify against those pinned constants.

Three security concerns

The architecture addresses three distinct security problems. They share the same flow but require different cryptographic answers.

Key exchange integrity. The companion must be certain it is exchanging keys with the correct extension, not an attacker's. The backend signs the key exchange parameters — a commitment to the key bundle, the target URL, a run ID, and a TTL. This signature only needs to hold for the duration of the exchange window (60 seconds): a forged signature within that window would allow an attacker to substitute a fake key bundle, but executing Shor's algorithm against a 256-bit elliptic curve key in under 60 seconds is not achievable on any near-term quantum computer.

Payload confidentiality against future quantum adversaries. The credential delivered through the exchange may remain sensitive for years. The run key must therefore be irrecoverable even by an adversary who records the exchange today and acquires a cryptographically relevant quantum computer (CRQC) later. This protection is permanent and completely independent of the exchange window.

Website registration legitimacy. Before initiating any exchange, the extension must confirm the website is a registered LOCQR participant. The backend issues signed registration certificates. These are long-lived (months to years) and must resist a future CRQC that could forge a certificate retroactively. This tier is fully post-quantum.

Threat model

Passive network attacker records the key exchange for later decryption. Defeated by the post-quantum component of the hybrid key exchange: a CRQC cannot derive the run key from recorded ciphertext.

Active MITM during key exchange attempts to substitute a fake key bundle during the 60-second exchange window. Defeated by the backend's Ed25519 signature: the companion rejects any exchange parameters that do not verify against its pinned key before proceeding.

Real-time quantum attack attempts to forge an Ed25519 signature within the 60-second window to conduct a live MITM attack. Not achievable on any near-term quantum computer.

Future quantum adversary acquires a CRQC and attempts to decrypt recorded recorded ciphertext. Defeated by ML-KEM-768: the run key cannot be derived from ciphertext without the extension's private key, which is never transmitted.

QR hijacker scans the displayed code before the legitimate user. Defeated by at-most-once key bundle delivery: the backend destroys the bundle on first fetch, converting this from a network race to a physical race.

Compromised or phishing website attempts XSS or credential theft. The extension operates outside the website's security context and verifies registration certificates independently. Data transferred to the website is out of scope once delivered.

Algorithm choices

ML-DSA-44 — registration signing

ML-DSA-44 (NIST FIPS 204, CRYSTALS-Dilithium) signs domain registration certificates. A registration certificate is long-lived; a future CRQC could be used to forge one retroactively. Full post-quantum signature security is required here. The large signature size (2420 bytes) is acceptable at registration-time, where certificates are exchanged once and cached.

ML-DSA-44 is not used for key exchange signing: its 2420-byte signatures would make a QR code unscannable at any practical error correction level.

Ed25519 — key exchange integrity

Ed25519 signs the backend's assertion over the key exchange parameters: the key bundle commitment, the target URL, a run ID, and a TTL. Its sole purpose is to prevent MITM substitution during the key exchange. It does not protect the delivered payload — that is the job of the run key.

Its 64-byte signatures are essential: compact enough to be carried in a scannable QR code. The quantum risk is bounded to the 60-second exchange window; forging Ed25519 in real-time within that window is not achievable on any near-term quantum computer.

Should the quantum outlook change, Ed25519 can be replaced for future exchanges without any effect on data already delivered. Past payloads are protected by ML-KEM-768, not by Ed25519. Upgrading the signing algorithm is a deployment change — new backend signing key, new pinned constants in extension and companion — not an architectural one.

Hybrid X25519 + ML-KEM-768 — run key exchange

The run key is derived from two parallel exchanges: X25519 (classical ECDH) and ML-KEM-768 (NIST FIPS 203, CRYSTALS-Kyber). Their shared secrets are combined and fed into HKDF. This hybrid provides defense in depth: if either primitive is broken, the run key remains protected by the other.

ML-KEM-768 is chosen over ML-KEM-512 because the payload may be sensitive for years. ML-KEM-512 provides approximately AES-128 equivalent security; ML-KEM-768 provides approximately AES-192. The more conservative choice is appropriate when the protected content has an indefinite sensitivity lifetime.

X25519 is retained in the hybrid for its compact 32-byte public key and negligible performance cost. Its quantum risk within the 60-second exchange window is negligible, and the ML-KEM-768 component ensures the run key is irrecoverable regardless.

AES-256-GCM — payload encryption

The session payload is encrypted with the derived run key. At 256-bit key length, Grover's algorithm reduces effective security to 128 bits — which remains practically unbreakable. GCM provides authenticated encryption: a tampered ciphertext fails authentication before decryption is attempted.

Nonce reuse — the primary GCM failure mode — is structurally prevented: each exchange derives a fresh key from a one-time key bundle, and exactly one payload encryption occurs per run.

HKDF-SHA256 — run key derivation

HKDF derives the run key from the combined X25519 and ML-KEM shared secrets:

run_key = HKDF-SHA256(
  ikm    = x25519_shared || kem_shared_secret,
  salt   = runId_utf8,
  info   = "locqr-run-key-v1",
  length = 32
)

runId_utf8 is the UUID v4 string in its standard hyphenated form, encoded as UTF-8 bytes (36 bytes). The salt binds the derived key to the specific run: a key bundle replayed under a different run ID produces a different key. The info string provides domain separation from other HKDF derivations within the same protocol.

Note: "run key" is this project's term for what cryptographic literature standardly calls a "session key" — a short-lived symmetric key scoped to one protocol execution.

HTTPS requirement

The extension enforces HTTPS at the origin level. A page served over HTTP receives cert_invalid / insecure_origin immediately; no certificate is inspected and no backend call is made. This is non-negotiable: the security scheme provides no meaningful protection on an unencrypted transport.

The registration certificate domain field is the bare hostname (test.locqr.dev). The signed token url field and the bundle upload origin field are the full HTTPS origin (https://test.locqr.dev). The extension checks that the cert's domain matches the hostname component of the sender.tab.url origin, and that the scheme is https:. Subdomains are not implicitly covered — a cert for locqr.dev does not validate for sub.locqr.dev.

RFC 8785 (JCS) — canonical JSON

All sign and verify operations require a canonical byte representation of the signed data. RFC 8785 (JSON Canonicalization Scheme) is mandatory across all components and platforms. Field ordering differences between platforms produce different digests and silent signature failures. Bespoke implementations are not permitted — a conforming library must be used on every platform.


Normative rule: hybrid construction ordering

In all hybrid (classical + post-quantum) constructions, the classical value precedes the post-quantum value in any concatenation. This applies to input key material for HKDF, hash commitments over public key bundles, and any future hybrid construction not yet defined.


Reference

Algorithms and key sizes

Algorithm Security concern Public key Sig / ciphertext
ML-DSA-44 Registration signing 1312 bytes 2420 bytes
ML-KEM-768 Session key exchange (PQ) 1184 bytes 1088 bytes (ciphertext)
X25519 Session key exchange (classical) 32 bytes
Ed25519 Key exchange integrity 32 bytes 64 bytes
AES-256-GCM Payload encryption
HKDF-SHA256 Run key derivation

Compile-time key constants

Constant Algorithm Held by Purpose
BACKEND_PUBLIC_KEY_B64 ML-DSA-44 Extension Verifies domain registration certs
ED25519_PUBLIC_KEY_B64 Ed25519 Extension Verifies key exchange tokens
ED25519_PUBKEY Ed25519 Companion Verifies key exchange tokens

All three are compiled at build time by sign-domain.js and never negotiated at runtime. Any key material received at runtime that does not match a pinned constant is rejected before verification runs. Key regeneration (--new-keys) patches all source files and requires redeployment of both the extension and the companion.

All npm dependencies providing cryptographic primitives must be version-pinned and hash-verified via lockfile. No runtime download of cryptographic code is permitted.

Library choices

These are pinned for all implementations. Changing a library mid-project requires re-verifying interoperability end-to-end.

Concern Runtime Library Version
ML-KEM-768 + ML-DSA-44 Browser / extension / web stub @oqs/liboqs-js (npm) 0.15.1
ML-KEM-768 + ML-DSA-44 Node.js setup script @oqs/liboqs-js (npm) 0.15.1
ML-KEM-768 + ML-DSA-44 Android / Kotlin BouncyCastle (liboqs-java for production)
ML-KEM-768 + ML-DSA-44 Java/Spring (production) liboqs-java
JCS (RFC 8785) Node.js canonicalize (npm) 3.0.0
JCS (RFC 8785) Browser canonicalize (same package, ESM build) 3.0.0
JCS (RFC 8785) Android / Kotlin io.github.erdtman:java-json-canonicalization (Maven Central)
JCS (RFC 8785) Java/Spring io.github.erdtman:java-json-canonicalization (Maven Central)

@oqs/liboqs-js: Published by PQCA (Post-Quantum Cryptography Alliance), WASM bindings to the reference liboqs implementation, zero npm dependencies, MIT licence. Covers both ML-KEM-768 and ML-DSA-44 in a single package. Tracks the NIST reference implementation directly, reducing the risk of subtle algorithm deviations. Unpacked size is ~17 MB; the WASM binary will be large in the extension bundle. Before writing any ML-KEM or ML-DSA code, verify that the async init() call is compatible with the Manifest V3 service worker environment: no DOM dependency, compatible with import() or importScripts.

Fallback: If @oqs/liboqs-js proves incompatible with the service worker context, @noble/post-quantum@0.6.1 (npm, pure TypeScript, audited by Trail of Bits) covers both algorithms and is unconditionally service-worker safe. It is not WASM; use it only if the WASM init path cannot be made to work. Any switch from @oqs/liboqs-js to @noble/post-quantum requires a full cross-component interoperability test before adoption.

JCS consistency: All four runtimes use libraries from the same author (Samuel Erdtman, editor of RFC 8785). This maximises the likelihood of byte-identical canonical forms across platforms. Any JCS library substitution requires a cross-platform signature round-trip test before adoption.

Deferred decisions

  • At-most-once key bundle delivery — the key bundle must be atomically destroyed on first fetch. This is a load-bearing security property: it converts a QR hijacking attack from a network race to a physical race. Implementation: Redis GETDEL or database DELETE ... RETURNING.
  • Delegate server trust chain — self-hosted delegate servers require the main backend to countersign the delegate's Ed25519 key. The pinned-key path is the only active path in the PoC.
  • Key rotation policy and procedure
  • Certificate revocation mechanism (bloom filter design — deferred)
  • Hardware-backed key storage in the companion app (deferred)
  • Android Keystore integration (deferred)

Algorithm alternatives

The following are drop-in substitutions that would change key sizes or security margins but require no architectural changes. None affect the system structure, flows, or data formats beyond the keys themselves.

ML-KEM-1024 in place of ML-KEM-768. Raises the KEM security level from approximately AES-192 to AES-256 equivalent. Public key grows from 1184 to 1568 bytes; ciphertext from 1088 to 1568 bytes. Since the key bundle is stored server-side and never carried in the QR code, there is no visual or UX cost — only marginally larger server storage and network payloads.

X448 in place of X25519. Uses Curve448 (224-bit classical security) rather than Curve25519 (128-bit). Public keys grow from 32 to 56 bytes. The practical improvement is modest given that the classical component is already in a hybrid with ML-KEM, but if ML-KEM-1024 is adopted, X448 brings the classical component to a consistent security level.

Falcon-512 in place of Ed25519. Falcon-512 (NIST FIPS 206) is the only post-quantum signature scheme compact enough to be a realistic candidate for QR-based signing. Its signatures are 666 bytes — substantially larger than Ed25519's 64 bytes, but far smaller than ML-DSA-44's 2420 bytes. A 666-byte signature, combined with the rest of the QR payload, produces a scannable code at around QR version 2025, which remains practical.

Adopting Falcon-512 would eliminate the one remaining classical signature from the system. The argument for keeping Ed25519 is that it only needs to hold for 60 seconds; the argument for Falcon-512 is that the QR size cost is manageable and a fully post-quantum system is cleaner. Either is defensible. The switch requires redeployment of backend, extension, and companion (new keys throughout) but no structural changes.

HKDF-SHA384 or SHA512 in place of SHA256. If ML-KEM-1024 is adopted, upgrading HKDF's hash brings key derivation to a consistent security level. The practical difference is negligible — HKDF uses the hash as a PRF, not for collision resistance — but it avoids mixing security levels across the scheme.