Безопасность: подпись и 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, так что по коду ответа всегда видно,
на каком именно звене цепочки команда была отклонена.