Wires the no-vanity-metrics policy from prose-only into structural
enforcement. Without this, the next time a count creeps into a
README it stays there until I notice; with this, the PR fails.
What lands:
- scripts/check-no-vanity-metrics.sh — bash scanner, dep-free,
targets marketing/spec/governance Markdown only (per-phase
progress docs, CHANGELOGs, conformance/notes/research dirs
exempt by design). Regex is tuned to the patterns we've actually
had to fix and rejects false positives on RFC / codec / version
references. Lines that legitimately need a number opt out with
`<!-- allow-vanity: <reason> -->`.
- .github/workflows/no-vanity-metrics.yml — CI workflow runs the
scanner on every PR and `main` push that touches Markdown.
- .githooks/pre-commit — same scanner, local guard so the failure
happens before push. Activated per-clone via
`scripts/install-hooks.sh`.
- CHARTER.md — adds a "Messaging — no vanity metrics" section
documenting the policy and pointing to the scanner.
Also strips the four remaining drift sites the scanner found on
first run: a ~5 LOC stub note, a "98 crates" + "≤ 120 LOC" mention
in jouleclaw-stack/examples/AGENTS.md, and "26 crates" in the PCI
scope-map doc. The CAMT.054 ISO 20022 message-name reference is
preserved via an allow-vanity marker.
8.6 KiB
PCI-DSS v4.0.1 Scope Map
This document declares, for each crate in the OpenPay workspace, whether that crate is in scope, connected-to-scope, or out-of-scope for PCI-DSS v4.0.1, and why. The categorisation follows the PCI SSC Information Supplement: Guidance for PCI DSS Scoping and Network Segmentation.
How the pci-scope cargo feature works
op-core declares an off-by-default cargo feature named pci-scope. With the feature off, the PaymentMethod enum exposes exactly four variants — Vault(VaultRef), Wallet(WalletRef), Emv(SecureBlob), A2a(A2aKey) — none of which carry raw PAN. With the feature on, a fifth variant PaymentMethod::RawPan(RawPan) becomes visible and the op_core::method::pci::RawPan type becomes constructible.
Only op-vault enables pci-scope unconditionally. Through cargo's workspace feature unification, any crate that transitively depends on op-vault (notably op-orchestrator, which depends on op-vault directly) also sees the RawPan variant at compile time, but the orchestrator's match arm for RawPan returns Error::DesignViolation rather than ever passing the variant down the rail. The variant exists in the type but cannot enter through the orchestrator's public API.
The cardholder-data environment (CDE) in code is therefore the set of:
op-vaultand its in-process implementations- Any platform vault adapter the operator implements (iOS Keychain, Android Keystore, KMS-encrypted blob store, HSM-backed remote vault)
- The rail driver code path that calls
Vault::detokenizeimmediately before posting to the acquirer
Everything else in the workspace handles only VaultRef, which under PCI SSC Tokenization Product Security Guidelines §3.3 carries no value for PAN recovery and is therefore out of scope.
Per-crate scope
| Crate | PCI-DSS scope | Handles CHD / SAD? | Notes |
|---|---|---|---|
op-core |
conditionally in scope | only with pci-scope feature ON |
Defines Money, Payment<S>, RailKind, and the gated RawPan type. Default build is out of scope; turning on pci-scope brings the binary into scope and triggers Requirements 3, 4, 6, 8, 10. |
op-iso20022 |
out of scope | no | Facade over the open-payments-iso20022 ISO 20022 message library. PACS/PAIN/CAMT messages carry account numbers, not PANs; A2A IBANs are payment-account identifiers, which PCI DSS does not regulate. |
op-emv |
in scope | yes — BER-TLV blobs from EMV kernels may contain Track 2 equivalent data (SAD per Req. 3.3) | EMV tap blobs from Secure Enclave / StrongBox are encrypted at the chip level. The blob crossing this crate is opaque ciphertext until decrypted in the HSM-backed acquirer path, but the handling of the blob is in scope because it represents a card-present transaction. |
op-vault |
in scope, CDE | yes — CardData holds raw PAN inside the process |
The CDE in code. CardData is constructible only inside a pci-scope-enabled caller, has Zeroize + ZeroizeOnDrop, and exposes only first_six / last_four / exp_* to non-crate callers. The reference InMemoryVault is for tests; production deployments wire an HSM- or KMS-backed implementation. |
op-fraud |
out of scope | no — receives features extracted from VaultRef + Money + device signals |
Pure scoring logic. The Scorer trait inputs are typed risk features; PAN never reaches the scorer. |
op-rails-card |
connected to scope | yes — calls Vault::detokenize on the submit path |
The driver crate is the only consumer of CardData outside op-vault. Drivers serialise PAN into the acquirer payload immediately and the local CardData value drops + zeroises. Requirements 4.2.1 (TLS to acquirer) and 8.6 (acquirer creds) attach here. |
op-rails-a2a |
out of scope | no — account numbers, not PANs | FedNow / PIX / SEPA Instant credit-transfer drivers. ISO 20022 pacs.008 payloads carry IBANs / routing+account pairs, which fall under bank regulation (NACHA, BSA, AML) but not PCI DSS. |
op-rails-crypto |
out of scope | no — public chain addresses + ERC-20 calldata | Stablecoin gateway. EVM addresses are public; the EvmSigner trait protects private keys, not cardholder data. Outside PCI DSS scope; FinCEN MSB rules apply instead. |
op-ffi-swift |
out of scope | no — opaque handles only | Swift bridge. The PAN never crosses the C ABI; only VaultRef strings and Payment<S> handles cross into Swift. |
op-ffi-jni |
out of scope | no — opaque handles only | Android JNI bridge. Same discipline as the Swift bridge. |
op-wasm |
out of scope | no — opaque handles only | Browser bridge. The browser handles a one-time-token returned by the vault's collection iframe; the JS side never sees a PAN. |
op-orchestrator |
connected to scope | no, but sees RawPan variant in its match arm |
The match arm for PaymentMethod::RawPan returns Error::DesignViolation rather than touching the value. Connected to scope per the PCI SSC Scoping Guidance §2.3 (segmentation-connected systems) because it directs traffic to systems that are in scope (the vault and the rail driver). Requirements 1, 2, 6, 8, 10 attach. |
op-ledger |
out of scope | no — VaultRef only |
Double-entry ledger. Account-level book entries do not include PAN; the masked first-six / last-four are sufficient for reconciliation. |
op-webhook |
out of scope | no — event payloads do not include PAN | Stripe-shaped outbound delivery with HMAC signing. Webhook payloads include psp_payment_id, VaultRef, Money, masked card metadata only. |
op-graph |
out of scope | no — VaultRef only |
Minigraf-backed persistence for ledger / webhook / reconciliation / refund / dispute / settlement / subscription / idempotency stores. No PAN is stored. |
op-reconciliation |
out of scope | no — masked card metadata only | Diffs ledger entries against CAMT.053 bank statements via deterministic UUID v5 keys derived from masked metadata. |
op-refund |
out of scope | no — references prior Payment<Captured> by id |
Refund state machine. Original payment id and Money are sufficient. |
op-dispute |
out of scope | no — chargeback evidence is the merchant's, not the cardholder's | Dispute workflow. Evidence attachments are operator-supplied PDFs / images; the crate does not introspect them. |
op-settlement |
out of scope | no — NACHA payout files carry merchant bank account info | Batch settlement and NACHA file generation. Merchant ACH details are not PCI scope; they are GLBA scope. |
op-server |
connected to scope | no, but is the network ingress | Axum HTTP server. Requirements 1.4 (boundary), 2 (config hardening), 4.2.1 (TLS), 6.4 (web-app vulnerabilities), 8 (auth), 10 (logging) attach. The server itself never holds CHD: card-collection traffic terminates at the vault, not at this server. |
op-driver-sdk |
out of scope | no — deterministic mocks | Driver-conformance harness with DeterministicCardAcquirer / DeterministicA2aGateway / DeterministicCryptoGateway. Used in CI; not deployed. |
op-subscriptions |
out of scope | no — VaultRef for card-on-file |
Recurring billing. Re-uses long-lived VaultRef produced by the vault under TokenizationPolicy::card_on_file(). |
op-fx |
out of scope | no — Quote, QuoteProvider only |
Foreign-exchange conversion. |
op-cli |
connected to scope | no, but operator credentials cross it | op operator CLI. Requirement 8 (auth) attaches via the API-key environment variable used to call op-server. |
Build profiles
The PCI-DSS-relevant cargo features are:
op-vault/in-memory— pulls inaes-gcm-sivandrand_core. EnablesInMemoryVault. Not for production. Use only for tests, thekiosk-linuxdemo, and thepci-zeroexample.op-core/pci-scope— exposes theRawPanvariant andop_core::method::pci::RawPan. Enabled transitively byop-vault. Any production CDE component must build with this feature; everything else builds without it.
The recommended deployment build:
# CDE: vault service binary
cargo build --release -p <your-vault-service> --features op-vault/<your-backend>
# Connected systems: orchestrator + rail drivers + server
cargo build --release -p op-server -p op-orchestrator -p op-rails-card -p op-rails-a2a
# Out-of-scope systems: ledger / reconciliation / settlement / subscriptions
cargo build --release -p op-ledger -p op-reconciliation -p op-settlement -p op-subscriptions
In a PCI-zero topology (see pci-zero-architecture.md), the merchant application server compiles only out-of-scope and connected-to-scope crates; the in-scope vault binary is a separate process, on a separate host, in a separate network segment.