Key Generation and Signing

Once the SDK is authenticated, it can be used to do operations on the TSM, such as generating keys and signing messages using the generated keys.

MPC Sessions

To generate a key in the TSM, the MPC nodes must be instructed to perform an MPC session. During the MPC session, the MPC nodes will interact with each other, often in several rounds, according to a specific MPC protocol.

To start an MPC session, all MPC nodes that participate in the session must agree on

  • A unique MPC session ID
  • The subset of MPC nodes that should participate in the MPC session

It is up to you to generate the session ID and choose the particular subset of MPC nodes for the MPC session, and you must make sure that this information is available to the SDK of each of the MPC nodes that should participate in the session.

The session ID must be a unique string that fits in the header of an HTTP request. You can use a helper method in the SDK to generate a session ID:

sessionID := tsm.GenerateSessionID()

Each MPC node in the TSM is identified by a unique integer (often numbered 0, 1, 2, 3, and so forth). So if your MPC nodes are numbered 0, 1, 2, 3, then you may, for example, choose the subset (1, 2, 3) for the particular MPC session.

players := []int{1,2,3}

The next step is to request the MPC session on each of the SDKs of the MPC nodes that participate in the MPC session. This is done by first creating a SessionConfig object containing the session ID and the player subset. Then, you call a specific method on the SDK, providing the session configuration as an input parameter.

Key Generation

In our case, we will use the session configuration to run an MPC session that generates a key in the TSM. This is done by calling the GenerateKey() method on the SDK, with the session configuration.

In the call to GenerateKey() an additional parameter threshold must also be provided. This is the security threshold for the key to be generated. The MPC session will generate the key as a secret sharing among the MPC nodes in the TSM, and a security threshold of t means that a secret sharing will be generated that keeps the key secret even if an attacker manages to steal up to t of the key shares.

To summarize, the MPC key generation session between MPC Node 1, 2, and 3 is started by invoking this on the SDKs controlling the three MPC nodes:

threshold := 1  // The security threshold of the key
context := context.Background()
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
curveName := "secp256k1"
keyID, err := client.ECDSA().GenerateKey(context, sessionConfig, threshold, curveName, "")

When GenerateKey() is called on the SDK, and it forwards the key generation request to its MPC node.

Importantly, the actual MPC session only starts when all the MPC nodes in the MPC session have received a key generation request from their respective SDKs. When an MPC node receives a request for an MPC session, it sends a message to the other MPC nodes in the session, and it only proceeds with the MPC session when it has received a message from all other MPC nodes in the session.

As a consequence, if only some of the MPC nodes in the session request the MPC session via their SDK, the MPC nodes will wait and time out after a while, returning an error message to the SDK. The default timeout is 10 seconds, which can be configured in the MPC node configuration files.

As a part of the MPC session, the MPC nodes will check that they agree on the threshold and the curveName. So, in addition to agreeing on the session ID and the player subset, the SDK operators must also make sure they use the same values for these parameters before calling the SDK.

If the MPC session is successful, the key will be generated as a secret shared among the MPC nodes, and each SDK will receive the new key's key ID.

Obtaining the Public Key

After key generation, the public key can be obtained like this:

publicKeys, err = client.ECDSA().PublicKey(ctx, keyID, nil)

📘

When to Trust the Public Key

The key generation protocol ensures that honest MPC nodes that complete the protocol without an abort will hold a correct share of the private key as well as a copy of the correct public key in its database. But the protocol does not guarantee that all honest MPC nodes will complete the protocol without an abort. In the worst case, a malicious MPC node may cause all or most of the honest MPC nodes to abort before the key generation session finishes, meaning that they will not get to store their key shares in their databases.

The PublicKey() method simply fetches the public key from a single MPC node. So it is important to use the public key, e.g., for creating a wallet address that is handed to users, only after you have received a message from all MPC nodes stating that they have succeeded the key generation session without an abort. Otherwise there may not be enough MPC nodes holding key shares of the private key in order to generate signatures. A good way to do this is to collect the public key from all MPC nodes in your application and then check that they are all equal.

Signing

Once a key has been generated, each SDK now holds a key ID, and it is ready to sign messages using the key.

