Как остановить подмену реквизитов реестром, который не покидает анклав
Мошенничество со счетами не ломает криптографию. Оно правит банковский счёт на настоящем инвойсе, и платёж уходит, потому что никто не спрашивает: платили ли мы этому поставщику на этот счёт раньше. Чтобы ответить, нужен реестр — ровно то, что и нужно атакующему.
Компрометация деловой переписки — самое дорогое почтовое преступление, и технически оно несложное.
Атакующий присылает правдоподобный инвойс от поставщика, которому компания действительно должна, подменив в нём счёт получателя на свой. Бухгалтерия платит, потому что в процессе никто ни разу не задаёт единственный полезный вопрос:
Платили ли мы этому поставщику на этот счёт раньше?
Почему ответ трудно хранить
Чтобы ответить, нужен реестр проверенных получателей. Этот реестр и есть вся сложность. Это полный список того, кому компания платит и куда, — ровно то, за чем охотится атакующий, и ровно то, что утекает вместе со взломанной базой поставщиков.
Защита, которая останавливает мошенничество ценой централизации самых ценных данных компании, — сомнительное улучшение. Поэтому вопрос сформулировался так: может ли реестр отвечать на запросы, оставаясь нечитаемым — в том числе для того, у кого есть доступы для этих запросов?
Почему конфиденциальные вычисления
Я сделал это контрактом, работающим внутри доверенной среды исполнения, чтобы реестр не покидал анклав. Три свойства при этом следуют из архитектуры, а не из операционной дисциплины.
Нельзя перебрать. Запрос требует передать кандидата — конкретный счёт. Неверный кандидат возвращает mismatch, не раскрывая правильного. Вызывающий с действующими API-доступами всё равно не может выгрузить платёжную книгу: интерфейс отвечает «да» или «нет» и ничего не отдаёт обратно.
Нет открытого текста на диске. Счета хранятся как SHA-256 отпечатки с солью, привязанной к арендатору, причём соль лежит вне скомпилированного артефакта. Будущая ошибка или слишком широко выданное право всё равно не поднимут наружу IBAN.
Нет сети — структурно. Набор возможностей контракта задан импортами его интерфейса: хранилище «ключ-значение», логирование, контекст арендатора, часы — и намеренно никакого HTTP. Значит, у него попросту нет маршрута, чтобы сделать исходящий вызов.
На последнем свойстве стоит остановиться. Автоматический агент, которому разрешено вызывать этот контракт, не может отправить реестр никуда — даже если prompt injection, спрятанная в обрабатываемом инвойсе, полностью его скомпрометирует. Обычная мера против «систему могут обмануть» — это инструкция ей не обманываться. Здесь канала утечки просто не существует.
Проверенное поведение
Развёрнуто в живой сети и прогнано от начала до конца.
| Сценарий | Результат |
|---|---|
| Тот же IBAN, переформатированный с дефисами и другим регистром | match / низкий риск — нормализация побеждает шум форматирования |
| Настоящий поставщик, счёт атакующего | mismatch / высокий риск / нужна проверка по другому каналу |
| Ранее не встречавшийся поставщик | unknown_vendor / повышенный — у первого платежа нет истории для сверки |
| Запрос статуса для дашборда | Возвращает факт регистрации и имя получателя, без идентификатора счёта |

История ротаций питает сигнал риска. Счёт, который никогда не менялся, делает появление нового более неожиданным, а недавнее изменение сообщается отдельно, а не принимается молча.
Сделано так, чтобы продолжало работать
Клиент спрашивал, хотят ли исполнители продолжать эксплуатировать сделанное или передать его. Архитектура исходит из того, что передача возможна всегда.
- Нет внешних зависимостей — ничего не сломается, когда сторонний API изменит контракт или включит лимиты.
- Нет модели. Вердикты — детерминированные сравнения. Их можно проверить, просто прочитав, и через год они дадут тот же ответ.
- Тестируется нативно. Крейт собирается и в WebAssembly, и под хостовую платформу, поэтому бизнес-логика гоняется под
cargo testбез эмуляции. - Один эксплуатационный секрет, задокументированный в исходниках, а не оставленный фольклором для того, кто примет проект.

Что сдано
- Rust-контракт, скомпилированный в WebAssembly-компонент на 179 КБ, 3 экспортируемые функции
- 6 модульных тестов плюс полная сквозная расшифровка прогона против живого развёртывания
- README, справочник по интерфейсу и письменный баг-репорт по 4 проблемам платформы, найденным по ходу, каждая с воспроизведением и обходным путём
Исходники открыты:
Artem-Sarkisian/z-tenant-payee-guard
Что здесь обобщается
Полезным ходом оказалось не железо. Полезным оказался отказ строить интерфейс, из которого данные вообще можно было бы извлечь.
Реестр, который возвращает записи, — это утечка, ожидающая доступов. Реестр, который только подтверждает кандидата, уже имеющегося у спрашивающего, отвечает на бизнес-вопрос и не даёт атакующему ничего для кражи. И этот выбор доступен в обычных системах тоже — задолго до того, как кто-то потянется за анклавом.