External Key Import (ECDSA)

Sometimes you may need to import an external key into the TSM, e.g., a key from an external key store or wallet. To do this, you will have to first create a secret sharing of the key, then wrap (encrypt) each share with the wrapping key of the MPC node that should receive the share, and then run an MPC session that imports the key.

The following shows you how to do this for an ECDSA key.

We will assume here that you want to import a Bitcoin or Ethereum private key, i.e., a raw scalar on the secp256k1 curve, and that it has the following hex encoding:

privateKeyHex := "90c3a45df61c8dd3c683a1772657473868c0bb416092d80b6a12d3aa314916d8"

We will also assume that the corresponding chain code has this hex encoding:

chainCodeHex := "7365df71160ca42df2fa3f447fb62f74c90e1996a7cacbd437d41a3638a49809"

📘

Chain Code is Optional

This example assumes that you are importing an extended key, consisting of the private key itself and a chain code. The extended key could for example be the BIP32 master key, or an extended key derived from a master key.

If the key you want to import is not an extended BIP32 key, you can simply ignore the chain code in the following. The TSM will then generate a random chain code, when you import the private key.

You will also need the public key that corresponds to the private key. If you don’t have the public key already, you can compute it from the private key as follows:

privateKey, err := hex.DecodeString(privateKeyHex)
curve, err := ec.NewCurve(ec.Secp256k1.Name())
x, err := curve.Zn().DecodeScalar(privateKey)
y := curve.G().Multiply(x)
pkixPubKey, err := tsmutils.ECPointToPKIXPublicKey(curve.Name(), y.Encode())

The next step is to create a secret sharing of the private key. In this example we will create a secret sharing for three MPC nodes identified by indices 0, 1, 2, and with a security threshold of 1:

threshold := 1
players := []int{0, 1, 2}
keyShares, err := tsmutils.ShamirSecretShare(threshold, players, ec.Secp256k1.Name(), privateKey)

Now each of the key shares must be encrypted under the wrapping key of the MPC node to which it should be sent. For example, assuming node1 is the SDK controlling MPC node 1, this can be done as follows:

ctx := context.Background()
wrappingKey, err := node1.WrappingKey().WrappingKey(ctx)
pub, err := x509.ParsePKIXPublicKey(wrappingKey)
rsaWrappingKey := pub.(*rsa.PublicKey)
wrappedShare1, err := tsmutils.Wrap(rsaWrappingKey, keyShares[1])
wrappedChainCode1, err := tsmutils.Wrap(rsaWrappingKey, masterChainCode)

The key import is then completed by requesting a key import MPC session as follows:

essionID := tsm.GenerateSessionID()
players := []int{1,2,3}
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
keyID, err = node1.ECDSA().ImportKeyShares(ctx, sessionConfig, threshold, wrappedShare1, wrappedChainCode1, pkixPubKey, "")

As usual, the MPC session only starts when all the nodes defined by the configuration have made this call on their SDK, and it only succeeds if they agree on the session meta data (sessionID, players ) as well as the value of threshold and pkixPubKey.

The wrapped chain code is an optional argument. If provided, the MPC nodes will make check that the same chain code was provided to all MPC nodes.

Importing from a BIP32 Master Seed or BIP32 Mnemonic Code

In the previous section we considered import of a regular private ECDSA key or an extended BIP32 key consisting of the private key and a chain code into the TSM.

In some cases, though, you may want to import keys based on a single BIP32 seed, or a BIP39 mnemonic code that encodes a BIP32 seed.

The TSM does not currently support BIP39, so if you hold a BIP39 mnemonic code, you first convert this to the corresponding BIP32 seed.

There are then two options:

  1. Convert the BIP32 seed to an extended key and import the extended key as explained above. Note that once imported like this, the TSM only supports non-hardened derivations. So if the BIP32 seed was used in an external wallet that follows BIP44, this will only work if you derive the extended BIP32 key for each derived BIP44 account before, and then imports these keys separately into the TSM. And these keys will then exist in the TSM as separate keys.
  2. If the first approach does not fit your use case, the TSM supports importing the BIP32 seed directly, and it lets you do hardened BIP32 derivations of keys, once they are imported. But this is currently only supported for specific threshold parameters, and the initial hardened derivations are quite resource demanding. You can read more about this approach in our section about hardened key derivation.