Like key generation, signatures are generated by running an MPC session. Once again, you must generate a session ID and choose a subset of MPC nodes. The nodes should be chosen among the nodes that participated in the key generation. You should also pick the message hash to be signed, for example:

sessionID := tsm.GenerateSessionID()
players := []int{1,2}
message := []byte("This message could be a transaction to be signed")
msgHash := sha256.Sum256(message)

You must now propagate this information to each of the SDK operators, in this example, the operators of the SDK for Node 1 and Node 2. They must each request the MPC session on their respective SDK using the key ID obtained from the key generation.

context := context.Background()
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
curveName := "secp256k1"
partialSignResult, err := client.ECDSA().Sign(context, sessionConfig, keyID, nil, msgHash[:])

📘

Enforcing a Signing Policy

As with key generation, the MPC session only takes place if all MPC nodes request this to happen. This allows you, in your application, to implement various signing policies. For example, the SDK controlling MPC node 1 may enforce a policy of transferring at most 10 BTC each day, while MPC Node 0 will require a user to press a button after inspecting the transaction details. These policies can be enforced simply by making sure that the SDKs of the MPC nodes only request the MPC signing session by calling Sign() on their SDK, once they have applied the policy.

If the signature generation MPC session is successful, each SDK obtains a partial signature. Your application must collect the partial signatures, and once collected, they can be combined into the final signature:

signature, err := tsm.ECDSAFinalizeSignature(msgHash[:], partialSignatures)

If msgHash is provided to ECDSAFinalizeSignature() the final signature is also validated. This lets you detect an invalid signature in the case where one of the MPC nodes was malicious and returned a bad partial signature.

A Complete Example

This section contains a complete example of generating a key, obtaining the public key, and using the key to sign a message.

The example assumes that a TSM is already running and that the MPC nodes are reachable at the addresses http://localhost:8500, http://localhost:8501, http://localhost:8502. You can follow the tutorial here to set up a TSM locally like this, or you can use the demo TSM hosted by Blockdaemon, as described here.

Note that the SDKs are invoked concurrently to start the key generation and signing MPC sessions in the example. This is because each call to the SDK blocks until all SDKs in the MPC session are called. In many real-world cases, the SDKs will run in different environments, e.g., on different servers or mobile devices.

package main

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"gitlab.com/Blockdaemon/go-tsm-sdkv2/v68/tsm"
	"golang.org/x/sync/errgroup"
	"sync"
)

