Перейти к основному содержимому

Безопасность: подпись и nonce

Поверх организационной аутентификации Fabric hlf-core проверяет ещё и подпись на уровне приложения — именно она отвечает на вопрос «кто совершил операцию», на который сам Fabric не отвечает. Проверку выполняет TransportFabricChaincodeReceiver при приёме каждой команды, и устроена она как цепочка, где каждое звено может отклонить команду.

Сначала определяется алгоритм подписи — из списка настроенных cryptoManagers (какие алгоритмы поддерживаются и что именно подписывается — в разделе «Криптография»). Затем проверяется, что в подписи вообще есть публичный ключ и алгоритм. Дальше идёт nonce — числовая строка, которая обязана быть строго больше предыдущего значения, хранящегося в состоянии по ключу →${userId}~nonce. Это и есть защита от повторного воспроизведения транзакции: один и тот же подписанный конверт нельзя применить дважды, потому что его nonce уже не пройдёт. И только после этого выполняется сама криптографическая верификация подписи — тем публичным ключом, который пришёл в конверте.

Для readonly-команд nonce не обновляется — они ничего не меняют, и наращивать счётчик тут незачем. Клиент получает текущий nonce readonly-командой NonceGetCommand, увеличивает его и использует при подписи следующей команды:

// Текущий nonce пользователя (readonly, подписи не требует)
const current = await transport.sendListen(new NonceGetCommand(userId));

// Следующий — строго больше, им и подписываем команду
const nonce = MathUtil.add(current, '1');
const signature = await TransportCommand.sign(command, new TransportCryptoManagerEd25519(), keys, nonce);
await transport.sendListen(command, { userId, signature });

Что проверяет транспорт, а что — прикладной слой​

Здесь важно не обмануться в границах ответственности. Транспорт доказывает ровно две вещи: подписавший владеет приватным ключом к тому публичному ключу, что приехал в конверте, и этот конверт не воспроизводится повторно (nonce). Но сам по себе он не сверяет, что этот публичный ключ действительно принадлежит пользователю с указанным userId — для транспорта userId и publicKey приходят из одного и того же конверта.

Связку «ключ принадлежит этому пользователю» делает слоем выше прикладной guard. В примерах это декоратор UserGuard: он грузит пользователя из состояния по userId, берёт его зарегистрированный cryptoKey и сверяет cryptoKey.value с публичным ключом из подписи (stub.userPublicKey) — при несовпадении команда отклоняется с FORBIDDEN. Там же проверяется статус пользователя (ACTIVE) и, при необходимости, роли. Проверки управляются опциями (isNeedCheckCryptoKey, isNeedCheckStatus), так что их можно осознанно ослаблять там, где это действительно нужно.

Практическое следствие: чувствительный обработчик без guard (или его аналога) защищён транспортом только от подделки подписи и replay, но не от того, что кто-то подпишет команду своим ключом, подставив чужой userId. Привязку к конкретному пользователю нужно включать явно — это сделано сознательно, чтобы транспорт не знал доменную модель пользователя, а guard не лез в криптографию.

Коды ошибок безопасности — по подписи, nonce и batch — определены в @hlf-core/transport-chaincode, так что по коду ответа всегда видно, на каком именно звене цепочки команда была отклонена.