Threat model
This page lists what Certichain protects, the parties that might attack it, what the design has to trust, and how each threat is handled. It describes the design in general terms and points to the code that enforces each control. It reflects the repository as of 2026-10-24.
Assets
| Asset | Why it matters | Where it lives |
|---|---|---|
| Integrity of the issuer list | A fake issuer could issue convincing credentials | IssuerRegistry on chain |
| Integrity of credential anchors and signatures | They are what a verifier checks | CredentialRegistry and its events |
| Correct status | A revoked credential must not pass | StatusRegistry |
| Confidentiality of documents | Transcripts and certificates are personal data | Encrypted containers on IPFS; keys with holders |
| Confidentiality of holder details | Names and student ids are personal data | Credential packages; only salted hashes on chain |
| Admin and guardian keys | They control the issuer list and the pause | Safe owners, guardian wallet |
| Issuer wallet keys | They sign credentials and derive document keys | Each institution's wallet |
| Server signing keys | Verifiers trust API answers and status lists signed by them | Server environment (API_SIGNER_KEY, STATUS_LIST_SIGNER_KEY) |
| Availability of verification | Verifiers must be able to check at any time | Public RPC endpoints, the web app |
Actors
| Actor | Capabilities assumed |
|---|---|
| Anonymous member of the public | Reads the chain, IPFS and public pages; calls public routes |
| Credential forger | Wants a credential that passes for a registered institution's |
| Dishonest holder | Alters a package, a document or a disclosure; presents someone else's credential |
| Curious verifier or recipient | Keeps what they were shown; tries to learn more |
| Compromised or malicious server, pinning or RPC provider | Reads, drops or swaps stored data; returns wrong answers |
| Phishing site | Asks a user's wallet to sign typed data or transactions |
| Malicious script in the page | A compromised dependency or injected script running in the app's origin |
| Thief of one key | Holds one issuer, guardian or Safe owner key |
Out of scope, as in SECURITY.md: attacks on third-party services themselves, social engineering, volume denial of service, and a fully compromised user device. An issuer is trusted for the content of its own credentials: if a registered institution signs a false statement, Certichain records it faithfully and the institution is accountable for it.
Trust boundaries
What each party is trusted for:
- The blockchain is trusted for ordering and for executing contract code as written. Sepolia is a test network and its security is lower than mainnet's.
- RPC endpoints are trusted to report chain state honestly for reads. The app uses several endpoints for failover, not as a quorum, so one dishonest endpoint could give a wrong read. A verifier who needs more assurance can use their own node through the offline script.
- The server is trusted for availability and for enforcing upload rules, never with plaintext documents or keys. Its signing keys are trusted only by verifiers who choose to use the API or status lists, and they hold no funds.
- The page code is trusted by its user, as for any web application that handles secrets in the browser. This is the main residual risk of client-side encryption (see below).
- The issuer is trusted for its own credentials only.
Threats and controls
Forgery and tampering
| Threat | Control | Code |
|---|---|---|
| Issue a credential as a registered institution without its key | The registry checks an EIP-712 signature by the issuer (ECDSA or ERC-1271) and that the issuer is registered and active | blockchain/contracts/v03/CredentialRegistry.sol |
| Replay a signed credential, on another chain or registry, or twice | The domain binds chain id and registry address; each (issuer, nonce) is accepted once | CredentialRegistry.sol |
| Register a look-alike institution | Only the admin registers issuers; names are unique and at most 128 bytes | blockchain/contracts/v03/IssuerRegistry.sol |
| Alter a package to claim other details | The verifier recomputes the credential id, the subject commitment and the metadata hash from the package and compares them with the chain | src/lib/credential-package.ts, src/lib/credential-disclosure.ts |
| Present a different document | The verifier hashes the file locally and compares it with the anchored documentHash | src/services/domain/credential-service.ts |
| Swap or edit a stored container | The AEAD tag covers the issuer and document hash; the decrypted file must hash to the anchored value; the result is a refusal, not a forged document | src/lib/doc-crypto/document.ts |
| Forge a grant file | The opening app checks the chain first: the credential must exist and name the grant's issuer and document hash | DocumentService.openGrant in src/services/domain/document-service.ts |
| Rewrite history | Nothing is deletable; status changes only move forward through defined transitions and are logged as events | blockchain/contracts/v03/StatusRegistry.sol |
Key compromise
| Threat | Control | Residual |
|---|---|---|
| Stolen issuer key | The institution or admin suspends or revokes affected credentials; the guardian can suspend the whole issuer at once; the admin can remove the issuer; in contract version 0.5.0 the admin can retire the key in favour of a successor so honest credentials stay valid | Credentials issued with the stolen key before detection exist until revoked. Documents the key could open stay readable to the thief if they can fetch them. |
| Stolen guardian key | The guardian can only pause and suspend issuers; the admin revokes the role | A pause or suspension until the admin acts (in 0.5.0 a stolen guardian key can also unpause) |
| Stolen Safe owner key | 2 of 3 signatures are needed; the Safe replaces the owner | Two stolen keys would control the admin; the admin transfer delay and the timelock delay give watchers time to react |
| Stolen server signing key | Keys hold no funds; the monitor checks that each key's address equals the published one; rotation is in the keys runbook | Answers signed with the stolen key are accepted by verifiers who pinned it until they update the pin |
Privacy
| Threat | Control | Residual |
|---|---|---|
| Learn a student's name or id from the chain | v0.3 stores only salted commitments and hashes; a test scans the contracts for plaintext fields | v0.2 legacy records store plaintext names and ids on chain |
| Guess a commitment from a class list | 32-byte random or wallet-derived salts | None known for the hashes themselves |
| Read documents from IPFS or the pinning service | AES-256-GCM, keys never leave devices except in the package, grants (wrapped) and links | Header reveals issuer, document hash and size rounded to 64 KiB |
| Replay a disclosure to another verifier | The holder signs the verifier's random challenge | A verifier keeps what they were shown |
| Link an access proof to a member | Semaphore hides which member proved | The sender's address is in the receipt |
See Privacy design.
Server-signed outputs
| Threat | Control | Code |
|---|---|---|
| Replay an old API answer | The caller sends a nonce; the signed envelope echoes it, with generatedAt and a 300 second validity window and the hash of the body | src/lib/api/envelope.ts |
| Pass an API verdict off for another file | The POST /api/v1/verify verdict signs the file's SHA-256 | src/lib/server/api/ |
| Serve a forged or stale status list | Lists are signed with the StatusList type, carry a validity window and a 5 minute time to live, and must match the address they were fetched from; checkers pin the publisher | src/lib/vc/status/list-credential.ts |
Abuse of server routes
| Threat | Control | Code |
|---|---|---|
| Upload by someone who is not an issuer | SIWE sign-in, an HS256 session cookie, and an issuer-registration check repeated every 5 minutes | src/lib/server/sign-in.ts, session.ts, issuer-session.ts |
| Replay a sign-in | Single-use nonce with a 5 minute life; domain, chain id and statement checked | src/lib/server/sign-in.ts |
| Flood the routes | Per-IP and per-wallet limits in Redis, for example 30 uploads per wallet per hour | src/lib/server/limits.ts, rate-limit.ts |
| Hostile files | Size limits before reading, allow-listed types checked by magic bytes, strict container parsing, padding checks, download only | src/lib/doc-crypto/payload.ts, container.ts, src/lib/server/upload-container.ts |
| Framing and sniffing | frame-ancestors 'none', X-Frame-Options: DENY, nosniff, a strict referrer policy; no-referrer and no-store on /open | src/lib/security-headers.ts |
Zero-knowledge access
| Threat | Control |
|---|---|
| Copy someone's proof from the mempool | The proof's message must be the sender's address |
| Use one proof twice | The gate records nullifiers per credential; the scope changes each day |
| A bystander burns a member's nullifier | The gate spends nullifiers itself rather than through the library's public validation function |
| Malicious circuit files | The browser uses only files whose SHA-256 matches the reviewed list; CI checks the verification keys against the verifier |
| Access to a revoked credential | Admission and proofs need the credential to be valid |
Residual risks
These are accepted for the test network and named in the handbook as items for review before production:
- Compromised page code. Client-side encryption is only as safe as the code that runs it. The content security policy has no
script-srcdirective yet (the code comment notes that a strict script policy needs nonces), dependencies are pinned, and an external review is planned. - Phishing for key signatures. A site that persuades a user to sign the
EncryptionKeytyped data for their address and chain can derive their encryption key. It still needs the containers to read anything. - Access already given. A recipient who saw a document, or kept a key and a copy, keeps it; key replacement protects only new copies.
- Single RPC view. Reads trust the configured endpoints.
- Quantum computers. X25519 and ECDSA are not post-quantum. The container format is versioned so the suite can change.
- No external audit yet. See Known limits.
Source files
blockchain/contracts/v03/CredentialRegistry.sol,IssuerRegistry.sol,StatusRegistry.sol,HolderRegistry.solblockchain/contracts/zk/CertichainAccessGate.solsrc/lib/credential-package.ts,src/lib/credential-disclosure.ts,src/lib/doc-crypto/,src/lib/disclosure/src/lib/api/envelope.ts,src/lib/vc/status/list-credential.tssrc/lib/server/sign-in.ts,session.ts,issuer-session.ts,limits.ts,rate-limit.ts,upload-container.tssrc/lib/security-headers.tsdocs/specs/P2-S01-encryption-design.md(section 5, the encryption threat table)
Tests
blockchain/test/v03/CredentialIssuance.test.ts,IssuerRegistry.test.ts,StatusRegistry.test.ts,NoPlaintext.test.ts,PauseClassification.test.ts,KeyLineage.test.tsblockchain/test/invariants/Invariants.test.ts,KeyLineage.invariants.test.tsblockchain/test/zk/AccessGate.test.ts,ProofCallGuard.test.ts,KeyCheck.test.tssrc/lib/doc-crypto/boundary.test.ts,access-matrix.test.ts,container.test.tssrc/lib/api/envelope.test.ts,src/lib/vc/status/list-credential.test.tssrc/lib/server/limits.test.ts,rate-limit.test.ts,src/lib/security-headers.test.tssrc/app/api/upload/route.test.ts,route.container.test.ts