Node.js SDK

Obtaining the SDK

You first need a username and a password to Blockdaemon Builder Vault's Nexus repository. Then you can add your credentials to your ~/.npmrc file using these commands:

npm config set @sepior:registry=https://nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/
npm config set //nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/:username=YOURUSERNAME
npm config set //nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/:_password=`echo -n 'YOURPASSWORD' | base64`
npm config set //nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/:email=MAILADDRESS

Note the backticks when setting the password, this will execute the command inside, when in a terminal.

In older versions of NPM you may also need this:

npm config set //nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/:always-auth=true 

Once authentication is set up in the ~/.npmrc file you can pull the TSM SDK as follows:

npm install @sepior/tsm

Example: Generating a Simple Signature

You can connect to a TSM, generate a key, and sign a messages as follows (assuming a TSM with two MPC nodes):

const { TSMClient, algorithms, curves } = require('@sepior/tsm');
const crypto = require("crypto");

let example = async function() {

  // Remember to change player count and threshold to match your TSM configuration
  let playerCount = 2;
  let threshold = 1;
  let tsmClient = await TSMClient.init(playerCount, threshold, [
        {
            url: 'http://tsm-node-1',
            userID: 'user',
            password: 'password1'
        },
        {
            url: 'http://tsm-node-2',
            userID: 'user',
            password: 'password2'
        }]);

    // Create a key in the TSM
    let keyID = await tsmClient.keygen(algorithms.EDDSA, curves.ED25519);
  
    // The message hash we want to sign
    const message = Buffer.from("some data to sign");

    // Sign the message hash using the key
    const chainPath = new Uint32Array();
    const signature = await tsmClient.sign(algorithms.EDDSA, keyID, chainPath, message);

    // Get the public key
    let pk = await tsmClient.publicKey(algorithms.EDDSA, keyID, chainPath);
  
    // Check that the signature is valid
    let isValid = await tsmClient.verify(algorithms.EDDSA, pk, message, signature, curves.ED25519);
    console.log("valid?", isValid)

    // Delete the key
    await tsmClient.delete(keyID);
    await tsmClient.close();

}

example();

Example: Partial Signing with Presignatures

In the example above we used a single SDK to control all the MPC nodes in the TSM.

Here we will instead use a TSM with three MPC nodes and a separate SDK for each of the nodes. This is useful for example if each MPC node is hosted by its own organisation, and each organisation wants to inspect the message before it is willing to participate in signature generation. In such a case, we require that each MPC node acknowledges the signature generation via its SDK, before the threshold signature is generated.

The following example also shows the use of presignatures. You can generate a number of presignatures for a given key. This requires several rounds of communication between the MPC nodes. Once presignatures are generated, the actual signing is non-interactive, in the sense that each SDK can obtain a partial signature from its MPC node without any interaction with the other MPC nodes.

Finally, the example shows the use of deterministic key derivation.

const { TSMClient, algorithms, curves } = require('@sepior/tsm');
const crypto = require("crypto");

let example = async function() {

    // Initialize a separate SDK for each MPC node in the TSM
    // Remember to change player count and threshold to match you configuration
    let playerCount = 3;
    let threshold = 1;
    let tsmClient1 = await TSMClient.init(playerCount, threshold, [
        {
            url: 'http://tsm-node-1',
            userID: 'user',
            password: 'password1'
        }]);

    let tsmClient2 = await TSMClient.init(playerCount, threshold, [
        {
            url: 'http://tsm-node-2',
            userID: 'user',
            password: 'password2'
        }]);


    let tsmClient3 = await TSMClient.init(playerCount, threshold, [
        {
            url: 'http://tsm-node-3',
            userID: 'user',
            password: 'password3'
        }]);


    // Step 1: Generate a key in the TSM

    // The three SDKs need to first agree on a unique session ID.
    let sessionID = "session1";

    // Each SDK must call keygenWithSessionID with the session ID.
    let results = await Promise.all([
        tsmClient1.keygenWithSessionID(algorithms.ECDSA, sessionID, curves.SECP256K1),
        tsmClient2.keygenWithSessionID(algorithms.ECDSA, sessionID, curves.SECP256K1),
        tsmClient3.keygenWithSessionID(algorithms.ECDSA, sessionID, curves.SECP256K1)]);

    // As a result of the interactive protocol, each SDK receives the key ID.
    keyID = results[0];
    console.log("Generated key with key ID:", keyID);


    // Step 2: Generate five pre-signatures

    sessionID = "session2"
    presigCount = 5;
    results = await Promise.all([
        tsmClient1.presigGenWithSessionID(algorithms.ECDSA, sessionID, keyID, presigCount),
        tsmClient2.presigGenWithSessionID(algorithms.ECDSA, sessionID, keyID, presigCount),
        tsmClient3.presigGenWithSessionID(algorithms.ECDSA, sessionID, keyID, presigCount)]);
    presigIDs = results[0];
    console.log("Generated presigs with IDs:", presigIDs);

  
    // Step 3: Create partial signatures of a message non-interactively using one of the presignatures.

    // The message hash to sign
    let msg = "some data to sign";
    let sha256 = crypto.createHash('sha256');
    sha256.update(msg);
    let msgHash = sha256.digest();

    // In this example we will not sign with the master key, but with a key derived from the
    // master key using the chain path m/0/3.
    let chainPath = new Uint32Array([0, 3]);

    // Create a set of partial signatures using one of the presignatures.
    // Note that this does not require interaction between the MPC nodes, so we don't have to do this in parallel.
    sessionID = "session3";
    presigID = presigIDs[0];
    let [partialSignature1] = await tsmClient1.partialSignWithPresig(algorithms.ECDSA, keyID, presigID, chainPath, msgHash);
    let [partialSignature2] = await tsmClient2.partialSignWithPresig(algorithms.ECDSA, keyID, presigID, chainPath, msgHash);
    let [partialSignature3] = await tsmClient3.partialSignWithPresig(algorithms.ECDSA, keyID, presigID, chainPath, msgHash);

  
    // Step 4: Combine the partial signatures into the final signature

    let [signature] = await tsmClient1.finalize(algorithms.ECDSA, [partialSignature1, partialSignature2, partialSignature3]);

    // Check that the signature is valid
    let [,pk] = await tsmClient1.publicKey(algorithms.ECDSA, keyID, chainPath);
    let isValid = await tsmClient1.verify(algorithms.ECDSA, pk, msgHash, signature, curves.SECP256K1);
    console.log("Is signature valid?", isValid)

}

example();

Example: Authenticate using mTLS

This example shows how to use mutual TLS authentication with the Node.js SDK against three TSM nodes.

const { TSMClient, algorithms, curves} = require('./tsmclient');

const crypto = require("crypto");
const fs = require("fs");

let example = async function() {
  const clientPrivateKey = fs.readFileSync('./mtlscerts/client/client.key');
  const clientCertificate = fs.readFileSync('./mtlscerts/client/client.crt');
	const serverPrivateKey = fs.readFileSync('./mtlscerts/server/server.key')

	const serverPublicKey = crypto.createPublicKey({
		key: serverPrivateKey,
			format: 'pem'
		});

	const derServerPublicKey = serverPublicKey.export({
  	type: 'spki',
    format: "der"
	});

  const urls = ["https://localhost:8097", "https://localhost:8098", "https://localhost:8099"];

  const mTLSClient = await TSMClient.initMTLS(urls, clientCertificate, clientPrivateKey, derServerPublicKey);

  const ecdsaKeyID = await mTLSClient.keygen(algorithms.ECDSA, curves.SECP256K1);
  console.log(ecdsaKeyID);

  const eddsaKeyID = await mTLSClient.keygen(algorithms.EDDSA, curves.ED448);
  console.log(eddsaKeyID);

  await mTLSClient.delete(ecdsaKeyID);
  await mTLSClient.delete(eddsaKeyID);
  await mTLSClient.close();
}

example();

You can find more examples in node_modules/@sepior/tsm/tsmclient.test.js.

Usage

Constant: algorithms

An enumeration of the available algorithms

Class: TSMClient

The TSMClient handles state and requests for the Blockdaemon Builder Vault TSM.
All the methods return promises.

init(playerCount, threshold, tsmNodes)

tsmClient = await TSMClient.init(playerCount, threshold, nodes);
The static init method sets up a connection to the backend, and the state is kept in the class.

  • playerCount the total number of players that defines the TSM.
  • threshold the security threshold for the TSM
  • tsmNodes an array of objects, one for each backend node, each of which contains:
    • url for the backend node
    • userID the username for the backend node
    • password the password for the backend node

keygen(algorithm, curve)

Generates a key for the algorithm with a specified curve.

  • algorithm an algorithm from the algorithms enumeration
  • curve OPTIONAL a string containing the name of the curve to use, defaults to secp256k1
  • Returns the keyID of the newly generated key

list()

Lists all the keys the user specified in the constructor, is allowed to see.

  • Returns

delete(keyID)

Deletes a key

  • keyID the ID of the key to delete

publicKey(algorithm, keyID)

Get the public key for a given keiID

  • algorithm an algorithm from the algorithms enumeration
  • keyID the ID for which to return the public key
  • Returns a crypto KeyObject

sign(algorithm, keyID, hash)

Signs a hashed value given the algorithm and keyID

  • algorithm an algorithm from the algorithms enumeration
  • keyID the ID of the key to use for signing
  • Returns the signature in DER format

close()

Removes the handles for the Blockdaemon Builder Vault TSM.
Must be called when the TSMClient is no longer in use, to avoid memory leaks

Building for Docker

As an example, the below Dockerfile will build an image containing the NPM package.

FROM node:current

WORKDIR /root

# Set up NPM auth
RUN npm config set @sepior:registry=https://nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/
RUN npm config set //nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/:username=YOURUSERNAME
RUN npm config set //nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/:_password=`echo -n 'YOURPASSWORD' | base64`
RUN npm config set //nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/:email=YOUREMAIL

RUN npm install --verbose @sepior/tsm --registry=https://nexus.sepior.net/repository/sepior-nodejs-tsm-sdk-group/

If you are building a Linux image on Macos with Apple silicon (e.g. M1), you must specify the platform in your build command:

docker build --no-cache --platform=linux/amd64 -t my-image .

Otherwise you will get errors containing

#12 22.15 npm ERR! gyp info spawn args [ 'V=1', 'BUILDTYPE=Release', '-C', 'build' ]
#12 22.15 npm ERR! /usr/lib/gcc/aarch64-alpine-linux-musl/11.2.1/../../../../aarch64-alpine-linux-musl/bin/ld: ../includes/libtsmclient.so(000000.o): Relocations in generic ELF (EM: 62)
#12 22.15 npm ERR! /usr/lib/gcc/aarch64-alpine-linux-musl/11.2.1/../../../../aarch64-alpine-linux-musl/bin/ld: ../includes/libtsmclient.so(000000.o): Relocations in generic ELF (EM: 62)
#12 22.15 npm ERR! /usr/lib/gcc/aarch64-alpine-linux-musl/11.2.1/../../../../aarch64-alpine-linux-musl/bin/ld: ../includes/libtsmclient.so(000000.o): Relocations in generic ELF (EM: 62)
#12 22.15 npm ERR! /usr/lib/gcc/aarch64-alpine-linux-musl/11.2.1/../../../../aarch64-alpine-linux-musl/bin/ld: ../includes/libtsmclient.so(000000.o): Relocations in generic ELF (EM: 62)
#12 22.15 npm ERR! /usr/lib/gcc/aarch64-alpine-linux-musl/11.2.1/../../../../aarch64-alpine-linux-musl/bin/ld: ../includes/libtsmclient.so(000000.o): Relocations in generic ELF (EM: 62)
#12 22.15 npm ERR! /usr/lib/gcc/aarch64-alpine-linux-musl/11.2.1/../../../../aarch64-alpine-linux-musl/bin/ld: ../includes/libtsmclient.so: error adding symbols: file in wrong format
#12 22.15 npm ERR! collect2: error: ld returned 1 exit status