Code Example

Here is a full example showing how to import an extended BIP32 key (i.e., a raw ECDSA secp256k1 private key and the corresponding chain code) into the TSM.

package main

import (
	"bytes"
	"context"
	"crypto/rsa"
	"crypto/x509"
	"encoding/hex"
	"gitlab.com/sepior/go-tsm-sdkv2/ec"
	"gitlab.com/sepior/go-tsm-sdkv2/tsm"
	"gitlab.com/sepior/go-tsm-sdkv2/tsm/tsmutils"
	"golang.org/x/sync/errgroup"
)

func main() {

	// This is the external key and chain code that we want to import into the TSM;
	// e.g. as recovered by ERS.
	// Note: The chain code is optional, and only relevant if you import an extended BIP32 key.
	// To just import a private ECDSA key, you can set the chain code to nil.

	privateKeyHex := "90c3a45df61c8dd3c683a1772657473868c0bb416092d80b6a12d3aa314916d8"
	chainCodeHex := "7365df71160ca42df2fa3f447fb62f74c90e1996a7cacbd437d41a3638a49809"

	privateKey, err := hex.DecodeString(privateKeyHex)
	if err != nil {
		panic(err)
	}
	chainCode, err := hex.DecodeString(chainCodeHex)
	if err != nil {
		panic(err)
	}

	// We first compute the public key corresponding to the private key.

	curve, err := ec.NewCurve(ec.Secp256k1.Name())
	x, err := curve.Zn().DecodeScalar(privateKey)
	if err != nil {
		panic(err)
	}
	y := curve.G().Multiply(x)
	pkixPubKey, err := tsmutils.ECPointToPKIXPublicKey(curve.Name(), y.Encode())
	if err != nil {
		panic(err)
	}

	// Create clients for three 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)
		}
	}

	// Split the private key into a secret sharing.

	threshold := 1 // Can also be set to 2
	players := []int{0, 1, 2}
	ecdsaKeyShares, err := tsmutils.ShamirSecretShare(threshold, players, ec.Secp256k1.Name(), privateKey)
	if err != nil {
		panic(err)
	}

	// Import one secret share into each MPC node, encrypted under that MPC node's public wrapping key.

	sessionID := tsm.GenerateSessionID()
	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

			wrappingKey, err := client.WrappingKey().WrappingKey(context.Background())
			if err != nil {
				return err
			}

			pub, err := x509.ParsePKIXPublicKey(wrappingKey)
			if err != nil {
				return err
			}
			rsaWrappingKey := pub.(*rsa.PublicKey)
			wrappedShare, err := tsmutils.Wrap(rsaWrappingKey, ecdsaKeyShares[i])
			if err != nil {
				return err
			}

			wrappedChainCode, err := tsmutils.Wrap(rsaWrappingKey, chainCode)
			if err != nil {
				return err
			}

			sessionConfig := tsm.NewStaticSessionConfig(sessionID, len(clients))
			keyIDs[i], err = client.ECDSA().ImportKeyShares(context.Background(), sessionConfig, threshold, wrappedShare, wrappedChainCode, pkixPubKey, "")
			return err

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

	// Test: All MPC nodes should agree on the new key ID.

	for _, keyID := range keyIDs {
		if keyID != keyIDs[0] {
			panic("keyID disagreement")
		}
	}

	// Test: Once imported, the public key should equal the public key we computed.

	for _, client := range clients {
		pubKey, err := client.ECDSA().PublicKey(context.Background(), keyIDs[0], nil)
		if err != nil {
			panic(err)
		}
		if !bytes.Equal(pkixPubKey, pubKey) {
			panic("public key disagreement")
		}
	}

}
package com.example;

