Security at qub
Effective date: 2 May 2026 Version: 1.0 — initial publication
For researchers — quick reference:
- Where to send reports: support@qub.social with the subject prefix
[SECURITY].- What to include: the vulnerability, steps to reproduce, and any proof-of-concept.
- Our response: we acknowledge receipt within 3 business days and aim to ship a fix within 90 days.
- Safe Harbor: we will not pursue legal action against good-faith research that follows the rules in §12 (no access to data that isn't yours, no service degradation, no retention of obtained data beyond what's needed to demonstrate the issue, give us a reasonable disclosure window).
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 — being able to point at a sealed message and prove "this existed at that exact moment, before the world could have fabricated it" matters more, not less, as AI improves. That 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:
- Minimise what the server can see. Plaintext content never leaves your device unencrypted. Where we must hold metadata, we hold the smallest amount consistent with the feature.
- Make compromise locally contained. A breach of any one component (our server, the email provider, a drand node) should not reveal sealed content that has not yet reached its reveal time.
- Make the protocol auditable. The sealed artefact is verifiable end-to-end with public cryptography. You do not need to trust qub the service to verify a qub the artefact.
2. Threat Model
2.1 What We Protect Against
- An attacker who gains full read access to our server infrastructure before reveal time. They learn metadata (timestamps, device identifiers, attestation records). They do not learn sealed plaintext.
- An attacker who intercepts traffic between your browser and our infrastructure. TLS terminates at our CDN edge; sealed payloads are already encrypted before transit.
- An attacker who tampers with a sealed payload after it reaches permanent storage. Tampering changes the body hash, which is bound into the signature chain; verification fails and the viewer refuses to render the qub.
- An attacker who tries to bind a forged author email to a signing key. Email attestation requires possession of both the private signing key and a one-time code delivered to the email inbox.
- A compromised drand beacon operator. The drand network uses threshold BLS signatures across multiple independent operators; a minority cannot forge early-release signatures.
2.2 What We Cannot Protect Against
We are honest about our limits. qub cannot defend against:
- A compromise of your device before you seal. Local keyloggers, malicious browser extensions, or physical access to an unlocked device can capture plaintext at the point of composition.
- A collapse of the drand threshold. Multiple independent organisations run the drand network specifically to make this difficult, but it is not cryptographically impossible: if enough operators collude, they could derive timelock keys early.
- The fundamental property of permanent storage: once data is written it is permanent. An attacker who obtains a valid copy of the sealed payload before reveal — and is willing to wait — cannot be stopped from reading it at reveal time. This is the feature, not a bug.
- A global adversary who breaks the underlying cryptography (AES-GCM, BLS12-381 pairing assumptions, SHA3-256, ML-DSA-65). If these primitives fall, the cryptographic ecosystem at large has larger problems.
3. Client-Side Cryptography
All content encryption happens in your browser before any network request is made.
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:
- 3-second round period
- Unchained mode (each round is independent)
- BLS12-381 G1 signatures
- Chain hash
52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
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
Sealed content is serialised using deterministic CBOR (RFC 8949 §4.2 core deterministic encoding). Two clients sealing the same content with the same parameters produce byte-identical payloads. This is load-bearing for verification: the body hash is computed over the canonical byte sequence, so any re-encoding of the payload breaks verification.
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 canonical body. The hash is bound into the signature chain. A viewer recomputes the hash on download and rejects the payload if the recomputed hash does not match.
The 32-byte content identifier qub_id is derived from a 92-byte preimage covering the protocol version, content type, created and unlock timestamps, the body hash, and the SHA3-256 of the optional NFC-normalised title. Concretely: a gateway, CDN, or any other party between the seal and the viewer cannot swap the visible pre-reveal title without the qub failing verification — what the creator wrote on the date-picker step is what every visitor sees on the countdown, or the qub does not render. Titles are capped at 100 NFC code points and are reviewed for control characters at validation time.
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.
Private signing keys are generated and stored on your device. They never leave your browser. Only public keys and attestation records reach our server.
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 — i.e., once the drand round signature for a qub's unlock time has been published — the canonical timelock layer alone would let anyone who indexed permanent storage bulk-decrypt every qub in storage. We close that channel with an additional symmetric encryption layer that wraps the timelock-encrypted bytes before they reach permanent storage (PROTOCOL.md §13).
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 by default. 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, every storage gateway, every CDN in front of either, and any monitoring system inside our infrastructure are observationally blind to K. Wrapped bytes in permanent storage are byte-indistinguishable from arbitrary ciphertext.
Net consequences:
- qub.social cannot decrypt its own corpus. A subpoena, server compromise, insider threat, or rogue admin reaches ciphertext, not plaintext. We literally lack the key.
- Fragment loss is unrecoverable. If you save a link without the fragment, or if a copy-paste tool strips it, the qub becomes unreadable forever — there is no recovery path on our side. The seal flow surfaces an explicit "save this URL" disclosure for this reason.
- Opt-in recovery. When you opt into creator lifecycle emails for a qub AND the email matches your verified identity, we accept K with the upload, store the full delivery URL on your identity's sealed-history record, and use it as the link in the seal-confirmation email. This trade — a server-side recovery channel in exchange for some end-to-end purity — engages only on explicit opt-in and only for that qub. The default posture is crypto-shredding.
The Worker's server-side /api/v1/seal endpoint (used by AI agents and other API callers) generates K server-side, returns it to the caller in wrapper_key_b64url, and never persists it. The trust posture for that path is identical: K leaves the Worker only as part of the response.
4. Transport and Edge
4.1 TLS
All traffic between your browser and our infrastructure uses TLS 1.3 terminated at our CDN edge. We do not operate origin servers you can reach directly; every request passes through our edge.
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 that pins outbound connect-src to 'self' plus the two drand endpoints listed in §4.3. The iframe runs with sandbox="allow-scripts allow-top-navigation-by-user-activation": the host page cannot read the iframe's DOM, and the iframe cannot navigate the host page except in direct response to a user click on the embed's CTA.
4.3 CORS and Fetch Scope
The browser client makes fetch requests only to:
- Our own API (
api.qub.socialand staging equivalents) - Storage gateways (read-only, for wrapped CBOR retrieval — §3.6)
- drand beacon endpoints (read-only, for reveal-time round signatures)
Each destination is whitelisted; an unexpected destination indicates a tampered build and is caught by SRI on deploy artefacts.
The embed iframe is loaded same-origin from qub.social and reaches the same destination set as the main client: wrapped CBOR is fetched directly from storage gateways and unwrapped in-browser using the wrapper key K from the embed's URL fragment, and the reveal-time round signature is fetched from drand. The embed iframe's CSP pins connect-src to exactly api.drand.sh and drand.cloudflare.com for drand traffic; the main SPA uses a broader fallback set (currently four endpoints — drand.cloudflare.com, api.drand.sh, api2.drand.sh, api3.drand.sh — configured in config/drand-endpoints.json) so a single endpoint outage does not break reveal. The iframe's CSP forbids any other origin, so a compromised payload cannot exfiltrate data to a third-party host.
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 small "public CORS" middleware applies a wildcard Access-Control-Allow-Origin: * only to the tight allowlist of endpoints that the embed loader and iframe actually call: the loader script, the iframe shell, /api/v1/qub/{tx_id} together with its /meta, /watch, and /view actions, /api/v1/handle/{handle} (so embeds can resolve byline attestations), and /api/v1/telemetry. Every other API endpoint preserves the qub.social-restricted CORS policy unchanged.
5.2 Storage
- Our metadata store holds entitlement records, identity records, attestation records, denylist entries, per-key rate-limit counters.
- Our object store holds structured event logs (NDJSON) and a cache of read-only qub JSON responses.
- Permanent storage holds wrapped sealed payloads (§3.6, PROTOCOL.md §13). We do not operate the permanent storage backend; we submit payloads through a server-side signing wallet and then rely on the network. Because the wrapper key K never reaches us by default, the bytes we sign and submit are opaque to us and to every party between us and permanent storage.
Plaintext content is never stored anywhere we operate. Our storage tier is metadata-only by construction.
5.3 Secrets
Secrets (signing wallets, API tokens for our payment and email providers, HMAC keys for magic-link tokens) are stored as platform-managed secrets. They are not visible in source control and are not readable by our runtime except by name. Rotation is manual and logged.
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 is invalidated immediately on use. Token contents are opaque; the server verifies the HMAC on redemption. If the token has been tampered with, the signature does not verify and the request is rejected.
We deliberately chose HMAC over stored random tokens so that a read-only compromise of our metadata store does not yield usable sign-in tokens; an attacker would need the HMAC key, which is a platform-managed secret.
6.2 API Keys (Developer Tier)
Developer API keys use the prefix qub_sk_ for easy recognition and grepability. Each key:
- Is bound to an account and a set of allowed IP CIDR ranges
- Can be rotated with a grace period in which both the old and new key are accepted
- Has independent rate-limit counters in our metadata store
- Is never logged in full; logs record only the key identifier
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:
- Possession of the private signing key (you sign a challenge)
- 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
Payment processing runs entirely inside Stripe's checkout UI. We never see card numbers, expiry dates, or CVCs, and we do not store any of Stripe's customer identifiers beyond the entitlement binding. Stripe's privacy and security statements govern their handling of that 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:
- Per-IP and per-key limits on seal, read, and auth endpoints
- Per-email limits on magic-link requests (prevents mailbox flooding)
- Per-counter-party limits on pact invite emails (ten per recipient address per UTC day, the primary spam-relay mitigation; honest pacts almost never approach the cap)
- Per-IP limits on telemetry submission
Limits are counters in our metadata store; exceeding them returns a 429 response with a Retry-After header.
8.3 Content Moderation
We cannot scan sealed content — it is encrypted on your device. We operate a denylist at the viewer layer: a qub on the denylist is refused by our viewer regardless of whether the stored payload is reachable. This is the full extent of moderation our architecture permits. We are transparent that denylisting does not remove data from permanent storage.
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. All dependencies are pinned to exact versions via lockfiles. A build on a clean checkout produces a deterministic artefact.
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
Every push runs:
- Formatting check
- Full lint sweep across all targets
- Doc-link verification
- Native test suite (hundreds of tests, including property-based tests)
- Headless browser tests
- i18n key-consistency script
- Server-side type check
- Server-side unit and property-based tests
Deploy workflows depend on CI passing. A branch cannot be merged to main until every gate is green.
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:
- Unit tests verify expected behaviour on known inputs, including test vectors derived from the protocol specification.
- Property tests generate thousands of arbitrary inputs and assert invariants: canonical CBOR round-trips, signature verification round-trips, email-binding predicates, pact acknowledgement determinism.
- Cross-implementation tests verify that our client and server implementations agree byte-for-byte on canonical encodings. This catches divergence between the two implementations before it reaches production.
11. Branch and Release Hygiene
main is only advanced by merging dev after CI is green. Deploy workflows are gated on CI passing and trigger only on pushes to main (production) and dev (staging) — never from PR branches or forks. There is no path by which an un-reviewed, un-CI-gated change reaches production.
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.
- Email
support@qub.socialwith the subject prefix[SECURITY]. - Describe the vulnerability, steps to reproduce, and any proof-of-concept.
- Give us a reasonable disclosure window (typically 90 days) before going public.
- Do not access data that does not belong to you, degrade service for other users, or retain data obtained during research beyond what is necessary to demonstrate the issue.
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:
- Research on the live qub.social service (not on test fixtures we publish for that purpose).
- Reverse-engineering of our published binaries and the open-source qub-core / qub-app crates.
- Any vulnerability class — protocol, application, infrastructure, supply-chain — that affects qub.
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:
- We are a small team. Our review depth does not match a large corporation's dedicated application-security function. We compensate with strict automated gates and a minimal attack surface, but we do not claim infallibility.
- The permanence of our storage backend is a one-way door. If a mistake causes sealed content to become decryptable earlier than intended, we cannot undo it. We treat the seal flow with commensurate care.
- The drand network is an external dependency. A catastrophic failure of drand would affect every qub's reveal behaviour. We monitor drand health and have contingency documentation for chain migration if required. For unlock dates more than 2 years out, the seal-time confirmation modal shows an explicit disclosure: long-horizon qubs depend on drand chain durability, and a future drand chain migration may require recovery steps to unlock the qub. For unlock dates more than 5 years out, you must check an extra box confirming you've read and accept this risk before the seal proceeds.
- Cryptographic primitives we rely on are standardised and widely reviewed, but cryptography evolves. Where we have choices (post-quantum signing, authenticated encryption), we pick the more conservative option.
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.0 | 2 May 2026 | Initial publication. |