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.
| Scenario | Result |
|---|---|
| Same IBAN, reformatted with dashes and different case | match / low risk — normalisation defeats formatting noise |
| Genuine vendor, attacker's account | mismatch / high risk / out-of-band check required |
| Never-seen vendor | unknown_vendor / elevated — a first payment has no history to check |
| Dashboard status query | Returns enrolment state and beneficiary name, no 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 testwith no emulation harness. - One operational secret, documented in the source rather than left as folklore for whoever inherits it.

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.