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
Updated 7 days ago