Все материалыENGINEERING · Безопасность · 4 МИН ЧТЕНИЯ

Как остановить подмену реквизитов реестром, который не покидает анклав

Мошенничество со счетами не ломает криптографию. Оно правит банковский счёт на настоящем инвойсе, и платёж уходит, потому что никто не спрашивает: платили ли мы этому поставщику на этот счёт раньше. Чтобы ответить, нужен реестр — ровно то, что и нужно атакующему.

Компрометация деловой переписки — самое дорогое почтовое преступление, и технически оно несложное.

Атакующий присылает правдоподобный инвойс от поставщика, которому компания действительно должна, подменив в нём счёт получателя на свой. Бухгалтерия платит, потому что в процессе никто ни разу не задаёт единственный полезный вопрос:

Платили ли мы этому поставщику на этот счёт раньше?

Почему ответ трудно хранить

Чтобы ответить, нужен реестр проверенных получателей. Этот реестр и есть вся сложность. Это полный список того, кому компания платит и куда, — ровно то, за чем охотится атакующий, и ровно то, что утекает вместе со взломанной базой поставщиков.

Защита, которая останавливает мошенничество ценой централизации самых ценных данных компании, — сомнительное улучшение. Поэтому вопрос сформулировался так: может ли реестр отвечать на запросы, оставаясь нечитаемым — в том числе для того, у кого есть доступы для этих запросов?

Почему конфиденциальные вычисления

Я сделал это контрактом, работающим внутри доверенной среды исполнения, чтобы реестр не покидал анклав. Три свойства при этом следуют из архитектуры, а не из операционной дисциплины.

Нельзя перебрать. Запрос требует передать кандидата — конкретный счёт. Неверный кандидат возвращает mismatch, не раскрывая правильного. Вызывающий с действующими API-доступами всё равно не может выгрузить платёжную книгу: интерфейс отвечает «да» или «нет» и ничего не отдаёт обратно.

Нет открытого текста на диске. Счета хранятся как SHA-256 отпечатки с солью, привязанной к арендатору, причём соль лежит вне скомпилированного артефакта. Будущая ошибка или слишком широко выданное право всё равно не поднимут наружу IBAN.

Нет сети — структурно. Набор возможностей контракта задан импортами его интерфейса: хранилище «ключ-значение», логирование, контекст арендатора, часы — и намеренно никакого HTTP. Значит, у него попросту нет маршрута, чтобы сделать исходящий вызов.

На последнем свойстве стоит остановиться. Автоматический агент, которому разрешено вызывать этот контракт, не может отправить реестр никуда — даже если prompt injection, спрятанная в обрабатываемом инвойсе, полностью его скомпрометирует. Обычная мера против «систему могут обмануть» — это инструкция ей не обманываться. Здесь канала утечки просто не существует.

Проверенное поведение

Развёрнуто в живой сети и прогнано от начала до конца.

СценарийРезультат
Тот же IBAN, переформатированный с дефисами и другим регистромmatch / низкий риск — нормализация побеждает шум форматирования
Настоящий поставщик, счёт атакующегоmismatch / высокий риск / нужна проверка по другому каналу
Ранее не встречавшийся поставщикunknown_vendor / повышенный — у первого платежа нет истории для сверки
Запрос статуса для дашбордаВозвращает факт регистрации и имя получателя, без идентификатора счёта
Вывод консоли: вердикт unknown_vendor с повышенным риском и требованием проверки по другому каналу, ниже ответ дашборда с именем получателя, но без идентификатора счёта
Незнакомый поставщик помечается как повышенный риск, а не блокируется; запрос дашборда возвращает статус регистрации без идентификатора счёта.

История ротаций питает сигнал риска. Счёт, который никогда не менялся, делает появление нового более неожиданным, а недавнее изменение сообщается отдельно, а не принимается молча.

Сделано так, чтобы продолжало работать

Клиент спрашивал, хотят ли исполнители продолжать эксплуатировать сделанное или передать его. Архитектура исходит из того, что передача возможна всегда.

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

Что сдано

  • Rust-контракт, скомпилированный в WebAssembly-компонент на 179 КБ, 3 экспортируемые функции
  • 6 модульных тестов плюс полная сквозная расшифровка прогона против живого развёртывания
  • README, справочник по интерфейсу и письменный баг-репорт по 4 проблемам платформы, найденным по ходу, каждая с воспроизведением и обходным путём

Исходники открыты:

Artem-Sarkisian/z-tenant-payee-guard

Что здесь обобщается

Полезным ходом оказалось не железо. Полезным оказался отказ строить интерфейс, из которого данные вообще можно было бы извлечь.

Реестр, который возвращает записи, — это утечка, ожидающая доступов. Реестр, который только подтверждает кандидата, уже имеющегося у спрашивающего, отвечает на бизнес-вопрос и не даёт атакующему ничего для кражи. И этот выбор доступен в обычных системах тоже — задолго до того, как кто-то потянется за анклавом.

Читать дальше