Key Generation and Signing

Once the SDK is authenticated, it can be used to do operations on the TSM, for example generate keys and sign 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 which fits in the header of a 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 := ec.Secp256k1.Name()
keyID, err := client.ECDSA().GenerateKey(context, sessionConfig, threshold, curveName, "")

When GenerateKey() is called on the SDK, it forwards the key generation request to it’s 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 have 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, but this 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 sharing among the MPC nodes, and each SDK will receive the key ID of the new key.

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 players will end up with the correct key share and the correct public key. But the protocol does guarantee that all honest players will succeed. In fact, all but one honest player may experience a session abort.

Since the call to PublicKey() simply fetches the public key from a single MPC node, it is important that this public key is only used, e.g., for creating a wallet address, when it is known that all players succeeded the key generation session.

In addition, unless you really trust the MPC node to which the SDK is connected, it may be necessary to retrieve the public key from the other MPC nodes and ensure that the public keys returned are all equal, before the public key is used. This prevents a single corrupt MPC node from returning a false public key where only the corrupt MPC node knows the private key.

Signing

Once a key have been generated, each SDK now holds a key ID, and they are 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 out to each of the SDK operators, in this example, the operator of the SDK for Node 1 and Node 2, and 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 := ec.Secp256k1.Name()
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 was successful, each SDK obtains a partial signature. The partial signatures must be collected by your application, 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, where a key is generated, the public key is obtained, and the key is used 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 to start the key generation and signing MPC sessions in the example, the SDKs are invoked concurrently. 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/sepior/go-tsm-sdkv2/ec"
	"gitlab.com/sepior/go-tsm-sdkv2/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, ec.Secp256k1.Name(), "")
			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.Compare(publicKeys[0], publicKeys[i]) != 0 {
			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));

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