import com.sepior.tsm.sdkv2.Client;
import com.sepior.tsm.sdkv2.Configuration;
import com.sepior.tsm.sdkv2.EcdsaWrappedKeyShare;
import com.sepior.tsm.sdkv2.SessionConfig;
import com.sepior.tsm.sdkv2.TsmUtils;

import org.web3j.crypto.*;

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

public class EcdsaImportExternalExample {

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


        // This is the external key and chain code that we want to import into the TSM;
        // e.g. as recovered by ERS.
        // Note: The chain code is optional, and only relevant if you import an extended BIP32 key.
        // To just import a private ECDSA key, you can set the chain code to nil.

        String privateKeyHex = "90c3a45df61c8dd3c683a1772657473868c0bb416092d80b6a12d3aa314916d8";
        byte[] privateKey = HexFormat.of().parseHex("90c3a45df61c8dd3c683a1772657473868c0bb416092d80b6a12d3aa314916d8");
        byte[] chainCode = HexFormat.of().parseHex("7365df71160ca42df2fa3f447fb62f74c90e1996a7cacbd437d41a3638a49809");


        // We first compute the public key corresponding to the private key, using a 3rd party library (web3j).

        byte[] publicKey = Sign.publicKeyFromPrivate(new BigInteger(privateKeyHex, 16)).toByteArray();
        publicKey[0] = 0x04; // SEC uses 0x04 to specify compressed format.
        System.out.println("Public key: " + bytesToHex(publicKey));
        byte[] pkixPubKey = TsmUtils.ecPointToPkixPublicKey("secp256k1", publicKey);


        // Create clients for three nodes.

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


        // Split the private key into a secret sharing.

        int threshold = 1; // Can also be set to 2
        int[] players = {0, 1, 2};
        Map<Integer, byte[]> ecdsaKeyShares = TsmUtils.shamirSecretShare(threshold, players, "secp256k1", privateKey);


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

        byte[][] wrappingKeys = {
                clients[0].getWrappingKey().getWrappingKey(),
                clients[1].getWrappingKey().getWrappingKey(),
                clients[2].getWrappingKey().getWrappingKey(),
        };


        // Encrypt each key share and chain code under the corresponding wrapping key.

        byte[][] wrappedKeyShares = {
                TsmUtils.wrap(wrappingKeys[0], ecdsaKeyShares.get(0)),
                TsmUtils.wrap(wrappingKeys[1], ecdsaKeyShares.get(1)),
                TsmUtils.wrap(wrappingKeys[2], ecdsaKeyShares.get(2)),
        };

        byte[][] wrappedChainCodes = {
                TsmUtils.wrap(wrappingKeys[0], chainCode),
                TsmUtils.wrap(wrappingKeys[1], chainCode),
                TsmUtils.wrap(wrappingKeys[2], chainCode),
        };

        // Import one secret share into each MPC node, encrypted under that MPC node's public wrapping key.

        String importSessionId = SessionConfig.generateSessionId();
        final SessionConfig importSessionConfig = SessionConfig.newSessionConfig(importSessionId, players, null);
        System.out.println("Importing wrapped key shares to players " + Arrays.toString(players));
        List<String> keyIDs = runConcurrent(
                () -> clients[0].getEcdsa().importKeyShares(importSessionConfig, threshold, wrappedKeyShares[0], wrappedChainCodes[0], pkixPubKey, null),
                () -> clients[1].getEcdsa().importKeyShares(importSessionConfig, threshold, wrappedKeyShares[1], wrappedChainCodes[1], pkixPubKey, null),
                () -> clients[2].getEcdsa().importKeyShares(importSessionConfig, threshold, wrappedKeyShares[2], wrappedChainCodes[2], pkixPubKey, null));
        String keyID = keyIDs.get(0);
        // Note: You may want to check that all three nodes return the same key ID, before you trust it.

