v0.595.0 (Private Alpha)
v1 — 2026-06-10 · SOC 2: audit deliberately deferred until commercially gated; this page is the structural answer.
Audience: InfoSec reviewers, procurement teams, and vendors completing a security questionnaire. This page is honest about what is shipped, what is in progress, and what is explicitly deferred. Every claim cites the artifact or code path that backs it.
A fully-compromised coordinator — stolen disk, compromised process, subpoenaed operator — yields:
It does not yield: original documents (since v0.117 the platform has no document upload and stores no document bytes — only platform-authored sample PDFs exist server-side), decryption keys (the ceremony Content Key lives only in the URL fragment and in exported artifacts — it is never sent to the server), plaintext face images (E2E design; see §2), or the ability to forge a signer's consent (SHA-256 chain integrity; see INV-3 below).
Source: docs/ZERO_TRUST_INVARIANTS.md
| Invariant | Statement | Enforcement evidence |
|---|---|---|
| INV-1 Content integrity | Every document, face, and chain block returned by the server is addressed by its SHA-256; any mismatch is rejected client-side. | blobstore keys are content hashes; verify.verify_chain re-derives every block hash; signer re-hashes the PDF vs the QR code. Tests: test_invariants.test_inv1_*, test_seal_and_qr.test_blobstore_self_verifies, test_verify.test_pdf_hash_mismatch. |
| INV-2 Chain independence | Ceremony validity is decidable from the chain document alone — no server contact required. | verify.verify_chain is a pure function; the browser port chain-verify.js mirrors it. Tests: test_invariants.test_inv2_*, full test_verify.py suite. |
| INV-3 No consent-forging key on the server | The server holds no key whose compromise lets it forge a signer's consent. v1 has no per-signer signing keys; a link is fabricatable only by producing a block whose hash the next signer already committed (SHA-256; computationally infeasible). | Tests: test_invariants.test_inv3_*; code-grep confirms no signer private keys in src/. |
| INV-4 Offline verifiability | A holder can verify a closed artifact with no coordinator. | Verifier SPA + chain-verify.js run fully client-side; OTS proof is checkable against Bitcoin independently. Test: test_invariants.test_inv4_offline_verify_closed_artifact. |
| INV-5 No lock-in | A participant can export and verify independently of the coordinator, forever — even if COSigna disappears. | Chain document is self-contained JSON; verify_chain accepts it directly; round-trips without loss. Test: test_invariants.test_inv5_export_roundtrip_verifies. |
Corrected 2026-08-18, replacing an earlier simplified table that overclaimed in two places (see the correction note at the end of this subsection). Written from the enemy's-eye view: assume the COSigna operator is compromised, the host seized, or the operator simply lying.
Two kinds of "cannot," never conflated. Some limits are mathematical (a hash preimage, an
AEAD tag, proof-of-work) — they hold against any adversary with the server's data and keys. Others
are structural (a route that does not exist, a key that is never transmitted) — they hold
against this deployed version of the software, enforced by code gates rather than physics. Neither
CANNOT below rests on a server-held key — a guarantee that did would be self-attestation wearing a
signature's clothes. Two keys the server does hold: the Phileos CORE-attestation key and the
witness key (phileos.py, witness.py) — every signature COSigna itself produces is inside the
blast radius of a seized host.
CAN:
1. Stall or refuse anything it mediates — invitations, status, the /chain export, the anchor
submission, the close itself (invitation dispatch is explicit best-effort in service.py).
2. Observe the full social graph and timing — accounts, per-signer contacts, roster, ordering, and
a timestamped audit log (store.py add_audit) — even for "secret" ceremonies (secret mode 404s
the public read-model; it changes nothing about what the server itself stores).
3. Read metadata around the document and confirm a candidate PDF against the stored doc_hash, and
(unless the organizer chose "omit/encrypt filename") the plaintext filename. It does not
hold the document body — see CANNOT #1.
4. Delay, drop, or reorder notifications (email, web push, reminders) — all delivery is
server-discretionary.
5. Reorder or substitute a signer — today. Roster, sequence, and the withdrawal-window
parameters are ordinary DB state, committed into no chain block from OPEN through CLOSE
(chain.py's block-hash function never includes the roster). There is no exposed reorder API,
but a malicious server needs none — it writes the DB directly. Confirmed gap, tracked as D1;
closing it (committing roster/order/window into the genesis chain link) is a scoped, ruled,
in-progress fix (NEOCORTEX engineering record) — not yet a shipped guarantee.
6. Mint any participant's bearer token (stateless HMACs of a server-held secret, auth.py) and act
as that participant at the HTTP layer. It still cannot produce that person's face under an E2E
ceremony key — see CANNOT #3.
7. Set the canonical block timestamps (the server clock); device-reported capture time is advisory
by design. Before a confirmed Bitcoin anchor lands, all times are server-asserted.
8. Exercise its legitimate levers abusively — auto-skip an overdue signer, expire a ceremony, abort
it, lock a signer out after failed attempts.
9. Self-attest at every layer, and re-sign at will. The witness signature, the Phileos CORE
attestation, and the envelope seal are all produced with server-held keys — evidence against
third parties, worth nothing against the server itself.
10. Destroy availability (delete blobs and rows) — denial, never disclosure of encrypted content.
CANNOT:
1. Read the document body — it never receives it. (Structural.) Document upload was removed
from the API in v0.117 (founder-sovereignty doctrine): the PDF is hashed in the browser and only
the hash travels; the blob-store write path has zero live callers, both pinned by the regression
suite (tests/test_server_s5.py, tests/test_server_s4.py). This is enforced by route-absence
plus a code gate, not by mathematics — a future version could add an upload route back.
2. Alter a document after commitment, undetected. (Mathematical.) Every block commits the
document's SHA-256; the verifier recomputes it from the presented PDF client-side, byte-pinned
(tests/test_verifier_kernel_guard.py). This binds a presented pair — it is not by itself a
defence against a chain rebuilt wholesale before anchoring; see CANNOT #5.
3. View or forge an E2E-encrypted face. (Mathematical, within its scope.) Where face
encryption is in force, faces encrypt client-side under AES-256-GCM with a per-ceremony key
distributed only in the URL fragment, never transmitted to the server; the chain commits only a
face hash. Two scope limits that must travel with this claim: (a) encryption is
per-ceremony, not universal — a ceremony created without participant emails and without an
explicit flag still accepts a plaintext selfie (recorded as such); (b) the guarantee is against
fabricating a new face, not against replaying a face_hash (and its blob) already held
within the same ceremony, since the content key is shared there.
4. Insert itself into verification. (Structural, and strong.) A closed proof verifies fully
offline and keylessly in the self-contained verifier
(scripts/build_offline_verifier.py/cosigna-verifier-offline.html), which makes no network
request except an optional human-clickable mempool.space link. No COSigna endpoint, account, or
uptime is required to check a proof.
5. Rewrite a presented chain silently, or backdate past a confirmed anchor.
(Mathematical, with two stated conditions.) Each block's hash is committed by the next block's
prev; altering any earlier block breaks every later one. Against a holder who already has a
copy, or against a chain whose CLOSE hash is Bitcoin-confirmed via OpenTimestamps, a rewrite is
detectable. Two conditions are load-bearing: (a) anchoring is opt-in, off by default on the
create API; (b) OTS returns a pending status on submission and only becomes complete once
Bitcoin-confirmed — until then the anchor commits nothing an adversary must respect. Absent a
confirmed anchor and a counterparty's own copy, a server holding all the blocks can rebuild the
whole chain, including in a different order — see CAN #5.
6. Alter an issued proof bundle without the holder's copy disagreeing. (Mathematical against
third parties; NOT a guarantee against the server.) The bundle's immutable fields are sealed by
a canonical digest, signed into the CORE attestation. Anyone who does not hold the Phileos
key cannot edit a sealed field without the recomputed digest diverging. The server holds that
key, so it can re-seal and re-sign an altered bundle and still satisfy the verifier's
pinned-fingerprint check. Against the server, the residual guarantee is only what the anchored
close-hash transitively commits.
Honest boundary — three gaps this statement does not paper over. (1) Ordering/roster integrity (CAN #5): until D1 lands, "the server is blind" is true for the document body and for E2E faces, not yet for sequence. (2) Every signature COSigna makes is self-signed (CAN #9): the CORE attestation, envelope seal, and witness signature all use server-held keys — they carry weight against third parties and none against the operator. The only trust root outside the server is the Bitcoin anchor, which is opt-in and only binding once confirmed. (3) Document blindness, the strongest property, is structural rather than mathematical (CANNOT #1) — genuinely enforced by a test gate today, but a property of this version, not a law.
Correction note (2026-08-18): the prior table on this page claimed the server "cannot substitute the document or a face undetectably" and "cannot backdate closure past what OTS attests" without qualification. Both overclaimed — see CAN #5 (roster/order mutability, an open gap) and CANNOT #5 above (anchoring's two load-bearing conditions). This section replaces that table with the fuller, independently fact-checked statement above.
Cookie-free sessions. COSigna sets no cookies. Session state is held in localStorage as a bearer token; Set-Cookie and document.cookie are never used. This is an intentional product differentiator. Source: docs/VERSIONS.md (v0.105.0); enforced by STATUS.md invariant 12.
No third-party assets or analytics. Every asset (fonts, JS, CSS) is served from the same origin. No third-party analytics scripts, no CDN includes. Source: UX/DESIGN_UI_SPEC.md; UX/CLAUDE_DESIGN_BRIEF.md.
First-party telemetry only, DNT/GPC-respecting. All telemetry is first-party and identifier-free: the client-error beacon (/telemetry/client-error), the anonymous option-flip beacon (/telemetry/option), the k-anonymous performance RUM beacon (/telemetry/perf, k=20 display floor), the ephemeral-visit A/B beacon (/telemetry/ab), and — since v0.523.0 — server-side aggregate request accounting (counts by cyclic hour-of-day / coarse path family / status class, plus edge-derived country codes; the schema is structurally incapable of holding an IP, raw path, UA, or session id, and a test asserts it). Since v0.558.0 a second, DATED breakdown (day + hour, not just cyclic hour-of-day) is also recorded, retained 90 days, and read behind an always-on k=5 suppression floor with no way to disable it from outside — a stricter floor than the cyclic table needs, since a dated hour count can single out a person in a way a bare cyclic one cannot. The client-side beacons honour the browser Do-Not-Track signal and fire nothing when DNT=1; the server-side accounting honours both DNT: 1 and Sec-GPC: 1 (Global Privacy Control) — an opted-out request is not counted at all, even in aggregate (v0.523.1; the stricter reading, chosen deliberately), and this applies identically to the dated breakdown. None of it captures PII, capability tokens, session IDs, or URL fragments. Source: UX/libs/telemetry.js (inline DNT check, capability-token redaction); UX/libs/perf-beacon.js; src/cosigna/server.py (_privacy_opted_out, _record_traffic, _classify_path); tests/test_traffic_counters.py (DNT/GPC suppression + schema-has-no-identifier tests for the cyclic table — the dated table's own equivalent coverage is owed, see DPIA-GDPR-CNIL/DPIA_amendment_traffic_country_analytics_EN.md §7.1).
End-to-end face encryption. The cryptographic core is implemented and independently reviewed. Per ceremony, a 32-byte Content Key (CK) is generated in the initiator's browser. Each consent selfie is encrypted with AES-256-GCM (nonce || ciphertext, AAD = SHA-256 of the plaintext) before upload. The server stores ciphertext only; it never receives CK. CK travels in the URL fragment (#k=...) which browsers strip before the request is sent. An independent Opus review confirmed: AES-256-GCM correct, AAD binding correct, face_hash integrity preserved, Python/JS wire-compatible, no key/plaintext/timing leak. Status: crypto core AND browser wiring shipped (src/cosigna/facecrypto.py, UX/mobile-signer/facecrypto.js, 31 parity tests; all 3 SPAs encrypt/decrypt since v0.1.1). Source: docs/E2E_FACE_ENCRYPTION.md.
No biometric templates. COSigna stores raw consent selfies (or their E2E-encrypted ciphertext), never a biometric embedding or template. This is a locked architectural decision (Q4 Option A, 2026-05-28). Source: NEOCORTEX/_archive/Q4_biometric_template.md; STATUS.md invariant 5.
Hash is the contract. Every PDF reference travels with its SHA-256. No code path trusts a PDF without re-verifying. Source: CLAUDE.md § Working rules; STATUS.md invariant 3.
GDPR erasure = crypto-erasure. Deleting a ceremony's CK renders every consent selfie of that ceremony unrecoverable noise while the hash commitments (legal weight of the chain) remain untouched. Single key destruction erases all faces of a ceremony at once, provably. Source: docs/E2E_FACE_ENCRYPTION.md §6; src/cosigna/lifecycle.py.
Secrets never in git. The repository mirrors to a third-party GitLab instance (gitlab.setip.io). This is precisely why no secrets may appear in any tracked file. All production secrets (HMAC key, witness Ed25519 key, admin token) are injected at docker run via --env-file (never per individual -e flags, which expose values in process listings). Source: CLAUDE.md § Notification/secrets rules; STATUS.md invariant 10; src/cosigna/server.py line 1: "No secrets in code (§16)".
Secrets rotation runbook. A documented rotation procedure covers four secret surfaces in priority order by blast radius: tunnel master token, GitLab PAT (scoped to write_repository), app secrets (COSIGNA_SECRET / COSIGNA_ADMIN_TOKEN via scripts/rotate-secrets.sh), and the Ed25519 witness key (rotate only on suspected compromise; old proofs keep verifying against the old public key). Source: docs/SECRETS_ROTATION.md.
Admin surface gating. /admin/* endpoints require the X-COSIGNA-Admin header equal to COSIGNA_ADMIN_TOKEN (runtime env var). Fails closed: an unconfigured token disables the surface. Source: src/cosigna/server.py lines 110, 3250.
Capability token scoping. Every signer/initiator action requires an HMAC capability token scoped to (cid, kind, subject). Cross-actor escalation (a signer performing initiator actions) is blocked at the token level. Source: src/cosigna/auth.py; tests test_auth.test_signer_and_initiator_tokens_differ, test_server.test_signer_cannot_use_initiator_endpoint.
Input validation. All hash parameters are validated against ^[0-9a-f]{64}$ (path traversal impossible). PDFs are structurally parsed via pypdf; face uploads are magic-byte checked; uploads are chunked with a size cap. Source: src/cosigna/server.py _validate_hash; tests test_server_s4.test_hash_path_traversal_rejected, test_server_s5.test_upload_polyglot_pdf_rejected.
Transport encryption. TLS terminates at the deployment layer (Caddy + Let's Encrypt on the Scaleway box). Not enforced in application code (documented residual R3 in docs/THREAT_MODEL.md).
Deployment. Single-host alpha on a Scaleway EU (Paris) VPS — migrated from the prior tunneled setup on 2026-07-06 (docs/DEPLOY_FOUNDER_CHECKLIST_ovh_scaleway.md). Server and worker run as Docker containers from a pinned image tag with a documented rollback path.
| Control | Status | Notes |
|---|---|---|
| ✅ Hash-chain integrity (INV-1..5) | Shipped | 3580 backend tests green (2026-08-04); adversarial suite in tests/test_invariants.py |
| ✅ No consent-forging key on server (INV-3) | Shipped | No signer private keys in src/; code-grep + test coverage |
| ✅ E2E face encryption — crypto core | Shipped | facecrypto.py + facecrypto.js, 31 parity tests, Opus-reviewed |
| ✅ No biometric templates | Shipped | Architectural lock; no template code path exists |
| ✅ Cookie-free sessions | Shipped | localStorage bearer; no Set-Cookie ever issued |
| ✅ No third-party assets / analytics | Shipped | Own-origin only; enforced by design spec + code review |
| ✅ DNT-respecting first-party telemetry | Shipped | UX/libs/telemetry.js; DNT check at load, token redaction |
| ✅ Secrets never in git | Shipped | --env-file injection; repo mirrors to third party (gitlab.setip.io) |
| ✅ Secrets rotation runbook | Shipped | docs/SECRETS_ROTATION.md; scripts/rotate-secrets.sh |
| ✅ Admin surface token-gated | Shipped | X-COSIGNA-Admin header; fails closed |
| ✅ Capability token scoping + cross-actor enforcement | Shipped | src/cosigna/auth.py; test suite |
| ✅ Input validation (path traversal, polyglot, size) | Shipped | _validate_hash; pypdf structural parse; magic-byte check |
| ✅ Face-access token gating | Shipped | GET /faces/{hash} requires HMAC face token; test_face_retrieve_requires_token |
| ✅ Crypto-erasure (Art. 17) | Shipped | src/cosigna/lifecycle.py; CK-destruction design in docs/E2E_FACE_ENCRYPTION.md §6 |
| ✅ Offline verifiability (INV-4/5) | Shipped | Verifier SPA + chain-verify.js fully client-side; OTS / Bitcoin anchor |
| ✅ OpenTimestamps / Bitcoin anchor | Shipped | Background task; never blocks close; stub vs real distinguished by anchor_verified field |
| ✅ COSIGNA_ADMIN_TOKEN set on live deployment | Shipped | Confirmed on the Scaleway production container: /admin/users returns 403 (not the fail-closed 503) unauthenticated, 2026-07-19 (docs/VERSIONS.md) |
| ✅ Offsite backups | Shipped | scripts/backup-tier0.sh — WAL-safe nightly snapshot (systemd timer) + off-box copy; first restore proven 2026-07-27 (every closed ceremony on the box re-verified GREEN from the restored bytes) |
| ☐ PostgreSQL (production DB) | Deferred, by choice | SQLite remains the production DB post-Scaleway-cutover — a deliberate choice (the migration was a file copy, not a DB change); Postgres path stays validated (test_store_portability.py) and unscheduled |
| ☐ Single-host deployment | Alpha state | One VPS — the Scaleway EU box itself, live since the 2026-07-06 cutover (docs/DEPLOY_FOUNDER_CHECKLIST_ovh_scaleway.md); no further host-topology change scheduled |
| ☐ Token revocation / short-lived query tokens | Residual R1/R1b | Stateless HMAC tokens valid until secret rotates; query-param token in server logs (documented in docs/THREAT_MODEL.md); mitigation path: per-ceremony versioning or short-lived JWTs |
| ☐ SOC 2 Type II | Deferred | See §6 |
To report a security issue: p@phileos.eu (PGP on request) or use the in-product /bug page (available at the live service URL), which routes directly to the operator.
We aim to acknowledge within 48 hours and to provide a status update within 7 days. There is no formal bug-bounty programme at this stage.
SOC 2 Type II attests that a service operator's controls around security, availability, processing integrity, confidentiality, and privacy operated effectively over an observation period — it is an audit of organisational and operational controls, not a certification of cryptographic architecture.
COSigna's blind-server architecture materially changes the risk calculus for several Trust Services Criteria (the server cannot produce plaintext documents or faces even under compulsion; consent integrity is mathematically enforced rather than procedurally attested), but it does not replace the audit: organisational controls around physical access, personnel, incident response, and vendor management are not covered by the zero-trust protocol and require a separate assessment.
The SOC 2 audit is deliberately deferred until the service is commercially gated — specifically, until Stripe live mode is active and the first paying cohort is onboarded (the Scaleway production migration itself completed 2026-07-06; being live on Scaleway does not by itself end Private Alpha). Pursuing the audit before those gates would produce a report over an alpha-phase control environment that does not represent the production posture. The structural answer to the gap, in the interim, is this document.
Last updated: 2026-08-18 (Tier-1 survey item #7: replaced the simplified "can/cannot" table with the fact-checked adversarial statement — two prior overclaims corrected, the roster/order gap (D1) admitted openly rather than left silent). Prior update: 2026-08-04. Originally published: 2026-06-10. Version: v1.
↑ Back to top