open-standards/openpay/crates
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 209e39c62d
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 13s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m59s
standards workspaces build + test / openpay (openpay, openpay, linux-arm64) (pull_request) Successful in 5m24s
op-screening: the audit log can outlive the process that wrote it
The crate header claims every `Screener::screen` call is recorded into a
chained, signed audit log "so that regulators can inspect the screening
history of any payment attempt after the fact". The log could not do
that. Its whole public surface was `new`, `verifying_key`, `entries`,
`record` and `verify`: no save, no load, and no way to supply an
existing key. The only constructor mints a fresh keypair from `OsRng`,
and every call site in the repo is `AuditLog::new()`.

So the trail died with the process, twice over. The rows were gone, and
the key was gone — which is the worse half: on the next run `new()`
mints a new keypair, so the verifying key a regulator ingested
out-of-band no longer matches anything, and rows an operator had
dumped to disk by hand could never be verified by any process, because
`verify` only ever checked against the key the running log happened to
own. "After the fact" was the one time the log could not be read.

The same crate already persists its other long-lived state —
`SanctionsIndex::save_snapshot` / `load_snapshot`, CBOR via ciborium —
so this follows that precedent rather than inventing one:

- `AuditLog::with_signing_key` signs with a key the operator already
  holds, instead of rotating it on every start.
- `save_entries` / `load_entries` persist the rows. The signing key is
  deliberately **not** written: a private key does not belong in a file
  beside the rows it signed, and this crate ships
  `compliance/hsm-kms-guidance.md` saying where it does belong.
- `resume(signing_key, entries)` continues the chain onto its real head,
  so the next row's `prev_hash` and `seq` follow the pre-restart rows
  rather than starting over. It verifies them first: resuming onto rows
  this key did not write would build a log that fails verification
  later, at the regulator, over a chain nobody can repair — so it fails
  here instead.
- `verify_entries(entries, verifying_key)` is a free function, which is
  the regulator's path: rows plus the published public key, with no
  signing key and no live log. `AuditLog::verify` delegates to it, so
  the two invariants have one implementation rather than two that can
  drift.

Additive throughout — `new`, `Default` and every existing call site are
unchanged.

Tests: a restart writes two rows, loads them back, resumes and appends a
third, and the whole chain verifies with `seq` continuing at 2; a
regulator verifies dumped rows from the public key alone, and both
invariants still bite on the detached rows (an edited row breaks its
chain hash at index 0, a removed row breaks the chain); resuming under
the wrong key is refused; and `a_fresh_log_cannot_verify_another_runs_rows`
pins the limit of `new()` that motivated all of it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 09:35:49 -04:00
..
op-3ds2 op-3ds2: an unconverted foreign amount is not under an EUR SCA cap 2026-10-17 04:27:27 -04:00
op-batch
op-bin ingest wave E: 3 crates · op-bin + smart-byte-tendermint + eoc-safety 2026-05-24 16:52:07 -04:00
op-bnpl chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-cli
op-connect op-connect: an unrun PEP screen is not a clearance 2026-10-17 04:13:10 -04:00
op-core
op-crypto-extra ingest wave F: 3 crates · op-crypto-extra + smart-byte-acdc + eoc-memory 2026-05-24 17:28:24 -04:00
op-dispute
op-dispute-evidence ingest wave A2: 3 crates · op-dispute-evidence + smart-byte-edv + eoc-agent 2026-05-24 11:42:26 -04:00
op-driver-sdk
op-emv
op-ewallets-asia chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-ffi-jni
op-ffi-swift
op-fraud Repo-wide toolchain refresh + jouleclaw policy/sandbox updates 2026-06-07 17:04:45 -04:00
op-fraud-graph Repo-wide toolchain refresh + jouleclaw policy/sandbox updates 2026-06-07 17:04:45 -04:00
op-fx
op-graph
op-iso8583
op-iso20022
op-ledger
op-mobile-wallets chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-openbanking ingest wave A4: 3 crates · smart-byte-atproto + op-openbanking + eoc-distributed 2026-05-24 12:22:09 -04:00
op-orchestrator Repo-wide toolchain refresh + jouleclaw policy/sandbox updates 2026-06-07 17:04:45 -04:00
op-payouts ingest wave B: 3 crates · op-payouts + smart-byte-nostr + eoc-streaming 2026-05-24 13:39:09 -04:00
op-rails-a2a
op-rails-card
op-rails-crypto
op-rails-x402 op-rails-x402: x402 agentic-payment rail (HTTP 402 acceptance) 2026-06-25 15:43:20 -04:00
op-reconciliation
op-refund
op-revrec
op-routing
op-screening op-screening: the audit log can outlive the process that wrote it 2026-10-17 09:35:49 -04:00
op-server
op-settlement
op-statements ingest wave C: 3 crates · op-statements + smart-byte-activitypub + eoc-observability 2026-05-24 13:51:09 -04:00
op-subscriptions
op-tax chore(deps): update dependencies for Rust 1.98, pinning the crypto generations 2026-08-26 12:19:50 -04:00
op-vault op-vault: test that a token is independent of the PAN, not free of "4242" 2026-09-30 23:07:55 -04:00
op-wasm
op-webhook fix(op-webhook): import KeyInit — hmac 0.13 moved new_from_slice off the Mac trait, breaking compute_signature 2026-08-26 09:28:40 -04:00