HSMs For Cryptos: Bitcoin, ETH & Stuff
Using HSMs for Crypto Wallets Creation, Storage and Transactions Signing for Bitcoin, Ethereum
Introduction
We use HSMs in critical businesses to issue, protect, hide the digital keys & certificates.
Hardware Security Modules protect data, identities, and transactions within the network by strengthening encryption processes as they are built to maintain secure cryptographic key generation, storage, and management mechanisms for various blockchains.
Background
When it is about creating crypto assets custody service, or an active based secure systems to hold crypto-assets, HSMs are a viable option.
Using the Code
Before using the code, you need to setup your Hardware Security Module.
Soft-HSM Setup
For the purpose of demo, we will be using an opensource Software Base HSM:
The OpenDnsSec soft-hsm SoftHSMv2-GIT handles and stores its cryptographic keys via the PKCS#11 interface.
The PKCS#11 interface spec defines how to communicate with cryptographic devices HSMs or smart cards...
SoftHSMv2 Installation
OpenDnsSec, you can get it on Windows/Linux(Ubuntu):
- Windows: Download Installer
- For Linux (Ubuntu):
$ sudo apt-get install -y softhsm2 opensc $ cat <<EOF | sudo tee /etc/softhsm/softhsm2.conf directories.tokendir = /var/lib/softhsm/tokens objectstore.backend = file log.level = DEBUG slots.removable = false EOF $ sudo mkdir /var/lib/softhsm/tokens $ sudo chown root:softhsm $_ $ sudo chmod 0770 /var/lib/softhsm/tokens $ sudo usermod -G softhsm keyless $ sudo usermod -G softhsm $(whoami) $ echo 'export SOFTHSM2_CONF=/etc/softhsm/softhsm2.conf' | tee -a ~/.profile $ source ~/.profile
SoftHSMv2 Configuration
You can read more about setup/configuration at developers.cloudflare.com.
After installation of SoftHSM2, you check slots configuration, but you will always see at least one present not initialized token: Slot 0.
You cannot use this slot unless you initialize it:
Next on, you need to initialize/create a slot in your HSM that will store and operate over your keys:
Windows
- Make sure to add the below environment variable given your installation directory.
- Within your SoftHSMv2/bin Installation Directory, open the terminal:
> set SOFTHSM2_CONF=C:...\SoftHSM2\etc\softhsm2.conf > set PATH=%PATH%;C:\Users\User...\SoftHSMv2\lib
- After installation of
SoftHSM2
, you can check your slot configuration with option–show-slots
:> softhsm2-util --show-slots
Available slots: Slot 0 Slot info: Description: SoftHSM slot ID 0x0 Manufacturer ID: SoftHSM project Hardware version: 2.6 Firmware version: 2.6 Token present: yes Token info: Manufacturer ID: SoftHSM project Model: SoftHSM v2 Hardware version: 2.6 Firmware version: 2.6 Serial number: Initialized: no User PIN init.: no Label:
- Initialize your slot (you will enter/confirms the pin(s)):
> softhsm2-util.exe --init-token --slot 0 --label "SLOT-1" The token has been initialized and is reassigned to slot 1647141831
Linux/Ubuntu
- After installation of
SoftHSM2
, you can check your slot configuration with option–show-slots
:$ softhsm2-util --show-slots
Available slots: Slot 0 Slot info: Description: SoftHSM slot ID 0x0 Manufacturer ID: SoftHSM project Hardware version: 2.6 Firmware version: 2.6 Token present: yes Token info: Manufacturer ID: SoftHSM project Model: SoftHSM v2 Hardware version: 2.6 Firmware version: 2.6 Serial number: Initialized: no User PIN init.: no Label:
- Initialize your first slot (you will enter/confirms the pin(s))
$ softhsm2-util --init-token --slot 0 --label "SLOT-1" The token has been initialized and is reassigned to slot 1647141831
Now you re-check the newly initialized slots (--show-slots
) and a new extra slot will appear which is prepared to be initialized whenever you need/create another new slot.
Let's Code
Now our HSM is ready, let us jump into some coding.
For this demo, we will be using node-js.
You should be having node/npm installed on your machine.
Create a new project and install required dependencies.
As stated, we are communicating via PKCS#11 interface.
We use graphene-pk11
and other libraries:
- Create an new folder hms-demo.
- Initialize
npm
project and install required node modules:> cd hsm-codeproject-demo > npm init ... > npm install graphene-pk11 > npm install eth-crypto > npm install axios > npm install bitcoinjs-lib > npm install bignumber.js > npm install web3 > npm install ethereumjs-tx > npm install ethereumjs-util
- Create a script codeproject.js and use the code inside:
const graphene = require("graphene-pk11"); const ethUtil = require("ethereumjs-util"); const EthereumTx = require("ethereumjs-tx").Transaction; const BigNumber = require("bignumber.js"); const Web3 = require("web3"); // any eth-rpc-url you own or have access to const HTTP_PROVIDER = 'http://localhost:8085'; const web3 = new Web3(new Web3.providers.HttpProvider(HTTP_PROVIDER)); // HSM INIT const Module = graphene.Module; // HSM LIB linux e.g. /usr/local/lib/softhsm/libsofthsm2.so // windows e.g. C:/SoftHSM2/lib/softhsm2-x64.dll const SOFTHSM_LIB = "C:/SoftHSM2/lib/softhsm2-x64.dll"; const mod = Module.load(SOFTHSM_LIB, "SoftHSM"); // init hsm module mod.initialize(); // load your created slot const M_SLOT = 0; const slot = mod.getSlots(M_SLOT); // get session const session = slot.open( graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION ); // your slot creation pins // login to session const M_PIN = "11111111"; session.login(M_PIN); // post login - show some info console.log("Logged In, HSM info:", { slotLength: mod.getSlots().length, mechanisms: slot.getMechanisms(), manufacturerID: slot.manufacturerID, slotDescription: slot.slotDescription }); // generate a KeyPair public/private ECSDA-P256K1 used for Bitcoin/Ethereum let mID = "<some-id-or-uuid>"; let hsmKeyPair = session.generateKeyPair(graphene.KeyGenMechanism.ECDSA, { id: Buffer.from(mID), label: "<some-label-wallet>", keyType: graphene.KeyType.ECDSA, token: true, verify: true, paramsECDSA: graphene.NamedCurve.getByName("secp256k1").value, }, { id: Buffer.from(mID), keyType: graphene.KeyType.ECDSA, label: "<some-label-wallet>", token: true, sign: true, }); console.log("hsmKeyPair Created"); // you can change some attribute on the generated keys hsmKeyPair.privateKey.setAttribute({ label: "my-other-label" }); hsmKeyPair.publicKey.setAttribute({ label: "my-other-label" }); let canRead = slot.flags & graphene.SlotFlag.TOKEN_PRESENT; // check flags and slots availability if (!canRead) { console.log("abort"); } // look-up your keyPair by id or label or other attributes: // Instance Representing the "Public" Key in HSM are hold its value // Instance Representing the "Private" Key in HSM "not!!" its value let hsmPbKeys = session.find({ class: graphene.ObjectClass.PUBLIC_KEY, id: Buffer.from(mID) }); let hsmPvKeys = session.find({ class: graphene.ObjectClass.PRIVATE_KEY, id: Buffer.from(mID) }); let validKeys = hsmPbKeys.length == hsmPvKeys.length == 1; if (!validKeys) { console.log("abort: validKeys"); return; } // now let us get the Raw Public Key used to create Bitcoin/Ethereum Address let hsmPbKey = hsmPbKeys.items(0); // the HSM Private Key Instance (do not contain the private key value) let hsmPvKey = hsmPvKeys.items(0); // the public key P is a Point on the Curve y2 = x3 + 7 // calculated via multiplying (DOT operation) // the private key e by the curve Generator G. // P = e.G // https://en.bitcoin.it/wiki/Secp256k1 let ecPoint = hsmPbKey.getAttribute('pointEC'); // According to ASN encoded value, the first 3 bytes are //04 - OCTET STRING //41 - Length 65 bytes //For secp256k1 curve it's always 044104 at the beginning if (ecPoint.length === 0 || ecPoint[0] !== 4) { console.log("abort: only uncompressed point format supported"); return; } let rawPublicKey = ecPoint.slice(3, 67); // finally the hex encoded public key let hexPublicKey = rawPublicKey.toString("hex"); // Bitcoin ADDRESS // create a compressed public key const ethCrypto = require("eth-crypto"); const compressedPubKey = ethCrypto.publicKey.compress(hexPublicKey); console.log("compressedPubKey", compressedPubKey); // calculate the Compressed PublicKey Hash // using the HSM - SHA256 // more on address and compressed keys: // https://bitcoin.stackexchange.com/a/3839 const compressedPubKeyHash = session .createDigest("sha256") .once(compressedPubKey) .toString("hex"); console.log("compressedPubKeyHash", compressedPubKeyHash); // using bitcoinjs-lib we can create a bitcoin address as well // for Pay to Script Hash Transaction P2PKH: const bufferPubKey = Buffer.from(compressedPubKey, "hex"); const bitcoin = require("bitcoinjs-lib"); // testnet ADDRESSVERSION 0x6F // mainnet ADDRESSVERSION 0x00 // https://en.bitcoin.it/wiki/Testnet const btcTestAddress = bitcoin.payments.p2pkh({ pubkey: bufferPubKey, network: bitcoin.networks.testnet }).address; console.log("Got btcTestAddress", btcTestAddress); // now let us build a transaction to spend the coins async function spendBitcoinsTestAsync (sourceAddress, receiverAddress, amountToSend) { // SOCHAIN APIs const sochain_network = "BTCTEST"; // 1 btc = 100 000 000 satoshis const satoshiToSend = amountToSend * 100000000; let fee = 0; let inputCount = 0; let outputCount = 2; const unspent = await axios.get( `https://sochain.com/api/v2/get_tx_unspent/${sochain_network}/${sourceAddress}` ); let totalAmountAvailable = 0; let inputs = []; let utxos = unspent.data.data.txs; for (const element of utxos) { let utxo = {}; utxo.satoshis = Math.floor(Number(element.value) * 100000000); utxo.script = element.script_hex; utxo.address = unspent.data.data.address; utxo.txId = element.txid; utxo.outputIndex = element.output_no; totalAmountAvailable += utxo.satoshis; inputCount += 1; inputs.push(utxo); } transactionSize = inputCount * 146 + outputCount * 34 + 10 - inputCount; fee = transactionSize * 20; const txInfo = await axios.get( `https://sochain.com/api/v2/tx/${sochain_network}/${inputs[0].txId}` ); const txHex = txInfo.data.data.tx_hex; // Check if we have enough funds to cover the transaction // and the fees assuming we want to pay 20 satoshis per byte if (totalAmountAvailable - satoshiToSend - fee < 0) { throw new Error("Balance is too low for this transaction"); } const psbt = new bitcoin.Psbt({ network: bitcoin.networks.testnet }); psbt.addInput({ hash: inputs[0].txId, index: inputs[0].outputIndex, nonWitnessUtxo: new Buffer.from(txHex, "hex"), }); psbt.addOutput({ address: receiverAddress, value: satoshiToSend }); // create a KeyPair HSM Wrapper const keyPair = { publicKey: bufferPubKey, sign: (hash) => { let signature = session.createSign("ECDSA", hsmPvKey).once(hash); console.log({ signature }); return signature; }, getPublicKey: () => pubKey, }; // sign and finalize the input(s) given the spend condition and UTXOs psbt.signInput(0, keyPair); psbt.finalizeInput(0); const serializedTx = psbt.extractTransaction().toHex(); console.log("serialized transaction hex", serializedTx); return serializedTx; } // e.g. spend the coins using spendBitcoinsTestAsync // get some testnet faucet coins before on your generated address // spendBitcoinsTestAsync(btcTestAddress, // "2MvvviSm2c8r9UugpvLxusB9QKRdAzqupEf", "0.0001"); /** * Ethereum Address and Transactions Generation */ // Ethereum addresses are generated from the Keccak-256 hash of the public key // and are represented as hexadecimal numbers. let keccak256PublicKeyHex = ethUtil.keccak256(rawPublicKey); // The last 20 bytes of the Keccak-256 hash are used to generate the address let last20Bytes = Buffer.from(keccak256PublicKeyHex, "hex").slice(-20); let ethAddress = `0x${last20Bytes.toString("hex")}`; console.log("Ethereum Address: ", ethAddress); // Ethereum Signature Specs // https://ethereum.stackexchange.com/questions/55245/ // why-is-s-in-transaction-signature-limited-to-n-21 const createEthSig = (data, address, privateKey) => { let flag = true; let tempsig; // the curve order const ORDER = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"; const secp256k1halfN = new BigNumber(ORDER, 16).dividedBy(new BigNumber(2)); while (flag) { // here, we sign using the HSM const sign = session.createSign("ECDSA", privateKey); tempsig = sign.once(data); ss = tempsig.slice(32, 64); s_value = new BigNumber(ss.toString("hex"), 16); if (s_value.isLessThan(secp256k1halfN)) flag = false; } const rs = { r: tempsig.slice(0, 32), s: tempsig.slice(32, 64), }; let v = 27; let pubKey = ethUtil.ecrecover(ethUtil.toBuffer(data), v, rs.r, rs.s); let addrBuf = ethUtil.pubToAddress(pubKey); let recovered = ethUtil.bufferToHex(addrBuf); if (address != recovered) { v = 28; pubKey = ethUtil.ecrecover(ethUtil.toBuffer(data), v, rs.r, rs.s); addrBuf = ethUtil.pubToAddress(pubKey); recovered = ethUtil.bufferToHex(addrBuf); } return { r: rs.r, s: rs.s, v: v }; }; // Generate an ethereum Transaction Sample function createEthTx(to, nonce, value, data, gasPrice = "0x00", gasLimit = 160000, chain = 'rinkeby') { // address signature first let address = ethAddress; let addressHash = ethUtil.keccak(address); // get the address signature first using the HSM Private Key let addressSign = createEthSig(addressHash, ethAddress, hsmPvKey); let txParams = { nonce: web3.utils.toHex(nonce), gasPrice, gasLimit, to, value: web3.utils.toBN(value), data: data || "0x00", r: addressSign.r, s: addressSign.s, v: addressSign.v, }; let tx = new EthereumTx(txParams, { chain }); let txHash = tx.hash(false); // RAW TX SIG let txSig = createEthSig(txHash, address, hsmPvKey); tx.r = txSig.r; tx.s = txSig.s; tx.v = txSig.v; let serializedTx = tx.serialize().toString("hex"); return serializedTx; } /** our earlier generated address: ------------------------------ from = ethAddress; some address/smart-contract: ---------------------------- to = '0xabe61b960d7c3f6802b21a130655497a14f2a8de'; the tx count of the 'from' address: ----------------------------------- nonce = 0; The ETH amount: --------------- value = '0'; The data - smartcontract method call etc: ----------------------------------------- data = '0x45123123..3213'; */ let signedEthTx = createEthTx ('0xabe61b960d7c3f6802b21a130655497a14f2a8de', '0', '0'); console.log("eth serializedTx", signedEthTx);
- Finally test your code:
On Windows, open the command line:
> SET SOFTHSM2_CONF=C:\Users\User\Desktop\dgc\foo-hsm-kit\SoftHSM2\etc\softhsm2.conf
> SET PATH=%PATH%;C:\Users\User\Desktop\dgc\foo-hsm-kit\SoftHSM2\lib\
> SET NODE_OPTIONS=--openssl-legacy-provider
> node codeproject
On Linux too:
> SET NODE_OPTIONS=--openssl-legacy-provider
> node codeproject
Your output execution:
Logged In, HSM info: {
slotLength: 2,
mechanisms: MechanismCollection {
lib: PKCS11 {
libPath: 'C:/Users/User/Desktop/dgc/foo-hsm-kit/SoftHSM2/lib/softhsm2-x64.dll'
},
innerItems: [
528, 544, 597, 592, 608, 624, 529, 545, 598,
593, 609, 625, 0, 1, 3, 5, 6, 9,
70, 64, 65, 66, 13, 14, 71, 67, 68,
69, 848, 288, 304, 305, 289, 290, 293, 4352,
4353, 306, 307, 310, 4354, 4355, 312, 4224, 4225,
4226, 4229, 4230, 4231, 8457, 8458, 4356, 4357, 4234,
8192, 16, 17, 18, 19, 20, 21, 22, 32,
8193, 33, 4160, 4161, 4176
],
classType: [class Mechanism extends HandleObject],
slotHandle: <Buffer 0e c6 30 79>
},
manufacturerID: 'SoftHSM project',
slotDescription: 'SoftHSM slot ID 0x7930c60e'
}
hsmKeyPair Created
secp256k1 unavailable, reverting to browser version
compressedPubKey 0344c092a1d92175700f694fd32db9c7e0151f042508dbed7a14042e59ad93fdb4
compressedPubKeyHash 76faa05be53b0e1eab3b6c5ac239d8194a95cf84bc9cb13b7ecd34a9ef5fdeb4
Got btcTestAddress mg9wZ6RsLBHYQKi5MCSSmkx1sYtLdm1q2g
Ethereum Address: 0x02c3bd3d8f698bf478604e05727a9d97928b5704
eth serializedTx f86080808302710094abe61b960d7c3f6802b21a130655497a14f2a8de80001
ca0c595946ab33613e01afac41ff500f6c9bb8316c4042ed1d575b4f31bddca25bea07cb2ee697c8
a1c08882da2e31fdb07bd47e6f7ebbff79754a5d6506b4ac2f8e7
- For bitcoin test transaction, you need to get some testnet coins and fillup your created wallet.
- Then, use the
spendBitcoinsTestAsync( )
to test its behavior:on testnet: ----------- const sourceAddress = 'mtFp8rJLcF1c6qyKnLLwMt23657SzDjao2'; const amountToSend = "0.0001"; const receiverAddress = "2MvvviSm2c8r9UugpvLxusB9QKRdAzqupEf"; const satoshiToSend = amountToSend * 100000000; serializedTx: '02000000012282533562981f8423b4e767d881b6f1d0f22d54bd 7cfdf593f335a655a0ab5a000000006c493046022100f7967355 fc00d2c9a50a9365714e84d2ea165789579e411f854d6e4f3e8d b5a8022100a94c1a3597119c9ff14af8d53bf776d4bf2587508c b2f062176fdbb40b91af7901210365db9da3f8a260078a7e8f8b 708a1161468fb2323ffda5ec16b261ec1056f455ffffffff0110 2700000000000017a914286a98d37345a4cd6952f8dd81db294c 92f47f758700000000'
Points of Interest
This is a base script handling the HSM-Crypto Bitcoin/Ethereum secp256K1 curve operations:
- Create and store keypairs
- Generate required ecda signature
You can use it or build on top for special requirements and needs.
If you like this article, do not miss giving it an upvote, your feedback is much appreciated!
History
- 28th October, 2022: Initial version