From 950ddb512caa37244ad6b3bbf750fdb403ddc685 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:23:09 -0700 Subject: [PATCH 1/5] docs(security): draft BOUNTY.md (not in force, OWNER TO FILL), reconciled tier table, no private-archive or PGP path Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- AUDIT_POSTURE.md | 5 +- BOUNTY.md | 122 +++++++++++++++++++++++++++++++++++++++++++++++ SECURITY.md | 32 +++++++------ 3 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 BOUNTY.md diff --git a/AUDIT_POSTURE.md b/AUDIT_POSTURE.md index 5e29da6..c967a94 100644 --- a/AUDIT_POSTURE.md +++ b/AUDIT_POSTURE.md @@ -61,11 +61,12 @@ See [`SECURITY.md`](SECURITY.md). All Tier-1 repos must include a `SECURITY.md` ## Audit history -Federation-wide audit history is summarized below. Individual audit reports live in each repo's `audits/` directory; pre-split audit reports live in [`citrate-monorepo-archive`](https://github.com/CitrateNetwork/citrate-monorepo-archive)'s `audits/`. +Federation-wide audit history is summarized below. Individual audit reports live in each repo's `audits/` directory. Pre-split audit reports are kept in a private archive and are not public. | Date | Auditor | Scope | Outcome | |---|---|---|---| -| Pre-split | various | Monorepo as of 2026-05-17 | See archive `audits/` | +| Pre-split | internal | Monorepo as of 2026-05-17 | Private archive, not public | +| 2026-09-24 | internal adversarial audit (pre-bounty) | Federation, public repos and live testnet | Findings under remediation; see [`BOUNTY.md`](BOUNTY.md) known issues | | Planned: Q3 2026 | TBD | `citrate-chain` Tier-1 pass before `v0.5.0` stable | — | ## Changelog of this document diff --git a/BOUNTY.md b/BOUNTY.md new file mode 100644 index 0000000..c4d0a7e --- /dev/null +++ b/BOUNTY.md @@ -0,0 +1,122 @@ +# Citrate Bug Bounty (draft) + +> **Status: DRAFT, not in force.** This document is not policy yet: the safe-harbor text needs counsel +> review, and reward amounts, the launch date and the PGP key are owner decisions, all marked +> `OWNER TO FILL` below. Until this banner is removed, report through the [security policy](SECURITY.md) +> as usual. + +This program covers the public repositories of the [`CitrateNetwork`](https://github.com/CitrateNetwork) +organization and the public Citrate testnet (chain id 40204). It extends [`SECURITY.md`](SECURITY.md), +which sets the reporting channels, response times and the 90-day coordinated-disclosure window. + +## How to report + +1. Use **GitHub private vulnerability reporting** on the affected repository (Security tab, "Report a + vulnerability"). This is the preferred channel. +2. Or email **security@citrate.ai**. A PGP key is not published yet (`OWNER TO FILL`); until it is, use + private vulnerability reporting for anything sensitive. +3. Include the repository and commit SHA (or release tag), the affected host if any, a reproducible + proof of concept, and your severity assessment. + +One issue per report. If a report chains several issues, say which link is new. + +## Ground rules + +- **Testnet only.** Chain 40204 is a public testnet. SALT has no cash value until mainnet (targeted Q2 + 2027). Nothing in this program pays out in SALT, and a finding does not "steal funds" in a + cash sense on testnet; we grade it by what the same flaw would do on mainnet. +- **Your own accounts only.** Use accounts and keys you create. Get test SALT from the public faucet. + Do not access, modify or move anything belonging to another account, member or tenant beyond the + minimum needed to show the issue, and stop as soon as you have shown it. +- **No personal data.** If you reach another person's data (identity evidence, messages, memories, + CRM records), stop, do not copy it, and report immediately. +- **Show, do not exploit.** Prove impact with the smallest possible demonstration: one transaction, + one request, one block. Do not halt the network, fork it, or leave it in a degraded state on purpose. + For consensus or liveness issues, a local multi-node reproduction is preferred to a live one. +- **Load limits on shared hosts.** `rpc.citrate.ai`, `bundler.citrate.ai`, `faucet.citrate.ai`, + `coordinator.citrate.ai`, `explorer.citrate.ai`, `docs.citrate.ai`, `membership.citrate.ai` and + `auth.citrate.ai` are shared by everyone. Stay under **5 requests per second** and **10,000 + requests per day** per host, never run volumetric or amplification tests against them, and + demonstrate denial-of-service findings against a local build instead. `OWNER TO FILL`: confirm or + change these limits. +- **No social engineering, phishing or physical attacks**, and no attacks on third-party services + (GitHub, Vercel, npm, PyPI, DNS providers) or on other users. +- **Keep it private** until the issue is fixed and disclosed under the coordinated-disclosure window. + +## Safe harbor + +If you follow these rules in good faith, we will treat your research as authorized, we will not +pursue or support legal action against you for it, and we will not ask a third party to do so. If a +third party brings an action against you for research done under this program, we will make it known +that you acted with our authorization. If you are unsure whether something is allowed, ask at +security@citrate.ai before you do it. `OWNER TO FILL`: counsel to confirm this wording. + +## In scope + +| Asset | What we want | +|---|---| +| `citrate-chain`: node, consensus, sequencer and mempool, execution (EVM, LVM, precompiles), storage, P2P, JSON-RPC and MCP API | Consensus safety and liveness, remote node crash or halt, state or balance corruption, signature or replay bypass, RPC auth bypass | +| `citrate-chain/contracts`: contracts **with code** on chain 40204 (see the [address page](https://docs.citrate.ai/chain/addresses)) | Unauthorized transfer, mint or burn, governance or role bypass, stuck funds, broken accounting | +| Account abstraction (Keyring) stack and `citrate-bundler` | Unauthorized user operations, sponsorship abuse, signature bypass | +| `citrate-identity` (OIDC, VERI) | Authentication or authorization bypass, token forgery, access to another person's verification data | +| `citrate-sdk-js`, `citrate-sdk-python` | Key or secret disclosure, signing the wrong thing, chain-id or replay mistakes | +| `citrate-core` (desktop full node), `citrate-agent-runtime`, `nist-agent`, `citrate-quorum` | Key extraction, sandbox or capability escape, HIC approval bypass | +| `citrate-compute-pool` (coordinator and worker), `citrate-inference-gateway` free routes | Escrow or payout theft, job hijack, coordinator auth bypass | +| `citrate-comms`, `citrate-memories` | Cross-tenant or cross-member access, relay confidentiality or integrity breaks | +| `citrate-explorer` | RPC allow-list bypass, stored XSS, auth bypass | +| Public web hosts listed under "Load limits" | Auth bypass, XSS, CSRF with real impact, secret exposure | + +Other public first-party repositories are in scope for code findings at the tier set in their +`AUDIT_TIER.md`. Forks of third-party code (for example the `chains` fork) are out of scope. + +## Not deployed or not running (out of scope for live impact) + +These exist in code or in the design but are not live. A report that they are missing is a duplicate. +A code-level flaw in them is accepted as a low-priority design finding, not a live exploit. + +| Surface | Status | +|---|---| +| Checkpoint finality (committee of 100, quorum 67) | Specified, not running. Confirmation is probabilistic (see `verification/claims.json` in citrate-chain) | +| Stake-gated proposer eligibility and a multi-producer validator set | Staged: off by default, enabled when a validator registry is configured. The testnet runs a single block producer operated by Citrate | +| Inference gateway paid routes (x402 and API-key metering) | Not mounted by the gateway binary; not deployed | +| Membership x402 endpoint | Not configured; not deployed | +| SALT bridge | Specified, not deployed | +| The 19 governance and cooperative contracts with no code on 40204 (AnchorRegistry, MeetingRegistry, GovernanceTemplateRegistry, GovernanceProtocolFactory, PolicyBinding, CapabilityGrant, VoteAllowance, Sortition, PatronageLedger, ModelCooperative, FacilitySBTImpl, NetworkSBTImpl, FacilitySBT, NetworkSBT, CitrateCooperativeFactory, CoopDeployer, CoopMembershipSBT, ContributionRewardPool, CoopGovernor) | In the address book, not deployed | +| Zero-knowledge compute tier | Research preview; not a production guarantee | +| TEE compute tier | Inert on 40204: no TEE oracle is registered with `ComputeVerifier` (`teeOracleCount()` is 0) | +| Passkey-only accounts | Not yet available on 40204 | + +## Out of scope + +- Everything listed in `SECURITY.md` under "Out of scope". +- Findings that need a compromised operator key, a malicious block producer acting alone on testnet + with no effect beyond what the single-producer topology already implies, or physical access. +- Missing security headers, banner or version disclosure, clickjacking on pages with no sensitive + action, SPF/DMARC settings, and rate limits on non-sensitive endpoints, unless you show real impact. +- Issues in dependencies that are already public upstream (report them upstream). +- Anything already on the known-issues list below. +- Automated scanner output without a working proof of concept. + +## Known issues + +Published per finding once fixed. `OWNER TO FILL`. + +## Rewards + +`OWNER TO FILL`. Severity is graded under the CIT-SEV rubric used by our audits, by the impact the +same flaw would have on mainnet. + +| Severity | Examples | Reward | +|---|---|---| +| Critical | Consensus safety break, unauthorized spend from any account, network-wide halt, key disclosure at scale | `OWNER TO FILL` | +| High | Theft or freeze of funds in a deployed contract, auth bypass on identity, cross-tenant data access | `OWNER TO FILL` | +| Medium | Single-node crash, limited griefing, privilege escalation with preconditions | `OWNER TO FILL` | +| Low | Hardening gaps with a concrete but limited impact | `OWNER TO FILL` | +| Documentation | A public claim that does not match the code or the live chain | `OWNER TO FILL` (recognition or swag) | + +Payment method, currency, KYC requirements for payout, and the eligibility rules for employees and +contractors are also `OWNER TO FILL`. + +## Contact + +security@citrate.ai diff --git a/SECURITY.md b/SECURITY.md index bad0619..7444598 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,9 +8,9 @@ This document covers all repositories under the [`CitrateNetwork`](https://githu Preferred (encrypted, no key exchange): use **GitHub private vulnerability reporting** — on the affected repository, open the **Security** tab → **Report a vulnerability**. This gives a private, GitHub-encrypted channel with no PGP key to fetch. -Alternatively, email **security@citrate.ai**. To encrypt an emailed report, fetch our PGP public key from `keys.openpgp.org` (search `security@citrate.ai`) or via the `Encryption` field of our [`security.txt`](https://citrate.ai/.well-known/security.txt). +Alternatively, email **security@citrate.ai**. We do not publish a PGP key yet, so send sensitive details through private vulnerability reporting rather than plain email. Our [`security.txt`](https://citrate.ai/.well-known/security.txt) lists the same contacts. -> GH-B-012: the previous PGP path pointed at `keys/security@citrate.ai.asc` in the **private** `citrate-monorepo-archive` repo, which no external reporter can read — the documented encryption path did not work. Use private vulnerability reporting instead. +Bounty policy: coming soon. It will be linked here once counsel has reviewed it (OWNER). Include in your report: - The repo + commit SHA (or version tag) where you observed the issue @@ -22,14 +22,18 @@ We acknowledge within **72 hours** and aim to triage within **5 business days**. ## Scope -Severity tiers and audit cadence per repo are documented in each repo's `AUDIT_TIER.md`. The TL;DR: +Severity tiers and audit cadence per repo are documented in each repo's `AUDIT_TIER.md`. The summary +below is the same table that appears on the [security posture page](https://docs.citrate.ai/security/posture). -| Tier | Audit policy | Vulnerability handling | -|---|---|---| -| **Tier 1** (chain, native app, SDKs, agent-runtime, gateway, compute-pool) | Full audit before every stable release | Coordinated disclosure; CVE assigned for high+ | -| **Tier 3** (docs, and other content/library repos) | Content review only | Triage as docs corrections, no CVE | +| Tier | Repositories | Audit policy | Vulnerability handling | +|---|---|---|---| +| **Tier 1**: consensus, value, keys, identity | `citrate-chain` (node, contracts, ZK), `citrate-core`, `citrate-identity`, `citrate-inference-gateway`, `citrate-compute-pool`, `citrate-coop`, `citrate-agent-runtime`, `citrate-sdk-js`, `citrate-sdk-python` | Full adversarial audit before every stable release | Coordinated disclosure; a GitHub Security Advisory (with a CVE request) for fixed High and Critical issues in released code | +| **Tier 3**: docs and content | `citrate-docs`, `.github`, and other content-only repositories | Content review | Triage as documentation corrections, no CVE | -Per-repo tier is authoritative in each repo's `AUDIT_TIER.md`. +A repository's own `AUDIT_TIER.md` is authoritative for that repository. A public repository without an +`AUDIT_TIER.md` is handled as Tier 1 for reports. + +No advisories have been published yet. The first will follow the fixes from the 2026-09 pre-bounty audit. ## Responsible disclosure @@ -53,13 +57,13 @@ We will **not** pursue legal action against researchers who: - Social engineering of team members. - Physical access attacks against operator hardware. -## Supply-chain integrity +## Supply-chain integrity (current practice) -- Crates published from `citrate-chain` are signed via cosign keyless OIDC. See the chain's `.github/workflows/release.yml` for the signing pipeline. -- npm packages from `citrate-sdk-*` are published with provenance attestations. -- SBOMs (CycloneDX) attach to every Tier-1 release. +- `citrate-chain`'s release workflow is built to sign artifacts with cosign (keyless OIDC) and attach CycloneDX SBOMs, but no public release carries signed assets yet: the signed `v0.5.0-beta2-tier2` build is still a draft. Treat current prereleases, including the `citrate-core` desktop builds, as unsigned and without SBOMs. +- `@citratelabs/sdk` on npm is published with a provenance attestation. `@citratelabs/marketplace-sdk` is not yet. +- Supply-chain hardening is in progress: required review and CI checks on every public repository, third-party GitHub Actions pinned to commit SHAs, and signed releases with SBOMs. -Verifying a release artifact: +Verifying a signed release artifact, once published: ```bash # cosign verify-blob with the issuer / identity from the release @@ -71,7 +75,7 @@ cosign verify-blob --certificate-identity-regexp 'https://github\.com/CitrateNet ## Audit firms + history -Per-repo audit history lives in each repo's `audits/` directory (when present) or in the [`citrate-monorepo-archive`](https://github.com/CitrateNetwork/citrate-monorepo-archive) for pre-split history. The next planned audit is the chain Tier-1 pass before the `v0.5.0` stable tag. +Per-repo audit history lives in each repo's `audits/` directory, when present. History from before the repositories were split is kept in a private archive and is not public. No external-firm audit has been completed yet; the next planned audit is the chain Tier-1 pass before the `v0.5.0` stable tag. ## Contact From d9e30efbd975b25ad056bd45f95c528ec4004343 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:23:09 -0700 Subject: [PATCH 2/5] docs(profile): HIC not HITL, paid rails not deployed, SHA pins, live endpoints only Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- README.md | 14 +++++++------- profile/README.md | 13 +++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8532574..4b1f8ea 100644 --- a/README.md +++ b/README.md @@ -224,10 +224,10 @@ cargo run --release -- --rpc-url http://127.0.0.1:8545 | Network name | Citrate | | Chain ID | `40204` (hex `0x9d0c`) | | RPC URL | `https://rpc.citrate.ai` | -| WebSocket | `wss://ws.citrate.ai` | +| WebSocket | not publicly served yet (run a local node for `ws://127.0.0.1:8546`) | | Block explorer | `https://explorer.citrate.ai` | | Faucet | `https://faucet.citrate.ai` | -| Chain spec | `citrate-chain/specs/testnet.toml` | +| Chain spec | `citrate-chain/node/config/testnet.toml` | > Mainnet target is Q2 2027; the current network is chain 40204. Confirm live endpoint > status in the [docs](https://docs.citrate.ai) before assuming availability. @@ -308,12 +308,12 @@ see each repo's README for specifics. | Repo | What it builds | Run | |---|---|---| -| [`citrate-inference-gateway`](https://github.com/CitrateNetwork/citrate-inference-gateway) | x402-metered inference gateway | `cargo run --release` | +| [`citrate-inference-gateway`](https://github.com/CitrateNetwork/citrate-inference-gateway) | Inference gateway (paid routes not deployed yet) | `cargo run --release` | | [`citrate-compute-pool`](https://github.com/CitrateNetwork/citrate-compute-pool) | Coordinator + workers for pooled training | `cargo run --release` | | [`citrate-cluster`](https://github.com/CitrateNetwork/citrate-cluster) | GPU-fleet and compute-cluster tooling | `cargo run --release` | | [`citrate-core`](https://github.com/CitrateNetwork/citrate-core) | Desktop app that runs a full node | `cargo run --release` | | [`citrate-comms`](https://github.com/CitrateNetwork/citrate-comms) | E2E-encrypted, server-blind team workspace | `pnpm install && pnpm dev` | -| [`citrate-quorum`](https://github.com/CitrateNetwork/citrate-quorum) | Human-in-the-loop governance surface for AI | `cargo run --release` | +| [`citrate-quorum`](https://github.com/CitrateNetwork/citrate-quorum) | HIC (Human In Control) governance surface for AI | `cargo run --release` | | [`citrate-identity`](https://github.com/CitrateNetwork/citrate-identity) | OIDC/OAuth2 authority (SIWE, passkeys) | `pnpm install && pnpm dev` | | [`citrate-memories`](https://github.com/CitrateNetwork/citrate-memories) | Content-addressed knowledge graph for agents | `cargo run --release` | | [`citrate-native`](https://github.com/CitrateNetwork/citrate-native) | Slint desktop wallet + agent client | `cargo run --release` | @@ -454,11 +454,11 @@ on: jobs: rust: if: hashFiles('Cargo.toml') != '' - uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@v1 # pin a tag or SHA, not @main + uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@5d24169b7acf6533b1eaba2399a25effc6c346d6 # pin a SHA, not @main js: if: hashFiles('package.json') != '' - uses: citratenetwork/.github/.github/workflows/reusable-js-ci.yml@v1 # pin a tag or SHA, not @main + uses: citratenetwork/.github/.github/workflows/reusable-js-ci.yml@5d24169b7acf6533b1eaba2399a25effc6c346d6 # pin a SHA, not @main ``` ### Release notifications @@ -474,7 +474,7 @@ no diff for a caller's reviewer to see. Pin to a tag or SHA so an upgrade is a reviewable change in the caller: ```yaml -uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@v1 # or @<40-hex-sha> +uses: citratenetwork/.github/.github/workflows/reusable-rust-ci.yml@<40-hex-sha> # no release tags exist yet ``` When breaking-change updates are made, cut a new tag here so consumer repos can pin against it. diff --git a/profile/README.md b/profile/README.md index 228f8f6..ad338ef 100644 --- a/profile/README.md +++ b/profile/README.md @@ -28,18 +28,18 @@ Licensor: **Citrate Inc.** Each repository's `LICENSE` file is authoritative. A - **`citrate-agent-runtime`** — Capability-scoped agent execution runtime + capsules. - **`citrate-sdk-js`** — TypeScript SDK (`@citratelabs/sdk`). - **`citrate-sdk-python`** — Python SDK. -- **`citrate-sdk-marketplace`** — Marketplace SDK (metered, pay-per-call inference). +- **`citrate-sdk-marketplace`**: Marketplace SDK (metered inference; the paid rails are not deployed yet). - **`citrate-docs`** — The Almanac: docs.citrate.ai. - **`citrate-explorer`** — CitrateScan, the AI-native BlockDAG explorer. ### Application layer / commercial core — BUSL-1.1 (source-available, converts to Apache-2.0) -- **`citrate-inference-gateway`** — x402-metered, pay-per-call AI inference gateway. +- **`citrate-inference-gateway`**: AI inference gateway. Paid calls (x402 and API-key metering) are not deployed yet. - **`citrate-compute-pool`** — Coordinator + workers for pooled AI training. - **`citrate-cluster`** — GPU-fleet and compute-cluster tooling. - **`citrate-core`** — Desktop app that turns your machine into a full node. - **`citrate-comms`** — End-to-end-encrypted, server-blind team workspace. -- **`citrate-quorum`** — Human-in-the-loop governance surface for AI. +- **`citrate-quorum`**: HIC (Human In Control) governance surface for AI. - **`citrate-identity`** — OIDC/OAuth2 authority with SIWE and passkeys. - **`citrate-memories`** — Content-addressed knowledge graph for agents. - **`citrate-native`** — Slint desktop wallet and agent client. @@ -65,9 +65,10 @@ Usage in any repo's `.github/workflows/ci.yml`: ```yaml jobs: rust: - # GH-B-003: pin to a release tag or a full commit SHA, never @main (a mutable - # branch: one push to this repo would change every caller's CI with no diff). - uses: CitrateNetwork/.github/.github/workflows/reusable-rust-ci.yml@v1 + # GH-B-003: pin to a full commit SHA, never @main (a mutable branch: one push + # to this repo would change every caller's CI with no diff). This repo has no + # release tags yet, so @v1 does not resolve; use a reviewed commit SHA. + uses: CitrateNetwork/.github/.github/workflows/reusable-rust-ci.yml@5d24169b7acf6533b1eaba2399a25effc6c346d6 with: working-directory: '.' apt-packages: 'libclang-dev cmake libssl-dev pkg-config libfontconfig1-dev' From 9e249e8c5fb1e532ffd8d4bbc3ce11767f026d0d Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:23:09 -0700 Subject: [PATCH 3/5] ci: public-truth check for org profile, README, SECURITY and BOUNTY Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- .github/workflows/ci.yml | 2 + scripts/ci/public-truth.sh | 6 ++ scripts/ci/public_truth.py | 116 +++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100755 scripts/ci/public-truth.sh create mode 100644 scripts/ci/public_truth.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ff5be1..6603b61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,5 @@ jobs: run: bash scripts/ci/workflow-guardrails.sh . - name: Canon-guardrail regression suite run: bash scripts/test-canon-guardrail.sh + - name: Public-truth check (PBA-L8 claims vs code and live network) + run: bash scripts/ci/public-truth.sh . diff --git a/scripts/ci/public-truth.sh b/scripts/ci/public-truth.sh new file mode 100755 index 0000000..7fae947 --- /dev/null +++ b/scripts/ci/public-truth.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# public-truth.sh: the org profile, README and security policy must not make claims +# the code or the live network contradict (PBA-L8). Logic lives in public_truth.py. +# Usage: bash scripts/ci/public-truth.sh [root] (exit 1 on any hit) +set -euo pipefail +exec python3 "$(dirname "${BASH_SOURCE[0]}")/public_truth.py" "${1:-.}" diff --git a/scripts/ci/public_truth.py b/scripts/ci/public_truth.py new file mode 100644 index 0000000..4de4e99 --- /dev/null +++ b/scripts/ci/public_truth.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Public-truth check for the org profile, README and security policy (PBA-L8). + +Text is normalised before matching (line breaks and runs of whitespace collapse to one +space, a hyphen split across a line break is joined, markdown emphasis is dropped), so a +reworded or re-wrapped claim is still caught. Exit 1 on any hit. + +Usage: python3 scripts/ci/public_truth.py [root] +""" +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +DASH = r"[\s\-‐-―]*" + + +def normalise(text: str) -> str: + t = re.sub(r"-\s*\n\s*", "-", text) # "Human-in-\n the-loop" -> "Human-in-the-loop" + t = re.sub(r"[*_`]+", "", t) # markdown emphasis / code ticks + t = re.sub(r"\s+", " ", t) + return t + + +def sentences(t: str) -> list[str]: + return re.split(r"(?<=[.!?|])\s+", t) + + +RULES: list[tuple[str, re.Pattern, re.Pattern | None]] = [ + ("owner rule: use HIC (Human In Control), never HITL", + re.compile(r"\bH\.?I\.?T\.?L(s|'s)?\b"), None), + ("owner rule: use HIC, never human-in-the-loop", + re.compile(r"human" + DASH + r"in" + DASH + r"(the" + DASH + r")?loop", re.I), None), + ("dead host (no DNS / 404 / 530)", + re.compile(r"wss?://ws\.citrate\.ai|scan\.citrate\.ai|rpc2\.citrate\.ai|mirror\.citrate\.ai", re.I), None), + ("citrate-chain/specs/testnet.toml does not exist (use node/config/testnet.toml)", + re.compile(r"specs/testnet\.toml"), None), + ("links a private repository", + re.compile(r"citrate-monorepo-archive|citrate-agentile-archive", re.I), None), + ("no PGP key is published for security@citrate.ai", + re.compile(r"keys\s*\.\s*openpgp\s*\.\s*org|PGP (public )?key (from|at|via)", re.I), None), + ("overstated supply-chain claim", + re.compile(r"(cosign[- ]signed|signed (via|with) cosign|SBOMs? \(?CycloneDX\)? attach|every (tier-1 )?release (ships|carries|includes|has)|CVE assigned for high)", re.I), + re.compile(r"in progress|once published|built to|not yet|no public release", re.I)), + ("paid inference rails described as live (not deployed)", + re.compile(r"(x402|pay" + DASH + r"per" + DASH + r"call)[^.|]{0,80}\b(live|available now|today|metered)\b|x402-metered", re.I), + re.compile(r"not (yet )?(deployed|live|mounted)", re.I)), + ("names an audit finding ID; public copy must not describe open findings", + re.compile(r"\bPBA-[A-Za-z0-9]+-\d+\b"), None), +] + + +def remote_v_tags(root: Path) -> list[str]: + """Release tags on origin. Works under a shallow checkout, which fetches no tags.""" + for cmd in (["git", "-C", str(root), "ls-remote", "--tags", "origin", "v*"], + ["git", "-C", str(root), "tag", "-l", "v*"]): + try: + out = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + except Exception: + continue + if out.returncode == 0: + return [l.split("refs/tags/")[-1] for l in out.stdout.split() if "v" in l] + return [] + + +def main() -> int: + root = Path(sys.argv[1] if len(sys.argv) > 1 else ".") + files = sorted(p for p in list(root.glob("*.md")) + list((root / "profile").glob("*.md")) if p.is_file()) + errs: list[str] = [] + for f in files: + rel = f.relative_to(root) + norm = normalise(f.read_text(errors="replace")) + for label, rx, qual in RULES: + for s in sentences(norm): + m = rx.search(s) + if not m: + continue + if qual is not None and qual.search(s): + continue + errs.append(f"{rel}: {label}: ...{s[max(0, m.start() - 40):m.end() + 60]}...") + + # Reusable-workflow pins must resolve. + tags = remote_v_tags(root) + for f in files: + for m in re.finditer(r"reusable-[a-z-]+\.yml@(v[0-9][\w.]*)", f.read_text(errors="replace")): + if m.group(1) not in tags: + errs.append(f"{f.relative_to(root)}: pins @{m.group(1)}, which is not a tag on origin; pin a commit SHA") + + bounty = root / "BOUNTY.md" + sec = root / "SECURITY.md" + if not bounty.exists(): + errs.append("BOUNTY.md is missing") + else: + b = bounty.read_text() + for section in ("## In scope", "## Not deployed or not running", "## Safe harbor", "## Known issues", "## Rewards"): + if section not in b: + errs.append(f"BOUNTY.md lacks section '{section}'") + ki = b.split("## Known issues", 1)[-1].split("\n## ", 1)[0] + if re.search(r"^\s*[-*|]\s", ki, re.M): + errs.append("BOUNTY.md Known issues must stay a placeholder until each finding is fixed and the owner publishes it") + if "not in force" in b and sec.exists() and "BOUNTY.md" in sec.read_text(): + errs.append("SECURITY.md links BOUNTY.md while BOUNTY.md is marked not in force (counsel sign-off pending)") + + for e in errs: + print(f"::error::public-truth: {e}") + if errs: + print(f"public-truth: FAIL ({len(errs)})") + return 1 + print(f"public-truth: OK ({len(files)} files)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From e0fe6e6f7ea8505b6825957ddb878130fc24881a Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:56:04 -0700 Subject: [PATCH 4/5] docs(security): no BOUNTY link or remediation wording in AUDIT_POSTURE; public-truth checks every .md for bounty links and tighter qualifiers Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- .github/workflows/ci.yml | 2 +- AUDIT_POSTURE.md | 2 +- BOUNTY.md | 2 +- SECURITY.md | 2 +- scripts/ci/public-truth.sh | 2 +- scripts/ci/public_truth.py | 23 ++++++++++++++--------- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6603b61..931d578 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,5 +28,5 @@ jobs: run: bash scripts/ci/workflow-guardrails.sh . - name: Canon-guardrail regression suite run: bash scripts/test-canon-guardrail.sh - - name: Public-truth check (PBA-L8 claims vs code and live network) + - name: Public-truth check (public-claims accuracy) run: bash scripts/ci/public-truth.sh . diff --git a/AUDIT_POSTURE.md b/AUDIT_POSTURE.md index c967a94..1a9350c 100644 --- a/AUDIT_POSTURE.md +++ b/AUDIT_POSTURE.md @@ -66,7 +66,7 @@ Federation-wide audit history is summarized below. Individual audit reports live | Date | Auditor | Scope | Outcome | |---|---|---|---| | Pre-split | internal | Monorepo as of 2026-05-17 | Private archive, not public | -| 2026-09-24 | internal adversarial audit (pre-bounty) | Federation, public repos and live testnet | Findings under remediation; see [`BOUNTY.md`](BOUNTY.md) known issues | +| 2026-09-24 | internal adversarial audit (pre-bounty) | Federation, public repos and live testnet | Remediation in progress | | Planned: Q3 2026 | TBD | `citrate-chain` Tier-1 pass before `v0.5.0` stable | — | ## Changelog of this document diff --git a/BOUNTY.md b/BOUNTY.md index c4d0a7e..a7aa93b 100644 --- a/BOUNTY.md +++ b/BOUNTY.md @@ -78,7 +78,7 @@ A code-level flaw in them is accepted as a low-priority design finding, not a li |---|---| | Checkpoint finality (committee of 100, quorum 67) | Specified, not running. Confirmation is probabilistic (see `verification/claims.json` in citrate-chain) | | Stake-gated proposer eligibility and a multi-producer validator set | Staged: off by default, enabled when a validator registry is configured. The testnet runs a single block producer operated by Citrate | -| Inference gateway paid routes (x402 and API-key metering) | Not mounted by the gateway binary; not deployed | +| Inference gateway paid routes (x402 and API-key metering) | Not deployed | | Membership x402 endpoint | Not configured; not deployed | | SALT bridge | Specified, not deployed | | The 19 governance and cooperative contracts with no code on 40204 (AnchorRegistry, MeetingRegistry, GovernanceTemplateRegistry, GovernanceProtocolFactory, PolicyBinding, CapabilityGrant, VoteAllowance, Sortition, PatronageLedger, ModelCooperative, FacilitySBTImpl, NetworkSBTImpl, FacilitySBT, NetworkSBT, CitrateCooperativeFactory, CoopDeployer, CoopMembershipSBT, ContributionRewardPool, CoopGovernor) | In the address book, not deployed | diff --git a/SECURITY.md b/SECURITY.md index 7444598..6a70dc7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -33,7 +33,7 @@ below is the same table that appears on the [security posture page](https://docs A repository's own `AUDIT_TIER.md` is authoritative for that repository. A public repository without an `AUDIT_TIER.md` is handled as Tier 1 for reports. -No advisories have been published yet. The first will follow the fixes from the 2026-09 pre-bounty audit. +No advisories have been published yet. ## Responsible disclosure diff --git a/scripts/ci/public-truth.sh b/scripts/ci/public-truth.sh index 7fae947..e88148f 100755 --- a/scripts/ci/public-truth.sh +++ b/scripts/ci/public-truth.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # public-truth.sh: the org profile, README and security policy must not make claims -# the code or the live network contradict (PBA-L8). Logic lives in public_truth.py. +# the code or the live network contradict (public-claims accuracy). Logic lives in public_truth.py. # Usage: bash scripts/ci/public-truth.sh [root] (exit 1 on any hit) set -euo pipefail exec python3 "$(dirname "${BASH_SOURCE[0]}")/public_truth.py" "${1:-.}" diff --git a/scripts/ci/public_truth.py b/scripts/ci/public_truth.py index 4de4e99..76470a7 100644 --- a/scripts/ci/public_truth.py +++ b/scripts/ci/public_truth.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Public-truth check for the org profile, README and security policy (PBA-L8). +"""Public-truth check for the org profile, README and security policy (public-claims accuracy). Text is normalised before matching (line breaks and runs of whitespace collapse to one space, a hyphen split across a line break is joined, markdown emphasis is dropped), so a @@ -42,10 +42,10 @@ def sentences(t: str) -> list[str]: ("no PGP key is published for security@citrate.ai", re.compile(r"keys\s*\.\s*openpgp\s*\.\s*org|PGP (public )?key (from|at|via)", re.I), None), ("overstated supply-chain claim", - re.compile(r"(cosign[- ]signed|signed (via|with) cosign|SBOMs? \(?CycloneDX\)? attach|every (tier-1 )?release (ships|carries|includes|has)|CVE assigned for high)", re.I), - re.compile(r"in progress|once published|built to|not yet|no public release", re.I)), + re.compile(r"(\b(are|is) cosign[- ]signed|cosign[- ]signed (releases|crates|artifacts)|signed (via|with) cosign|SBOMs? \(?CycloneDX\)? attach|every (tier-1 )?release (ships|carries|includes|has)|CVE assigned for high)", re.I), + re.compile(r"^.{0,25}(built to|once published|not yet|no public release|in progress)", re.I)), ("paid inference rails described as live (not deployed)", - re.compile(r"(x402|pay" + DASH + r"per" + DASH + r"call)[^.|]{0,80}\b(live|available now|today|metered)\b|x402-metered", re.I), + re.compile(r"(x402|pay" + DASH + r"per" + DASH + r"call|paid (inference|routes|calls))[^.|]{0,80}\b(live|available now|today|metered|generally available|in production)\b|x402-metered", re.I), re.compile(r"not (yet )?(deployed|live|mounted)", re.I)), ("names an audit finding ID; public copy must not describe open findings", re.compile(r"\bPBA-[A-Za-z0-9]+-\d+\b"), None), @@ -77,8 +77,10 @@ def main() -> int: m = rx.search(s) if not m: continue - if qual is not None and qual.search(s): - continue + if qual is not None: + window = s[max(0, m.start() - 40): m.end() + 40] + if re.search(r"not (yet )?(deployed|live|mounted)|built to|once published|not yet|no public release", window, re.I) and not re.search(r"\bare cosign|\bis cosign", s[m.start():m.end()], re.I): + continue errs.append(f"{rel}: {label}: ...{s[max(0, m.start() - 40):m.end() + 60]}...") # Reusable-workflow pins must resolve. @@ -98,10 +100,13 @@ def main() -> int: if section not in b: errs.append(f"BOUNTY.md lacks section '{section}'") ki = b.split("## Known issues", 1)[-1].split("\n## ", 1)[0] - if re.search(r"^\s*[-*|]\s", ki, re.M): + body = re.sub(r"\s+", " ", ki).strip() + if re.search(r"^\s*([-*|]|\d+[.)])\s", ki, re.M) or body not in ("Published per finding once fixed. `OWNER TO FILL`.",): errs.append("BOUNTY.md Known issues must stay a placeholder until each finding is fixed and the owner publishes it") - if "not in force" in b and sec.exists() and "BOUNTY.md" in sec.read_text(): - errs.append("SECURITY.md links BOUNTY.md while BOUNTY.md is marked not in force (counsel sign-off pending)") + if "not in force" in b: + for f in files: + if f.name != "BOUNTY.md" and re.search(r"BOUNTY\.md", f.read_text(errors="replace")): + errs.append(f"{f.relative_to(root)} links BOUNTY.md while BOUNTY.md is marked not in force (counsel sign-off pending)") for e in errs: print(f"::error::public-truth: {e}") From 3569c0644db0174f3cda3f17c7dfa741927f36d8 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 00:13:20 -0700 Subject: [PATCH 5/5] chore: move the draft bounty policy to its own PR; public-truth passes with or without BOUNTY.md Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- BOUNTY.md | 122 ------------------------------------- scripts/ci/public_truth.py | 5 +- 2 files changed, 4 insertions(+), 123 deletions(-) delete mode 100644 BOUNTY.md diff --git a/BOUNTY.md b/BOUNTY.md deleted file mode 100644 index a7aa93b..0000000 --- a/BOUNTY.md +++ /dev/null @@ -1,122 +0,0 @@ -# Citrate Bug Bounty (draft) - -> **Status: DRAFT, not in force.** This document is not policy yet: the safe-harbor text needs counsel -> review, and reward amounts, the launch date and the PGP key are owner decisions, all marked -> `OWNER TO FILL` below. Until this banner is removed, report through the [security policy](SECURITY.md) -> as usual. - -This program covers the public repositories of the [`CitrateNetwork`](https://github.com/CitrateNetwork) -organization and the public Citrate testnet (chain id 40204). It extends [`SECURITY.md`](SECURITY.md), -which sets the reporting channels, response times and the 90-day coordinated-disclosure window. - -## How to report - -1. Use **GitHub private vulnerability reporting** on the affected repository (Security tab, "Report a - vulnerability"). This is the preferred channel. -2. Or email **security@citrate.ai**. A PGP key is not published yet (`OWNER TO FILL`); until it is, use - private vulnerability reporting for anything sensitive. -3. Include the repository and commit SHA (or release tag), the affected host if any, a reproducible - proof of concept, and your severity assessment. - -One issue per report. If a report chains several issues, say which link is new. - -## Ground rules - -- **Testnet only.** Chain 40204 is a public testnet. SALT has no cash value until mainnet (targeted Q2 - 2027). Nothing in this program pays out in SALT, and a finding does not "steal funds" in a - cash sense on testnet; we grade it by what the same flaw would do on mainnet. -- **Your own accounts only.** Use accounts and keys you create. Get test SALT from the public faucet. - Do not access, modify or move anything belonging to another account, member or tenant beyond the - minimum needed to show the issue, and stop as soon as you have shown it. -- **No personal data.** If you reach another person's data (identity evidence, messages, memories, - CRM records), stop, do not copy it, and report immediately. -- **Show, do not exploit.** Prove impact with the smallest possible demonstration: one transaction, - one request, one block. Do not halt the network, fork it, or leave it in a degraded state on purpose. - For consensus or liveness issues, a local multi-node reproduction is preferred to a live one. -- **Load limits on shared hosts.** `rpc.citrate.ai`, `bundler.citrate.ai`, `faucet.citrate.ai`, - `coordinator.citrate.ai`, `explorer.citrate.ai`, `docs.citrate.ai`, `membership.citrate.ai` and - `auth.citrate.ai` are shared by everyone. Stay under **5 requests per second** and **10,000 - requests per day** per host, never run volumetric or amplification tests against them, and - demonstrate denial-of-service findings against a local build instead. `OWNER TO FILL`: confirm or - change these limits. -- **No social engineering, phishing or physical attacks**, and no attacks on third-party services - (GitHub, Vercel, npm, PyPI, DNS providers) or on other users. -- **Keep it private** until the issue is fixed and disclosed under the coordinated-disclosure window. - -## Safe harbor - -If you follow these rules in good faith, we will treat your research as authorized, we will not -pursue or support legal action against you for it, and we will not ask a third party to do so. If a -third party brings an action against you for research done under this program, we will make it known -that you acted with our authorization. If you are unsure whether something is allowed, ask at -security@citrate.ai before you do it. `OWNER TO FILL`: counsel to confirm this wording. - -## In scope - -| Asset | What we want | -|---|---| -| `citrate-chain`: node, consensus, sequencer and mempool, execution (EVM, LVM, precompiles), storage, P2P, JSON-RPC and MCP API | Consensus safety and liveness, remote node crash or halt, state or balance corruption, signature or replay bypass, RPC auth bypass | -| `citrate-chain/contracts`: contracts **with code** on chain 40204 (see the [address page](https://docs.citrate.ai/chain/addresses)) | Unauthorized transfer, mint or burn, governance or role bypass, stuck funds, broken accounting | -| Account abstraction (Keyring) stack and `citrate-bundler` | Unauthorized user operations, sponsorship abuse, signature bypass | -| `citrate-identity` (OIDC, VERI) | Authentication or authorization bypass, token forgery, access to another person's verification data | -| `citrate-sdk-js`, `citrate-sdk-python` | Key or secret disclosure, signing the wrong thing, chain-id or replay mistakes | -| `citrate-core` (desktop full node), `citrate-agent-runtime`, `nist-agent`, `citrate-quorum` | Key extraction, sandbox or capability escape, HIC approval bypass | -| `citrate-compute-pool` (coordinator and worker), `citrate-inference-gateway` free routes | Escrow or payout theft, job hijack, coordinator auth bypass | -| `citrate-comms`, `citrate-memories` | Cross-tenant or cross-member access, relay confidentiality or integrity breaks | -| `citrate-explorer` | RPC allow-list bypass, stored XSS, auth bypass | -| Public web hosts listed under "Load limits" | Auth bypass, XSS, CSRF with real impact, secret exposure | - -Other public first-party repositories are in scope for code findings at the tier set in their -`AUDIT_TIER.md`. Forks of third-party code (for example the `chains` fork) are out of scope. - -## Not deployed or not running (out of scope for live impact) - -These exist in code or in the design but are not live. A report that they are missing is a duplicate. -A code-level flaw in them is accepted as a low-priority design finding, not a live exploit. - -| Surface | Status | -|---|---| -| Checkpoint finality (committee of 100, quorum 67) | Specified, not running. Confirmation is probabilistic (see `verification/claims.json` in citrate-chain) | -| Stake-gated proposer eligibility and a multi-producer validator set | Staged: off by default, enabled when a validator registry is configured. The testnet runs a single block producer operated by Citrate | -| Inference gateway paid routes (x402 and API-key metering) | Not deployed | -| Membership x402 endpoint | Not configured; not deployed | -| SALT bridge | Specified, not deployed | -| The 19 governance and cooperative contracts with no code on 40204 (AnchorRegistry, MeetingRegistry, GovernanceTemplateRegistry, GovernanceProtocolFactory, PolicyBinding, CapabilityGrant, VoteAllowance, Sortition, PatronageLedger, ModelCooperative, FacilitySBTImpl, NetworkSBTImpl, FacilitySBT, NetworkSBT, CitrateCooperativeFactory, CoopDeployer, CoopMembershipSBT, ContributionRewardPool, CoopGovernor) | In the address book, not deployed | -| Zero-knowledge compute tier | Research preview; not a production guarantee | -| TEE compute tier | Inert on 40204: no TEE oracle is registered with `ComputeVerifier` (`teeOracleCount()` is 0) | -| Passkey-only accounts | Not yet available on 40204 | - -## Out of scope - -- Everything listed in `SECURITY.md` under "Out of scope". -- Findings that need a compromised operator key, a malicious block producer acting alone on testnet - with no effect beyond what the single-producer topology already implies, or physical access. -- Missing security headers, banner or version disclosure, clickjacking on pages with no sensitive - action, SPF/DMARC settings, and rate limits on non-sensitive endpoints, unless you show real impact. -- Issues in dependencies that are already public upstream (report them upstream). -- Anything already on the known-issues list below. -- Automated scanner output without a working proof of concept. - -## Known issues - -Published per finding once fixed. `OWNER TO FILL`. - -## Rewards - -`OWNER TO FILL`. Severity is graded under the CIT-SEV rubric used by our audits, by the impact the -same flaw would have on mainnet. - -| Severity | Examples | Reward | -|---|---|---| -| Critical | Consensus safety break, unauthorized spend from any account, network-wide halt, key disclosure at scale | `OWNER TO FILL` | -| High | Theft or freeze of funds in a deployed contract, auth bypass on identity, cross-tenant data access | `OWNER TO FILL` | -| Medium | Single-node crash, limited griefing, privilege escalation with preconditions | `OWNER TO FILL` | -| Low | Hardening gaps with a concrete but limited impact | `OWNER TO FILL` | -| Documentation | A public claim that does not match the code or the live chain | `OWNER TO FILL` (recognition or swag) | - -Payment method, currency, KYC requirements for payout, and the eligibility rules for employees and -contractors are also `OWNER TO FILL`. - -## Contact - -security@citrate.ai diff --git a/scripts/ci/public_truth.py b/scripts/ci/public_truth.py index 76470a7..d408582 100644 --- a/scripts/ci/public_truth.py +++ b/scripts/ci/public_truth.py @@ -93,7 +93,10 @@ def main() -> int: bounty = root / "BOUNTY.md" sec = root / "SECURITY.md" if not bounty.exists(): - errs.append("BOUNTY.md is missing") + # No bounty policy is published yet: nothing may point readers at one. + for f in files: + if re.search(r"BOUNTY\.md", f.read_text(errors="replace")): + errs.append(f"{f.relative_to(root)} links BOUNTY.md, which does not exist yet (bounty policy: coming soon)") else: b = bounty.read_text() for section in ("## In scope", "## Not deployed or not running", "## Safe harbor", "## Known issues", "## Rewards"):