Binance Chain Key and Signature Conversion

Binance Chain Key and Signature Conversion

these are some notes i wrote during my internship in 2019 while trying to integrate AWS CloudHSM with Binance Chain's JS SDK

Setup

You can do this in a clean folder without needing to clone any repository, or if you want to save space, open up the node CLI from a project folder that already has @binance-chain/javascript-sdk as a dependency installed.

Convert Mnemonic into PEM Private key

Reference: https://bitcoin.stackexchange.com/a/66622, https://stackoverflow.com/a/48102827/5740053

To convert a seed phrase into a private key (to be imported to the HSM), do the following commands:

$ npm install @binance-chain/javascript-sdk
$ node
> const B = require('@binance-chain/javascript-sdk');
> B.crypto.getPrivateKeyFromMnemonic('insert the mnemonic here');
'a0d2c4d6e87b9594905848bcefb47bc38d2a24b4a6f42541e0b817b597b8e026'

In this example, a0d2c4d6e87b9594905848bcefb47bc38d2a24b4a6f42541e0b817b597b8e026 is the raw private key. We need to convert it to a format that OpenSSL can recognize.

echo 302e0201010420 a0d2c4d6e87b9594905848bcefb47bc38d2a24b4a6f42541e0b817b597b8e026 a00706052b8104000a | xxd -p -r | openssl ec -inform der

302e0201010420 is the prefix for a DER-formatted private key, and a00706052b8104000a is the suffix. Those bytes can be treated as constant. We pipe that into xxd -p -r to convert it to binary, then we pipe the binary DER key into openssl ec -inform der, where ec is for elliptic curve keys, and -inform der specifies the input format.

You should get an output like this:

read EC key
writing EC key
-----BEGIN EC PRIVATE KEY-----
MC4CAQEEIKDSxNboe5WUkFhIvO+0e8ONKiS0pvQlQeC4F7WXuOAmoAcGBSuBBAAK
-----END EC PRIVATE KEY-----

The public key can also be derived by modifying the flags for openssl in the previous command into:

openssl ec -inform der -pubout -text -conv_form compressed

You should get an output like this:

read EC key
Private-Key: (256 bit)
priv:
    a0:d2:c4:d6:e8:7b:95:94:90:58:48:bc:ef:b4:7b:
    c3:8d:2a:24:b4:a6:f4:25:41:e0:b8:17:b5:97:b8:
    e0:26
pub:
    03:7e:9d:e6:9d:02:a0:86:23:f8:68:15:90:31:80:
    b6:28:3f:30:13:b5:16:f4:ba:ba:52:9f:76:62:1c:
    d1:d2:1a
ASN1 OID: secp256k1
writing EC key
-----BEGIN PUBLIC KEY-----
MDYwEAYHKoZIzj0CAQYFK4EEAAoDIgADfp3mnQKghiP4aBWQMYC2KD8wE7UW9Lq6
Up92YhzR0ho=
-----END PUBLIC KEY-----

To generate a Binance address from the public key, run the following:

$ node
> const B = require('@binance-chain/javascript-sdk');
> B.crypto.getAddressFromPublicKey('037e9de69d02a08623f86815903180b6283f3013b516f4baba529f76621cd1d21a')
'tbnb1whgs68y3kwesskej4mqmk858u9kgynkrn6zg2r'
> // Set the prefix for Mainnet - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VVV
> B.crypto.getAddressFromPublicKey('037e9de69d02a08623f86815903180b6283f3013b516f4baba529f76621cd1d21a', 'bnb')
'bnb1whgs68y3kwesskej4mqmk858u9kgynkra0tv2j' //                                                           ^^^

Convert CloudHSM's DER signature to Binance

Binance's Javascript API accepts <r,s> values for the signature when using a SigningDelegate. By default, CloudHSM provides a DER-encoded signature when signing through the key_mgmt_util, so we have to convert it.

Note that signatures generated when using the PKCS#11 library will already be in the correct <r,s> format, but still needs to be checked for compliance with BIP-0062.

DER signatures have the following structure:

0x30 [b1] 0x02 [lr] [vr] 0x02 [ls] [vs]

If vr or vs is more than 32 bytes, it is safe to simply drop the first byte. They have to be 32 bytes long.

To convert it to a Binance-compatible signature, we need the following structure:

[vr] [vs]

BIP-0062 Compliance

https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#low-s-values-in-signatures

Based on BIP-0062, the s value in signatures have to be between 0x1 and 0x7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 5D576E73 57A4501D DFE92F46 681B20A0 (inclusive). If the s value is too high, it needs to be replaced:

s' = 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141 - s

Extracting the <r,s> values using OpenSSL

It is possible to extract the r and s values using OpenSSL with the following command

$ xxd -ps test.sig | xargs | sed 's/ //g'
3045022018a07af46a749c641056c8bcd290aff90bd041f6ee145d70b9940c0d2ae0100a02210098dfa5a2e4630684dcefb17b9a92782eb802442ee3e0e8123d67d9b138706a4b
| | | | |                                                               | | |
| | | |  \ - r value                                                    | |  \ - s value
| | |  \ - length of r value                                            |  \ - length of s value
| |  \ - marker                                                          \ - marker
|  \ - length of the rest of the data
 \ - indicates DER-formatted signature
$ openssl asn1parse -inform DER -in test.sig
    0:d=0  hl=2 l=  69 cons: SEQUENCE
    2:d=1  hl=2 l=  32 prim: INTEGER           :18A07AF46A749C641056C8BCD290AFF90BD041F6EE145D70B9940C0D2AE0100A
   36:d=1  hl=2 l=  33 prim: INTEGER           :98DFA5A2E4630684DCEFB17B9A92782EB802442EE3E0E8123D67D9B138706A4B

In the above example, notice how the s value is 33 bytes long. OpenSSL drops the first 00 byte safely. In the same example, the s value is larger than 0x7FFFFFFF ..., so we need to get a lower value.

0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
0x98DFA5A2E4630684DCEFB17B9A92782EB802442EE3E0E8123D67D9B138706A4B
__________________________________________________________________ subtract
0x67205a5d1b9cf97b23104e84656d87d002ac98b7cb67b829826a84db97c5d6f6

Hence, the new combined <r,s> values are:

18A07AF46A749C641056C8BCD290AFF90BD041F6EE145D70B9940C0D2AE0100A 67205a5d1b9cf97b23104e84656d87d002ac98b7cb67b829826a84db97c5d6f6

Optionally, the signature can be converted to a Node.js Buffer for other uses:

const signature = Buffer.from('18A07A...C5D6F6', 'hex');