func main() {

	// Create clients for each of the nodes

	configs := []*tsm.Configuration{
		tsm.Configuration{URL: "http://localhost:8500"}.WithAPIKeyAuthentication("apikey0"),
		tsm.Configuration{URL: "http://localhost:8501"}.WithAPIKeyAuthentication("apikey1"),
		tsm.Configuration{URL: "http://localhost:8502"}.WithAPIKeyAuthentication("apikey2"),
	}

	clients := make([]*tsm.Client, len(configs))
	for i, config := range configs {
		var err error
		if clients[i], err = tsm.NewClient(config); err != nil {
			panic(err)
		}
	}

	// Generate an ECDSA key

	threshold := 1                  // The security threshold for this key
	keyGenPlayers := []int{0, 1, 2} // The key should be secret shared among all three MPC nodes
	keyGenSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), keyGenPlayers, nil)

	fmt.Println("Generating key using players", keyGenPlayers)
	ctx := context.Background()
	keyIDs := make([]string, len(clients))
	var eg errgroup.Group
	for i, client := range clients {
		client, i := client, i
		eg.Go(func() error {
			var err error
			keyIDs[i], err = client.ECDSA().GenerateKey(ctx, keyGenSessionConfig, threshold, "secp256k1", "")
			return err
		})
	}

	if err := eg.Wait(); err != nil {
		panic(err)
	}

	// Validate key IDs

	for i := 1; i < len(keyIDs); i++ {
		if keyIDs[0] != keyIDs[i] {
			panic("key IDs do not match")
		}
	}
	keyID := keyIDs[0]
	fmt.Println("Generated key with ID:", keyID)

	// Get the public key

	var derivationPath []uint32 = nil // We don't use key derivation in this example

	publicKeys := make([][]byte, len(clients))
	for i, client := range clients {
		var err error
		publicKeys[i], err = client.ECDSA().PublicKey(ctx, keyID, derivationPath)
		if err != nil {
			panic(err)
		}
	}

	// Validate public keys

	for i := 1; i < len(publicKeys); i++ {
		if !bytes.Equal(publicKeys[0], publicKeys[i]) {
			panic("public keys do not match")
		}
	}
	publicKey := publicKeys[0]
	fmt.Println("Public key:", hex.EncodeToString(publicKey))

	// We can now sign with the created key

	message := []byte("This is a message to be signed")
	msgHash := sha256.Sum256(message)

	signPlayers := []int{0, 1} // We want to sign with the first two MPC nodes
	sessionID := tsm.GenerateSessionID()
	signSessionConfig := tsm.NewSessionConfig(sessionID, signPlayers, nil)

	fmt.Println("Creating signature using players", signPlayers)
	partialSignaturesLock := sync.Mutex{}
	var partialSignatures [][]byte
	for _, player := range signPlayers {
		player := player
		eg.Go(func() error {
			if partialSignResult, err := clients[player].ECDSA().Sign(ctx, signSessionConfig, keyID, derivationPath, msgHash[:]); err != nil {
				return err
			} else {
				partialSignaturesLock.Lock()
				partialSignatures = append(partialSignatures, partialSignResult.PartialSignature)
				partialSignaturesLock.Unlock()
				return nil
			}
		})
	}

	if err := eg.Wait(); err != nil {
		panic(err)
	}

	signature, err := tsm.ECDSAFinalizeSignature(msgHash[:], partialSignatures)
	if err != nil {
		panic(err)
	}

	// Verify the signature relative to the signed message and the public key

	if err = tsm.ECDSAVerifySignature(publicKey, msgHash[:], signature.ASN1()); err != nil {
		panic(err)
	}

	fmt.Println("Signature:", hex.EncodeToString(signature.ASN1()))
}
package com.example;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import com.sepior.tsm.sdkv2.Configuration;
import com.sepior.tsm.sdkv2.Client;
import com.sepior.tsm.sdkv2.Ecdsa;
import com.sepior.tsm.sdkv2.SessionConfig;
import com.sepior.tsm.sdkv2.EcdsaPartialSignResult;
import com.sepior.tsm.sdkv2.EcdsaSignature;

public class EcdsaSignExample {

    public static void main(String[] args) throws Exception {

        // Create a client for each MPC node

        Configuration[] configs = {
            new Configuration("http://localhost:8500"),
            new Configuration("http://localhost:8501"),
            new Configuration("http://localhost:8502"),
        };
        configs[0].withApiKeyAuthentication("apikey0");
        configs[1].withApiKeyAuthentication("apikey1");
        configs[2].withApiKeyAuthentication("apikey2");

        Client[] clients = {
            new Client(configs[0]),
            new Client(configs[1]),
            new Client(configs[2]),
        };


        // Generate an ECDSA key

        final int[] keyGenPlayers = {0, 1, 2}; // The key should be secret shared among all three MPC nodes
        final int threshold = 1; // The security threshold for this key
        final String curveName = "secp256k1"; // We want the key to be a secp256k1 key (e.g., for Bitcoin)
        final int[] derivationPath = null; // In this example we do not use key derivation
        
        String keyGenSessionId = SessionConfig.generateSessionId();
        final SessionConfig keyGenSessionConfig = SessionConfig.newSessionConfig(keyGenSessionId, keyGenPlayers, null);

        System.out.println("Generating key using players " + Arrays.toString(keyGenPlayers));
        List<String> results = runConcurrent(
                () -> clients[0].getEcdsa().generateKey(keyGenSessionConfig, threshold, curveName, null),
                () -> clients[1].getEcdsa().generateKey(keyGenSessionConfig, threshold, curveName, null),
                () -> clients[2].getEcdsa().generateKey(keyGenSessionConfig, threshold, curveName, null));
        String keyId = results.get(0);

        System.out.println("Generated key with ID: " + keyId);


        // Get the public key from one of the MPC nodes

        byte[] publicKey = clients[0].getEcdsa().publicKey(keyId, derivationPath);
        System.out.println("Public key: 0x" + bytesToHex(publicKey));

        // Remember: Depending on your use case, you may need to check that all or at least threshold + 1 clients agree on the
        // public key, before using it, e.g. for creating a cryptocurrency account.


        // Sign a message using the private key

        final int[] signPlayers = {1, 2}; // We sign with threshold + 1 players, in this case MPC node 1, 2
        final byte[] msgHash = new byte[32];  // Normally, this is the SHA256 hash of the message.

        System.out.println("Signing message with players " + Arrays.toString(signPlayers));
        String signSessionId = SessionConfig.generateSessionId();
        final SessionConfig signSessionConfig = SessionConfig.newSessionConfig(signSessionId, signPlayers, null);
        List<EcdsaPartialSignResult> signResults = runConcurrent(
                () -> clients[1].getEcdsa().sign(signSessionConfig, keyId, derivationPath, msgHash),
                () -> clients[2].getEcdsa().sign(signSessionConfig, keyId, derivationPath, msgHash));
        byte[][] partialSignatures = {signResults.get(0).getPartialSignature(), signResults.get(1).getPartialSignature()};
        EcdsaSignature signature = Ecdsa.finalizeSignature(msgHash, partialSignatures);
        System.out.println("Signature: 0x" + bytesToHex(signature.getSignature()));

        // Validate the signature

        boolean valid = Ecdsa.verifySignature(publicKey, msgHash, signature.getSignature());
        System.out.println("Signature validity: " + valid);

    }


    @SafeVarargs
    static <T> List<T> runConcurrent(Supplier<T>... players) throws Exception {
        List<T> result = new ArrayList<>(players.length);
        Queue<Exception> errors = new ConcurrentLinkedQueue<>();
        for (int i = 0; i < players.length; i++) {
            result.add(null);
        }
        Thread[] threads = new Thread[players.length];
        for (int i = 0; i < players.length; i++) {
            final int index = i;
            Thread thread = new Thread() {
                public void run() {
                    try {
                        T runResult = players[index].get();
                        result.set(index, runResult);
                    } catch (Exception e) {
                        errors.add(e);
                    }
                }
            };
            threads[i] = thread;
            thread.start();
        }
        for (int i = 0; i < players.length; i++) {
            threads[i].join();
        }
        if (!errors.isEmpty()) {
            throw new RuntimeException("One of the threads failed executing command", errors.remove());
        }
        return result;
    }

    static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars);
    }

}

const {  
  TSMClient,
  Configuration,
  SessionConfig,
  curves,
} = require("@sepior/tsmsdkv2");
const crypto = require('crypto');

const main = async () => {

  // Create clients for each of the nodes

  const configs = [
    {
      url: "http://localhost:8500",
      apiKey: "apikey0",
    },
    {
      url: "http://localhost:8501",
      apiKey: "apikey1",
    },
    {
      url: "http://localhost:8502",
      apiKey: "apikey2",
    },
  ];

  const clients = [];

  for (const rawConfig of configs) {
    const config = await new Configuration(rawConfig.url);
    await config.withAPIKeyAuthentication(rawConfig.apiKey);
    const client = await TSMClient.withConfiguration(config);

    clients.push(client);
  }

  // Generate an ECDSA master key

  const threshold = 1; // The security threshold for this key
  const keygenPlayers = [0, 1, 2]; // The key should be secret shared among all three MPC nodes
  const keygenSessionConfig = await SessionConfig.newSessionConfig(
    await SessionConfig.GenerateSessionID(),
    new Uint32Array(keygenPlayers),
    {}
  );

  const keyIds = ["", "", ""];

  console.log(`Generating key using players ${keygenPlayers}`);

  const generateKeyPromises = [];

  for (const [i, client] of clients.entries()) {
    const func = async () => {
      const ecdsaApi = client.ECDSA();
      keyIds[i] = await ecdsaApi.generateKey(
        keygenSessionConfig,
        threshold,
        curves.SECP256K1,
        ""
      );
    };
    generateKeyPromises.push(func());
  }

  await Promise.all(generateKeyPromises);

  // Validate key IDs

  for (let i = 1; i < keyIds.length; i++) {
    if (keyIds[0] !== keyIds[i]) {
      console.log("Key ids do not match");
      return;
    }
  }

  const keyId = keyIds[0];

  console.log(`Generated key with id: ${keyId}`);

  // Get the public key

  const derivationPath = new Uint32Array([]); // We don't use key derivation in this example

  const publickeys = [];

  for (const client of clients) {
    const ecdsaApi = client.ECDSA();
    publickeys.push(await ecdsaApi.publicKey(keyId, derivationPath));
  }

  // Validate public keys

  for (let i = 1; i < publickeys.length; i++) {
    if (!Buffer.from(publickeys[0]).equals(publickeys[i])) {
      console.log("Public keys does not match");
      return;
    }
  }

  const publicKey = publickeys[0];

  console.log(`Public key: ${Buffer.from(publicKey).toString("hex")}`);

  // We can now sign with the created key

  const message = "This is a message to be signed";
  const messageHash = crypto.createHash("sha256").update(message).digest();

  const signPlayers = [0, 1]; // We want to sign with the first two MPC nodes
  const sessionId = await SessionConfig.GenerateSessionID();
  const signSessionConfig = await SessionConfig.newSessionConfig(
    sessionId,
    new Uint32Array(signPlayers),
    {}
  );

  console.log(`Creating signature using players ${signPlayers}`);

  const partialSignatures = [];

  const partialSignaturePromises = [];

  for (const playerIdx of signPlayers) {
    const func = async () => {
      const ecdsaApi = clients[playerIdx].ECDSA();

      const partialSignResult = await ecdsaApi.sign(
        signSessionConfig,
        keyId,
        new Uint32Array([]),
        messageHash
      );

      partialSignatures.push(partialSignResult);
    };

    partialSignaturePromises.push(func());
  }

  await Promise.all(partialSignaturePromises);

  const ecdsaApi = clients[0].ECDSA();

  const signature = await ecdsaApi.finalizeSignature(
    messageHash,
    partialSignatures
  );

  // Verify the signature relative to the signed message and the public key

  try {
    const result = await ecdsaApi.verifySignature(
      publicKey,
      messageHash,
      signature.signature
    );
    console.log(result);
    console.log(
      `Signature: ${Buffer.from(signature.signature).toString("hex")}`
    );
  } catch (e) {
    console.log(e);
  }
}

main().catch((e) => console.log(e));

#include <dlfcn.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
// types.h is released as part of the TSM release
#include <types.h>

typedef struct Tsm_Funcs
{
	void (*TsmByteArray_Free)(tsm_byte_array *);

	void (*Tsm_Free)(_handle);
	int (*Tsm_NewConfiguration)(char *, tsm_config_handle *);
	int (*Tsm_ConfigurationWithAPIKeyAuthentication)(tsm_config_handle, char *);
	int (*Tsm_NewClient)(tsm_config_handle, tsm_client_handle *);

	int (*Tsm_NewStaticSessionConfig)(char *, int, tsm_session_config_handle *);
	void (*Tsm_GenerateSessionID)(char *);

	int (*Tsm_ECDSA_GenerateKey)(tsm_client_handle, tsm_session_config_handle, int, char *, char *, char *);
	int (*Tsm_ECDSA_Sign)(tsm_client_handle, tsm_session_config_handle, char *, tsm_int_array *, tsm_byte_array *, tsm_byte_array *);
	int (*Tsm_ECDSA_PublicKey)(tsm_client_handle, char *, tsm_int_array *, tsm_byte_array *);

	int (*Tsm_ECDSAFinalizeSignature)(tsm_byte_array *, tsm_byte_array_array *, tsm_byte_array *, int *);
	int (*Tsm_ECDSAVerifySignature)(tsm_byte_array *, tsm_byte_array *, tsm_byte_array *);
} Tsm_Funcs;

typedef struct setup
{
    Tsm_Funcs f;
    tsm_client_handle client0Handle;
    tsm_client_handle client1Handle;
} setup;


int setup_tsm(setup *s);
int ecdsa_generate_key(setup* s, char* curveName, char* desiredName, char* outKeyID);
int ecdsa_sign(setup* s, char* keyID, tsm_int_array *chainPath, tsm_byte_array *message, tsm_byte_array *outPartialSignature0, tsm_byte_array *outPartialSignature1);

int main(int argc, char* argv[])
{
	setup s;
	int rc;
	
	printf("Setting up TSM\n");
	rc = setup_tsm(&s);
	if (rc != 0) {
		printf("ERROR: failed to set up tsm: rc = %d\n", rc);
		return rc;
	}

	printf("Generating Key\n");
	char keyID[128];
	rc = ecdsa_generate_key(&s, "secp256k1", "", (char *) keyID);
	if (rc != 0) {
		printf("ERROR: failed to generate key: rc = %d\n", rc);
		return rc;
	}

	printf("Getting public key\n");
	tsm_int_array noChainPath = {NULL, 0};
	tsm_byte_array publicKey = {NULL, 0};
	rc = s.f.Tsm_ECDSA_PublicKey(s.client0Handle, (char *) keyID, &noChainPath, &publicKey);
	if (rc != 0) {
		printf("ERROR: failed to get public key: rc = %d\n", rc);
		return rc;
	}

	printf("Signing message\n");
	unsigned char messageBytes[32];
	tsm_byte_array message = {(uint8_t *)messageBytes, sizeof(messageBytes)};
	tsm_byte_array sig0 = {NULL, 0};
	tsm_byte_array sig1 = {NULL, 0};
	rc = ecdsa_sign(&s, (char *) keyID, &noChainPath, &message, &sig0, &sig1);
	if (rc != 0) {
		printf("ERROR: failed to sign message: rc = %d\n", rc);
		return rc;
	}

	printf("Collecting signature\n");
	tsm_byte_array partialSigsArray[2];
	partialSigsArray[0] = sig0;
	partialSigsArray[1] = sig1;
	tsm_byte_array_array partialSigs = {(tsm_byte_array *)&partialSigsArray, 2};
	tsm_byte_array signature = {NULL, 0};
	int recoveryID;
	rc = s.f.Tsm_ECDSAFinalizeSignature(&message, &partialSigs, &signature, &recoveryID);
	if (rc != 0) {
		printf("ERROR: failed to collect (finalized) signature: rc = %d\n", rc);
		return rc;
	}

	printf("Verify signature\n");
	rc = s.f.Tsm_ECDSAVerifySignature(&publicKey, &message, &signature);
	if (rc != 0) {
		printf("ERROR: failed to verify signature: rc = %d\n", rc);
		return rc;
	}

	printf("Cleaning up\n");
	s.f.TsmByteArray_Free(&publicKey);
	s.f.TsmByteArray_Free(&sig0);
	s.f.TsmByteArray_Free(&sig1);
	s.f.TsmByteArray_Free(&signature);

	printf("Done\n");
	return 0;
}

int load(Tsm_Funcs *funcs)
{
	const char *sdkPath = getenv("SDK_PATH");
	void *handle = dlopen(sdkPath, RTLD_LAZY);
	
	if (!handle)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}

	funcs->TsmByteArray_Free = dlsym(handle, "TsmByteArray_Free");
	if (!funcs->TsmByteArray_Free)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}

	funcs->Tsm_Free = dlsym(handle, "Tsm_Free");
	if (!funcs->Tsm_Free)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}
	funcs->Tsm_NewConfiguration = dlsym(handle, "Tsm_NewConfiguration");
	if (!funcs->Tsm_NewConfiguration)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}
	funcs->Tsm_ConfigurationWithAPIKeyAuthentication = dlsym(handle, "Tsm_ConfigurationWithAPIKeyAuthentication");
	if (!funcs->Tsm_ConfigurationWithAPIKeyAuthentication)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}
	funcs->Tsm_NewClient = dlsym(handle, "Tsm_NewClient");
	if (!funcs->Tsm_NewClient)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}

	funcs->Tsm_NewStaticSessionConfig = dlsym(handle, "Tsm_NewStaticSessionConfig");
	if (!funcs->Tsm_NewStaticSessionConfig)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}
	funcs->Tsm_GenerateSessionID = dlsym(handle, "Tsm_GenerateSessionID");
	if (!funcs->Tsm_GenerateSessionID)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}

	funcs->Tsm_ECDSA_GenerateKey = dlsym(handle, "Tsm_ECDSA_GenerateKey");
	if (!funcs->Tsm_ECDSA_GenerateKey)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}
	funcs->Tsm_ECDSA_Sign = dlsym(handle, "Tsm_ECDSA_Sign");
	if (!funcs->Tsm_ECDSA_Sign)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}
	funcs->Tsm_ECDSA_PublicKey = dlsym(handle, "Tsm_ECDSA_PublicKey");
	if (!funcs->Tsm_ECDSA_PublicKey)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}

	funcs->Tsm_ECDSAFinalizeSignature = dlsym(handle, "Tsm_ECDSAFinalizeSignature");
	if (!funcs->Tsm_ECDSAFinalizeSignature)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}
	funcs->Tsm_ECDSAVerifySignature = dlsym(handle, "Tsm_ECDSAVerifySignature");
	if (!funcs->Tsm_ECDSAVerifySignature)
	{
		fprintf(stderr, "Error: %s\n", dlerror());
		return -1;
	}

	return 0;
}


int setup_sdk(setup* s, char* url, char* apiKey, tsm_client_handle* handle) {
	tsm_config_handle config;
	int rc = s->f.Tsm_NewConfiguration(url, &config);
	if (rc != 0) {
		printf("WARNING: could not create configuration: rc = %d\n", rc);
		return rc;
	}

	rc = s->f.Tsm_ConfigurationWithAPIKeyAuthentication(config, apiKey);
	if (rc != 0) {
		printf("WARNING: could not set API key for configuration: rc = %d\n", rc);
		s->f.Tsm_Free(config);
		return rc;
	}

	rc = s->f.Tsm_NewClient(config, handle);
	if (rc != 0) {
		printf("WARNING: could not create client: rc = %d\n", rc);
	}

	s->f.Tsm_Free(config);
	return rc;
}

int setup_tsm(setup *s)
{
	int rc;
  
	load(&s->f);

	rc = setup_sdk(s, "http://localhost:8500", "apikey0", &s->client0Handle);
	if (rc != 0) {
		printf("WARNING: could not create client 0: rc = %d\n", rc);
		return rc;
	}
	rc = setup_sdk(s, "http://localhost:8501", "apikey1", &s->client1Handle);
	if (rc != 0) {
		printf("WARNING: could not create client 1: rc = %d\n", rc);
		return rc;
	}

	return 0;
}

#define run_threads(N, thread_function, clients) \
	;\
	pthread_t threads[N]; \
	for (int i = 0; i < N; i++) { \
		rc = pthread_create(&threads[i], NULL, thread_function, (void *)&clients[i]); \
		if (rc != 0) { \
			printf("ERROR: failed setting up thread for client %d: rc = %d\n", i, rc); \
			return rc; \
		} \
	} \
	for (int i = 0; i < N; i++) { \
		pthread_join(threads[i], NULL); \
	} \
	for (int i = 0; i < N; i++) { \
		if (clients[i].rc != 0) { \
			printf("ERROR: client %d returned a failure: rc = %d\n", i, rc); \
			return clients[i].rc; \
		} \
	}

typedef struct SessionInfo
{
	Tsm_Funcs*                f;
	tsm_client_handle         clientHandle;
	tsm_session_config_handle sessionConfig;
	int                       rc;
	void*                     io;
} SessionInfo;

typedef struct GenerateKeyIO
{
	int threshold;
	char* curveName;
	char* desiredKeyID;
	char outKeyID[128];
} GenerateKeyIO;

void *thread_ecdsa_generate_key(void *threadArgs)
{
	SessionInfo *threadData = (SessionInfo *) threadArgs;
	GenerateKeyIO* io = (GenerateKeyIO *) threadData->io;

	threadData->rc = threadData->f->Tsm_ECDSA_GenerateKey(threadData->clientHandle, threadData->sessionConfig, io->threshold, io->curveName, io->desiredKeyID, io->outKeyID);

	pthread_exit(NULL);
}

int ecdsa_generate_key(setup* s, char* curveName, char* desiredName, char* outKeyID) {
	Tsm_Funcs* f = &(s->f);

	char sessionID[128];
	f->Tsm_GenerateSessionID(sessionID);

	tsm_session_config_handle sessionConfig;
	int rc = f->Tsm_NewStaticSessionConfig(sessionID, 2, &sessionConfig);
	if (rc != 0) {
		printf("ERROR: failed generating session configuration: rc = %d\n", rc);
		return rc;
	}

	GenerateKeyIO io0 = {1, curveName, desiredName, ""};
	GenerateKeyIO io1 = {1, curveName, desiredName, ""};
	SessionInfo clients[] = {
		{f, s->client0Handle, sessionConfig, 0, &io0},
		{f, s->client1Handle, sessionConfig, 0, &io1}
	}

	run_threads(2, thread_ecdsa_generate_key, clients);

	if (strcmp(io0.outKeyID, io1.outKeyID) != 0) {
		printf("ERROR: nodes did not return the same key ID: %s = %s\n", io0.outKeyID, io1.outKeyID);
		return -1;
	}
       
	strncpy(outKeyID, io0.outKeyID, 128);

	f->Tsm_Free(sessionConfig);

	return 0;
}

typedef struct SignIO
{
	char* keyID;
	tsm_int_array *chainPath;
	tsm_byte_array *message;
	tsm_byte_array outPartialSignature;
} SignIO;

void *thread_ecdsa_sign(void *threadArgs)
{
	SessionInfo *threadData = (SessionInfo *) threadArgs;
	SignIO* io = (SignIO *) threadData->io;

	threadData->rc = threadData->f->Tsm_ECDSA_Sign(threadData->clientHandle, threadData->sessionConfig, io->keyID, io->chainPath, io->message, &io->outPartialSignature);

	pthread_exit(NULL);
}

int ecdsa_sign(setup* s, char* keyID, tsm_int_array *chainPath, tsm_byte_array *message, tsm_byte_array *outPartialSignature0, tsm_byte_array *outPartialSignature1) {
	Tsm_Funcs* f = &(s->f);

	char sessionID[128];
	f->Tsm_GenerateSessionID(sessionID);

	tsm_session_config_handle sessionConfig;
	int rc = f->Tsm_NewStaticSessionConfig(sessionID, 2, &sessionConfig);
	if (rc != 0) {
		printf("ERROR: failed generating session configuration: rc = %d\n", rc);
		return rc;
	}

	SignIO io0 = {keyID, chainPath, message, {NULL, 0}};
	SignIO io1 = {keyID, chainPath, message, {NULL, 0}};
	SessionInfo clients[] = {
		{f, s->client0Handle, sessionConfig, 0, &io0},
		{f, s->client1Handle, sessionConfig, 0, &io1}
	}

	run_threads(2, thread_ecdsa_sign, clients);

	outPartialSignature0->size = io0.outPartialSignature.size;
	outPartialSignature0->data = io0.outPartialSignature.data;
	outPartialSignature1->size = io1.outPartialSignature.size;
	outPartialSignature1->data = io1.outPartialSignature.data;

	f->Tsm_Free(sessionConfig);

	return 0;
}

Running the program should produce output like this:

Generating key using players [0 1 2]
Generated key with ID: 8gC3xPDNmAn6zGJV2F4je9eWiq7q
Public key: 3056301006072a8648ce3d020106052b8104000a0342000444f317a6678cc0d0e3e4bb8c6578672b802fd62b44bd7485f93215fc768236097ed5c1e26c799d6cf59df8bee75eb7a35918dcbe4f6f8407b36401e21132e8eb
Creating signature using players [0 1]
Signature: 304402206563a91fd1a4eb6bdf1d7a1882fc0ed4cb8f2a04c1effc0aba44b4d7be471b73022031fde0dc99e368d2a47a34038413206312513e0309631b46b1a46ae6ed9dd97d

What’s Next