Moving a Key from One TSM to Another TSM

Suppose we have two TSMs, which we will call TSM A and TSM B, that we have generated a key with keyID in TSM A as explained here, and that we want to obtain a copy of this key in TSM B.

We will assume that TSM A consists of two MPC nodes, with player indices 1, 2, respectively operated by the two SDKs clientA1 and clientA2. We will also assume that TSM B consists of two MPC nodes, with player indices 1, 2 and operated by SDKs clientB1 and clientB2.

You first need to obtain the wrapping keys from the target TSM (TSM B):

ctx := context.Background()
wrapB1 := clientB1.WrappingKey().WrappingKey(ctx)
wrapB2 := clientB2.WrappingKey().WrappingKey(ctx)

Then you request a key export MPC session on the source TSM (TSM A). Since this is an MPC session, you first need to pick a session ID and set up the MPC session configuration:

players := []int{1,2}
sessionID := tsm.GenerateSessionID()
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
derivationPath := []uint32{} // Export the key itself, not a key derived from the key.

Then run the key export MPC session, by calling the two SDKs on the source TSM (TSM A) concurrently:

// These calls will block until the MPC session is done, so make sure to 
// run the two calls in separate processes or goroutines.
share1, err := clientA1.ECDSA().ExportKeyShares(ctx, sessionConfig, keyID, derivationPath, wrapB1)
share2, err := clientA2.ECDSA().ExportKeyShares(ctx, sessionConfig, keyID, derivationPath, wrapB2)

Each output share consists of a wrapped key share, the wrapped chain code (which is used for key derivation), and the public key. Finally, run another MPC session on the target TSM to import the wrapped key shares:

players := []int{1,2}
sessionID := tsm.GenerateSessionID()
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
// Again, run the next two calls in separate processes or goroutines
keyID, err := clientB1.ECDSA().ImportKeyShares(ctx, sessionConfig, threshold, share1.WrappedKeyShare, share1.WrappedChainCode, keyShare1.PKIXPublicKey, desiredKeyID)
keyID, err := clientB2.ECDSA().ImportKeyShares(ctx, sessionConfig, threshold, share1.WrappedKeyShare, share2.WrappedChainCode, keyShare2.PKIXPublicKey, desiredKeyID)

The threshold is the security threshold for the imported key. This must equal the threshold that you used when you generated the key.

The desiredKeyID is optional. If provided, this will be the ID of the imported key. This can be used to make sure that the key ends up with the same ID in both TSMs.

📘

Note

The current import and export methods require that the MPC destination nodes have the same player indices as the source MPC nodes, and the security threshold of the key sharing among the destination nodes will be the same as the security threshold of the sharing among the source nodes.

Code Example

A complete code example follows here:

package main

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

// This example shows how to migrate a key from a source TSM to a destination TSM.
func main() {

	// Create clients for the source TSM

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

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

	// Create clients for the target TSM. We use MPC nodes from the same TSM here, but you can import to
	// MPC nodes in another TSM as well, as long as the MPC nodes in the destination TSM have the same player indices.

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

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

	// Generate a key in the source TSM

	players := []int{0, 1} // Key shares can currently only be moved between nodes with the same player indices.
	threshold := 1         // The security threshold for this key
	keyGenSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
	fmt.Println("Generating key using players", players, " in the source TSM")
	ctx := context.Background()
	srcKeyIDs := make([]string, len(srcClients))
	var eg errgroup.Group
	for i, client := range srcClients {
		client, i := client, i
		eg.Go(func() error {
			var err error
			srcKeyIDs[i], err = client.ECDSA().GenerateKey(ctx, keyGenSessionConfig, threshold, "secp256k1", "")
			return err
		})
	}
	if err := eg.Wait(); err != nil {
		panic(err)
	}
	srcKeyID := srcKeyIDs[0]

	// Get wrapping keys for the MPC nodes in the destination TSM

	wrappingKeys := make([][]byte, len(dstClients))
	for i, client := range dstClients {
		var err error
		wrappingKeys[i], err = client.WrappingKey().WrappingKey(context.Background())
		if err != nil {
			panic(err)
		}
	}

	// Export wrapped key shares from the source TSM

	fmt.Println("Exporting key sharing from players", players, " in the source TSM")
	var derivationPath []uint32 = nil // We want to export the key itself, not a derivation of the key.
	wrappedKeyShares := make([]tsm.ECDSAWrappedKeyShare, len(srcClients))
	exportSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
	for i, client := range srcClients {
		client := client
		i := i
		eg.Go(func() error {
			wrappedKeyShare, err := client.ECDSA().ExportKeyShares(context.Background(), exportSessionConfig, srcKeyID, derivationPath, wrappingKeys[i])
			if err != nil {
				return err
			}
			wrappedKeyShares[i] = *wrappedKeyShare
			return nil
		})
	}
	err := eg.Wait()
	if err != nil {
		panic(err)
	}

	// Import the key shares into the destination TSM

	importSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
	dstKeyIDs := make([]string, len(dstClients))
	fmt.Println("Importing key sharing to players", players, " in the target TSM")
	for i, client := range dstClients {
		client := client
		i := i
		eg.Go(func() error {
			var err error
			dstKeyIDs[i], err = client.ECDSA().ImportKeyShares(context.Background(), importSessionConfig, threshold, wrappedKeyShares[i].WrappedKeyShare, wrappedKeyShares[i].WrappedChainCode, wrappedKeyShares[i].PKIXPublicKey, "")
			return err
		})
	}
	err = eg.Wait()
	if err != nil {
		panic(err)
	}

	dstKeyID := dstKeyIDs[0]

	// Test that destination public key equals the original public key

	srcPubKey, err := srcClients[0].ECDSA().PublicKey(context.Background(), srcKeyID, derivationPath)
	if err != nil {
		panic(err)
	}
	dstPubKey, err := dstClients[0].ECDSA().PublicKey(context.Background(), dstKeyID, derivationPath)
	if err != nil {
		panic(err)
	}
	if !bytes.Equal(srcPubKey, dstPubKey) {
		panic("public key was different after moving key destination nodes")
	}

	fmt.Println("Key was successfully moved from source nodes to target nodes")

}

package com.example;

import com.sepior.tsm.sdkv2.*;

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

public class EcdsaExportImportExample {

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

        // Create clients for two MPC nodes in the source TSM

        Configuration[] srcConfigs = {
                new Configuration("http://localhost:8500"),
                new Configuration("http://localhost:8501"),
        };
        srcConfigs[0].withApiKeyAuthentication("apikey0");
        srcConfigs[1].withApiKeyAuthentication("apikey1");
        Client[] srcClients = {
                new Client(srcConfigs[0]),
                new Client(srcConfigs[1]),
        };


        // Create clients for two MPC nodes in the target TSM. We use MPC nodes from the same TSM here, but you can
        // import to MPC nodes in another TSM as well, as long as the MPC nodes in the destination TSM have the same
        // player indices.

        Configuration[] dstConfigs = {
                new Configuration("http://localhost:8500"),
                new Configuration("http://localhost:8501"),
        };
        dstConfigs[0].withApiKeyAuthentication("apikey0");
        dstConfigs[1].withApiKeyAuthentication("apikey1");
        Client[] dstClients = {
                new Client(srcConfigs[0]),
                new Client(srcConfigs[1]),
        };


        // Generate an ECDSA key secret shared among player 0 and 1 in the source TSM

