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

Криптография и подпись команд

Подпись в hlf-core — не часть Fabric, а уровень приложения: Fabric подтверждает, что транзакция пришла от узла организации, а hlf-core отвечает на вопрос «кто именно её совершил». Поэтому вся криптография берётся из @ts-core/common и его крипто-пакетов и остаётся одинаковой, подписываем мы команду для блокчейна или для обычного сервиса — chaincode лишь проверяет то, что клиент уже подписал.

Что именно подписывается​

Ключевая идея в том, что алгоритм не подписывает конверт целиком и не подписывает «сырые» байты запроса. Он подписывает одну каноническую строку, которую строит TransportCryptoManager.toSign:

  • `${name}_${nonce}` — если у команды нет полезной нагрузки;
  • `${name}_${value}_${nonce}` — где value это детерминированный JSON запроса, полученный через ObjectUtil.sortKeys(request, true).

Сортировка ключей здесь принципиальна: и клиент при подписи, и chaincode при проверке должны получить байт в байт одну и ту же строку независимо от того, в каком порядке поля легли в объект. Благодаря этому подпись покрывает имя команды, её аргументы и nonce сразу — подменить что-либо из этого, не сломав подпись, нельзя. А поскольку строку собирают обе стороны из одних и тех же данных, все алгоритмы подписывают, по сути, один и тот же материал — различается только сам криптографический примитив.

Абстракция подписи​

Подпись описывается интерфейсом ISignature (класс Signature) из @ts-core/common — это объект из четырёх полей:

ПолеНазначение
valueсама подпись (hex-строка)
publicKeyпубличный ключ подписанта (для Metamask — Ethereum-адрес)
nonceчисловая строка, монотонно растущая на пользователя — см. «Безопасность»
algorithm?имя алгоритма; опционально, Ed25519 считается умолчанием и по сети не передаётся

Ключи живут парой IKeyAsymmetric — { privateKey, publicKey }. А конкретный алгоритм прячется за ITransportCryptoManager с методами sign(command, nonce, privateKey), verify(command, signature) и геттером algorithm. Все реализации наследуют абстрактный TransportCryptoManager, чей статический sign ставит nonce = Date.now(), если его не передали, и возвращает готовый ISignature. Эта единообразная абстракция и позволяет менять алгоритм, не трогая остальной код: chaincode выбирает нужный менеджер по полю algorithm в подписи.

Поддерживаемые алгоритмы​

Каждый алгоритм поставляется отдельным пакетом (кроме Ed25519, встроенного в @ts-core/common) и даёт свой TransportCryptoManager*. Чтобы chaincode принимал алгоритм, соответствующий менеджер достаточно добавить в массив cryptoManagers приёмника.

АлгоритмМенеджерХешОснова / примечание
Ed25519 (умолчание)TransportCryptoManagerEd25519SHA-512tweetnacl, встроен в @ts-core/common
ECDSA secp256k1TransportCryptoManagerSecp256k1SHA-256@noble/curves; есть signDigest/recover/address (Ethereum) и custody-режим над готовым digest
RSATransportCryptoManagerRSASHA-256node-rsa, PKCS8 PEM, 2048 бит
ГОСТ Р 34.10TransportCryptoManagerGostR3410ГОСТ Р 34.11@vostokplatform/crypto-gost-js, российская криптография
MetamaskTransportCryptoManagerMetamaskBackend / ...FrontendKeccak-256EIP-191 personal_sign; publicKey это Ethereum-адрес, verify восстанавливает его из подписи

Отдельного внимания заслуживает secp256k1: помимо обычной подписи он умеет подписывать и проверять готовый 32-байтный digest (signDigest / verifyDigest), что нужно для custody-сценариев и порогового подписания (DKLS23), а также вычислять Ethereum-адрес из ключа. Пакеты @ts-core/eth и @ts-core/btc криптографию не реализуют — это read-only клиенты блокчейна; для подписи Ethereum используется пара secp256k1 / Metamask.

Подключение алгоритмов на chaincode​

Какие алгоритмы chaincode согласен принимать — задаётся массивом cryptoManagers при создании приёмника. В простом случае это один Ed25519:

import { TransportCryptoManagerEd25519 } from '@ts-core/common';
import { TransportFabricChaincodeReceiver } from '@hlf-core/transport-chaincode';

const transport = new TransportFabricChaincodeReceiver(logger, {
cryptoManagers: [new TransportCryptoManagerEd25519()],
nonSignedCommands: [CoinGetCommand.NAME, UserGetCommand.NAME],
});

Добавить алгоритм — значит добавить его менеджер в этот массив; Ed25519 живёт в @ts-core/common, остальные — в отдельных пакетах. Контракт может одновременно принимать несколько алгоритмов, и тогда клиент волен подписывать команду любым из них:

import { TransportCryptoManagerEd25519 } from '@ts-core/common';
import { TransportCryptoManagerRSA } from '@ts-core/crypto-rsa';
import { TransportCryptoManagerGostR3410 } from '@ts-core/crypto-gost';
import { TransportCryptoManagerSecp256k1 } from '@ts-core/crypto-secp256k1';

const transport = new TransportFabricChaincodeReceiver(logger, {
cryptoManagers: [
new TransportCryptoManagerEd25519(),
new TransportCryptoManagerRSA(),
new TransportCryptoManagerGostR3410(),
new TransportCryptoManagerSecp256k1(),
],
});

Больше ничего менять не нужно: при проверке validateSignature выбирает менеджер по полю signature.algorithm, и команда, подписанная любым из зарегистрированных алгоритмов, пройдёт, а подписанная незарегистрированным — будет отклонена с CommandSignatureAlgorithmUnknownError. Поддержка ГОСТ Р 34.10 рядом с Ed25519 здесь не случайна — для регуляторного контура важна совместимость с российской криптографией, и сменный менеджер позволяет держать несколько схем в одном контракте.

Поток подписи и проверки​

На клиенте подпись строится в несколько строк — берём ключи, формируем команду, подписываем её и кладём подпись в опции при отправке:

import { Ed25519, IKeyAsymmetric, ISignature, TransportCommand, TransportCryptoManagerEd25519 } from '@ts-core/common';

// Ключи подписанта
const keys: IKeyAsymmetric = Ed25519.from(privateKey);

// Любая команда, меняющая состояние
const command = new CoinTransferCommand({ coinUid, object, target, value });

// Подпись команды приватным ключом подписанта
const signature: ISignature = await TransportCommand.sign(
command,
new TransportCryptoManagerEd25519(),
keys,
);

// userId и signature уедут в chaincode внутри опций команды
await transport.sendListen(command, { userId, signature });

Опции команды — это тот самый конверт TransportFabricCommandOptions, в котором едут userId и signature. На стороне chaincode TransportFabricChaincodeReceiver в методе validateSignature проверяет, что подпись, её algorithm и publicKey присутствуют, находит менеджер по алгоритму в списке cryptoManagers (незнакомый алгоритм — отказ) и вызывает TransportCryptoManager.verify. Менеджер пересобирает ту же каноническую строку name_value_nonce уже из пришедшей команды и проверяет signature.value ключом signature.publicKey.

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