Key management
Certichain uses three kinds of keys: wallet keys that people hold, derived keys that the browser recomputes from wallet signatures and never stores, and server secrets set as environment variables. This page lists all of them by name. Values never appear in these docs; example.env lists the variable names.
Key hierarchy
Wallet keys (held by people)
| Key | Held by | Controls | If lost | If stolen |
|---|---|---|---|---|
| Safe owner keys (3) | Admin owners | 2 of 3 sign admin actions on the registries and timelock proposals | Two of three still work; the Safe replaces the owner | One key alone does nothing; two control the admin, slowed by the admin delay and the timelock |
| Guardian key | The guardian | Pause; suspend an issuer; unpause in contract version 0.5.0 | The admin grants another guardian | Unwanted pauses until the admin revokes the role |
| Issuer wallet key | Each institution | Issues and manages its credentials; derives its student salts, encryption key and badge key | The institution can no longer issue or manage with that key; documents encrypted to it cannot be opened by the issuer (holders still open theirs with the package) | See Threat model |
| Holder wallet key | Each student (optional) | Receives the inbox, signs disclosures, manages access, derives a recipient key | The issuer can bind a new wallet by invitation | The thief can act as the holder for those features; the package is a separate secret |
Deployer key (PRIVATE_KEY, local only) | The person deploying | Deploys contracts; holds no admin role after a handover | Nothing live depends on it | Test ether only; the handbook says to drain and delete it after a handover |
Derived keys (never stored)
| Derived value | Typed data signed | Derivation | Lifetime |
|---|---|---|---|
| Student salt | StudentSalt (domain Certichain Student Salt, no chain) | keccak-256 of the signature | Recomputed when needed |
| Encryption key (X25519) | EncryptionKey (domain Certichain keys, chain id), signed twice | HKDF-SHA256, salt certichain.enc-key.v1, then HPKE DeriveKeyPair | Memory, for the page session |
| Open Badge key (Ed25519) | OpenBadgeKey, signed twice | HKDF-SHA256, salt certichain.badge-key.v1 | Memory, for the page session |
| Access identity (Semaphore and X25519) | AccessIdentity, signed twice | HKDF-SHA256, salt certichain.access-identity.v1, two labelled seeds | Memory, or a file for a random identity |
| Document key (AES-256) | None: random per document | 32 random bytes | Lives in the package, grants (wrapped), links and key update files |
Details are in Signatures and Encryption. Derived keys depend on the wallet signing deterministically; a wallet that does not is refused.
Every derived value is a function of the issuer wallet's signature. If an institution moves to a new wallet key:
- its student salts change, so enrolment commitments made with the old key no longer match new ones;
- its encryption key changes, so the new key cannot open documents wrapped only to the old one;
- its badge key changes, and badges need a new key binding.
In contract version 0.5.0, retiring a key in favour of a successor keeps earlier credentials valid and manageable by the successor, but the handbook notes that encrypted documents and custom record types stay with the key that created them. Plan key changes with Key retirement.
The key-derivation typed data includes a keyVersion. The app encrypts with version 1 and tries up to four versions when opening, which leaves room for a rotation of the derived key without changing wallets.
Server secrets (environment variables)
Set as server-only variables (marked Sensitive on the hosting platform). Variables starting with NEXT_PUBLIC_ are public by design and hold addresses, never secrets.
| Variable | Purpose | Holds funds | If missing |
|---|---|---|---|
SESSION_SECRET | Signs issuer session cookies (HS256); at least 32 characters | No | Sign-in and uploads are unavailable |
PINATA_JWT (preferred), or PINATA_API_KEY and PINATA_API_SECRET_KEY | The scoped JWT pins, lists and removes containers; the key pair can only pin | No (billing) | Uploads fail; existing pins stay. Without the JWT, issuers cannot find or remove their own copies |
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN | Nonces, rate limits, monitor state | No | Routes that need the store report that configuration is missing |
STATUS_LIST_SIGNER_KEY | Signs status lists; its address is published as NEXT_PUBLIC_STATUS_LIST_PUBLISHER | No, must stay empty | Status lists are off |
API_SIGNER_KEY | Signs verification API answers; its address is published as NEXT_PUBLIC_API_SIGNER | No, must stay empty | The API answers that it is unavailable |
API_KEYS | SHA-256 hashes of API keys for higher limits | No | Callers get anonymous limits |
CRON_SECRET | Lets the scheduler call the monitor | No | The monitor refuses calls |
OPS_WEBHOOK_URL | Where alerts are posted | No | Alerts are not delivered |
OPS_PROBE_API_KEY | API key the canary probe uses | No | The probe uses anonymous limits |
RPC_URL, RPC_URLS (server), NEXT_PUBLIC_RPC_URL, NEXT_PUBLIC_RPC_URLS (browser) | Chain reads; may embed provider keys | No (billing) | Fallback endpoints take over; with none, reads fail |
INFURA_API_KEY, ETHERSCAN_API_KEY | Deployment and contract verification scripts | No | Deploy and verify scripts cannot run |
The monitor compares the address derived from each server signing key with the published address and alerts on a mismatch (src/lib/server/ops/wiring.ts). It also warns ahead of expiry dates listed in OPS_KEY_EXPIRIES (OPS_WARN_DAYS).
Rotation
The handbook's keys runbook (docs/process/runbooks/keys.md) gives step-by-step procedures. In summary:
| Key | How to rotate | Who is affected |
|---|---|---|
| Status list publisher, API signer | Create a new funds-free key, set it and its published address together, announce the new address | Verifiers who pinned the old address must update the pin |
| Pinata and RPC tokens | Create a new token, replace the variable, revoke the old token | No one, if done in that order |
SESSION_SECRET | Replace the value | Every issuer signs in again |
CRON_SECRET, OPS_WEBHOOK_URL | Replace in the scheduler and the server together | Monitoring only |
| Safe owner | Safe owner change, signed by the threshold | Admin owners |
| Guardian | Admin revokes and grants GUARDIAN_ROLE | Guardian only |
| Issuer wallet | Register the new key and, in contract version 0.5.0, retire the old one in favour of it through the admin | The institution; see the warning above |
| Document key | Replace key on the issuer dashboard, then give the holder the key update file | The holder; recipients of old grants and links lose access to new copies |
Rules for anyone handling keys
- Never paste a private key, token or secret into code, logs, documents, issues or chat.
- Server signing keys must hold no funds; if one receives funds, treat it as exposed.
- Keep
.envfiles out of version control (they are gitignored); useexample.envfor names only. - Each person who holds a wallet key should use a standard deterministic wallet account, not a contract wallet, for the features that derive keys.
Source files
example.env(variable names)src/lib/server/config.ts,src/lib/server/session.tssrc/lib/server/ops/wiring.tssrc/lib/commitments.ts,src/lib/doc-crypto/identity-key.ts,src/lib/vc/openbadges/standard/key.ts,src/lib/zk/access-identity.tssrc/services/documents/wallet-key-session.ts,src/services/domain/document-service.tsblockchain/contracts/v03/IssuerRegistry.soldocs/process/runbooks/keys.md,docs/process/runbooks/admin-keys.md(handbook)
Tests
src/lib/server/config.test.tssrc/lib/doc-crypto/identity-key.test.ts,src/lib/vc/openbadges/standard/key.test.ts,src/lib/zk/access-identity.test.ts,src/lib/commitments.test.tsblockchain/test/v03/KeyLineage.test.ts,blockchain/test/invariants/KeyLineage.invariants.test.ts- E2E journey
e2e/journeys/key-retirement.journey.ts