Skip to main content

Security: Signature and Nonce

On top of Fabric's organizational authentication, hlf-core also verifies an application-level signature — it is precisely this that answers the question "who performed the operation", which Fabric itself does not answer. The verification is performed by TransportFabricChaincodeReceiver when it receives each command, and it is arranged as a chain in which every link can reject the command.

First the signature algorithm is determined — from the list of configured cryptoManagers (which algorithms are supported and what exactly is signed is covered in the Cryptography section). Then it is checked that the signature even contains a public key and an algorithm. Next comes the nonce — a numeric string that must be strictly greater than the previous value stored in state under the key →${userId}~nonce. This is the protection against replaying a transaction: the same signed envelope cannot be applied twice, because its nonce will no longer pass. And only after this is the cryptographic verification of the signature itself performed — with the public key that arrived in the envelope.

For readonly commands the nonce is not updated — they change nothing, and there is no point in bumping the counter here. The client obtains the current nonce with the readonly NonceGetCommand, increments it and uses it when signing the next command:

// The user's current nonce (readonly, requires no signature)
const current = await transport.sendListen(new NonceGetCommand(userId));

// The next one — strictly greater, and we sign the command with it
const nonce = MathUtil.add(current, '1');
const signature = await TransportCommand.sign(command, new TransportCryptoManagerEd25519(), keys, nonce);
await transport.sendListen(command, { userId, signature });

What the transport checks, and what the application layer checks​

Here it is important not to be misled about the boundaries of responsibility. The transport proves exactly two things: the signer owns the private key to the public key that arrived in the envelope, and this envelope is not being replayed (nonce). But by itself it does not verify that this public key actually belongs to the user with the specified userId — for the transport, userId and publicKey come from one and the same envelope.

The binding "this key belongs to this user" is done one layer up by the application guard. In the examples this is the UserGuard decorator: it loads the user from state by userId, takes their registered cryptoKey and compares cryptoKey.value with the public key from the signature (stub.userPublicKey) — on a mismatch the command is rejected with FORBIDDEN. There too the user's status (ACTIVE) and, if necessary, roles are checked. The checks are controlled by options (isNeedCheckCryptoKey, isNeedCheckStatus), so they can be deliberately relaxed where that is really needed.

A practical consequence: a sensitive handler without a guard (or its equivalent) is protected by the transport only against signature forgery and replay, but not against someone signing a command with their own key while substituting someone else's userId. Binding to a specific user must be enabled explicitly — this is done deliberately, so that the transport does not know the user domain model and the guard does not meddle in cryptography.

The security error codes — for the signature, nonce and batch — are defined in @hlf-core/transport-chaincode, so the response code always shows at which exact link of the chain the command was rejected.