Threat model
Design assumptions and adversarial analysis; an internal document, not an independent audit.
This manual targets 0.1.0-beta.3. Read the source and availability policy before installing. Historical reports retain their original scope.
Date: 2026-08-29
Version modeled: 0.1.0-beta.1
Executive summary
PPOps deliberately removes spending authority from the reconciler, so its
highest risks are confidentiality and payment-integrity failures rather than
direct fund withdrawal: theft of the viewing or merchant-signing keys, false or
withheld state from RPC/PPOI dependencies, compromise of the large pinned
RAILGUN dependency graph, and exposure or substitution of recovery bundles. The
Bearer API is acceptable only under the assumed single-merchant local/private
deployment; direct Internet exposure would materially raise authentication and
denial-of-service risk. Evidence anchors: src/railgun/engine.ts
(RailgunViewOnlyEngine.start), src/runtime.ts (PPOpsRuntime.create) and
src/api/app.ts (createApiApp).
Scope and assumptions
In scope:
- Production runtime under
src/, including the Hono API, CLI, configuration, SQLite repositories, EIP-712 descriptor, view-only RAILGUN engine/scanner, reconciliation, webhook delivery, runtime lock and recovery commands. - Deployment controls in
Dockerfileanddocker-compose.yml. - Build and dependency boundary in
package.json,package-lock.jsonand.github/workflows/ci.yml. - Tests and gate scripts only as evidence; they are not modeled as production entry points.
- The independently executed
tools/ppops-payerharness at the shared repository, supply-chain and signed-request boundary only. Its payer host and spending wallet remain outside the merchant-runtime trust domain.
Out of scope:
- Correctness of RAILGUN contracts, circuits, cryptography, PPOI protocol and upstream nodes beyond how PPOps consumes them.
- A globally observing timing adversary, payer endpoint compromise, voluntary merchant disclosure, post-payment spending behavior and operating-system or hypervisor compromise.
- Payer mnemonic custody, public self-signer custody and operating-system state on the separate payer host.
- Hardhat and the files under
patches/; they reproduce a controlled upstream gate and are not runtime dependencies.
Assumptions used because no correction was received after the context check-in:
- One merchant controls each instance; there is no tenant boundary inside PPOps.
- The API binds to loopback, or Docker publishes it only on host loopback. It is not directly exposed to the Internet.
- The merchant operator controls configuration and secret-file paths. RPC/PPOI services and blockchain inputs remain externally trustable only to the degree documented below.
- The merchant backend can protect the single administrative Bearer token and verify webhook replay windows/event IDs.
- Host filesystem access controls and disk/backup protection exist outside the application; PPOps SQLite is not application-level encrypted.
Open questions that would materially change risk ranking:
- Will any pilot put PPOps behind a public reverse proxy, share an instance between merchants, or accept requests from untrusted browser clients?
- Will the pilot use mainnet/value-bearing funds, and what payment volume and maximum tolerable confirmation delay apply?
- Are host disks and off-host backups encrypted and access-audited, and can the merchant signer be moved to a KMS/HSM after beta?
If the first answer is yes, TM-002 and TM-008 become high priority and this beta profile must add proxy authentication/TLS, per-client authorization, rate limits and abuse monitoring before exposure.
System model
Primary components
- CLI and daemon: parses a fixed command/option set, loads configuration and
secrets, acquires a single-process lock, starts the engine and Hono server, and
schedules scans. Evidence:
src/cli.ts(main,serve,init) andsrc/runtime.ts(PPOpsRuntime.create). - Authenticated API: creates and reads intents, exposes settlement/event
state and verifies descriptors. It uses secure headers, a 64 KiB body limit,
Zod schemas and constant-time Bearer-token digest comparison. Evidence:
src/api/app.ts(createApiApp) andsrc/security/auth.ts(bearerTokenMatches). - Intent and descriptor engine: creates random references/nonces, signs
EIP-712 descriptors with an independent merchant identity key and verifies
them against an expected signer plus instance policy. Evidence:
src/intents/service.ts(IntentService.create,verifyDescriptor) andsrc/security/descriptor.ts. - View-only RAILGUN engine/scanner: imports a shareable viewing key, rejects
full-wallet/signing access, synchronizes TXOs and reduces strict encrypted
memos to opaque references. Evidence:
src/railgun/engine.ts(RailgunViewOnlyEngine) andsrc/railgun/scanner.ts(RailgunScanner). - Reconciler and stores: persists commercial metadata/projections/outbox in
SQLite and encrypted RAILGUN state in LevelDOWN. It credits only matched,
finalized and PPOI-spendable settlements. Evidence:
src/db/database.ts,src/reconciliation/service.tsandsrc/reconciliation/projection.ts. - Webhook worker: sends persisted event JSON to one operator-configured URL
over HTTPS with HMAC, timestamp and event ID, disabling redirects. Evidence:
src/events/webhook.ts(WebhookDeliveryService). - Recovery tooling: creates offline checksum-inventoried backups, optionally
containing secret values, and restores only after profile/fingerprint checks.
Evidence:
src/backup.ts(createBackup,restoreBackup).
Data flows and trust boundaries
- Merchant backend → API: commercial reference, atomic amount, expiry and
Bearer credential cross local HTTP. All operational routes require one Bearer
token; request bodies are limited and Zod-validated. PPOps has bounded
in-memory per-source limits, but no application TLS, per-client roles or
distributed rate limiter. Evidence:
src/api/app.tsandsrc/security/rate-limit.ts. - API → intent engine → SQLite: validated commercial metadata crosses an
in-process boundary and is written through parameterized SQLite statements.
File/directory modes restrict other local users, but commercial metadata is
plaintext at the application layer. Evidence:
src/intents/service.tsandsrc/db/database.ts(insertIntent). - Private files → runtime: configuration, wallet state, viewing
capability, API token, merchant signing key, RAILGUN DB key and optional
webhook key cross the local filesystem boundary. They must be bounded regular
files owned by the current user without group/other access on POSIX. PPOps
rejects symlinks, opens with
O_NOFOLLOWwhere available and rechecks the opened descriptor identity before parsing; secret values are validated and never API-returned. Evidence:src/security/private-file.ts,src/security/secrets.tsandsrc/runtime.ts. - PPOps → payer: an EIP-712 descriptor and encrypted-memo reference leave
the merchant host through the public checkout/request endpoint or a local
request file. Authenticity depends on the payer obtaining the expected signer
independently. The reference payer verifies every duplicated request field,
serializes wallet access with a local runtime lock and reserves an intent in
an owner-only write-ahead submission journal before broadcasting. Gate A
records a precomputed signed transaction hash; Gate B records the bounded fee
identity, payer, exact encrypted-submission quote fingerprint and nullifiers
before Waku submission. After proof generation, a configured RPC majority
must simulate the exact populated calldata before any encrypted-request or
journal effect. Before simulation, a read-only admission check rejects inputs
held by another active journal record; the later reservation rechecks the same
policy. A quote may rotate only when Broadcaster, token and fee rate remain
identical. A Waku-returned hash remains untrusted metadata until the full
payer wallet derives the canonical transaction hash from those nullifiers.
Recovery precedes every explicit ambiguity retry; a retry must preserve the
signed request, payer and complete nullifier set, exclude all previously
attempted Broadcaster identities and remain within a three-retry cap. A
different intent cannot reserve any unresolved nullifier. Evidence:
src/security/descriptor.ts,tools/ppops-payer/src/request.ts,tools/ppops-payer/src/security/runtime-lock.tsandtools/ppops-payer/src/security/submission-journal.ts. - Payer → RAILGUN/blockchain: the payer submits the private transfer. Public
chain artifacts are outside PPOps control; the privacy gate demonstrates that
the plaintext reference/memo is absent from the V2 commitment leaf. Evidence:
scripts/encrypted-memo-leaf-gate.ts,artifacts/privacy-report.jsonandtools/ppops-payer/src/railgun/broadcaster-transfer.ts. Broadcaster mode also crosses operator-pinned fee-signer, DNS/Waku and selected-Broadcaster trust boundaries; it does not claim network-layer anonymity or relay availability. An earlier value-bearing lineage reached two selected identities without a usable response and remains reserved. A later isolated lineage resolved a canonical transaction and completed Gate B on its first submission; that success does not remove these metadata or availability dependencies. - RAILGUN runtime ↔ RPC/PPOI: encrypted chain history, receipts, block tags,
PPOI statuses and timing cross outbound HTTPS/JSON-RPC and SDK-specific
protocols. PPOps validates configured chain/deployment identity and fails
payment credit closed unless finality and spendability agree. Receipt, block
and finalized-height reads use a majority quorum across configured RPCs;
PPOps still does not verify cryptographic RPC proofs and inherits PPOI trust.
Evidence:
src/railgun/engine.ts,src/railgun/scanner.tsandsrc/railgun/rpc-quorum.ts. - Reconciler → SQLite outbox → merchant webhook: payment state crosses local
SQLite and then outbound HTTPS. HMAC authenticates integrity/origin but does
not hide payload from the webhook receiver; at-least-once delivery requires
receiver-side timestamp and event-ID dedupe. Evidence:
src/events/webhook.tsandsrc/db/database.ts(outbox_events). - Operator → CLI/config/backup: trusted operator arguments and JSON select
local paths, endpoints and recovery targets. Schemas, non-overlapping path
checks, runtime locking, no-overwrite defaults, path inventory and move-aside
restore reduce accidents; a malicious operator already controls the instance.
Evidence:
src/config.ts,src/cli.ts,src/security/runtime-lock.tsandsrc/backup.ts. - Developers/registry → build: npm packages, GitHub Actions and the Docker
base image cross the software-supply-chain boundary. Versions are locked and
CI is read-only for verification; Actions and the base image are immutable
pinned, high/critical advisories fail CI and a CycloneDX SBOM is retained. The
pinned RAILGUN tree still carries moderate/low advisories and a large legacy
surface. Evidence:
package-lock.json,.github/workflows/ci.yml,Dockerfileanddocs/OPERATIONAL-PROFILE.md.
Diagram
flowchart LR
subgraph PayerZone
Payer["Payer wallet"]
end
subgraph MerchantHost
Backend["Merchant backend"]
API["Authenticated API"]
Core["Intent reconciler"]
Scanner["View only scanner"]
SQL["SQLite outbox"]
Level["Encrypted LevelDB"]
Secrets["Secret files"]
end
subgraph ExternalServices
Chain["RAILGUN chain"]
RPC["RPC provider"]
POI["PPOI service"]
Hook["Webhook receiver"]
end
Backend -->|Bearer HTTP| API
API --> Core
Secrets --> Core
Secrets --> Scanner
Core -->|Signed descriptor| Payer
Payer -->|Private transfer| Chain
Chain --> RPC
RPC --> Scanner
POI --> Scanner
Scanner --> Level
Scanner --> Core
Core --> SQL
SQL -->|HTTPS HMAC| Hook
Assets and security objectives
| Asset | Why it matters | Security objective (C/I/A) |
|---|---|---|
| RAILGUN shareable viewing key | Reveals balances, history, counterpart-relevant memos and the receiver payment graph; cannot spend | C, I |
| Merchant EIP-712 signing key | Authorizes payer-facing descriptors; theft can redirect future payments | C, I |
| API token | Grants administrative read/create access to all local intent and settlement metadata | C, I |
| Webhook HMAC key | Authenticates confirmation events to the merchant backend | C, I |
| RAILGUN DB encryption key and LevelDB | Protect and persist the view-only wallet/scan state | C, I, A |
| Commercial references in SQLite | Link private settlements to invoices/customers and are the primary metadata PPOps promises not to publish | C, I, A |
| Settlement/projection/outbox state | Controls whether orders are considered paid and whether events are delivered once per transition | I, A |
| Expected merchant signer distribution | Root of trust that prevents checkout/descriptor substitution | I |
| RPC/PPOI responses | Drive finality and spendability decisions and expose request timing to providers | C, I, A |
| Recovery bundles | Can contain all privacy and service-identity secrets except a RAILGUN spending key | C, I, A |
| Build artifacts and lockfile | Compromise can execute code with access to every runtime secret | I |
Attacker model
Capabilities
- A remote blockchain participant can send arbitrary incoming RAILGUN notes and strict-looking random PPOps memo references to the merchant receiver.
- RPC/PPOI providers can observe source IP/timing, omit or delay data, return inconsistent responses or become unavailable.
- A network attacker can target a mistakenly exposed API or webhook path; under the assumed deployment, the operational API is not directly Internet-routable.
- A local unprivileged user, malware, stolen backup recipient or compromised dependency may attempt to read files or execute within the PPOps process.
- A supply-chain attacker can target npm dependencies, mutable CI actions, downloaded RAILGUN artifacts or container bases.
Non-capabilities
- The ordinary remote attacker does not initially possess the API token, expected merchant signer, host filesystem access or operator-controlled configuration.
- PPOps never receives a RAILGUN mnemonic/spending key, so compromise of the intended process alone does not directly authorize spending.
- The model does not assume a break of EIP-712/ECDSA, RAILGUN encryption or hash primitives.
- The operator is trusted. An operator who deliberately changes keys, endpoints or binaries already controls the security outcome.
Entry points and attack surfaces
| Surface | How reached | Trust boundary | Notes | Evidence (repo path / symbol) |
|---|---|---|---|---|
| Health endpoints | GET /v1/live, /v1/ready, /v1/health |
local network → daemon | Unauthenticated, metadata-minimal; readiness fails on failed/stale scans | src/api/app.ts; src/api/server.ts |
| Public checkout | /pay/:id, /pay/:id/request.json |
payer → daemon | 128-bit unguessable intent ID, no commercial reference, CSP/no-store and rate limit | src/api/app.ts / checkoutIntent |
| Operational API | /v1/intents, /runtime, /settlements, /events, /outbox, /metrics |
backend/client → daemon | Single Bearer role; 64 KiB body cap; socket-address rate limits | src/api/app.ts; src/security/auth.ts; src/security/rate-limit.ts |
| Descriptor parser/verifier | API or descriptor-verify CLI |
payer/operator data → EIP-712 verifier | Strict Zod object and external expected signer | src/security/descriptor.ts |
| CLI option parser | Local process arguments | operator shell → privileged runtime | Fixed allowlist; rejects spending-key option | src/cli.ts / parseOptions, assertAllowed |
| Configuration JSON | --config file |
local filesystem → runtime | Private-file identity/owner/size checks, Zod schema, loopback/HTTPS/path policy | src/config.ts / loadConfig; src/security/private-file.ts |
| Secret files | Configured paths | local filesystem → runtime memory | Private-file identity/owner/size checks and kind-specific formats | src/security/secrets.ts / readSecret; src/security/private-file.ts |
| Wallet-state JSON | Persistent state file | local filesystem → SDK wallet loader | Strict schema, private-file mode, fingerprint and address checks | src/railgun/engine.ts / WalletStateSchema, loadOrCreateWallet |
| RAILGUN TXOs/memos | SDK scan | chain/SDK → reconciler | Positive ERC-20 and strict memo parser; RPC normalization concurrency capped | src/railgun/scanner.ts / scan, normalizeTXO |
| RPC finality/receipt reads | Configured JSON-RPC URLs | external providers → scanner | Majority receipt/block/finality quorum, conservative height and divergence failure; no cryptographic proof | src/railgun/rpc-quorum.ts; src/railgun/scanner.ts |
| PPOI statuses | RAILGUN SDK callbacks/state | external PPOI → eligibility logic | HTTPS outside loopback; preflight health; explicit bucket mapping; unknown fails closed | src/railgun/ppoi-preflight.ts; src/railgun/scanner.ts / bucketToPOIStatus |
| Artifact-store paths/data | RAILGUN SDK | dependency/network-derived SDK state → filesystem | Root-containment check and private mode | src/railgun/engine.ts / safeArtifactPath, artifactStore |
| Webhook response/redirect | Outbound fetch | configured receiver → outbox worker | Remote HTTPS, redirect disabled, bounded timeout/retries | src/events/webhook.ts |
| Mainnet evidence CLI | live API, RPC quorum, receiver stats, snapshot files | operator/API/RPC/receiver → release evidence | HTTPS-or-loopback endpoints, strict schemas/profile, fresh receipt/block quorum checks, streamed-body/pagination caps, keyed redacted fingerprints, fail-closed phase comparison and merchant-signed final report | src/pilot/mainnet-gate.ts; test/mainnet-gate.test.ts |
| Backup manifest/files | restore --input |
recovery media → local state/secrets | Strict inventory/path/digest/profile/fingerprint checks; checksums unauthenticated | src/backup.ts / verifyBackup, restoreBackup |
| npm/CI/container build | Install or CI run | registry/action/base image → executable | Lockfile, action/image pins, high/critical audit gate and SBOM; moderate/low advisories remain | package-lock.json; .github/workflows/ci.yml; Dockerfile |
Top abuse paths
- Steal the payment graph: compromise the merchant host or an unencrypted recovery bundle → read the shareable viewing key/LevelDB → decrypt historical receiver notes and correlate opaque references with local SQLite → disclose merchant payment history, even though spending remains impossible (TM-001, TM-006, TM-010).
- Redirect future payments: obtain the merchant EIP-712 signing key → sign a valid-looking descriptor with an attacker receiver → exploit a payer that trusts only the recovered key and not an independently pinned merchant profile → receive future payments (TM-003).
- Abuse an accidentally public API: discover a non-loopback deployment → steal/brute-force/recover the single Bearer token from another component → enumerate invoice-linked state or create fraudulent intents → cause metadata loss or payment confusion (TM-002).
- Manipulate settlement availability/integrity: operate or compromise a majority of the selected RPC providers or the PPOI path → omit receipts, delay finality or feed misleading spendability state → keep legitimate orders unpaid or subvert an inherited trust boundary (TM-004, TM-009).
- Exploit the dependency boundary: publish/compromise a transitively loaded RAILGUN/GraphQL/Web3 package or artifact path → obtain code execution in the daemon → read viewing/signing/API keys and alter SQLite events (TM-005).
- Substitute or corrupt a backup: gain write access to recovery media →
replace both files and the unauthenticated checksum manifest → restore stale
or attacker-chosen state, or steal a bundle created with
--include-secrets→ cause privacy disclosure and operational takeover (TM-006). - Replay merchant events: capture a valid HMAC webhook request at or after the receiving backend → replay it to a receiver that ignores timestamp and event-ID dedupe → duplicate fulfillment even though PPOps outbox state is idempotent (TM-007).
- Exhaust reconciliation capacity: send many decryptable notes with strict-looking unknown references or flood an exposed API with requests → force wallet decryption, RPC calls, SQLite growth or CPU/memory pressure → delay legitimate payment confirmation (TM-008).
- Trigger a semantic false state: exploit a newly changed TXO/PPOI field, settlement-identity collision or projection regression not covered by the pinned gate → cross the paid threshold incorrectly or emit a misleading reversal/confirmation (TM-009).
- Exploit ambiguous relay state: lose or forge a Broadcaster response after Waku submission → tempt the operator to create a fresh payment with inputs that may already be in flight → double-pay or permanently strand the local workflow. The payer instead reserves nullifiers before Waku, recovers first, permits only bounded conflicting same-nullifier variants through previously unattempted identities and then requires manual review. Before any reservation or Waku effect, the payer rejects cross-intent input overlap and a strict RPC majority must simulate the exact final populated calldata (TM-003, TM-004, TM-009).
Threat model table
| Threat ID | Threat source | Prerequisites | Threat action | Impact | Impacted assets | Existing controls (evidence) | Gaps | Recommended mitigations | Detection ideas | Likelihood | Impact severity | Priority |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| TM-001 | Local malware, host intruder, malicious backup reader | Read access to the merchant host, process memory or recovery material | Exfiltrate the viewing key and correlate RAILGUN history with SQLite | Full receiver payment-graph disclosure; no direct spend | Viewing key, LevelDB, commercial metadata | Owner-only bounded regular files; symlink rejection, no-follow open and opened-file identity recheck; separate encrypted RAILGUN DB key; no API/log return; view-only signature/full-wallet rejection (src/security/private-file.ts, src/security/secrets.ts, src/railgun/engine.ts) |
Process must hold the viewing key; no OS keyring/TEE; SQLite links remain plaintext | Require full-disk encryption, dedicated OS identity, restricted egress, secret-manager mounts and rotation/re-import procedure; consider isolating scanner in a least-privileged process | Alert on secret/config permission changes, unexpected outbound destinations, backup access and wallet fingerprint changes | Medium: host/backup compromise is realistic for self-hosted software, but requires local or supply-chain foothold | High: destroys the central privacy property for the receiver history | high |
| TM-002 | Remote/local network attacker or compromised merchant app | API is exposed or Bearer token is stolen | Enumerate state, create intents or exhaust API resources using the single administrator credential | Commercial metadata disclosure and payment-workflow manipulation | API token, intents, settlement/event data, availability | Loopback default and explicit allowRemote; constant-time Bearer comparison; body/header/time limits; in-process rate limits; Docker host-loopback publish (src/config.ts, src/api/app.ts, docker-compose.yml) |
No built-in TLS, token scopes, dual-token rotation or per-client audit identity; in-memory limits are per process | Keep loopback; if proxied, require TLS/mTLS or strong proxy auth, IP policy, distributed rate limits and request audit IDs | Alert on 401/413/429 rates, new source addresses, intent-create spikes and non-loopback binds | Low under stated local-only assumption; medium/high if public | High because the token exposes all merchant operational metadata | medium, conditional high if public |
| TM-003 | Host intruder, checkout attacker with stolen signer | Merchant EIP-712 signing key is obtained, or payer learns expected signer through the compromised channel | Sign a descriptor for an attacker-controlled recipient/profile | Theft of future payer funds and merchant impersonation | Merchant signing key, expected-signer distribution, payer funds | Independent key from RAILGUN wallet; strict EIP-712 schema; recovered and embedded signer must match; instance profile/expiry checked (src/security/descriptor.ts, src/intents/service.ts) |
Key is a plaintext file in process; trusted-signer distribution is outside repo; no rotation/key ID | Publish signer fingerprint over a separately authenticated channel; document rotation/revocation; move signer to KMS/HSM or offline signer before mainnet | Monitor unexpected signer/profile changes and descriptors not present in local intent DB | Medium: requires host/channel compromise, but signer is online for every intent | High: can redirect new payments even though existing RAILGUN funds remain safe | high |
| TM-004 | Malicious/failed RPC or PPOI operator; network observer | A majority of RPC sources or inherited PPOI path is compromised, or endpoints observe timing | Omit, delay or falsify receipt/finality/PPOI data and observe scan timing | Payment censorship/delay, privacy leakage and potentially unsafe state | Settlement integrity/availability, request-timing privacy | Exact chain/deployment check; Arbitrum finalized enforcement; majority RPC quorum; matched + FINALIZED + SPENDABLE gate; unknown PPOI fails closed; recent-finalized receipt recheck (src/config.ts, src/railgun/rpc-quorum.ts, src/railgun/scanner.ts) |
No cryptographic RPC proofs; PPOI is inherited trust; providers can correlate timing and a quorum can fail availability | Use administratively independent or self-hosted privacy-preserving RPCs, production PPOI and egress policy; investigate every divergence | Metadata-free ready/scan metrics, provider divergence failures, PPOI bucket age and confirmation latency | Medium: outages/inconsistency are common and provider observation is inherent | High: can censor fulfillment; integrity requires majority/PPOI compromise | high |
| TM-005 | npm/action/base-image/artifact supply-chain attacker | A vulnerable/replaced dependency path is reachable during install/build/runtime | Execute code or write outside intended artifact state, then read secrets/alter reconciliation | Complete application confidentiality/integrity compromise, but still no RAILGUN spending key | All process-held secrets/state, build outputs | Exact lock/pins; compatible security overrides; high/critical audit gate; CycloneDX SBOM; SHA/digest-pinned Actions/base image; artifact containment; non-root read-only resource-bounded container (package.json, src/railgun/engine.ts, Dockerfile, .github/workflows/ci.yml) |
6 moderate and 30 low findings in the legacy RAILGUN/Web3/BZZ tree; install scripts and the SDK remain in the secret-bearing process | Obtain upstream dependency pruning/fixes; isolate the SDK process/container if the maintenance cost is accepted; enforce runtime egress allowlist; rerun primitive gate on every upgrade | Dependency-diff alerts, lockfile review, image/SBOM scanning and unexpected-module/egress monitoring | Medium: exploit reachability is unproven, but network-facing SDK code and stale packages remain | High: runtime code execution reaches viewing/signer/API keys and payment state | high |
| TM-006 | Stolen media, malicious backup writer, mistaken operator | Access to a backup or restore input; secret bundle option increases impact | Read included keys or replace files and recompute the unsigned SHA-256 inventory | Privacy/identity takeover, rollback or corrupted reconciliation state | Recovery bundle, all service keys, SQLite/LevelDB availability/integrity | Offline runtime lock; new-output/no-overwrite defaults; strict inventory/path checks; profile and high-entropy key fingerprints; move-aside restore (src/backup.ts, src/security/runtime-lock.ts) |
Bundle is not encrypted/authenticated by PPOps; checksums do not resist malicious replacement; forced restore is multi-file, not globally atomic | Use external authenticated encryption and versioned immutable storage; require operator verification of signer/fingerprint through a second channel; test restores on isolated hosts | Audit backup creation/read/restore, alert on fingerprint/profile changes and retain pre-restore paths | Medium: backup mishandling is a common self-hosted failure mode | High when secrets are included; otherwise high integrity/availability impact | high |
| TM-007 | Network observer with endpoint access, compromised webhook receiver | Valid request is captured or receiver does not enforce replay policy | Replay a signed event or exploit at-least-once retry after receiver success/PPOps failure | Duplicate fulfillment or disclosure of operational amount/status graph to receiver | Webhook key, fulfillment integrity, event metadata | Remote HTTPS required; redirects rejected; HMAC covers timestamp/key ID/event ID/raw body; stable event IDs, persisted outbox and dead-letter-only replay endpoint; loopback pilot receiver persists dedupe IDs and the gate requires an identical replay (src/events/webhook.ts, src/db/database.ts, src/pilot/webhook-receiver.ts, src/pilot/mainnet-gate.ts) |
Production receiver verification, replay cache and dual-key acceptance remain outside repo; HMAC provides no confidentiality from receiver; operator records are needed to substantiate gate phases | Production receiver must enforce a timestamp window and durable event-ID dedupe; follow staged dual-key rotation in the runbook | Alert on duplicate event IDs, stale timestamps, signature failures, retry/dead-letter spikes | Medium: retries are normal and receiver mistakes are plausible | Medium: affects fulfillment and metadata, not spending authority | medium |
| TM-008 | Remote note spammer or client of an exposed API | Attacker can pay/send notes or reach API; RAILGUN scanning is resource intensive | Generate strict-looking memos, oversized/request bursts or slow connections to consume scan/RPC/DB resources | Delayed confirmation and daemon availability loss | Scanner/API/SQLite availability | 64 KiB bodies; server timeouts/header cap; in-process rate limits; 8-way normalization cap; one active scan; pagination caps; readiness/metrics; Compose CPU/memory/PID limits (src/api/app.ts, src/api/server.ts, src/railgun/scanner.ts, docker-compose.yml) |
No unmatched-settlement retention quota; note decryption cost is inherited; per-process rate limits need an edge control when scaled | Keep proxy limits if exposed, add retention after observing real volumes and maintain scan-lag/RSS/disk alerts | Monitor scan duration, unmatched/settlement growth, event-loop lag, RSS, disk and HTTP 413/429 | Medium: note spam is permissionless but costs the attacker; API risk depends on exposure | Medium: availability delay with fail-closed payment state | medium |
| TM-009 | Upstream SDK change, malicious chain data, implementation bug | A TXO/PPOI/finality shape changes or an edge case escapes tests | Cause identity collision, premature credit, regression or duplicate transition | Incorrect order fulfillment or missed/repeated confirmation | Settlement identity, projections, outbox integrity | Pinned versions; immutable identity comparison; SQLite uniqueness/transaction; orthogonal statuses; bigint; restart/partial/over/late/reorg/PPOI and RPC disagreement tests; property invariants; coverage gate; primitive gate and signed mainnet self-pilot report (src/reconciliation/service.ts, src/db/database.ts, test/, artifacts/mainnet-gate-report.json) |
Direct engine TXO API is not a stable high-level contract; the fresh Arbitrum evidence is one controlled self-pilot, not an independently operated long-lived fixture | Maintain a strict fixture, obtain an external pilot and rerun schema-contract/primitive/mainnet gates on SDK upgrades | Alert on identity collisions, paid→nonpaid transitions, conflicts and confirmation latency anomalies | Low-to-medium with pins and gates; rises on upgrade or unusual behavior | High: false confirmation can cause real-world fulfillment loss | high |
| TM-010 | Disk thief, local user, forensic snapshot or over-privileged backup agent | Access to SQLite/WAL or host snapshots without full process compromise | Read plaintext invoice/customer references and their settlement links | Commercial metadata disclosure without needing the viewing-key secret file | SQLite commercial metadata, event graph | Directory 0700, DB 0600, minimal logs/events and no public descriptor metadata (src/db/database.ts, src/events/event-factory.ts, artifacts/privacy-report.json) |
No application-level SQLite encryption; WAL/backups carry the same data | Require encrypted disks/snapshots, dedicated UID, restrictive backup ACLs and documented retention/deletion; consider SQLCipher only after explicit design/review | Audit file/snapshot access and unexpected backup copies; periodically run canary scan | Low-to-medium depending host/storage controls, which remain unconfirmed | High for merchant privacy, but does not reveal spending authority | medium |
Criticality calibration
- Critical: plausible compromise with no special operator mistake that either enables direct loss across deployments or silently subverts all payment decisions. Examples: a pre-auth remote-code-execution path in a publicly reachable default daemon; a dependency compromise distributed in an official image that exfiltrates all keys; accepting a RAILGUN spending key and exposing a remote spend path. No current threat is ranked critical under the local-only and no-spending-key assumptions.
- High: compromise of a central privacy/integrity asset or sustained false payment decisions requiring a realistic foothold. Examples: viewing-key exfiltration (TM-001), merchant-signer theft (TM-003), dependency code execution (TM-005), or provider manipulation/semantic false confirmation (TM-004/TM-009).
- Medium: bounded metadata, fulfillment or availability impact with existing controls or deployment-dependent prerequisites. Examples: replay at a poorly implemented webhook receiver (TM-007), note/API resource exhaustion (TM-008), local plaintext SQLite disclosure (TM-010), or API-token abuse while loopback remains enforced (TM-002).
- Low: small information or noisy operational effects with easy recovery.
Examples: learning only
ready/startingfrom health, rejected malformed descriptors, or failed unauthenticated requests that reveal no merchant data.
Focus paths for security review
| Path | Why it matters | Related Threat IDs |
|---|---|---|
src/railgun/engine.ts |
Holds viewing capability, artifact filesystem boundary and explicit no-spend assertions | TM-001, TM-005 |
src/railgun/scanner.ts |
Converts untrusted SDK/RPC/PPOI data into normalized payment candidates | TM-004, TM-008, TM-009 |
src/reconciliation/service.ts |
Determines matching, immutable identity and transactional state transitions | TM-004, TM-009 |
src/reconciliation/projection.ts |
Encodes the exact paid/partial/late eligibility invariant | TM-009 |
src/db/database.ts |
Stores plaintext commercial metadata and implements idempotency/outbox transactions | TM-007, TM-009, TM-010 |
src/security/descriptor.ts |
Defines the payer-facing signed trust envelope and parser | TM-003 |
src/intents/service.ts |
Couples signed descriptors to the configured merchant profile and local reference | TM-003, TM-009 |
src/security/secrets.ts |
Enforces formats/permissions for every runtime secret | TM-001, TM-002, TM-003 |
src/security/private-file.ts |
Establishes the no-symlink, owner-only and opened-file-identity boundary for sensitive local inputs | TM-001, TM-002, TM-003, TM-006 |
src/api/app.ts |
Main network entry point, authentication coverage and sensitive response surface | TM-002, TM-008, TM-010 |
src/events/webhook.ts |
External egress, HMAC construction and retry/dead-letter behavior | TM-007 |
src/pilot/mainnet-gate.ts |
Reads sensitive operational state and decides whether redacted restart/restore evidence is internally consistent | TM-002, TM-007, TM-009, TM-010 |
src/backup.ts |
Restores attacker-supplied files and optionally handles every service secret | TM-001, TM-006, TM-010 |
src/config.ts |
Controls binding, endpoint trust and all storage/secret paths | TM-002, TM-004, TM-006 |
package-lock.json |
Captures the large vulnerable upstream dependency surface | TM-005 |
Dockerfile |
Defines runtime privilege, copied dependencies and base-image trust | TM-005, TM-008 |
scripts/trust-boundary-check.ts |
Prevents payer spending code from entering the merchant build/image boundary | TM-001, TM-003, TM-005 |
tools/ppops-payer/src/security/submission-journal.ts |
Rejects cross-intent nullifier reuse during both preparation and reservation, bounds same-nullifier retries and separates rejection, ambiguity, reported hash and canonical transaction identity | TM-003, TM-009 |
tools/ppops-payer/src/railgun/broadcaster-transfer.ts |
Bounds fee/payment, validates and quorum-simulates the exact populated call before Waku, reserves recovery identity, rotates retry identity and binds receipts to nullifier-derived canonical identity | TM-003, TM-004, TM-005, TM-009 |
Quality check
- Covered HTTP, CLI, config, secrets, wallet state, chain TXOs, RPC/PPOI, artifact paths, webhook, mainnet evidence, restore and build entry points.
- Represented every identified trust boundary in at least one threat.
- Separated production/runtime behavior from CI, Docker and gate/test code.
- Recorded the explicit non-response to the deployment check-in and marked public-exposure conclusions conditional.
- Stated open questions that can change likelihood/priority.
- Anchored existing controls and gaps to repository paths/symbols without including secret values.