Security at qub

Effective date: 23 September 2026 Version: 1.1 — implementation-accuracy review


For researchers — quick reference:

Full details are in §12 (Coordinated Disclosure).


Who We Are

qub.social is operated by VSPRY AUSTRALIA PTY LIMITED (ABN 41 631 026 330), Level 38, 71 Eagle Street, Brisbane QLD 4000, Australia. References to "qub", "we", "us", and "our" mean that entity.

Security contact: support@qub.social with the subject prefix [SECURITY].


1. Our Approach

qub is trust infrastructure. The product is worthless if it is not secure, so security is not a feature — it is the substrate. This page describes, in concrete terms, how we safeguard our stack, your data, and the integrity of sealed content.

The value of a verifiable temporal commitment grows as more of the internet becomes machine-generated. A verified storage transaction or transparency-log anchor can establish that ciphertext existed no later than its block time; the sealed artifact separately proves content integrity, drand-round binding, and any authorship signatures. Keeping those claims distinct is the bar this page is held to.

We do not ask you to trust us. We design so that the trust required of us is as small as possible, and where trust is required we explain exactly what is being trusted and why.

Three principles drive every design decision:


2. Threat Model

2.1 What We Protect Against

2.2 What We Cannot Protect Against

We are honest about our limits. qub cannot defend against:


3. Client-Side Cryptography

In the default browser message flow, content encryption happens before the upload request. Two explicit paths differ: Builder /api/v1/seal deliberately sends plaintext and caller-generated K to the Worker for in-memory sealing, and pact staging/co-signing sends the signed structured pact to the service so it can finalise the bilateral artifact. Neither exception should be mistaken for browser-path end-to-end encryption.

3.1 Timelock Encryption

qub uses tlock — identity-based encryption keyed to a future drand beacon round. Encryption proceeds in your browser using the drand network's public key; the decryption key is released publicly by the drand network only when the target round is reached. Nobody, including us, can reconstruct the decryption key ahead of time.

We target the quicknet chain:

The quicknet chain's public key and genesis time are compiled into the client. We do not fetch chain parameters at runtime, so a malicious node cannot substitute a chain we control.

3.2 Symmetric Encryption

The tlock scheme wraps an AES-256-GCM content key. AES-GCM provides authenticated encryption: a single bit flipped in the ciphertext causes decryption to fail, rather than producing silently-corrupted plaintext.

3.3 Canonical Serialisation

Protocol structures are serialised using deterministic CBOR (RFC 8949 §4.2 core deterministic encoding). Two implementations encoding the same logical structure produce identical CBOR. Complete sealed payloads are not deterministic: tlock and outer-wrapper encryption use fresh randomness. The body hash is computed over the raw body bytes, while canonical encoding makes the surrounding signed/wire structures unambiguous.

We wrote the CBOR encoder by hand for both our client and server implementations rather than relying on a generic serialisation library — the requirement is exactness, not ergonomics, and property tests run in both implementations to verify they agree.

