When developing blockchain projects, how you handle the private keys is one of the most important things you need to think about. Losing a private key means losing all the control over the account, and even if the breach is solved, the account can not be used again.
While developing a project using XRPL, we researched on the best ways of handling the private keys for a server. Ideally we would be able to use some kind of hardware module that signs transactions for us, while the private key never leaves the module. While no such solution exists for the specific use case of signing blockchain transactions, KMS can handle secp256k1 elliptic curve signatures, which is exactly the curve used by XRPL (and most blockchains). KMS is one of the most popular Key management solutions in the market, and it meets enterprise-level security certifications, meaning we can be sure we are using the maximum level of security to protect our keys.
The AWS credentials used to programatically call AWS still need to be properly secured, as anybody with access to them could call AWS and generate valid signed transactions signed with your private key. The advantage however is that upon detection of the attack, the credentials can be invalidated, and after re-securing the server, the same account could still be used, or even if the credentials are eliminated before the attacker has time to sign a transaction with them, the funds are completely secure. To minimise the potential impact of a breach on the server, it is a good idea to implement a hot / cold wallet system similar to what the exchanges use. The hot wallet being the one which has credentials stored in the server. In this case only the funds in the hot wallet at the time of the breach would be compromised, while the private key and the account would remain safe for resuming operations after the problem is solved.
The issue is KMS is not designed with the purpose of blockchain in mind. It basically gives us access to two useful functions: One that returns the public key, and one that signs a given byte array with the private key and returns the result.
While this is enough to sign a transaction, the process of generating the specific data to be signed, and generating a valid transaction in the exact format Ripple expects it, is not so straight forward as it could seem.
Luckily for us, this problem has been solved for the Ethereum blockchain, which gives us very useful code to be adapted to XRPL. The way the signature is generated, however, is different in some details. While we will explain everything needed in this article, it is still interesting to read the explanations in the Ethereum article, as it provides some good explanations and it can be interesting to learn the differences between blockchain protocols. It is recommended to have a basic understanding of elliptic curve cryptography to fully understand this article and the code. Here is a video explaining the basics:
1. Set up a key on KMS
The first thing we need is to set up a key in AWS KMS. We need to use a ECC_SECG_P256K1 asymmetric key, which will allow us to generate ECDSA secp256k1 signatures.
You will then need to generate an access key id and secret with access to the KMS calls needed. These are the credentials that need to be well protected, as anyone with access to them can call KMS to sign data with your keys.
2. Getting the XRPL account address
KMS gives us access to the public key (never to the private key). From the public key it is possible to get the address, to be able to receive transactions, however it is not as straightforward as it is for Ethereum. KMS returns the public key in uncompressed form, which is what Ethereum uses, but Ripple uses the public keys in compressed form. In Elliptic curve cryptography, a public key represents a point in 2 dimensions, which satisfies the curve equation. The uncompressed form of a public key is expressed by a starting “04” byte (RFC 5480). Because of the properties of elliptic curves, at a certain x axis, only 2 values for y will satisfy the equation for the curve, since elliptic curves are symmetric on the x axis. One of them will be odd and the other will be even. The compressed form of the public key consists of a first byte “02” or “03” depending on if the y is odd or even, and the x axis, which generates a public key half as long. To get the compressed form of the public key, we need to parse the uncompressed form from the AWS response, and then convert it to the compressed form, to be turned into the address.
Finding the address then is easier, as we can make use of the xrpl library, which exports deriveAddress(publicKey).
3. Generating and signing a transaction
First, we will define a transaction that we wish to sign:
With elliptic curve cryptography, what is signed is a hash of the data we want to sign. In our case, the hash used by Ripple is sha256. We first use an xrpl function which will generate the hex payload to be signed from the transaction, and then we hash this payload which will give us the hash to be signed. Note that it is important to convert the hex payload into a Buffer before hashing, or the signature generated will not be valid. We then can send the last 32 characters of the hash to KMS to be signed.
Once we get back the signature from KMS, we are still not done, as a particular peculiarity of EC signatures makes it so you can find 2 different valid signatures by changing the sign of S. This could allow for replay attacks by sending the same transaction with the inverted signature. The solution most blockchains have implemented consists on only accepting signatures with an S value below the middle of the curve, so only one of the two signatures will be accepted by the network. KMS does not handle this for us, so we need to check and invert the value of S if needed.
We then convert the R and S values back into DER format, which is the signature format expected by Ripple.
If all the steps have been followed exactly, we should have a valid Ripple signature for our transaction. And we only need to add it to the transaction object, and encode it into a transaction payload to be broadcasted to the blockchain.
4. Broadcasting the transaction
The only step left is to broadcast the transaction to the network. If everything is correct the transaction will go through and we will have successfully broadcasted a transaction to the Ripple network that was signed on a KMS hardware module. After a successful execution of our test script, we should see something similar to this:
Note that we first need to fund at least 10XRP to the account before it can broadcast transactions, otherwise an error will be thrown.
Code
I have created an npm package with all the code needed to sign xrpl transactions on kms, and published it as xrpl-kms. The code and examples are in the following repository: