open-standards/openpay/compliance/pci-zero-architecture.md
Transaction Science a437feb8ab Parallel ingest: 9 issues' worth of work across openpay / smart-byte / eoc
openpay/
  compliance/                     PCI-DSS readiness package (scope-map,
                                  pci-zero-architecture, hsm-kms-guidance,
                                  qsa-checklist), citing PCI-DSS v4.0.1
                                  requirements. (#1)
  examples/pci-zero/              Runnable example demonstrating a deployment
                                  where merchant code never touches a PAN. (#1)
  crates/op-core/                 Tokenized<Card> typestate + NetworkToken
                                  + CardNetwork + Cryptogram. (#2)
  crates/op-rails-card/network_token/
                                  NetworkTokenProvider trait + VTS adapter
                                  + MDES adapter, with deterministic stubs
                                  behind a 'live' feature flag. (#2)
  crates/op-driver-sdk/conformance/network_token.rs
                                  Conformance harness for third-party
                                  NetworkTokenProvider implementations. (#2)
  crates/op-cli/src/ledger/       'openpay ledger as-of' subcommand for
                                  bi-temporal queries. (#3)
  crates/op-orchestrator/src/telemetry.rs
                                  OpenTelemetry span emission per
                                  Payment<S> transition with semantic-
                                  conventions tags. (#3)
  docs/observability/             Grafana dashboard JSON + wiring guide
                                  for the bi-temporal ledger lens. (#3)

smart-byte/
  smart-byte-rs/                  Rust reference implementation: envelope
                                  encode/decode (canonical CBOR + BLAKE3
                                  content addressing), Ed25519 sign +
                                  verify, lockstep BFT commit MVP, Iroh
                                  transport scaffold, CLI. (#4)
  spec/identity_and_key_rotation.md
                                  Adopts KERI SAIDs + key-event log
                                  format; explicitly does not adopt ACDC
                                  chaining wholesale. (#5)
  conformance/v1/                 21 test vectors covering canonicalisation,
                                  SAID computation, signature verification,
                                  lockstep commit, gossip propagation.
                                  CI gate enforces spec/vector coupling. (#6)

eoc/
  eoc-rs/                         Rust + WASM reference implementation:
                                  4-stage cascade (cache -> kv -> graph ->
                                  neural), joule counters via RAPL / NVML /
                                  powermetrics, CLI, wasm-bindgen surface.
                                  18 tests pass. (#7)
  eoc-bench/                      Benchmark harness, MT-Bench and Router
                                  case sets, 'joules per MT-Bench point'
                                  metric, 'eoc-bench run | compare' CLI. (#8)
  standards/sci-for-ai-submission.md
                                  Submission proposal mapping EOC's
                                  receipt schema to GSF SCI-for-AI. (#9)
  cookbook/                       HuggingFace-style cookbook (markdown +
                                  runnable Python) demonstrating EOC's
                                  cascade with HF AI Energy Score. (#9)
  docs/bench-v0.md                Reproducibility doc for eoc-bench.

Workspace state:
- openpay clippy -- -D warnings: clean across all touched crates
- smart-byte-rs cargo test --workspace + clippy: clean
- eoc-rs cargo test --workspace + clippy: clean (18 tests)
- eoc-bench cargo check --workspace + clippy + 15 tests: clean

Rust 1.95.0 edition 2024 pinned at the open-standards root via
rust-toolchain.toml; every workspace inherits.

Closes #1 #2 #3 #4 #5 #6 #7 #8 #9.
2026-05-23 13:24:05 -04:00

89 lines
11 KiB
Markdown

# PCI-Zero Architecture
A deployment topology where merchant application servers never see a primary account number (PAN) and therefore qualify for SAQ-A or SAQ-A-EP rather than SAQ-D. The cardholder-data environment (CDE) collapses to a single tier — the vault — and every other OpenPay component runs outside it.
The runnable counterpart of this document is `examples/pci-zero`. That binary wires the same components in-process so the data-flow is auditable as a single Rust program; the *deployment* topology described here splits those components across hosts and networks.
## Topology
```
┌────────────────────────────────────────────────────────────────────────┐
│ Customer browser / device │
│ │
│ • collection-iframe.js (served from vault.example.com domain) │
│ • TLS 1.3 to vault host (cert-pinned) │
│ • PAN entry happens here; merchant JS bundle CANNOT read the iframe │
│ (Same-Origin Policy isolates the iframe document) │
└────────────────────────────────────────────────────────────────────────┘
│ │
│ raw PAN (TLS 1.3) │ window.postMessage(VaultRef)
▼ ▼
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ CDE network segment │ │ Merchant application network │
│ ──────────────────────────────── │ │ ──────────────────────────────── │
│ │ │ │
│ ┌────────────────────────────┐ │ │ ┌────────────────────────────┐ │
│ │ Vault service │ │ │ │ Merchant app server │ │
│ │ • op-vault impl │ │ │ │ • op-server │ │
│ │ • mTLS-fronted HTTP API │ │ │ │ • op-orchestrator │ │
│ │ • detokenize/tokenize/ │ │ │ │ • op-rails-card driver │ │
│ │ delete/exists │ │ │ │ • op-ledger / op-graph │ │
│ │ │◄─┼────┼──┤ stores │ │
│ │ Holds: encrypted PAN + │ │mTLS│ │ │ │
│ │ pci-scope build │ │ │ │ Holds: VaultRef tokens │ │
│ └──────────────┬─────────────┘ │ │ │ NEVER holds: raw PAN │ │
│ │ │ │ │ Build: pci-scope = OFF │ │
│ ▼ unwrap DEK │ │ └──────────────────────────────┘ │
│ ┌────────────────────────────┐ │ └──────────────────────────────────┘
│ │ KMS / HSM │ │ │
│ │ • AWS KMS / GCP KMS / │ │ │ Vault::detokenize
│ │ HashiCorp Vault Transit │ │ │ via mTLS, only at
│ │ / Fortanix DSM │ │ │ acquirer submit time
│ │ • FIPS 140-2 L3 or L4 │ │ ▼
│ │ • Holds: KEKs only │ │ ┌──────────────────────────────────┐
│ └────────────────────────────┘ │ │ Acquirer (Hyperswitch / direct) │
│ │ │ • PCI-DSS Level 1 service │
└──────────────────────────────────┘ │ provider; their scope, not │
▲ │ ours, once PAN is posted │
│ └──────────────────────────────────┘
│ Vault::detokenize (mTLS)
│ initiated by op-rails-card during authorize()
└──────────────────────────────────────────────────────────────
```
### Trust boundaries
1. **Browser ↔ Vault.** The collection iframe runs from a domain controlled by the vault, not the merchant. The merchant's outer page receives a `VaultRef` via `window.postMessage`; the merchant's JavaScript bundle cannot read the iframe document and so cannot read the PAN. This is the same pattern Stripe Elements, Adyen Components, and Braintree Drop-in use to keep merchants on SAQ-A.
2. **Merchant app server ↔ Vault service.** Only at acquirer submit time, the merchant's `op-rails-card` driver code calls `Vault::detokenize(VaultRef)` over a mutually authenticated TLS connection to the vault service. The detokenized `CardData` is serialised straight into the acquirer payload and the local Rust value drops + `Zeroize`s before the function returns.
3. **Vault service ↔ KMS / HSM.** The vault never holds raw key material in memory long-term. The data-encryption key (DEK) used to encrypt each tokenized PAN is wrapped under a key-encryption key (KEK) held by the KMS / HSM. On `detokenize`, the vault calls the KMS to unwrap the DEK, decrypts the ciphertext, and immediately zeroises the DEK. Pattern matches AWS KMS *envelope encryption* and HashiCorp Vault *Transit* secret engine.
4. **Vault service ↔ acquirer.** The vault may post directly to the acquirer ("network token" pattern with Visa Token Service / Mastercard MDES) or hand `CardData` back to the merchant app server for a single submit call. The example uses the latter; both designs are valid, and the second still keeps the merchant on the connected-to-scope side because the merchant process holds `CardData` for the duration of the acquirer HTTP round-trip and then drops it.
## PCI-DSS v4.0.1 requirements satisfied by this topology
| Req. | Title | How this topology satisfies |
|---|---|---|
| 1.4 | Boundary security between trusted and untrusted networks | CDE network segment terminates at the vault host. Inbound rules: mTLS from app servers, TLS 1.3 from browsers (cert-pinned). No other ports. |
| 2.2.1 | Configuration standards | Vault host runs a single binary built with `--features op-vault/<backend>` plus `--features op-core/pci-scope`. No general-purpose OS services. |
| 3.2.1 | Account data is not stored after authorization unless there is a legitimate business need | The merchant app server does not store PAN. Card-on-file goes into the vault under `TokenizationPolicy::card_on_file()`; everything downstream sees only `VaultRef`. |
| 3.3.1 | SAD is not stored after authorization | EMV Track 2 equivalent never persists past the `op-emv` parse step; the parsed blob is forwarded to the acquirer encrypted and not retained. |
| 3.5.1 | PAN is rendered unreadable wherever it is stored | Vault stores AES-256-GCM-SIV ciphertext (RFC 8452, misuse-resistant). The DEK is wrapped by KMS-held KEK. |
| 3.6.1 | Cryptographic keys are managed throughout their lifecycle | KMS / HSM (AWS KMS, GCP KMS, HashiCorp Vault, Fortanix DSM) provides rotation, access control, and audit logging on the KEK. See `hsm-kms-guidance.md`. |
| 3.7.1 | Key-management policies and procedures are documented | Operators document KEK rotation schedule (typical: 365 days for KEKs, 90 days for DEKs), separation of duties (KMS admin ≠ vault admin), and emergency-rotation runbook. |
| 4.2.1 | Strong cryptography during transmission over open / public networks | TLS 1.3 between browser and vault; mTLS between merchant app server and vault; TLS 1.2+ between vault and acquirer (acquirer's requirement). |
| 6.2.4 | Engineering practices to prevent or mitigate common software attacks | `Money` is integer minor units; no `f64` floats. `Payment<S>` is a typestate machine; refund-before-capture is a compile error. `CardData` has `Zeroize` + `ZeroizeOnDrop`. `Vault` trait does not distinguish "not found" from "auth failed" (no oracle). |
| 8.3.1 | Strong authentication for all access into the CDE | Vault service requires mTLS for app-server callers (workload identity) and operator MFA for human admins. |
| 10.2.1 | Audit logs capture all individual user accesses to cardholder data | Vault emits a structured `tracing` event on every `tokenize` / `detokenize` / `delete`, with the calling workload identity, timestamp, `VaultRef`, and outcome. Logs ship to a write-once audit store. |
| 11.4.1 | External and internal penetration testing | Performed against the CDE segment (the vault) and the connected-to-scope boundary (the app server). Out-of-scope crates do not require dedicated pen-testing. |
## What this topology does **not** cover
- **EMV tap.** If the device is a Linux kiosk with a tap reader (see `examples/kiosk-linux`), the EMV blob from the reader's secure element is its own SAD-handling concern. Use a P2PE-validated reader so the EMV path stays SAD-free between the chip and the acquirer.
- **Stored card-on-file.** Card-on-file (recurring billing) requires the vault to persist long-lived `VaultRef → ciphertext` mappings. The vault becomes a long-running database; treat as Req. 3.5 storage scope.
- **Refunds.** A refund references the original `psp_payment_id`, not the PAN, so refunds run entirely outside the CDE.
## Pointer to runnable example
`examples/pci-zero/` ships a single-process Rust binary that demonstrates the same data-flow in code: a fake browser submits PAN to an in-process vault, the vault returns a `VaultRef`, the orchestrator runs the intent through a card rail, and the rail driver calls `Vault::detokenize` once at acquirer submit time. The example asserts via type-level evidence (`#![forbid(unsafe_code)]`, no `RawPan` import outside `op_vault`, no `CardData` value crossing process boundaries) that the merchant code path never holds raw PAN.