Skip to content

Security: CIRISAI/CIRISServer

SECURITY.md

Security policy

Reporting a vulnerability

Two private channels, either is fine:

  • GitHub private vulnerability reporting — the Security tab on any of the repositories in scope below. Enabled on all of them.
  • Email info@ciris.ai — put SECURITY in the subject line. This is the same address CIRISAgent's policy uses, so a reporter who finds either repository first lands in the same place.

Do not open a public issue for anything you believe is exploitable.

Disclosure: we ask for two weeks before public disclosure, to patch and notify users — matching CIRISAgent's published policy. If two weeks is not enough for a fix we will say so and agree a date with you rather than let it slide silently.

There is no published PGP key. If you need one before sending details, say so in a one-line email and one will be minted and published rather than exchanged in private.

Useful in a report, in rough order of value: the version or commit, the transport and backend (Reticulum / HTTPS, SQLite / Postgres), what you observed, and the smallest reproduction you have. A packet capture or a failing test is worth more than a description. If you have only a suspicion, send the suspicion.

What to expect, and what to do if you do not get it

target
acknowledgement 3 working days
initial assessment — in scope, severity, whether it reproduces 10 working days
fix or a dated plan 30 days for anything remotely exploitable

This is one person with no rota. Illness, travel or a dead laptop are all plausible reasons for silence, and none of them should leave you holding a live finding with nowhere to put it. So: if you have had no acknowledgement after 5 working days, open a public issue that says only that you sent a report on a given date and have not heard back. No details, no repro. That is not a disclosure, it is a ping with a timestamp, and it is explicitly welcome.

See GOVERNANCE.md §2 for why the response capacity is one person and what is being done about it.

Disclosure

Coordinated, with a default of 90 days from acknowledgement to public disclosure, whichever comes first of that and a shipped fix.

  • Shorter if it is being exploited, or if the fix is trivial and shipping it discloses it anyway.
  • Longer only by agreement, and only for a reason we will state publicly afterwards — usually that a fix must land in a substrate crate (ciris-persist / ciris-edge / ciris-verify) and be adopted by a pinned tag here before it is safe to describe.
  • You will be credited by name or handle in the commit and CHANGELOG.md unless you ask not to be.

There is no bug bounty. There is no money. Saying so up front is more useful than letting you discover it after the work.

Scope

In scope:

artefact where
CIRISServer — this repo, the ciris-server binary and the composition https://github.com/CIRISAI/CIRISServer
CIRISPersist — corpus, admission, the Registry-of-Record https://github.com/CIRISAI/CIRISPersist
CIRISEdge — transport and replication over Reticulum / HTTPS / packet radio https://github.com/CIRISAI/CIRISEdge
CIRISVerify — the hybrid crypto core https://github.com/CIRISAI/CIRISVerify
the vendored KMP client client/ — desktop, Android, iOS
the published wheels PyPI ciris-server, abi3, all published platforms
release binaries the v* tag artefacts, and their Sigstore signatures

Out of scope, but tell us anyway if it looks bad: the public website, CI cache infrastructure (CIRISCache), and vulnerabilities in third-party dependencies — report those upstream first, then tell us so the pin can move.

