Skip to main content

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​

AssetWhy it mattersWhere it lives
Integrity of the issuer listA fake issuer could issue convincing credentialsIssuerRegistry on chain
Integrity of credential anchors and signaturesThey are what a verifier checksCredentialRegistry and its events
Correct statusA revoked credential must not passStatusRegistry
Confidentiality of documentsTranscripts and certificates are personal dataEncrypted containers on IPFS; keys with holders
Confidentiality of holder detailsNames and student ids are personal dataCredential packages; only salted hashes on chain
Admin and guardian keysThey control the issuer list and the pauseSafe owners, guardian wallet
Issuer wallet keysThey sign credentials and derive document keysEach institution's wallet
Server signing keysVerifiers trust API answers and status lists signed by themServer environment (API_SIGNER_KEY, STATUS_LIST_SIGNER_KEY)
Availability of verificationVerifiers must be able to check at any timePublic RPC endpoints, the web app

Actors​

ActorCapabilities assumed
Anonymous member of the publicReads the chain, IPFS and public pages; calls public routes
Credential forgerWants a credential that passes for a registered institution's
Dishonest holderAlters a package, a document or a disclosure; presents someone else's credential
Curious verifier or recipientKeeps what they were shown; tries to learn more
Compromised or malicious server, pinning or RPC providerReads, drops or swaps stored data; returns wrong answers
Phishing siteAsks a user's wallet to sign typed data or transactions
Malicious script in the pageA compromised dependency or injected script running in the app's origin
Thief of one keyHolds 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​

ThreatControlCode
Issue a credential as a registered institution without its keyThe registry checks an EIP-712 signature by the issuer (ECDSA or ERC-1271) and that the issuer is registered and activeblockchain/contracts/v03/CredentialRegistry.sol
Replay a signed credential, on another chain or registry, or twiceThe domain binds chain id and registry address; each (issuer, nonce) is accepted onceCredentialRegistry.sol
Register a look-alike institutionOnly the admin registers issuers; names are unique and at most 128 bytesblockchain/contracts/v03/IssuerRegistry.sol
Alter a package to claim other detailsThe verifier recomputes the credential id, the subject commitment and the metadata hash from the package and compares them with the chainsrc/lib/credential-package.ts, src/lib/credential-disclosure.ts
Present a different documentThe verifier hashes the file locally and compares it with the anchored documentHashsrc/services/domain/credential-service.ts
Swap or edit a stored containerThe 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 documentsrc/lib/doc-crypto/document.ts
Forge a grant fileThe opening app checks the chain first: the credential must exist and name the grant's issuer and document hashDocumentService.openGrant in src/services/domain/document-service.ts
Rewrite historyNothing is deletable; status changes only move forward through defined transitions and are logged as eventsblockchain/contracts/v03/StatusRegistry.sol

Key compromise​

ThreatControlResidual
Stolen issuer keyThe 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 validCredentials 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 keyThe guardian can only pause and suspend issuers; the admin revokes the roleA pause or suspension until the admin acts (in 0.5.0 a stolen guardian key can also unpause)
Stolen Safe owner key2 of 3 signatures are needed; the Safe replaces the ownerTwo stolen keys would control the admin; the admin transfer delay and the timelock delay give watchers time to react
Stolen server signing keyKeys hold no funds; the monitor checks that each key's address equals the published one; rotation is in the keys runbookAnswers signed with the stolen key are accepted by verifiers who pinned it until they update the pin

Privacy​

ThreatControlResidual
Learn a student's name or id from the chainv0.3 stores only salted commitments and hashes; a test scans the contracts for plaintext fieldsv0.2 legacy records store plaintext names and ids on chain
Guess a commitment from a class list32-byte random or wallet-derived saltsNone known for the hashes themselves
Read documents from IPFS or the pinning serviceAES-256-GCM, keys never leave devices except in the package, grants (wrapped) and linksHeader reveals issuer, document hash and size rounded to 64 KiB
Replay a disclosure to another verifierThe holder signs the verifier's random challengeA verifier keeps what they were shown
Link an access proof to a memberSemaphore hides which member provedThe sender's address is in the receipt

See Privacy design.

Server-signed outputs​

ThreatControlCode
Replay an old API answerThe caller sends a nonce; the signed envelope echoes it, with generatedAt and a 300 second validity window and the hash of the bodysrc/lib/api/envelope.ts
Pass an API verdict off for another fileThe POST /api/v1/verify verdict signs the file's SHA-256src/lib/server/api/
Serve a forged or stale status listLists 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 publishersrc/lib/vc/status/list-credential.ts

Abuse of server routes​

ThreatControlCode
Upload by someone who is not an issuerSIWE sign-in, an HS256 session cookie, and an issuer-registration check repeated every 5 minutessrc/lib/server/sign-in.ts, session.ts, issuer-session.ts
Replay a sign-inSingle-use nonce with a 5 minute life; domain, chain id and statement checkedsrc/lib/server/sign-in.ts
Flood the routesPer-IP and per-wallet limits in Redis, for example 30 uploads per wallet per hoursrc/lib/server/limits.ts, rate-limit.ts
Hostile filesSize limits before reading, allow-listed types checked by magic bytes, strict container parsing, padding checks, download onlysrc/lib/doc-crypto/payload.ts, container.ts, src/lib/server/upload-container.ts
Framing and sniffingframe-ancestors 'none', X-Frame-Options: DENY, nosniff, a strict referrer policy; no-referrer and no-store on /opensrc/lib/security-headers.ts

Zero-knowledge access​

ThreatControl
Copy someone's proof from the mempoolThe proof's message must be the sender's address
Use one proof twiceThe gate records nullifiers per credential; the scope changes each day
A bystander burns a member's nullifierThe gate spends nullifiers itself rather than through the library's public validation function
Malicious circuit filesThe browser uses only files whose SHA-256 matches the reviewed list; CI checks the verification keys against the verifier
Access to a revoked credentialAdmission 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-src directive 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 EncryptionKey typed 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.sol
  • blockchain/contracts/zk/CertichainAccessGate.sol
  • src/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.ts
  • src/lib/server/sign-in.ts, session.ts, issuer-session.ts, limits.ts, rate-limit.ts, upload-container.ts
  • src/lib/security-headers.ts
  • docs/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.ts
  • blockchain/test/invariants/Invariants.test.ts, KeyLineage.invariants.test.ts
  • blockchain/test/zk/AccessGate.test.ts, ProofCallGuard.test.ts, KeyCheck.test.ts
  • src/lib/doc-crypto/boundary.test.ts, access-matrix.test.ts, container.test.ts
  • src/lib/api/envelope.test.ts, src/lib/vc/status/list-credential.test.ts
  • src/lib/server/limits.test.ts, rate-limit.test.ts, src/lib/security-headers.test.ts
  • src/app/api/upload/route.test.ts, route.container.test.ts