Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / security / blockchain

HSMs For Cryptos: Bitcoin, ETH & Stuff

4.67/5 (3 votes)
28 Oct 2022CPOL2 min read 5.7K   45  
Using HSMs for Crypto Wallets Creation, Storage and Transactions Signing for Bitcoin, Ethereum
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.

Image 1

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):

Image 2

  • Windows: Download Installer
  • For Linux (Ubuntu):
    BAT
    $ 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:
    BAT
    > 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:
    JavaScript
    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:

BAT
> 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:

BAT
> 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)