6

Does somebody knows how to properly use AWS CloudHSM to sign ethereum transactions (Keccak256)?

3
  • 2
    yup I've done it with colleagues - will try and post later.
    – Woodstock
    Jul 25 '19 at 14:04
  • 1
    @Woodstock I'm interested in AWS CloudHSM if you have some links it will be helpful.
    – Ismael
    Jul 26 '19 at 17:56
  • 1
    OK created an answer
    – Woodstock
    Jul 28 '19 at 8:50
11

OK,

so when we (me and the ShaneT) implemented hardware signing (private key stored exclusively on AWS CloudHSM) there were a number of things we learned. Hopefully this gives you a guide on how you can achieve it. Unfortunately I can't directly share the code as it's my firm's IP.

Here is the actual tx we first made using AWS CloudHSM on mainnet :)

Considerations:

  • Most HSM products support a proprietary interface and a generic interface called PKCS#11. PKCS#11 is just a fancy term for a standard way to talk to a HSM.

  • AWS supports both prop and PKCS#11 interaction, you should use PKCS#11.

  • Keccak256 is irrelevant with regard to HSM, it's the fact that AWS supports the SECP256k1 curve that allows it to sign ETH transactions.

  • ECDSA (elliptic curve digital signature algo) is bigger than ETH or BTC. i.e. you may hit hurdles where AWS CloudHSM is returning valid ECDSA signatures, however they are invalid Ethereum transactions, such as if the HSM returns a sig with a high S value.

  • There exists many libs in Web3, however JS is the predominant language, don't fight it :)

Approximate guide to implementation:

  • Follow this guide meticulously in order to setup the infrastructure/HSM. If you skip even a little you are gonna get rekt. p.s. I used Amazon Linux 2, it's easier.

  • Use this (Graphene) lib for JS PKCS#11, I tried them all this is the one to use imo.

  • If you can use ecrecover with an r, s, and v value that results in the calculation of the correct Eth address you've done it.

Steps to Signature:

1) Open connection to HSM create a public private key pair using SECP256k1.

2) Retrieve the public key for the pair and calculate the corresponding ETH address.

3) You now have a Eth address for which the HSM owns the private key.

4) Create a tx like so:

  const txParams = {
    nonce: '0x' + nonce.toString(16),
    gasPrice: '0x09184e72a00',
    gasLimit: '0x27100',
    to: '0x4D8519890C77217A352d3cC978B0b74165154421', 
    value: web3.utils.toHex(web3.utils.toWei('0.01', 'ether')),
    chainId: 4
  };

5) With a handle to the private key request signature, remember you're signing a hash of the payload:

  const sign = session.createSign('ECDSA', yourPrivateKey);
  const sig = sign.once(msgHash);

6) Remember, r and s are the first and second 32 bytes of the ECDSA sig, v is a calculated value and ethereum bound concept representing chainID.

  const rs = {
    r: sig.slice(0, 32),
    s: sig.slice(32, 64)
  };

7) CRITICAL: Due to EIP-2 you must loop until you achieve a ECDSA sig with an s value on the right side of the curve, or else it will be a bad Eth tx.

i.e. where s < curve.n/2

if (s > curve.n / 2) id = id ^ 1; // Invert id if s of signature is over half the n

8) Assuming all is implemented correctly you should be able to use ecrecover to check the validity of the HSM created sig.

if (ethUtil.ecrecover(msgHash, 27, rsvOdd.r, rsvOdd.s).toString('hex') === yourETHPublicKeyString)

If that's all good, then you've done it! You can now submit your tx and it should be mined.

This sounds tricky but it's totally doable.

Questions, just shout.

12
  • 2
    Rather than looping until you get an s which satisfies EIP-2, you can just take s = curve.n - s, and flip the v value. From EIP-2 "Allowing transactions with any s value with 0 < s < secp256k1n, as is currently the case, opens a transaction malleability concern, as one can take any transaction, flip the s value from s to secp256k1n - s, flip the v value (27 -> 28, 28 -> 27), and the resulting signature would still be valid." The point of EIP-2 is to mandate one of these two valid signatures. Aug 27 '19 at 20:14
  • I did exactly the same, but my ethaddress generating during signing is different every single time. Any idea why ? Dec 20 '19 at 7:28
  • 1
    So, R and S are the outputs of the ECDSA signature process. Sometimes the S value can randomly be on the 'wrong side' on the curve. This is not wrong from an ECDSA POV, but is from an EIP POV. So you must loop until you get an S value which is not 'high'. How are you calculating V? @user2805885 Rather than looping until you get an s which satisfies EIP-2, you can just take s = curve.n - s, and flip the v value.
    – Woodstock
    Dec 23 '19 at 17:34
  • 1
    Wow. Thanks alot for the input. I was able to resolve the issue.I am using Gemalto SafeNet Luna PCIe HSM and it worked. Dec 24 '19 at 2:19
  • 1
    For future readers trying to ensure S is below the halfn of the curve (for Ethereum's curve, secp256k1), its value (curve.n/2) is 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 May 15 '20 at 23:19
3

I had implemented a solution based @Woodstock steps using SoftHSM and Graphene. The source code is available in https://github.com/wshbair/HSM2ETH

Hope this will help

3
  • Did you try this with AWS Cloud HSM that uses Cavium? Mar 17 '20 at 14:56
  • Not yet, but I think as the AWS CloudHSM is accessed via PKCS#11 it should work. Mar 18 '20 at 0:22
  • @WazenShbair exactly! nice work, clean code.
    – Woodstock
    Apr 28 '20 at 8:37
2

I published a medium article about this integration.

Please also see my working code on github. This integration uses AWS KMS (Cloud HSM behind the scenes).

One thing that some of the other answers do not mention is that you don't have to keep looping through signatures to find a valid one. You can invert the result if you're on the "wrong" side of the curve. See example below.

let secp256k1N = new BN("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16); // max value on the curve
let secp256k1halfN = secp256k1N.div(new BN(2)); // half of the curve
// Because of EIP-2 not all elliptic curve signatures are accepted
// the value of s needs to be SMALLER than half of the curve
// i.e. we need to flip s if it's greater than half of the curve
if (s.gt(secp256k1halfN)) {
    console.log("s is on the wrong side of the curve... flipping - tempsig: " + tempsig + " length: " + tempsig.length);
    // According to EIP2 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2.md
    // if s < half the curve we need to invert it 
    // s = curve.n - s
    s = secp256k1N.sub(s);
    console.log("new s: " + s.toString(10));
    return { r, s }
}
// if s is less than half of the curve, we're on the "good" side of the curve, we can just return
return { r, s }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.