Skip to content

Security: solutionkanu12/certael

Security

SECURITY.md

Certael Security Model

Security Position

Certael handles financial asset claims, evidence, eligibility, risk interpretation, and potentially transaction routing. Security is a product requirement, not a final audit step.

Core Trust Principle

Never trust one source merely because it is onchain or because multiple APIs agree.

Source independence matters.

Primary Threat Classes

Asset / Evidence Fraud

  • fake asset registration
  • forged issuer documents
  • forged attestations
  • stale attestations
  • conflicting evidence
  • asset substitution
  • manipulated NAV
  • manipulated reserve/collateral values
  • double collateralization
  • compromised custodian/administrator
  • colluding evidence providers

Onchain / Contract

  • privilege escalation
  • broken access control
  • replay
  • signature misuse
  • nonce errors
  • reentrancy
  • unauthorized upgrades
  • malicious admin actions
  • front-running
  • denial of service
  • accounting errors
  • paused-state bypass
  • external contract risk

Oracle / Data

  • stale prices
  • manipulated prices
  • oracle outage
  • API compromise
  • data poisoning
  • inconsistent timestamps
  • source-correlation mistaken for independence

Risk Engine

  • score manipulation
  • gaming deterministic thresholds
  • malicious evidence weighting
  • stale risk state
  • AI hallucination presented as fact
  • unexplainable scoring

Compliance / Identity

  • credential forgery
  • replayed credentials
  • expired credentials
  • privacy leakage
  • raw KYC exposure
  • incorrect jurisdiction logic
  • sanctions/eligibility staleness

Exit Engine

  • slippage manipulation
  • stale quote execution
  • malicious route
  • approval abuse
  • fake token / spoofed asset
  • sandwich / MEV exposure
  • partial-fill/refund mishandling
  • fee manipulation
  • route-data spoofing
  • external DEX/bridge failure

Backend / Web

  • secret leakage
  • injection
  • broken auth
  • insecure direct object references
  • SSRF
  • unsafe file/document handling
  • rate-limit bypass
  • dependency compromise
  • supply-chain attacks

Security Requirements

Evidence

Every claim must preserve provenance. Issuer-provided data must remain distinguishable from independent verification. Conflicts must not be silently resolved by AI.

Fixture / demo data

Fixture catalogs, adapters, and selectors are a presentation boundary, not a verification boundary.

  • Fixture data must remain explicitly identifiable (dataOrigin: "fixture", demo: true, fixture- ids, fixture: sources).
  • Fixture evidence must never be promoted to live or independently verified institutional evidence.
  • Catalog VERIFIED records are demo reconstructions of a future verification workflow. They are not produced by untrusted ingest and are not live audits.
  • Issuer-provided fixture evidence remains ISSUER / SELF independence. It must not be labeled INDEPENDENT.

Untrusted evidence ingestion (backend foundation)

External evidence payloads are a trust boundary even before HTTP exists.

  • Validate at the ingest/reconcile boundary. Unknown keys, prototype-polluting keys, and system-owned fields (id, contentDigest, origin, independence, provenance) are rejected.
  • Submitters cannot assign VERIFIED, CONFLICTED, STALE, or INDEPENDENT.
  • Preserve claimed hash and claimed string provenance as claimed data. Do not materialize claimed strings as EvidenceRef provenance. Certael's contentDigest is a storage integrity hash of the received payload, not independent verification.
  • Reconciliation reports conflicts and correlated-only agreement. It does not write upgraded verification state and does not drop a conflicting side.
  • This foundation performs no network I/O and has no live database.

Untrusted asset persistence inputs

Asset create/update payloads are also a trust boundary.

  • Asset evidence references are resolved by ID through the canonical evidence repository before persistence. Caller-supplied state, independence, source metadata, and hashes do not override the stored record.
  • Unresolved evidence and evidence belonging to another asset are rejected. A caller cannot attach fabricated VERIFIED or INDEPENDENT evidence.
  • General asset updates cannot reassign issuerId or chainId, cannot remove or mutate existing evidence references, and cannot change lifecycle status.
  • Lifecycle transitions require a future explicitly authorized service; this foundation has no authorization context and therefore permits only a same-status update.

AI

AI may investigate and explain. AI must not:

  • fabricate evidence
  • unilaterally change authoritative state
  • bypass deterministic policy
  • execute privileged financial actions without authorization

Smart Contracts

Before Mainnet:

  • unit tests
  • negative-path tests
  • access-control tests
  • fuzz tests where meaningful
  • invariant tests where meaningful
  • static analysis
  • manual review
  • privileged-role review
  • external-integration review
  • deployment configuration review

Immutable contract core

The shared CertaelCore deployment is non-upgradeable. Protocol changes require a new deployment, and chain adapters must explicitly select supported versions.

  • Asset identity, evidence, integrity, and risk records are append-only; identifiers cannot be reused and there are no update or delete functions.
  • Evidence callers supply a logical identifier, but the contract derives the canonical storage key from PROTOCOL_VERSION, an issuer/verifier domain separator, assetId, and the logical ID. Issuer and verifier paths therefore cannot squat or block one another, including across assets.
  • Result identifiers remain globally unique. Reusing a result ID for another asset or result kind is rejected rather than silently creating an asset-scoped alias.
  • The default admin manages roles only and has no implicit issuer, verifier, or result-committer authority. Any operational authority granted to an admin account is explicit and evented. The final default admin cannot be revoked, preventing permanent governance lockout.
  • Issuer managers authorize issuer accounts but cannot register assets or anchor evidence unless separately authorized for those operations.
  • Issuer evidence is classified by the contract as SELF_REPORTED / ISSUER. Issuers cannot select VERIFIED or INDEPENDENT.
  • VERIFIED / INDEPENDENT anchoring is isolated behind VERIFIER_ROLE and requires a provenance commitment. This proves verifier attribution, not the factual truth of offchain evidence.
  • Integrity/risk result anchoring requires RISK_COMMITTER_ROLE, a supporting-evidence commitment, and a policy/version commitment. Results remain attributed conclusions, not independent proof.
  • The core makes no external calls, holds no funds, uses no tx.origin, has no delegate call or proxy mechanism, and exposes no arbitrary-call surface.
  • Asset and result logical identifiers remain adapter inputs and must use validated canonical encoding. Evidence storage-key domain separation is enforced onchain rather than delegated to adapters.
  • Role-key custody, verifier governance, result methodology, adapter version selection, and the availability of event/indexing infrastructure remain external trust boundaries.
  • The target EVM revision is not defined by repository architecture decisions. Contract-only development deliberately leaves evm_version unpinned. Foundry currently resolves that compiler default to Osaka, but this is not a BOT/X Layer compatibility decision and those artifacts are not approved for deployment. Each deployment profile must pin and verify the revision against that network's official documentation before any deployment build.

Secrets

  • never commit private keys/API secrets
  • use environment variables
  • provide .env.example with placeholders only
  • scan history/diff before public push

External Integrations

Every chain, DEX, bridge, oracle, API, wallet, SDK, and package is a trust boundary. Verify current official documentation and supported versions before integration.

Audit Workflow

For every meaningful change:

  1. identify new trust boundary
  2. identify attack surface
  3. implement tests
  4. review diff
  5. update this file if assumptions changed

Before Mainnet:

  1. full contract suite
  2. fuzz/invariant suite
  3. static analysis
  4. dependency/secrets scan
  5. backend/web security review
  6. external integration review
  7. deployment verification
  8. post-deploy contract/address/role verification
  9. internal audit report

Known Security Design Principle

Certael should prefer transparent evidence and explainability over opaque "trust scores." A user must be able to inspect why a state/score exists.

Current Trust Boundaries Added

  • Untrusted evidence ingest/reconcile inputs in src/backend/evidence (no network adapter yet).
  • Integrity evaluation requests in src/backend/integrity (deterministic risk/eligibility engine).
    • The engine is a trust boundary. The caller supplies assetId and nothing else. Evidence, required claims, freshness policy, and the evaluation instant are resolved from trusted ports (CanonicalEvidenceSource, IntegrityPolicySource, IntegrityClock). A request carrying evidence, now, claims, staleAfterMs, state, independence, provenance, or any unknown key is rejected (FORBIDDEN_CALLER_FIELD), never partially honored.
    • Structural validation is not authorization. Passing shape checks never grants trust; only the trusted evidence boundary can carry VERIFIED / INDEPENDENT.
    • The engine never assigns VERIFIED or INDEPENDENT itself. Canonical records that claim INDEPENDENT without VERIFIED, or VERIFIED without provenance, fail the evaluation closed rather than being silently downgraded.
    • Required claims are authoritative policy. A policy with no required claims is rejected, so a caller can never improve a result by omitting what must be proven. A missing policy fails closed (POLICY_NOT_FOUND).
    • ALLOW additionally requires the independent verification the policy demands. ATTESTED / issuer-sourced evidence alone can only reach REVIEW.
    • Freshness is policy-controlled: a mandatory positive staleAfterMs (with optional per-claim overrides), a bounded maxClockSkewMs, and explicit handling of future-dated observations, which are quarantined and excluded from the score. Stale evidence loses its trust tier, so stale canonical evidence cannot satisfy the independent-verification requirement.
    • Weighting is Sybil/correlation resistant. Evidence sharing a source or provenance root is collapsed into one correlation component per claim, components are aggregated by worst-case rather than by mean, and adverse evidence sets a score floor. Repeating or laundering favorable evidence cannot dilute adverse evidence, and agreement traceable to a single correlated source is reported as correlated-only agreement rather than corroboration.
    • Conflicts are graded by trust. Comparably trusted disagreement is MATERIAL (denial). A single canonical independently verified source disputed only by lower-trust evidence is a LOW_TRUST_DISPUTE: it blocks ALLOW but cannot by itself force a denial, so low-trust or spoofed evidence cannot overpower canonical trusted evidence.
    • The request object itself is hostile, not just its values. Reading a request's prototype, its keys, or its assetId can execute attacker-controlled code (a throwing getter, a Proxy trap). evaluate never propagates such a throw: request parsing returns a typed INVALID_REQUEST (request.threw), and the whole evaluation is wrapped so anything escaping an inner guard returns a typed INTERNAL (evaluate.unhandled) with no detail from the thrown value. assetId is read exactly once, so a mutating getter cannot swap it after validation. A trusted clock whose getTime throws returns INVALID_CLOCK.
    • Correlation inputs are explicitly bounded before correlation runs. Correlation reads every record's source and every provenance source, so source and each provenance source are capped at MAX_EVIDENCE_SOURCE_LENGTH (512) and the provenance chain at MAX_EVIDENCE_PROVENANCE_REFS (64) refs. Overflow is a typed INVALID_EVIDENCE error (evidence.source-too-long, evidence.provenance-too-many, evidence.provenance-source-too-long), checked before assertedValue canonicalization, so an oversized record cannot turn evaluation into unbounded work.
    • assertedValue is a hostile input even from the trusted store. Cyclic, BigInt, undefined, symbol, function, non-finite, non-plain-object, oversized, and prototype-polluting values are returned as typed Result errors instead of throwing. Money crossing this boundary must be a canonical integer-string Money value; floating-point money is rejected.
    • Output is canonically ordered using locale-independent comparisons, so semantically equivalent evidence permutations produce identical results.
    • No network I/O, no live chain calls, no external LLM dependency, no AI-invented evidence.
  • Passport assembly in src/backend/passport (the Passport read model over canonical records).
    • The Passport is a presentation boundary, not a verification boundary. It reads canonical src/domain records and never upgrades one. It assigns no VERIFIED or INDEPENDENT state, writes nothing, performs no network I/O, and has no AI dependency.
    • Whole-Passport verification requires claim coverage. A single canonical VERIFIED / INDEPENDENT record proves one claim, never an asset, and can never by itself carry the document to INDEPENDENTLY_VERIFIED. That state requires a trusted coverage policy naming the claims this asset must prove and every one of them backed by qualifying evidence. The headline otherwise distinguishes PARTIALLY_VERIFIED, INSUFFICIENT_VERIFICATION_COVERAGE, and VERIFICATION_COVERAGE_UNKNOWN. With no coverage policy the Passport fails conservatively to VERIFICATION_COVERAGE_UNKNOWN rather than asserting verification, and a policy declaring no required claims is rejected so an empty policy cannot trivially satisfy coverage.
    • Only policy-judged freshness is verification-grade. AGE_ONLY means no trusted window judged the record; "not known to be stale" is not "known to be current", so unknown freshness never supports a verification claim. Stale, integrity-flagged, and future-dated records are likewise excluded. Qualifying evidence that fails only on freshness is reported separately as unqualifiedIndependentEvidenceIds, and PassportVerification.limits names every reason a stronger claim was withheld.
    • Issuer- and self-sourced evidence reaches ISSUER_REPORTED_ONLY at best and is never presented as independent verification.
    • Data origin degrades, never upgrades. Each input carries its own tag and they are combined conservatively: one fixture-derived or demo input makes the whole document fixture/demo, an empty tag list is treated as fixture/demo, and a mixed set raises MIXED_DATA_ORIGIN. A live asset record read alongside fixture evidence cannot be labelled live, which is the fixture-to-live promotion this file forbids.
    • An unresolved conflict outranks a clean record: DISPUTED takes precedence over INDEPENDENTLY_VERIFIED. The Passport reports conflicts and never resolves, drops, or averages a side.
    • Records that fail isEvidence are excluded and reported as UNREADABLE_EVIDENCE_RECORD, never rendered. This is what rejects VERIFIED evidence carrying no provenance chain.
    • Cross-record identity is checked. An issuer record whose id is not the issuer the asset names is discarded as ISSUER_RECORD_MISMATCH rather than displayed, and an integrity report or risk assessment for another asset is discarded rather than borrowed. Attaching the wrong legal entity or the wrong verdict to an asset is the substitution threat named above.
    • Absence is attributed, never blank. NOT_RECORDED, UNAVAILABLE, and NOT_MODELLED are distinct, so an unread source is never displayed as a value the issuer omitted, and a missing integrity or risk result is never displayed as a clean one.
    • Freshness requires policy. With no trusted freshness window the Passport reports evidence age and makes no staleness verdict. It does not invent a threshold, because choosing one is a policy decision.
    • Assembly fails closed on a structurally invalid asset record, an asset id that does not match the request, or a clock that throws. No partial Passport is rendered; a partial Passport would still read as an authoritative document. Route-level error state prints no underlying error message, only Next.js's own digest.
    • Rendering code contains no trust logic: it maps closed enums to wording. Adding a state upstream fails the typecheck rather than falling through to a default label.

Proof-of-Reality surface (/asset/[assetId]/proof-of-reality)

  • The surface is a presentation layer over the deterministic integrity engine. Verdicts, scores, eligibility decisions, conflicts, and missing/stale/quarantined findings are read only from IntegrityEvaluation; the UI contains no trust logic of its own and cannot upgrade, dilute, or resolve anything.
  • The fixture canonical-evidence boundary (src/fixtures/integrity.ts) admits only the catalog's ingestible evidence. Catalog VERIFIED / STALE / CONFLICTED demo reconstructions are excluded from the evaluated set, so no record that merely exists can manufacture a verdict or independent-verification state. Excluded records are shown with an explicit "not in the evaluated set" label.
  • Per-asset policy (required claims, freshness, requireIndependentVerification) is authored on the trusted side and never accepted from the UI; a missing policy or failed evaluation renders a fail-closed "no evaluation available" state, never a clean one.
  • Evidence independence is always shown alongside state; the surface never implies that evidence is independently verified because it exists.

Portfolio surface (/portfolio/[ownerId])

  • Presentation boundary over canonical Portfolio positions. The page cannot create holdings, convert currencies, or treat a missing currentValue / costBasis as zero.
  • Account totals are exact integer-string sums of recorded same-currency amounts. Mixed currencies and incomplete values fail open as attributed absence (MIXED_CURRENCY, INCOMPLETE_VALUES, NO_POSITIONS), never as a invented book value.
  • An asset record whose id does not match the position is discarded (ASSET_RECORD_MISMATCH) rather than displayed — the substitution threat named above.
  • Owner mismatch and an invalid portfolio fail closed. No partial account is rendered.
  • Data origin degrades, never upgrades. The application-chrome /portfolio destination is the demonstration owner handle until a live session exists; that handle is not verified identity.

Integrity Agent surface (/asset/[assetId]/integrity-agent)

  • Investigation layer, not a verification boundary and not a source of truth (D-009).
  • Cites the deterministic engine, recorded discrepancies, recorded risk factors, and recorded evidence. It performs no network LLM call, invents no evidence, and cannot change a score.
  • A missing engine evaluation is not a clean verdict and not a fallback score.
  • Issuer- and self-sourced evidence remains labelled as such. The agent cannot promote it to INDEPENDENT / VERIFIED.
  • An evaluation for a different asset fails closed (EVALUATION_ASSET_MISMATCH).

Issuer portal (/issuer, /issuer/[issuerId])

  • Read surface over issuer identity, registered assets, and issuer-sourced evidence. There is no write path and no authorized lifecycle transition on this surface.
  • Issuer-sourced evidence is always presentedAsIndependent: false. Registration is identity, not verification of backing.
  • A missing issuer, or an issuer record for a different id, fails closed. Assets registered to another issuer are discarded rather than substituted.

Certael ID / Compliance surface (/asset/[assetId]/compliance)

  • The compliance read model is a presentation boundary over canonical asset, eligibility, evidence, and Proof-of-Reality records. It writes nothing, performs no network I/O, and does not create identity credentials.
  • ALLOW is displayable only when canonical eligibility and the deterministic Proof-of-Reality evaluation both return ALLOW, evidence is present, and the evidence set contains provenance-backed VERIFIED / INDEPENDENT evidence with no missing, conflicting, stale, or quarantined findings. Otherwise the result is downgraded or denied conservatively.
  • Missing eligibility or Proof-of-Reality is shown as UNKNOWN; missing evidence is never treated as compliance. Asset jurisdiction is shown only as the canonical asset field, not as investor permission logic.
  • Eligibility reasons are preserved as recorded policy text. The surface does not infer KYC, AML, identity, legal assurance, permitted jurisdictions, or remediation beyond those reasons.
  • Fixture/demo tags remain visible and cannot be promoted by the compliance assembler to live data or independent verification.
  • The route follows D-033 chain exposure: an asset on a chain this build does not expose is treated as not found, so the surface never serves an asset under a network it is not on.

Exit presentation boundary (/asset/[assetId]/exit)

  • The Exit surface is read-only and consumes canonical ExitPlan, eligibility, risk, and evidence records through the tagged CertaelDataAdapter. It performs no wallet, chain, market, quote, or settlement call and exposes no transaction action.
  • AVAILABLE means only that the connected canonical plan records a modeled route as available. It never means Certael can execute the route, guarantee liquidity or settlement, or preserve a quote. Expired recorded quotes become UNKNOWN rather than remaining actionable.
  • Missing modeled routes and fields are NOT_RECORDED; capabilities absent from the canonical ExitOption model are NOT_MODELED. Disconnected sources and missing plans fail closed to an unavailable/unknown presentation rather than creating a default route.
  • Eligibility is displayed as constraint context, not transaction authorization. Exit-related risk factors retain their supplied evidence state and independence, and the surface does not invent a recommendation, price, counterparty, loan offer, redemption window, or settlement promise.
  • The route follows D-033 chain exposure: an asset on a chain this build does not expose is treated as not found, matching every other asset-scoped surface.

BOT Mainnet CertaelCore read adapter (src/adapters/bot)

  • The official BOT Mainnet RPC endpoint and the deployed CertaelCore are a trust boundary. The adapter connects only to chain id 677 (0x2a5), rejects any other configured chain, requires an https RPC URL, and verifies before any read that eth_chainId matches, that code exists at the configured address (eth_getCode), and that PROTOCOL_VERSION() equals keccak256("CERTAEL_CORE_V1").
  • Reads are restricted to the contract's genuine getters; no metadata is fabricated. Onchain commitments are opaque bytes32 hashes; an anchored commitment proves only that a record was anchored, never that its offchain content is factually verified or independently sourced.
  • Not-found reverts (AssetNotRegistered, RecordNotFound) map to null; any other RPC failure is a typed, fail-closed error. Reads before a successful initialize() are refused.

Live data adapter (src/adapters/live)

  • The production data path maps only persisted repository records to the tagged CertaelDataAdapter (dataOrigin: "live", demo: false). It never consults the fixture catalogue, so fixture issuers, assets, or evidence can never surface as production records.
  • BOT assets are cross-checked against CertaelCore registrations through an explicit domain-id -> onchain-id map; an asset whose registration is missing or unmapped is withheld, never relabelled.
  • Proof-of-Reality fails closed to null until a trusted live policy/evidence source exists; missing persisted snapshots return null (UNKNOWN), never an invented value.
  • Production selection is explicit (NEXT_PUBLIC_CERTAEL_DATA_SOURCE=live) and never falls back to fixtures when the production adapter cannot be built (DECISIONS.md D-042).

Cloudflare D1 persistence (src/backend/persistence/d1)

  • The D1 database is a production trust boundary. It is reached only through the existing repository ports from server-side code; the browser never accesses the binding (src/adapters/live/cloudflare.ts is the only seam that reads the Cloudflare runtime context).
  • Fixture identities cannot enter production D1 records. The repository boundary rejects fixture-* ids and fixture: sources with FIXTURE_IDENTITY_REJECTED; canonical records carry no demo flag, so the identity marks are the guard.
  • Financial values are stored losslessly. Money and quantity are exact canonical values (integer base-unit strings, currency/decimals) inside JSON TEXT or INTEGER columns — never SQLite REAL, and no implicit null -> zero conversion (a NULL column reads back as absent).
  • Append-only semantics are enforced by the schema. Integrity/risk/ eligibility/exit snapshots are keyed (asset_id, evaluated_at); a repeated key is APPEND_ONLY_VIOLATION, so historical snapshots are never silently overwritten.
  • Every read is validated before entering trusted domain code. Rows are decoded with the canonical validators; malformed JSON or invalid records fail closed with CORRUPT_STORAGE and are never rendered partially.
  • SQL is prepared only, assembled from fixed column maps; there is no dynamic SQL and no injection surface.

BOT/D1 consistency rules

  • Asset: a persisted BOT asset is rendered only when its registration can be verified onchain (getAsset through the explicit domain-id -> onchain-id map); an unverifiable or unmapped registration is withheld, never relabelled as "onchain registered".
  • Issuer: a persisted issuer address may be checked with isAuthorizedIssuer(address). Unauthorized does not erase persisted identity; authorization does not verify legal identity, collateral, backing, or documents. An address mismatch fails closed (ADDRESS_MISMATCH).
  • Evidence: an onchain evidence commitment proves anchoring and attribution only. It never proves the factual correctness of the offchain evidence; no persisted record is upgraded because it is anchored.
  • Integrity/risk: an onchain result commitment proves that a result was anchored by a role holder under a policy commitment. It does not make the methodology independently correct.
  • Certael never invents relationships CertaelCore does not support.

Production administrative write boundary (src/backend/admin, POST /api/admin/[kind])

  • The only production write surface. Kinds are allowlisted (issuer, asset, evidence, integrity/risk/eligibility/exit snapshots, integrity policy); there is no arbitrary CRUD and no generic SQL endpoint.
  • Authentication: a bearer token matching the worker secret CERTAEL_ADMIN_TOKEN, compared in constant time. The secret is never a NEXT_PUBLIC value and never in source; a missing secret fails the boundary closed (503). The operator label (X-Certael-Operator) is an audit label only, not an authentication factor.
  • Every write runs the existing validated services: canonical validation, relationship checks, fixture-identity rejection, append-only protection, and untrusted-ingest clamps (no claimed VERIFIED/INDEPENDENT). dryRun replays the full validation path without writing.
  • Successful writes are audit-logged (append-only admin_write_audit): operator, request id, timestamp, entity, and a SHA-256 payload digest — never the payload itself, never secrets.
  • The browser can never reach the boundary: route handlers are server-side only and the D1 binding is resolved solely from the Cloudflare runtime context.

Mainnet Gate

No Mainnet deployment or meaningful financial execution is considered production-ready merely because a transaction succeeds. Deployment must satisfy verification, security, documentation, and role/configuration checks.

There aren't any published security advisories