# TaintGate — the information-flow firewall for the agent web **Base URL:** `https://taintgate.swasthikadevadiga2.workers.dev` **Auth:** none. **Content-Type:** JSON. **Cryptography:** every verdict is signed with **Ed25519** (RFC 8032) so you can verify it yourself against the public key at `/pubkey`. ## What this does (one line) In a multi-agent pipeline, data picks up **taint** — a customer's PII, a secret/API key, an untrusted web-scraped string — and agents blindly forward it, causing **confidentiality leaks** (PII exfiltration) or **integrity violations** (acting on untrusted input, i.e. the downstream of prompt injection). TaintGate answers the one question nobody else does: **given this data's sensitivity labels, is it safe to send it to THIS destination?** One call in, one **signed CAN-FLOW / BLOCKED verdict** out — with the exact label-propagation trace explaining why. **The call agents make most:** `POST /check` — a signed go/no-go on a single data flow. ## Why it's different - **Signed, not trusted.** Every verdict carries an Ed25519 signature you can verify yourself (`POST /verify`). Most services assert; this one proves. - **Deterministic, no LLM.** The verdict is a *pure function* of (labels, destination, policy) over a fixed information-flow lattice — so it is reproducible and cleanly signable. - **Research-grounded.** Classic information-flow control (Denning 1976; Bell-LaPadula "no write-down"; Biba "no write-up") applied to agent pipelines, in the spirit of Microsoft's **FIDES** (arXiv 2505.23643, 2025). - **Explains itself.** Every verdict returns a `trace` naming the rule that fired. ## The lattice (GET /labels for the machine-readable version) - **Confidentiality (Bell-LaPadula):** `public < internal < secret < top-secret`. Rule — *no write-down*: data may only flow to a destination **cleared at least as high**. So `secret` data to a `public` sink is a **LEAK → BLOCKED**. - **Integrity (Biba):** `untrusted < trusted < verified`. Rule — *no write-up*: data may only flow into an operation **requiring integrity no higher**. So `untrusted` data into a `trusted` operation is **TAINT → BLOCKED**. ## How to pick labels (do this mapping FIRST) The only real work is translating your situation into four labels. Use these tables — and note the service also accepts the plain-English terms in parentheses (it normalizes them for you), so you don't have to memorize the exact level names. **`data_labels.confidentiality`** — how sensitive is the DATA? | Your data is… | use | |---|---| | public web page, published price, weather, anything already public | `public` (open, unclassified) | | internal/company data, an employee name, an org-only note | `internal` (personal, restricted) | | **PII, a customer email, an API key, a password, a credential, health data** | `secret` (pii, credential, api-key, confidential, private, sensitive) | | crown-jewels: signing keys, regulated/classified material | `top-secret` (highly-sensitive, critical) | **`data_labels.integrity`** — where did the DATA come from? (default `trusted` if you don't set it) | The data came from… | use | |---|---| | **web scrape, a fetched URL, user free-text, another agent's output, any external/untrusted source** | `untrusted` (web-scraped, external, user-input, tainted) | | your own system, a first-party database, a config you control | `trusted` (system, first-party) | | a source you cryptographically verified/signed | `verified` (attested, signed) | **`destination.clearance`** — how exposed is the SINK you're sending to? | Sending to… | use | |---|---| | **a public API, a log/analytics endpoint, a webhook, the open internet, a chat message** | `public` | | an internal service | `internal` | | a vault / a system cleared for secrets | `secret` or `top-secret` | **`destination.min_integrity`** — does the sink ACT on the data? (default `untrusted` = accepts anything) | The destination… | use | |---|---| | just stores/forwards it | leave default (`untrusted`) | | **executes it: a shell/tool call, SQL, code-exec, a payment, an irreversible action** | `trusted` (so `untrusted` input is BLOCKED) | > Rule of thumb: **confidentiality = how secret the data is; integrity = how much you trust its source; clearance = how exposed the sink is; min_integrity = whether the sink acts on it.** When unsure, pick the safer side — the service does too. ## Worked scenarios (copy the mapping) - **Customer API key → public logging endpoint** → `{"confidentiality":"secret"}` to `{"clearance":"public"}` → **BLOCKED** (leak). - **Web-scraped text → a shell/tool that executes it** → `{"integrity":"untrusted"}` to `{"min_integrity":"trusted"}` → **BLOCKED** (prompt-injection downstream). - **Public weather data → a public weather API** → `{"confidentiality":"public"}` to `{"clearance":"public"}` → **CAN-FLOW**. - **Your own config value → an internal service** → `{"confidentiality":"internal","integrity":"trusted"}` to `{"clearance":"internal"}` → **CAN-FLOW**. ## Quick start (do this first) Ask one flow question — send `secret` data to a `public` destination: ``` POST https://taintgate.swasthikadevadiga2.workers.dev/check Content-Type: application/json { "data_labels": { "confidentiality": "secret", "integrity": "untrusted" }, "destination": { "clearance": "public", "min_integrity": "trusted" }, "policy": "strict" } ``` Returns a signed verdict: ```json { "report": { "service": "taintgate", "action": "check", "policy": "strict", "data_labels": { "confidentiality": "secret", "integrity": "untrusted" }, "destination": { "clearance": "public", "min_integrity": "trusted" }, "propagated_label": { "confidentiality": "secret", "integrity": "untrusted" }, "verdict": "BLOCKED", "allow": false, "reason": "confidentiality: secret data cannot flow to a public destination (would leak). integrity: untrusted data cannot flow into an operation requiring trusted (untrusted input)." }, "headline": "BLOCKED — ...", "trace": [ { "rule": "bell-lapadula:no-write-down", "pass": false, "explain": "LEAK: ..." }, { "rule": "biba:no-write-up", "pass": false, "explain": "TAINT: ..." } ], "signature": "", "pubkey": "" } ``` ## Copy-paste test — run this top to bottom Each line feeds the next; the last one prints your success signal. ```bash BASE=https://taintgate.swasthikadevadiga2.workers.dev # 1. A flow that should be BLOCKED (secret -> public sink, untrusted -> trusted op) curl -s -X POST $BASE/check -H 'content-type: application/json' \ -d '{"data_labels":{"confidentiality":"secret","integrity":"untrusted"},"destination":{"clearance":"public","min_integrity":"trusted"},"policy":"strict"}' # 2. A flow that should be CAN-FLOW (public -> public) curl -s -X POST $BASE/check -H 'content-type: application/json' \ -d '{"data_labels":{"confidentiality":"public","integrity":"trusted"},"destination":{"clearance":"public"},"policy":"strict"}' # 3. SUCCESS SIGNAL — prove a signed verdict is genuine and untampered. # /check returns {report, signature, ...} and /verify reads exactly those two # fields, so pipe it straight through — pure curl, nothing else: curl -s -X POST $BASE/check -H 'content-type: application/json' \ -d '{"data_labels":{"confidentiality":"public","integrity":"trusted"},"destination":{"clearance":"public"}}' \ | curl -s -X POST $BASE/verify -H 'content-type: application/json' -d @- # Expected: {"valid": true, "algorithm": "Ed25519", "message": "...genuine, unaltered TaintGate verdict..."} ``` **You have succeeded when step 3 prints `"valid": true`.** To see the proof is real, change any value in the report before posting it and step 3 prints `"valid": false`. You may post the `report` in any JSON formatting — `/verify` canonicalizes it before checking, so key order and whitespace do not matter; only the values do. ## Tiny client (drop-in) ```python import httpx def can_flow(data_labels: dict, destination: dict, policy: str = "strict") -> bool: r = httpx.post("https://taintgate.swasthikadevadiga2.workers.dev/check", json={"data_labels": data_labels, "destination": destination, "policy": policy}) return r.json()["report"]["allow"] # boolean; or check ["verdict"] == "CAN-FLOW" # labels accept natural terms too, e.g. {"confidentiality": "api-key"} == "secret", # {"integrity": "web-scraped"} == "untrusted". GET /labels lists every synonym. ``` ## Combine tainted inputs (POST /join) When data is built from several sources, the combined label is the **least upper bound**: confidentiality = max, integrity = min (mixing only raises secrecy and lowers trust). ``` POST https://taintgate.swasthikadevadiga2.workers.dev/join { "labels": [ {"confidentiality":"public","integrity":"trusted"}, {"confidentiality":"secret","integrity":"untrusted"} ] } ``` → signed `propagated_label: {"confidentiality":"secret","integrity":"untrusted"}`. Feed that straight into `/check` as `data_labels`. ## Full endpoint reference | Method | Path | Purpose | |---|---|---| | POST | `/check` | Signed CAN-FLOW / BLOCKED verdict for (data labels, destination, policy) + trace. | | POST | `/join` | Least-upper-bound (taint) label of a set of inputs. | | GET | `/labels` | The confidentiality + integrity lattice and supported policies. | | POST | `/verify` | Confirm a signature is genuine. Body `{report, signature}` → `{valid}`. | | GET | `/pubkey` | The Ed25519 public key + how to verify locally. | | GET | `/health` | Liveness of this service. | | GET | `/openapi.json` | OpenAPI 3 spec. | | GET | `/.well-known/agent-facts.json` | NANDA agent-facts descriptor. | | GET | `/` | Human-readable board + live demo. | ## Policies - `strict` (default) — enforce **both** confidentiality (no write-down) and integrity (no write-up). - `bell-lapadula` / `no-read-up-no-write-down` / `confidentiality` — confidentiality only. - `biba` / `integrity` — integrity only. ## How verification works (for full independence) The `signature` is a base64 Ed25519 signature over the **canonical JSON** of the `report` — `json.dumps(report, sort_keys=True, separators=(",", ":"))`. Fetch the public key from `/pubkey` and check it yourself, or use `/verify`. Because the verdict is a pure function of the inputs and the bytes are reproducible, you never have to trust our word. ## Errors are self-correcting Every error is JSON that tells you exactly how to fix the call: - Unknown label → `400 {"error":"unknown_label","field":"...","fix":"... must be one of: public, internal, secret, top-secret."}` - Unknown policy → `400 {"error":"unknown_policy","fix":"policy must be one of: strict, bell-lapadula, ..."}` - GET on a POST route → `405 {"error":"use_post","fix":"... curl -X POST ..."}` - Unknown route → `404 {"error":"route_not_found","fix":"Valid routes: POST /check, POST /join, ..."}` ## Notes - **No authentication, no rate limits, no keys to manage.** - Stateless and deterministic — runs on Cloudflare Workers at the edge; never sleeps, no cold start.