Security practices

v0.595.0 (Private Alpha)

COSigna — Security Practices

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.


1. The structural answer: what a breach of the COSigna server yields

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).

Zero-Trust Invariants — INV-1 through INV-5

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.

What a fully malicious server can and cannot do (adversarial statement)

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.


2. Data handling

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.


3. Operational security

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.


4. Honest current-state table

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

5. Vulnerability disclosure

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.


6. SOC 2 position

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
COSigna — patent EP3665600B1 · US11531746B2 · FR3070079B1
Report bugCOSigna — a Cryptographic Ring of Mutual Consent · The ring is strong because the loop is closed.The ring is strong because the loop is closed.