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

11 KiB

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 + Zeroizes 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.