All insightsENGINEERING · Security · 4 MIN READ

Stopping invoice fraud with a register that never leaves the enclave

Invoice fraud does not break cryptography. It edits a bank account on a genuine invoice, and payment goes out because nothing asks whether this vendor was ever paid at this account. Answering that needs a register — which is exactly what an attacker wants.

Business email compromise is the most expensive email crime there is, and it is not technically sophisticated.

An attacker sends a real-looking invoice from a supplier the company genuinely owes money to, with the beneficiary account swapped for their own. Accounts payable pays it, because nothing in the process ever asks the one useful question:

Have we ever paid this vendor at this account before?

Why the answer is hard to store

Answering it requires a register of known-good payees. That register is the whole difficulty. It is the complete list of who a company pays and where — exactly what an attacker is after, and exactly what a breached vendor database gives away.

A control that stops fraud by centralising the most valuable data in the company is not obviously an improvement. So the question became: can the register answer queries without being readable, even by whoever holds the credentials to query it?

Why confidential computing

I built it as a contract running inside confidential-computing hardware, so the register never leaves the enclave. Three properties then come from the architecture rather than from operational discipline.

No enumeration. A lookup requires supplying a candidate account. A wrong candidate returns mismatch without revealing the correct one. A caller holding valid API credentials still cannot extract the payment book — the interface answers yes or no, and never reads back.

No plaintext at rest. Accounts are stored as tenant-salted SHA-256 fingerprints, with the salt held outside the compiled artifact. A future bug, or an over-broad permission grant, still cannot surface an IBAN.

No network, structurally. The contract's capability set is declared by its interface imports: key-value storage, logging, tenant context, clock — and deliberately no HTTP. It therefore has no route to make an outbound call at all.

That last property is the one worth dwelling on. An automated agent authorised to call this contract cannot send the register anywhere, even if a prompt injection hidden in the invoice it is processing fully compromises that agent. The usual mitigation for "the automated system might be tricked" is a policy instructing it not to be. Here the exfiltration channel does not exist.

Verified behaviour

Deployed to a live network and exercised end to end.

ScenarioResult
Same IBAN, reformatted with dashes and different casematch / low risk — normalisation defeats formatting noise
Genuine vendor, attacker's accountmismatch / high risk / out-of-band check required
Never-seen vendorunknown_vendor / elevated — a first payment has no history to check
Dashboard status queryReturns enrolment state and beneficiary name, no account identifier
Console output showing an unknown_vendor verdict at elevated risk requiring an out-of-band check, and a dashboard status response carrying the beneficiary name but no account identifier
A never-seen vendor is flagged as elevated rather than blocked, and the dashboard query returns enrolment state without an account identifier.

Rotation history feeds the risk signal. An account that has never changed makes a new one more surprising, and a recent change is itself reported rather than silently accepted.

Built to keep running

The client asked whether contributors wanted to keep operating what they built or hand it over. The design assumes the latter is always possible.

  • No outbound dependency — nothing breaks when a third-party API changes its contract or starts rate-limiting.
  • No model. Verdicts are deterministic comparisons. They can be audited by reading them, and they return the same answer next year.
  • Natively testable. The crate compiles both to WebAssembly and to a host target, so the business logic runs under cargo test with no emulation harness.
  • One operational secret, documented in the source rather than left as folklore for whoever inherits it.
Terminal showing six unit tests and one documentation test passing on the host target, then a release build for wasm32-wasip2 producing a 179 KB WebAssembly component
The same crate under cargo test on the host target, then built for wasm32-wasip2 — tests need no emulator.

Delivered

  • Rust contract compiled to a 179 KB WebAssembly component, 3 exported functions
  • 6 unit tests plus a full end-to-end transcript against the live deployment
  • Complete README, interface reference, and a written bug report covering 4 platform issues hit while building, each with a reproduction and a workaround

Source is public:

Artem-Sarkisian/z-tenant-payee-guard

What generalises

The useful move was not the hardware. It was refusing to build the interface that would have made the data extractable.

A register that returns records is a breach waiting for credentials. A register that only confirms a candidate you already hold answers the business question and gives an attacker nothing to steal — and that choice is available in ordinary systems too, long before anyone reaches for an enclave.

Related reading