How to hash and sign Ledger requests
Date | Responsible | Changes |
---|---|---|
September 7, 2022 | @Željko Rumenjak | Initial version |
January 27, 2023 | @Željko Rumenjak | Instructions about hashing signature custom data |
March 8, 2023 | @Željko Rumenjak | Simplified hashing algorithm by reducing number of hashing operations performed |
Each ledger operation needs to be signed by a private key that has permissions to perform that operation. We need to have a deterministic way of generating hashes and signatures in order to be able to verify them on other devices.
Hashing
The first part of this process is making sure to generate stable string representations of our content in order to always get the same hash for the same input. We are using JSON as our wire transport, so it makes sense to use JSON for hashing as well.
JSON is not a deterministic format, so JSON serializers in various programming languages don’t necessarily provide the same output for the same input data. To resolve this, ledger uses a JSON serialization standard described in RFC 8785. The RFC document describes the rules for serialization, there are reference implementations for several popular programming languages linked in the specification as well.
Many existing serializers should be compatible with the rules described in the RFC, the most important rule that usually needs to be implemented manually is the ordering of JSON object properties. Each JSON object in the output needs to have all properties alphabetically ordered.
Some serializers implemented in static programming languages actually output object properties in the same order as they are defined in classes, so the most basic way to accomplish this requirement can be to simply sort all properties in your strongly typed objects.
Example payload serialization in nodeJS:
Here we use a third party package called safe-stable-stringify
which is compatible with the rules from RFC 8785. There are many such packages available on npm.
After we have a canonically serialized data, now we can hash it. For hashing we will use SHA-256
hashing algorithm, and we will calculate our hash by performing the following steps:
- Serialize the input data using RFC 8785 compatible serializer
- Hash the serialized data using
SHA-256
as ahex
encoded string
Example implementation in nodeJs:
serializeData
function used here is the serialization function from previous example.
Signature digests
Ledger also supports attaching additional data when signing objects. This can be done by including data in the custom
property of the signature object.
Signing only the primary payload hash like the one we generated in the previous chapter wouldn’t allow us to ensure the integrity of this additional data included in signatures. This is why an additional signature digest is calculated in these situations.
To include additional data in the hash we are using double hashing algorithm. Double hashing has some other benefits besides the ability to extend the hash with additional data. Most important among those is that it prevents certain cryptographic attacks. That is why signature digest is used for all signatures, regardless if they include custom data.
Steps to calculate a signature digest are as follows:
- Create a primary payload hash following the steps from previous chapter
- Serialize the additional data (
custom
) that is added in the signature using the serialization algorithm from the previous chapter, use an empty string ifcustom
doesn’t exists - Perform another
SHA-256
hash by concatenating the hash from step 1 with the serializedcustom
data of the signature:sha256(dataHash + serializedCustomData)
and return this hash as ahex
encoded string - Use the hash from step 3 as the signature digest
Here is an example implementation in nodeJs:
serializeData
function used here is the function from previous chapter.
Signing
We can now sign ledger requests by signing the digest we calculated previously with our private key using ed25519
compatible implementation. In nodeJS we can perform this by using the standard node crypto
package: