Key Lifecycle Management

The TSM provides a number of methods to manage the life of a key.

Listing and Deleting Keys

You can get a list of the IDs of all keys for which a given MPC node holds shares:

ctx := context.Background()
keyIDs, err := node.KeyManagement().ListKeys(ctx)

Given a specific key ID, you can delete the corresponding key share on the MPC node like this:

err := node.KeyManagement().DeleteKeyShare(ctx, keyID)

Note that ListKeys and DeleteKeyShare operate on a single MPC node. So the fact that a single MPC node returns a key ID does not necessarily mean that the key “exists” in the TSM. Likewise, deleting the key share may not delete the key itself from the TSM. Generally, a key “exists” in the TSM if a sufficient number of MPC nodes (usually t+1, where t is the security threshold of the key) holds shares of the key in order to generate MPC signatures using the key.

Key Resharing

Suppose you have a key with ID keyID in the TSM. The secret sharing of the key can then be refreshed by running an MPC key resharing session.

First choose the MPC session meta data, that is, the session ID, and the set of nodes to participate. All nodes holding key shares of the key must participate in the resharing.

sessionID := tsm.GenerateSessionID()
players := []int{ 0, 1, 2 }  // This assumes the key was generated among Node 0, 1, 2 
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)

Then run the MPC session by calling this method on all SDKs:

ctx := context.Background()
err := node.ECDSA().Reshare(ctx, sessionConfig, keyID)

If the MPC session succeeds, the secret sharing of the key in the TSM will have been replaced by a fresh random secret sharing (of the same key).

Importantly, if the operation fails, for some reason, it should be retried until it succeeds. After this operation is called for a given key, and until it succeeds, other operations involving the same key might fail.

📘

Key Resharing and Key Share Backup

Care must be taken if you use key resharing together with our key share backup feature. If you restore a key share on a single MPC node from an old key share backup that was created in an earlier reshare epoch, this will make the entire key unavailable, since the key shares on the MPC nodes are then no longer related. The solution is to either avoid using local key share backup with resharing, or to make sure that you take a new share backup after each reshare operation.

📘

Key Resharing and Presignatures

Any presignatures for a given key is automatically deleted when the key sharing is refreshed.

Code Example

Here is a full code example, showing how to reshare a key. After resharing, the key will be the same, but it will be shared with a fresh, randomized secret sharing.

package main

import (
	"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)
	}
	keyID := keyIDs[0]
	fmt.Println("Generated key with ID:", keyID)

	// Get the public key from one of the nodes

	var derivationPath []uint32 = nil // We don't use key derivation in this example
	publicKey, err := clients[0].ECDSA().PublicKey(ctx, keyID, derivationPath)
	fmt.Println("Public key:", hex.EncodeToString(publicKey))

	// Re-randomize the secret sharing of the key

	resharePlayers := []int{0, 1, 2} // Must be same set of nodes as used when the key was generated
	reshareSessionID := tsm.GenerateSessionID()
	reshareSessionConfig := tsm.NewSessionConfig(reshareSessionID, resharePlayers, nil)
	for _, client := range clients {
		client := client
		eg.Go(func() error {
			err := client.ECDSA().Reshare(ctx, reshareSessionConfig, keyID)
			return err
		})
	}
	if err = eg.Wait(); err != nil {
		panic(err)
	}
	fmt.Println("Completed resharing of key", keyID)

	// Test: Create a signature with the re-shared key, and see if it's valid with the original public 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 is valid")
}
package com.example;

import com.sepior.tsm.sdkv2.*;

import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;

public class EcdsaReshareExample {

    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)

        String keyGenSessionId = SessionConfig.generateSessionId();
        final SessionConfig keyGenSessionConfig = SessionConfig.newSessionConfig(keyGenSessionId, keyGenPlayers, null);
        System.out.println("Generating key using players " + Arrays.toString(keyGenPlayers));
        List<String> keyGenResults = 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 = keyGenResults.get(0);
        System.out.println("Generated key with ID: " + keyId);

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

        int[] derivationPath = null; // We don't use key derivation in this example
        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.

        // Re-randomize the secret sharing of the key

        String reshareSessionId = SessionConfig.generateSessionId();
        int[] resharePlayers = {0, 1, 2}; // Must use the same players as used for the key generation
        final SessionConfig reshareSessionConfig = SessionConfig.newSessionConfig(reshareSessionId, resharePlayers, null);
        System.out.println("Resharing key " + keyId + " on players " + Arrays.toString(resharePlayers));
        runConcurrent(
                () -> {
                    clients[0].getEcdsa().reshare(reshareSessionConfig, keyId);
                    return null;
                },
                () -> {
                    clients[1].getEcdsa().reshare(reshareSessionConfig, keyId);
                    return null;
                },
                () -> {
                    clients[2].getEcdsa().reshare(reshareSessionConfig, keyId);
                    return null;
                });
        System.out.println("Completed resharing key " + keyId);

        // Test: Create a signature with the re-shared key, and see if it's valid with the original public key

        final byte[] msgHash = new byte[32];  // Normally, this is the SHA256 hash of the message.
        final int[] signPlayers = {1, 2}; // We sign with threshold + 1 players, in this case MPC node 1, 2
        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);
        boolean valid = Ecdsa.verifySignature(publicKey, msgHash, signature.getSignature());
        System.out.println("Validity of signature generated using the reshared key: " + valid);

    }


    @SafeVarargs
    static <T> List<T> runConcurrent(Supplier<T>... players) throws Exception {
        List<T> result = new ArrayList<T>(players.length);
        Queue<Exception> errors = new ConcurrentLinkedQueue<Exception>();
        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');

async function main() {
  // 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);

  const keyId = keyIds[0];

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

  // Get the public key from one of the nodes

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

  const ecdsaApi = clients[0].ECDSA();
  const publicKey = await ecdsaApi.publicKey(keyId, derivationPath);

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

  // Re-randomize the secret sharing of the key

  const resharePlayers = [0, 1, 2]; // Must be same set of nodes as used when the key was generated
  const reshareSessionId = await SessionConfig.GenerateSessionID();
  const reshareSessionConfig = await SessionConfig.newSessionConfig(
    reshareSessionId,
    new Uint32Array(resharePlayers),
    {}
  );

  const resharePromises = [];

  for (const client of clients) {
    const func = async () => {
      const ecdsaApi = client.ECDSA();
      await ecdsaApi.reshare(reshareSessionConfig, keyId);
    };

    resharePromises.push(func());
  }

  await Promise.all(resharePromises);

  console.log(`Complete resharing of key ${keyId}`);

  // Test: Create a signature with the re-shared key, and see if it's valid with the original public 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 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 was valid");
  } catch (e) {
    console.log(e);
  }
}

main()

This produces a result like:

Generating key using players [0, 1, 2]
Generated key with ID: ieRBiR3PL7KCcFAh3KZZzRB18ReB
Public key: 0x3056301006072A8648CE3D020106052B8104000A03420004759A0B9CD513510B364D07EAF836F5BC34B48926A09A1560A6CE95C5ED88F54C7907888E49E74481B353D4C6E77B06E86AF304224A4ECF4FA38F5D8DD2A53C17
Resharing key ieRBiR3PL7KCcFAh3KZZzRB18ReB on players [0, 1, 2]
Completed resharing key ieRBiR3PL7KCcFAh3KZZzRB18ReB
Signing message with players [1, 2]
Validity of signature generated using the reshared key: true