        // Check: The TSM should return the correct public key.
        byte[] pubKey = clients[0].getEcdsa().publicKey(keyID, null);
        if (!Arrays.equals(pubKey, pkixPubKey)) {
            throw new RuntimeException("Public key mismatch");
        }

        System.out.println("Imported key into the TSM; key ID = " + keyID);

    }


    @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 elliptic = require("elliptic");

async function main() {
  
  // This is the external key and chain code that we want to import into the TSM;
  // e.g. as recovered by ERS.
  // Note: The chain code is optional, and only relevant if you import an extended BIP32 key.
  // To just import a private ECDSA key, you can set the chain code to nil.

  const privateKeyHex =
    "90c3a45df61c8dd3c683a1772657473868c0bb416092d80b6a12d3aa314916d8";
  const chainCodeHex =
    "7365df71160ca42df2fa3f447fb62f74c90e1996a7cacbd437d41a3638a49809";

  const privateKey = Buffer.from(privateKeyHex, "hex");
  const chainCode = Buffer.from(chainCodeHex, "hex");

  // Create clients for three 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);
  }

  // We first compute the public key corresponding to the private key.

  const ec = new elliptic.ec(curves.SECP256K1);
  const key = ec.keyFromPrivate(privateKey);
  const x = key.getPrivate();

  const g = ec.g;

  const y = g.mul(x);

  const apiUtils = clients[0].Utils();

  const pkixPubKey = await apiUtils.ecPointToPKIXPublicKey(
    curves.SECP256K1,
    Buffer.from(y.encode("hex", false), "hex")
  );
  // Split the private key into a secret sharing.

  const threshold = 1; // Can also be set to 2
  const players = [0, 1, 2];
  const ecdsaKeyShares = [
    ...(
      await apiUtils.shamirSecretShare(
        threshold,
        new Uint32Array(players),
        curves.SECP256K1,
        privateKey
      )
    ).values(),
  ];

  // Import one secret share into each MPC node, encrypted under that MPC node's public wrapping key.

  const sessionId = await SessionConfig.GenerateSessionID();
  const keyIds = ["", "", ""];

  const keyIdPromises = [];

  for (const [i, client] of clients.entries()) {
    const func = async () => {
      const wrappingApi = client.WrappingKey();
      const wrappingKey = await wrappingApi.wrappingKey();

      const utilsApi = client.Utils();
      const wrappedShare = await utilsApi.wrap(wrappingKey, ecdsaKeyShares[i]);

      const wrappedChainCode = await utilsApi.wrap(wrappingKey, chainCode);

      const sessionConfig = await SessionConfig.newStaticSessionConfig(
        sessionId,
        players.length
      );

      const ecdsaApi = client.ECDSA();

      keyIds[i] = await ecdsaApi.importKeyShares(
        sessionConfig,
        threshold,
        wrappedShare,
        wrappedChainCode,
        pkixPubKey,
        ""
      );
    };

    keyIdPromises.push(func());
  }

  await Promise.all(keyIdPromises);

  // Test: All MPC nodes should agree on the new key ID.

  for (let i = 1; i < keyIds.length; i++) {
    if (keyIds[0] !== keyIds[i]) {
      console.log("KeyID disagreement");
      return;
    }
  }

  // Test: Once imported, the public key should equal the public key we computed.

  for (const client of clients) {
    const ecdsaApi = client.ECDSA();
    const pubKey = await ecdsaApi.publicKey(keyIds[0], new Uint32Array([]));

    if (!Buffer.from(pkixPubKey).equals(pubKey)) {
      console.log("Public key disagreement");
      return;
    }
  }

  console.log("All public key equals");
}

main()

Running the example should produce output similar to this:

Public key: 04C0B07B9F729AAC4CAC31BF58B53B3A58A7B4916E290C32A97A437A974958513E40C8825ACBF2F69172AD33D32687C5EFF63AF727B61DC656885BF89F65ABCA44
Importing wrapped key shares to players [0, 1, 2]
Imported key into the TSM; key ID = FzJZff6PxMiks1XZ3HGQqsFtmWDu