Security practices

v0.538.5 (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 the server can and cannot do

The server CAN The server CANNOT (clients detect it)
Refuse service / degrade availability Forge or alter a signer's consent
See ceremony metadata (who, when) Substitute the document or a face undetectably
Hold the additive witness key Fabricate a chain link or a closure
Order, relay, notify Make a tampered chain verify
Count credits (billing) Backdate closure past what OTS attests

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

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 is assumed at the deployment layer (tunnel termination). Not enforced in application code (documented residual R3 in docs/THREAT_MODEL.md).

Deployment. Currently a single-host alpha on a tunneled VPS (Scaleway EU migration planned). Both 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 901 backend tests green; 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 In progress Env var exists; not yet confirmed set on the production container; pending go-live hardening (docs/PRODUCTION_GOLIVE_RUNBOOK.md)
☐ Offsite backups Not yet started SQLite snapshot to a separate host; pending Scaleway/Postgres decision
☐ PostgreSQL (production DB) In progress SQLite today (alpha); Postgres migration path validated (test_store_portability.py); Scaleway cutover gated on founder decisions
☐ Single-host deployment Alpha state One VPS today; Scaleway EU production environment planned (docs/DEPLOY_FOUNDER_CHECKLIST_ovh_scaleway.md)
☐ 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 the Scaleway production environment is live, the Stripe live mode is active, and the first paying cohort is onboarded. 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-06-10. Version: v1.

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