        final int[] players = {0, 1};
        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, players, null);
        System.out.println("Generating key in source TSM using players " + Arrays.toString(players));
        List<String> results = runConcurrent(
                () -> srcClients[0].getEcdsa().generateKey(keyGenSessionConfig, threshold, curveName, null),
                () -> srcClients[1].getEcdsa().generateKey(keyGenSessionConfig, threshold, curveName, null));
        String srcKeyId = results.get(0);
        System.out.println("Generated key with ID: " + srcKeyId);


        // Get wrapping keys for the MPC nodes in the destination TSM

        //wrappingKeys = make([][]byte, len(dstClients))
        byte[][] wrappingKeys = {
                dstClients[0].getWrappingKey().getWrappingKey(),
                dstClients[1].getWrappingKey().getWrappingKey(),
        };


        // Export wrapped key shares from the MPC nodes in the source TSM

        String exportSessionId = SessionConfig.generateSessionId();
        final SessionConfig exportSessionConfig = SessionConfig.newSessionConfig(exportSessionId, players, null);
        System.out.println("Exporting wrapped key shares from players " + Arrays.toString(players) + " from the source TSM");
        List<EcdsaWrappedKeyShare> exportedKeyShares = runConcurrent(
                () -> srcClients[0].getEcdsa().exportKeyShares(exportSessionConfig, srcKeyId, derivationPath, wrappingKeys[0]),
                () -> srcClients[1].getEcdsa().exportKeyShares(exportSessionConfig, srcKeyId, derivationPath, wrappingKeys[1]));


        // Import the wrapped key shares into the destination TSM

        String importSessionId = SessionConfig.generateSessionId();
        final SessionConfig importSessionConfig = SessionConfig.newSessionConfig(importSessionId, players, null);
        System.out.println("Importing wrapped key shares from players " + Arrays.toString(players) + " into the destination TSM");
        List<String> dstKeyIds = runConcurrent(
                () -> {
                    byte[] wrappedKeyShare = exportedKeyShares.get(0).getWrappedKeyShare();
                    byte[] wrappedChainCode = exportedKeyShares.get(0).getWrappedChainCode();
                    byte[] pubKey = exportedKeyShares.get(0).getPkixPublicKey();
                    return dstClients[0].getEcdsa().importKeyShares(importSessionConfig, threshold, wrappedKeyShare, wrappedChainCode, pubKey, "");
                },
                () -> {
                    byte[] wrappedKeyShare = exportedKeyShares.get(1).getWrappedKeyShare();
                    byte[] wrappedChainCode = exportedKeyShares.get(1).getWrappedChainCode();
                    byte[] pubKey = exportedKeyShares.get(1).getPkixPublicKey();
                    return dstClients[1].getEcdsa().importKeyShares(importSessionConfig, threshold, wrappedKeyShare, wrappedChainCode, pubKey, "");
                });
        String dstKeyId = results.get(0);
        System.out.println("Imported key into destination TSM: " + dstKeyId);

        // Test the key migration by checking that destination public key matches source public key

        byte[] srcPublicKey = srcClients[0].getEcdsa().publicKey(srcKeyId, derivationPath);
        byte[] dstPublicKey = dstClients[0].getEcdsa().publicKey(dstKeyId, derivationPath);
        if (!Arrays.equals(srcPublicKey, dstPublicKey)) {
            throw new Exception("public key was different");
        }

    }


    @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");