Already known and documented — a report that only restates one of these is not a finding, but a new exploitation path on one of them is:

  • The receive plane is peer-blind. Consent is SEND-only and does not gate what a node accepts; any admitted key writes signed rows naming anyone. Subject-side consent exists for capacity:* alone (CC#46). FSD/MESH_GOVERNANCE_AND_ADMIN_OPS.md §0.
  • Equivocation is free. A key can sign contradictory claims to different peers at the same asserted_at; both verify; nothing compares them.
  • Every removal is an append on a pull-only plane. Revoke, purge, erase, de-admit and halt alike. Unreached holders never learn.
  • The halt latch is not reversible over the network. src/accord_halt.rs latches to disk and exit(42)s; check_halt_gate refuses boot. A halted node is not running, so the un-halt cannot be delivered to it. Since CIRISServer#347 it can be found: the latch records a release binding, and src/accord_release.rs verifies an accord-cosigned lifecycle:active token against the baked accord genesis with no network, peer, quorum or database — bound to one node and one latch instance, journaled either way. A halt still has no TTL, no cohort scope, and no preview, and a release still needs the accord quorum (it is not an operator override).
  • Ingest is deliberately open. src/ingest_http.rs and POST /v1/accord/canonical/gossip-partial are unauthenticated by design — "ingest is open and cheap; admission is the gate" (FSD/THREAT_MODEL.md). A way through admission is very much a finding; the openness of ingest is not.
  • Rate limiting is one quota on one path. PeerWriteQuota ships in persist (v22.0.0, federation/replication/admission.rs) and guards put_attestation at 600 writes per 60 s per attesting_key_id, with a 4096-peer tracked-set cap. Nothing else in the system is rate limited, and DEFAULT_OPERATIONAL_PAGE_LIMIT is u32::MAX. A resource exhaustion on any other plane is in scope and probably real.

The crypto, stated honestly

The substrate is hybrid Ed25519 + ML-DSA-65 for signatures throughout, with X-Wing (X25519 + ML-KEM-768) for the MLS epoch keys on the realtime A/V spine. The current pins are ciris-verify v10.6.3 / ciris-persist v24.2.0 / ciris-edge v15.9.1.

The crypto core has not had an external review. No third-party audit, no formal verification, no published cryptanalysis. The underlying primitives come from established implementations, but everything above them is ours and unreviewed: the hybrid composition and its verification order, the envelope canonicalization the signature covers, the admission gates, the m-of-n family and quorum resolution, the key-record and delegation walk. Those are where the interesting bugs would be, and nobody outside this project has looked at them.

Two known-real examples of the class, so this is not an abstract disclaimer:

  • A signed-row liveness refresh rewrote four envelope-covered columns while preserving the signature, so every peer refused the row — corrupt by construction, because a monotonic guard forced the covered field to advance (CIRISPersist#541).
  • Two validators over the same bytes returned opposite verdicts: verify_bundle passed a genesis bundle that put_public_key refused, so a producer got a green light and shipped an artefact that could not install (CIRISPersist#554).

Neither was found by review. Both were found by something real running.

Treat any assurance claim you cannot trace to a file in these repos as absent.

Artefact integrity

  • Wheels publish to PyPI by Trusted Publishing (OIDC): owner CIRISAI, repository CIRISServer, workflow publish-pypi.yml, environment pypi. There is no stored API token, so there is no token to steal.
  • Every tag's wheel is gated through the CIRISConformance cohabitation suite on both SQLite and Postgres before publish. A failure blocks the publish.
  • Release binaries are signed with Sigstore keyless (cosign sign-blob) using the workflow's OIDC identity; each artefact ships a .sig and a .pem.
  • A build-pipeline hybrid keypair exists (CIRIS_BUILD_ED25519_SECRET / CIRIS_BUILD_MLDSA_SECRET) and its public halves can be blessed by an accord holder (.github/workflows/export-pipeline-pubkeys.yml). The signed BuildManifest self-attestation that would make the pipeline a first-class participant in the conformance fabric is a documented TODO in .github/workflows/release.yml, not a shipped feature.

What this project does not have

Verified against the repositories and their settings, so a researcher does not waste time assuming otherwise:

  • No cargo-audit or cargo-deny job in .github/workflows/.
  • Dependabot security updates: disabled.
  • Secret scanning and push protection: disabled.
  • No reproducible builds. A wheel cannot currently be rebuilt bit-for-bit and compared to the published one.
  • No security-response rota, no on-call, no second responder (GOVERNANCE.md §2.4).

None of these is hard. They are unpaid. If one of them matters to you more than the others, saying so in an issue is a legitimate way to change the order.

There aren't any published security advisories