A regression test asserts the canonical wire format contains no qub-brand byte sequence beyond the protocol-primitive qub_id field key. The wire format is intentionally brand-agnostic — any conforming viewer (ours or a third party's) can render any qub from permanent storage, regardless of which deployment sealed it. The test is a tripwire that prevents a future change from accidentally baking a brand reference into bytes that, once in permanent storage, cannot be rewritten.

3.4 Body Hashing and Pre-Reveal Integrity

Every sealed payload carries a SHA3-256 hash of its raw body bytes. The hash is bound into qub_id and, when authorship signing is enabled, into the V2 signature input. A viewer recomputes it after decryption and rejects a mismatch.

The 32-byte content identifier qub_id is derived from a 108-byte preimage covering the protocol version, content type, created and unlock timestamps, optional outcome timestamp (or its zero sentinel), target drand round, body hash, and SHA3-256 of the optional NFC-normalised title. A gateway or CDN cannot change any bound field consistently and still pass re-derivation. Titles are capped at 100 NFC code points and rejected for the shared hostile/control-codepoint class (including bidi overrides, zero-width characters, the tag block, BOM, C0, C1, and DEL).

3.5 Signing (ML-DSA-65)

Authorship signing uses ML-DSA-65 (FIPS 204), a NIST-standardised post-quantum signature scheme. We deliberately chose a post-quantum primitive for signing because sealed content is permanent: a signature that verifies today must still verify decades from now, including after large-scale quantum computers become practical.

Signing keys are generated in the browser. The local secret is wrapped under a non-extractable WebCrypto key before IndexedDB storage. If the account-scoped cross-device recovery feature is used, an AEAD-encrypted portable key blob is stored server-side; its secret-key ciphertext is bound to the immutable account id, and the service validates the public envelope but cannot decrypt the secret material. Raw private-key bytes are not sent to the server. Public keys and attestation records are stored for verification and identity display.

The same in-browser tlock decryption applies inside the qub embed: when a sealed qub is rendered through <qub-embed> on a third-party page, decryption still happens in the embed iframe in the viewer's browser. The embed does not change the trust model — plaintext is never decrypted on a qub server.

3.6 Public Attribution — Opt-In

Sealed qubs carry no on-chain pointer to their creator unless the creator explicitly chooses to attach one. When you seal a qub the reference app emits an Author storage tag (a 64-char hex fingerprint of your signing public key) only when "Public attribution" is enabled on the date-picker step. With the toggle off — the default — no Author tag is written and the qub is unattributed in permanent storage: nothing in storage links the upload to your handle, your email, or your other qubs. With the toggle on, the fingerprint resolves to your @handle via the attestation chain in §6.3 / §10 and the viewer countdown shows "Sealed by @{handle}" before reveal.

This is a deliberate guard against the enumeration risk an always-on Author tag would create: a third party who learns a creator's fingerprint could otherwise search permanent storage by the tag and reconstruct that creator's full historical output. Opt-in attribution closes that channel — only qubs the creator explicitly chooses to attribute appear under a fingerprint in permanent storage.

The /u/{handle} profile page is a verified-identity card — handle, optional display name + URL, "verified email" pill (no address), and the cryptographic fingerprint short form. It does not list a creator's qubs. Visitors who want to see a specific qub from a creator follow that qub's delivery URL directly.

3.7 Outer Encryption Wrapper

Even after timelock decryption is mathematically possible—once the drand signature for the bound round has been published—the canonical timelock layer alone would let an indexer bulk-decrypt discoverable qubs. Private delivery closes that channel with an additional symmetric layer around the timelock-encrypted bytes (Protocol §13). Public delivery intentionally omits the wrapper so notification, embed, and discovery links can work without a secret fragment.

The wrapper uses AES-256-GCM, a NIST-standardised authenticated cipher, with a fresh 256-bit key K generated per qub by your browser's CSPRNG. K is bound to the qub's qub_id as authenticated additional data, so a key from one qub cannot be reused to decrypt a different qub.

K never reaches our servers in the default private browser flow. It is encoded into the URL fragment of the share link (https://qub.social/c/<tx_id>#<base64url(K)>). Browsers do not transmit URL fragments to servers—RFC 3986 places the fragment outside the request—so qub.social, storage gateways, CDNs, and request monitoring are blind to K in that flow. The stored OuterWrapper is recognisable structured CBOR, but its authenticated ciphertext field hides the inner SealedQub structure and cannot be opened without K.

Net consequences:

The Worker's server-side /api/v1/seal endpoint (used by AI agents and other API callers) requires the caller to generate K with a CSPRNG, retain it locally, and supply it as wrapper_key_b64url. The Worker necessarily sees both plaintext and K in memory on this explicitly trusted path, but persists neither. A mandatory Idempotency-Key prevents a lost response from creating a second billed qub, while the caller-retained K can be combined with the replayed fragment-less URL. This differs from the default browser path, where K never reaches the Worker unless the creator explicitly enables recovery.


4. Transport and Edge

4.1 TLS

Browser traffic to qub is served over HTTPS at the Cloudflare edge. Responses set HTTP Strict Transport Security (max-age=63072000; includeSubDomains; preload). The exact negotiated TLS version and cipher suite are governed by the active edge configuration rather than asserted by application code. We do not expose a separately reachable origin server.

4.2 Content Security

The compiled client is served with strict content-type and cache headers. The SPA shell is a single origin. We do not embed third-party scripts for analytics or advertising. The two third-party touchpoints in the product are both narrowly scoped: the purchase flow leaves the SPA entirely with a full-page redirect to Stripe-hosted checkout (https://checkout.stripe.com/…) — Stripe's UI never executes in our origin and we never see card data — and the seal flow loads Cloudflare's Turnstile widget, a privacy-preserving CAPTCHA alternative that Cloudflare renders inside its own sandboxed iframe. Neither party can read the rest of the page.

The qub embed iframe (served from qub.social/embed/{tx_id} and loaded into third-party sites by embed.js) carries its own Content-Security-Policy. Its connect-src allowlist is 'self', https://qub.social, https://arweave.net, https://ar-io.dev, https://permagate.io, https://api.drand.sh, and https://drand.cloudflare.com. The iframe runs with sandbox="allow-scripts allow-top-navigation-by-user-activation" (not allow-same-origin): the host page cannot read its DOM, and it cannot navigate the host except after a user action.

4.3 CORS and Fetch Scope

The browser client makes fetch requests only to:

The embed's destinations are enforced by its CSP. The main SPA's intended destinations are fixed in code and configuration and are exercised by browser and integration checks; Subresource Integrity is not a network-destination control.

The embed fetches stored bytes through the allowlisted qub/storage origins, unwraps private payloads in-browser using K from its URL fragment, and fetches reveal-time round signatures from the two allowlisted drand origins. The main SPA uses the four-endpoint fallback set in config/drand-endpoints.json (drand.cloudflare.com, api.drand.sh, api2.drand.sh, and api3.drand.sh) so one endpoint outage does not block reveal. The embed CSP denies connections outside its explicit list.


5. Server-Side Infrastructure

5.1 Serverless Edge

Our API runs entirely on a managed serverless runtime at the edge. There are no VMs, no containers, and no persistent server processes we administer. This dramatically reduces the attack surface we are responsible for: we do not run an OS, a web server, or an application runtime that we must patch.

A separate public-CORS middleware applies Access-Control-Allow-Origin: * to the following implemented path set: /embed.js, /embed/v1.js, everything under /embed/; /api/v1/telemetry; /api/v1/openapi.json; everything under /api/v1/qub/ (including bytes, metadata, proof, engagement, notify, and push subroutes); everything under /api/v1/log/; public handle lookups under /api/v1/handle/; and public avatar reads under /api/v1/identity/avatar/. Its preflight permits GET, POST, and OPTIONS with the Content-Type request header. This prefix-based surface is broader than only the calls the embed currently makes, so every handler below those prefixes must continue to enforce its own validation, authentication, rate limits, and abuse controls. Other API paths retain the qub.social-restricted CORS policy.

5.2 Storage

The default browser message flow does not persist plaintext on qub infrastructure. Builder /api/v1/seal handles plaintext and K in memory but persists neither. Pact staging necessarily stores the signed structured pact until it is co-signed, retracted, or expires. Opt-in recovery stores a delivery capability (the full fragment-bearing link) so it can be recovered later. We therefore do not describe the whole storage tier as “metadata-only.”

5.3 Secrets

Secrets (signing wallets, provider tokens, and HMAC keys) are supplied through platform secret/environment bindings rather than source control. Runtime components receive only the bindings they need. Rotation and overlap procedures are component-specific; we do not claim a single universal automatic or audited rotation mechanism.

5.4 Logging and Telemetry

Structured JSON logs are written on every API request with a correlation ID surfaced in the X-Request-Id response header. Client telemetry is anonymous — no device identifier, no IP address, no content preview. Events are buffered in memory and flushed on a best-effort basis; a failed flush is discarded, not retried. Telemetry is designed to be disableable at the network layer without affecting the product.


6. Authentication

6.1 Magic-Link Sign-In

Sign-in uses a single-use, HMAC-signed token delivered to your email inbox. The link is valid for 15 minutes and redemption is atomically claimed so concurrent or replayed use fails closed. On success the browser receives an opaque __Host-qub_session cookie with Secure, HttpOnly, SameSite=Strict, and Path=/ attributes.

Sessions have a 30-day idle limit and 90-day absolute limit, rotate after 24 hours, and accept only the immediately previous generation for a 120-second lost-response grace period. Sensitive account mutations require authentication within the preceding 10 minutes. The HMAC signing secret is a platform binding; a metadata-only read does not by itself mint a valid token.

6.2 API Keys (Developer Tier)

Developer API keys use the prefix qub_sk_ for easy recognition and grepability. Each key:

Admin key-management endpoints are gated behind a separate admin credential.

6.3 Email Attestation (Authorship Signing)

Binding an email address to a signing key requires:

  1. Possession of the private signing key (you sign a challenge)
  2. Possession of the email inbox (you enter a 6-digit code delivered by email)

Either alone is insufficient. Revocation is a signed record on your own account and takes effect immediately; viewers fetching the attestation see the revoked state and display accordingly.


7. Payments

Card entry and processing run inside Stripe-hosted checkout. We never receive card numbers, expiry dates, or CVCs. We do store Stripe customer and subscription identifiers, subscription state, and period data on entitlement/API-key records so access, renewals, metering, cancellation, and refunds can be reconciled. Stripe's privacy and security statements govern its handling of payment data.

The seal endpoint cross-checks the entitlement record against the device identifier and, for signed-in users, against the linked identity. An entitlement cannot be reused across devices without the user explicitly restoring it via magic-link sign-in.


8. Abuse Resistance

8.1 Bot Detection

The seal flow is gated by a privacy-preserving CAPTCHA alternative that does not use cookies for tracking and does not fingerprint for advertising. A failed challenge is rejected by our edge Worker before any seal-side processing happens.

8.2 Rate Limiting

Rate limits are enforced at several layers:

Counters and atomic claims are distributed across KV, Durable Objects, and platform rate-limit bindings according to the endpoint's consistency requirements. Rate-limited requests return 429; endpoints that can calculate a retry window include Retry-After.

8.3 Content Moderation

The default browser-upload route cannot scan the body: it receives only the client-sealed artifact. The Builder /api/v1/seal route sees plaintext transiently, and pact staging holds structured terms until finalisation, but those trust exceptions do not turn the general byte-blind upload path into a content scanner. Operational moderation is a denylist at the viewer layer: a denylisted qub is refused by our viewer regardless of whether the stored payload remains reachable. Denylisting does not retract durable bytes, transparency-log entries, or permanent-network data already published.

Abuse reports are rate-limited using a one-way hash of the reporter's IP; we do not store IPs in the clear for this purpose.


9. Supply Chain and Build Integrity

9.1 Toolchain Pinning

Compiler and runtime versions are pinned in repository configuration and dependencies are resolved through committed lockfiles. CI checks generated-file freshness and reproducibility-sensitive invariants. We do not make the stronger claim that every clean build is bit-for-bit identical across all supported machines.

9.2 Lints and Static Analysis

The workspace enables our strictest lint groups at the deny level. CI treats every warning — including documentation-link warnings — as a build failure. This is deliberate: we use the lint strictness as a tripwire for subtle regressions.

9.3 CI Gates

The CI workflow covers formatting and strict lints; Rust, WASM/browser, Worker, embed, and API tests; type checking; code coverage; mutation/invariant checks; dependency and workflow static analysis; i18n keys, coverage, drift, and hostile-codepoint checks; generated-doc/API/knowledge-base freshness; document inventory and internal-link checks; stylesheet and bundle budgets; and OpenAPI validation. Some expensive mutation jobs are scheduled rather than run on every push.

A single required ci roll-up remains red if any required job fails. Protected-branch and deploy workflows consume that result rather than duplicating a smaller security gate.

9.4 Mutation Testing

A weekly job runs mutation testing against the security-critical pure modules: hashing, canonical CBOR, seal, unlock, the wire-format newtypes, the protocol type validators, and the handle namespace. Mutation testing answers "does our test suite catch subtly wrong code?" — if a mutated implementation still passes all tests, we know we have a test-coverage gap and address it.

9.5 Git Hooks

Local hooks (pre-commit, pre-push) mirror the CI gates so regressions are caught before they leave the developer's machine. Hooks are installed via a repo script; they are not bypassed in our workflow and the CI is the authoritative gate if they are skipped.


10. Testing

Security-critical code carries three kinds of tests:


11. Branch and Release Hygiene

Feature branches advance staging only through a Gate 1 pull request: required ci green, no unresolved change request, no merge conflict, and a clean reviewed tree; the merge is squash-and-delete. main advances only through the Gate 2 staging → main pull request and preserves ancestry with a merge commit. Direct branch pushes are not the release workflow.

Staging and production deploys are triggered from the corresponding protected staging and main branch states after CI. Pull-request code and fork credentials do not receive deploy secrets.

Secrets used in deploy workflows are scoped to the deploy environment by our CI platform. They are not available to pull-request workflows from forks.


12. Coordinated Disclosure

If you believe you have found a security vulnerability in qub, we want to hear about it quickly and we commit to handling the report professionally.

We acknowledge receipt within three business days and keep you informed as we investigate. With your consent, we credit reporters in release notes.

12.1 Safe Harbor

If your research follows the rules above (good-faith investigation, no harm to other users or the service, reasonable disclosure window), we will not pursue legal action against you, and we will not ask law enforcement to. We treat your work as authorised testing and we'd rather you find the bug than someone else.

This Safe Harbor applies to:

It does not apply to social engineering of qub team members, denial-of-service tests, or accessing other users' data beyond what's needed to demonstrate the issue. If you're unsure whether something falls inside the Safe Harbor, ask first using the same [SECURITY] subject prefix.


13. Honest Limitations

Security is a practice, not a state. Some limitations are worth naming directly:


14. Changes to This Page

Material changes are noted by updating the effective date at the top. Where a change reflects a concrete security improvement, we describe it briefly in the public changelog. Where a change reflects a policy clarification, we describe what changed and why.

For questions about anything on this page, email support@qub.social with the subject prefix [SECURITY].


15. Change Log

Version Effective date Summary
1.1 23 September 2026 Reconciled cryptographic claims, delivery modes, storage, CSP, sessions, API keys, payments, CI, and release workflow with the implemented system.
1.0 2 May 2026 Initial publication.