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:
-chain/javascript-sdk
$ npm install @binance
$ 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]
-
b1
is a single byte value specifying the length of the rest of the data, starting from the first0x02
byte lr
is the length ofvr
-
vr
is the actualr
value of the signature ls
is the length ofvs
-
vs
is the actuals
value of the signature
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');