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.