In this article, we will see how to use HSMs in critical businesses to issue, protect, hide the digital keys & certificates.
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):
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");
const HTTP_PROVIDER = 'http://localhost:8085';
const web3 = new Web3(new Web3.providers.HttpProvider(HTTP_PROVIDER));
const Module = graphene.Module;
const SOFTHSM_LIB = "C:/SoftHSM2/lib/softhsm2-x64.dll";
const mod = Module.load(SOFTHSM_LIB, "SoftHSM");
mod.initialize();
const M_SLOT = 0;
const slot = mod.getSlots(M_SLOT);
const session = slot.open(
graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION
);
const M_PIN = "11111111";
session.login(M_PIN);
console.log("Logged In, HSM info:", {
slotLength: mod.getSlots().length,
mechanisms: slot.getMechanisms(),
manufacturerID: slot.manufacturerID,
slotDescription: slot.slotDescription
});
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");
hsmKeyPair.privateKey.setAttribute({
label: "my-other-label"
});
hsmKeyPair.publicKey.setAttribute({
label: "my-other-label"
});
let canRead = slot.flags & graphene.SlotFlag.TOKEN_PRESENT;
if (!canRead) {
console.log("abort");
}
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;
}
let hsmPbKey = hsmPbKeys.items(0);
let hsmPvKey = hsmPvKeys.items(0);
let ecPoint = hsmPbKey.getAttribute('pointEC');
if (ecPoint.length === 0 || ecPoint[0] !== 4) {
console.log("abort: only uncompressed point format supported");
return;
}
let rawPublicKey = ecPoint.slice(3, 67);
let hexPublicKey = rawPublicKey.toString("hex");
const ethCrypto = require("eth-crypto");
const compressedPubKey = ethCrypto.publicKey.compress(hexPublicKey);
console.log("compressedPubKey", compressedPubKey);
const compressedPubKeyHash = session
.createDigest("sha256")
.once(compressedPubKey)
.toString("hex");
console.log("compressedPubKeyHash", compressedPubKeyHash);
const bufferPubKey = Buffer.from(compressedPubKey, "hex");
const bitcoin = require("bitcoinjs-lib");
const btcTestAddress = bitcoin.payments.p2pkh({
pubkey: bufferPubKey,
network: bitcoin.networks.testnet
}).address;
console.log("Got btcTestAddress", btcTestAddress);
async function spendBitcoinsTestAsync
(sourceAddress, receiverAddress, amountToSend) {
const sochain_network = "BTCTEST";
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;
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
});
const keyPair = {
publicKey: bufferPubKey,
sign: (hash) => {
let signature = session.createSign("ECDSA", hsmPvKey).once(hash);
console.log({
signature
});
return signature;
},
getPublicKey: () => pubKey,
};
psbt.signInput(0, keyPair);
psbt.finalizeInput(0);
const serializedTx = psbt.extractTransaction().toHex();
console.log("serialized transaction hex", serializedTx);
return serializedTx;
}
let keccak256PublicKeyHex = ethUtil.keccak256(rawPublicKey);
let last20Bytes = Buffer.from(keccak256PublicKeyHex, "hex").slice(-20);
let ethAddress = `0x${last20Bytes.toString("hex")}`;
console.log("Ethereum Address: ", ethAddress);
const createEthSig = (data, address, privateKey) => {
let flag = true;
let tempsig;
const ORDER =
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141";
const secp256k1halfN = new BigNumber(ORDER, 16).dividedBy(new BigNumber(2));
while (flag) {
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
};
};
function createEthTx(to, nonce, value, data, gasPrice = "0x00",
gasLimit = 160000, chain = 'rinkeby') {
let address = ethAddress;
let addressHash = ethUtil.keccak(address);
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);
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;
}
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
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