// This example shows how to migrate a key from a source TSM to a destination TSM.
async function main() {

  // Create clients for the source TSM

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

  const srcClients = [];

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

    srcClients.push(client);
  }

  // Create clients for the target TSM. We use MPC nodes from the same TSM here, but you can import to
  // MPC nodes in another TSM as well, as long as the MPC nodes in the destination TSM have the same player indices.

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

  const dstClients = [];

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

    dstClients.push(client);
  }

  // Generate a key in the source TSM

  const players = [0, 1]; // Key shares can currently only be moved between nodes with the same player indices.
  const threshold = 1; // The security threshold for this key
  console.log(new Uint32Array(players));

  const keyGenSessionConfig = await SessionConfig.newSessionConfig(
    await SessionConfig.GenerateSessionID(),
    new Uint32Array(players),
    {}
  );

  console.log(`Generating key using players ${players} in the source TSM`);

  const srcKeyIds = ["", ""];

  const srcKeyPromises = [];

  for (const [i, client] of srcClients.entries()) {
    const func = async () => {
      const ecdsaApi = client.ECDSA();
      srcKeyIds[i] = await ecdsaApi.generateKey(
        keyGenSessionConfig,
        threshold,
        curves.SECP256K1,
        ""
      );
    };

    srcKeyPromises.push(func());
  }

  await Promise.all(srcKeyPromises);

  const srcKeyId = srcKeyIds[0];

  // Get wrapping keys for the MPC nodes in the destination TSM

  const wrappingKeys = [new Uint8Array([]), new Uint8Array([])];

  for (const [i, client] of dstClients.entries()) {
    const wrappingKeyApi = client.WrappingKey();
    wrappingKeys[i] = await wrappingKeyApi.wrappingKey();
  }

  // Export wrapped key shares from the source TSM
  console.log(
    `Exporting key sharing from players ${players} in the source TSM`
  );

  const derivationPath = new Uint32Array([]); // We want to export the key itself, not a derivation of the key.

  /**
   * @property {Uint8Array} wrappedKeyShare
   * @property {Uint8Array} wrappedChainCode
   * @property {Uint8Array} pkixPublicKey
   */

  const wrappedKeyShares = [{}, {}];

  const exportSessionConfig = await SessionConfig.newSessionConfig(
    await SessionConfig.GenerateSessionID(),
    new Uint32Array(players),
    {}
  );

  const wrappedKeyPromises = [];

  for (const [i, client] of srcClients.entries()) {
    const func = async () => {
      const ecdsaApi = client.ECDSA();

      wrappedKeyShares[i] = await ecdsaApi.exportKeyShares(
        exportSessionConfig,
        srcKeyId,
        derivationPath,
        wrappingKeys[i]
      );
    };

    wrappedKeyPromises.push(func());
  }

  await Promise.all(wrappedKeyPromises);

  // Import the key shares into the destination TSM
  const importSessionConfig = await SessionConfig.newSessionConfig(
    await SessionConfig.GenerateSessionID(),
    new Uint32Array(players),
    {}
  );

  const dstKeyIds = ["", ""];

  console.log(`Importing key sharing to players ${players} in the target TSM`);

  const dstKeyPromises = [];

  for (const [i, client] of dstClients.entries()) {
    const func = async () => {
      const ecdsaApi = client.ECDSA();
      dstKeyIds[i] = await ecdsaApi.importKeyShares(
        importSessionConfig,
        threshold,
        wrappedKeyShares[i].wrappedKeyShare,
        wrappedKeyShares[i].wrappedChainCode,
        wrappedKeyShares[i].pkixPublicKey,
        ""
      );
    };

    dstKeyPromises.push(func());
  }

  await Promise.all(dstKeyPromises);

  const dstKeyId = dstKeyIds[0];

  // Test that destination public key equals the original public key

  const srcClientEcdsa = srcClients[0].ECDSA();
  const srcPubKey = await srcClientEcdsa.publicKey(srcKeyId, derivationPath);

  const dstClientEcdsa = dstClients[0].ECDSA();
  const dstPubKey = await dstClientEcdsa.publicKey(dstKeyId, derivationPath);

  if (!Buffer.from(srcPubKey).equals(dstPubKey)) {
    console.log("public key was different after moving key destination nodes");
  } else {
    console.log("Key was successfully moved from source nodes to target nodes");
  }
}

main()

Running the example, you should see an output similar to this:

Generating key in source TSM using players [0, 1]
Generated key with ID: 3ASrdlLTID6cL8k7mgKx0fqL0DRk
Exporting wrapped key shares from players [0, 1] from the source TSM
Importing wrapped key shares from players [0, 1] into the destination TSM
Imported key into destination TSM: 3ASrdlLTID6cL8k7mgKx0fqL0DRk