Security

Reporting a vulnerability

Please report vulnerabilities privately, not in public issues:

- GitHub: _Security → Report a vulnerability_ on this repository (private advisory), or - email **hello@prokura.network** with "SECURITY" in the subject.

Include what you found, how to reproduce it, and the impact you expect. We acknowledge reports within 3 working days and keep you informed until a fix is released. Please give us 90 days, or until a fix is out, before disclosing publicly. We do not run a bug bounty yet.

Supported versions

The project is pre-release. Only the latest commit on `main` receives security fixes; there are no maintained release branches yet.

What the system does and does not do

- **No funds are held.** Payments execute on regulated rails (Mollie, and for x402 a licensed partner holding a pre-funded, capped balance). The software never holds money, stablecoins or wallet keys. - **No model in the decision.** The policy gate (`packages/gate`) is a pure, deterministic function: no I/O, no clock, no randomness, no LLM. Every decision can be re-run offline from its evidence record. - **No identity issuance, KYC, guarantees or custody.** Principals bind existing keys; the credential proves delegated authority, not identity.

Key custody boundaries

| Key / secret                                                        | Held by                                                                                       | Never reaches                                        |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Agent Ed25519 key (signs intents, TAP requests, AP2 authorizations) | The agent runtime (MCP server, browser agent, SDK user)                                       | The API, the approval app, merchants                 |
| Principal signing key (issues credentials, status lists)            | Dev: derived from a local key file on the API host. Staging/prod: KMS (not implemented in M2) | Agents, merchants, evidence exports                  |
| Evidence server key (signs records, Nachweis exports, daily roots)  | The API host (dev: derived from the same local key file)                                      | Agents                                               |
| Approver passkeys                                                   | The approver's device (platform authenticator)                                                | Everyone: only public keys and assertions are stored |
| Payment rail credentials (Mollie API key, x402 partner API key)     | The API process environment                                                                   | Agents, MCP tool results, evidence, logs             |
| Tenant API keys                                                     | The client; the API stores SHA-256 hashes only                                                | Logs, evidence                                       |
| Demo session tokens (`pks_…`, read-only, one mandate)               | The visitor's browser; the API stores SHA-256 hashes only                                     | Other mandates, any write endpoint                   |

What is public and what is not

The public demo on prokura.network ([docs/public-api.md](docs/public-api.md)) exposes some data without any key. This is the complete list:

| Public (no key)                                                                                                                                                                         | Not public                                                                                       |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Demo-tenant feed: record hash, prev hash, kind, decision, reason codes, rail id, time, agent DID, self-declared agent name, currency, amount bucket, per-mandate sequence and prev hash | Payees, exact amounts, intents, references, principals and their names, mandate ids, credentials |
| Statistics: decision counts of the demo tenant and the breach counter over the whole chain                                                                                              | Any record of any other tenant (they never enter the feed or the decision counts)                |
| Server public key, daily roots and hourly checkpoints (hashes only)                                                                                                                     | Rail credentials, private keys, API keys, session tokens                                         |
| Stateless Nachweis verification of a file the caller already has                                                                                                                        | Nothing is stored or fetched for it                                                              |
| Approval details, behind an unguessable 15-minute capability link (as before)                                                                                                           |                                                                                                  |

A session token reveals only its own mandate's full records, Nachweis and proofs, and only until the mandate expires plus 24 hours. CORS is open to the website origins on the public and session endpoints only.

Threat model (summary)

| Threat                                         | Mitigation                                                                                                                                                                                                                                                  |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prompt-injected or misbehaving agent           | The gate, not the model, decides; payee allow-lists, purposes, rails, per-payment and period caps, four-eyes step-up with a passkey. Agents cannot raise their own limits outside development pre-approval.                                                 |
| Replayed intent                                | Unique nonce per agent and a ±5-minute timestamp window, enforced by the gate; replayed TAP requests are refused by nonce.                                                                                                                                  |
| Stolen agent key                               | Revocation via the Bitstring Status List (checked on every decision), caps and four-eyes bound the damage.                                                                                                                                                  |
| Stolen tenant API key                          | Hashed at rest; scoped to one tenant's principals and mandates; rate-limited. Step-up payments still need the principal's passkey.                                                                                                                          |
| Leaked approval link                           | Links expire after 15 minutes and are single-use; approving needs a passkey registered to the principal; the WebAuthn challenge binds the assertion to one decision and intent.                                                                             |
| Tampered or rewritten evidence (insider)       | Records are append-only (database triggers), hash-chained and signed; signed daily Merkle roots let third parties detect rewrites; Nachweis exports verify offline without trusting the server.                                                             |
| Server-side request forgery via x402 resources | The resource host must match an allow-listed payee domain; plain HTTP and private addresses are refused unless explicitly enabled for development.                                                                                                          |
| Merchant asking more than authorised (x402)    | The API re-reads the 402 challenge itself and refuses if the amount differs from the signed intent.                                                                                                                                                         |
| Abuse of the public demo                       | Demo hard limits in code (EUR 100 per payment, EUR 150 per day, 10-minute mandates, demo store payees only, test-mode rails re-checked at payment time), 5 issuances per client per hour counted in the database, unsigned grants only for the demo tenant. |
| Forged passkey grant or cross-site approval    | Issuance needs an assertion over a single-use, core-issued challenge bound to tenant, principal and purpose; origins and RP IDs come from allowlists; evidence records the RP and origin actually used.                                                     |
| Undetected rail call without authority         | The breach counter (`/v1/public/stats`) recomputes from the chain every settled rail call that lacks an allow or a completed approval; `verifyNachweis` flags the same inside one export.                                                                   |
| Card data exposure                             | No PAN or card data anywhere; card entry happens on the rail's hosted checkout. PCI scope: none.                                                                                                                                                            |

Known limitations (pre-release)

- Principal and evidence keys use development custody (a local key file); KMS integration is planned before any production use. - Decisions are serialised per mandate inside one API process; running several instances needs a database lock. - TAP verification in the example merchant keeps its nonce cache in memory. - DNS rebinding is not mitigated for x402 resource fetches beyond the hostname checks. - A physical-phone passkey approval has been tested manually only; CI uses a virtual authenticator over HTTPS.

Data residency

Production runs on a server in Germany, next to the core. Demo evidence names the principal with an opaque session id.

Disclosure

The process in the document above is the process. Write to security@prokura.network. There is no bounty yet. Arena terms, when a lawyer has filled the TODO markers, are a separate page.