Emergency Recovery (ECDSA)

This article will show you how to create an ERS backup of an ECDSA key in the TSM and how the key can later be recovered from the backup. In this example, we will use the TSM SDK to do the recovery, but on request, we can also provide an open-source Go reference implementation of the recovery and validation.

The example in this section assumes that you have access to a TSM and that you have access to the TSM SDK. Our Getting Started tutorials provide more information about how to get to that point.

Creating Recovery Data

Creating recovery data requires that the MPC nodes in the TSM are configured to allow this. See this section for more about how to configure this in the TSM.

We first need to generate a key in the TSM so we have something to recover. For this we run an MPC key generation session as follows:

sessionID := tsm.GenerateSessionID()
players := []int{0, 1, 2} // We want to generate the key as a sharing among these players
threshold := 1 // The security threhsold of the key
sessionConfig := tsm.NewSessionConfig(sessionID, threshold, players, nil)
ctx := context.Background()
keyID, err = client.ECDSA().GenerateKey(ctx, sessionConfig, threshold, ec.Secp256k1.Name(), "")

For the MPC session to start, you must make this call using each of the three MPC nodes, and they must all agree on the session parameters (sessionID, players, threshold). A complete, running example of this can be found below.

Once the key is generated, we can now continue to create ERS recovery data for the key.

We first choose an ERS label and generate an ERS key:

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
)

ersLabel := []byte("exampleLabel")
ersPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
ersPublicKey, err := x509.MarshalPKIXPublicKey(&ersPrivateKey.PublicKey)

In this example, we generated the private ERS key in the clear. In a production setting, the ERS key will instead often be generated inside an HSM, and only the public ERS key will be exported from the HSM.

We can now run an MPC session that generates recovery data for the key as follows:

sessionID := tsm.GenerateSessionID()
sessionConfig := tsm.NewSessionConfig(sessionID, threshold, players, nil)
partialRecoveryData, err := client.ECDSA().GenerateRecoveryData(ctx, sessionConfig, keyID, ersPublicKey, ersLabel)

As with key generation, this call has to be done for each MPC node.

📘

Ensuring Validity of the ERS Public Key

The returned recovery data essentially consists of the private key share, encrypted under the provided ERS public key. For security, it’s important to enforce that the correct ERS public key is used, and not just some rogue public key. As part of the generation of the recovery data, the MPC nodes will compare the ERS public key that was provided by the SDK, and the MPC session will abort if they are not equal. So the way to ensure that recovery data uses the correct ERS public key is to let each SDK (or at least some of the SDKs) validate the ERS public key before the call to GenerateRecoveryData is made.

If the MPC session was successful, each SDK will receive partial recovery data. The partial data must be collected, and combined into the final recovery data:

recoveryData, err := tsm.ECDSAFinalizeRecoveryData(recoveryDataArray, &ersPrivateKey.PublicKey, ersLabel)

The recoveryData now contains all the key shares of the key, each encrypted under the ERS public key, and the recovery data can be stored and later used for recovery. The recovery data is not sensitive since the secret key shares are encrypted under the ERS public key.

Validating Recovery Data

The recovery data will often be stored for a while, maybe by some 3rd party. It contains a zero-knowledge proof that allows anyone to validate it, without being able to decrypt it.

Given the public ERS key, the ERS label, and the public ECDSA key, the recovery data can be validated, using the zero-knowledge proof, with this:

publicKey, err := ecdsaClient.PublicKey(keyID, nil) // Get the public key from the TSM
err = tsm.ECDSAValidateRecoveryData(recoveryData, publicKey, ersPublicKey, ersLabel)

We know the recovery data is valid if the call to RecoveryInfoValidate does not return an error. This means it contains key shares that can be decrypted with the provided ERS label and the private ERS key corresponding to the provided ERS public key, and that the decrypted key shares are indeed shares of the private key corresponding to the provided public key.

Anyone can validate the recovery data. It does not require the ERS private key to validate, and the key shares in the recovery data remain encrypted under the ERS public key.

Validation also does not require access to any MPC nodes. The ECDSAValidateRecoveryData() method in the TSM SDK is a static method. If you don't have access to a TSM SDK, you can validate the recovery data using our open-source ERS reference implementation or implement the validation method yourself.

Validating the recovery data regularly lets you detect if someone has corrupted or tampered with it. Also, if transferring the recovery data from one person to another, the receiving person may want to re-validate the recovery data if he doesn't trust the sender.

The only important thing to remember when validating, is to use the correct ERS public key, ERS label, and ECDSA public key, since the validity is relative to these keys.

Key Recovery

Given the ERS label and the private ERS key, the recovery data can later be used to recover the private ECDSA key from the recovery data. This can be done like this:

recoveredECDSAPrivateKey, err := tsm.ECDSARecoverPrivateKey(recoveryData, ersPrivateKey, ersLabel)

Here we used a static helper method in the TSM SDK for convenience. You can also recover the ECDSA key using our open-source reference implementation or implement the recovery code yourself.

📘

Key Recovery with Black Box Decryption

In the above example, we used the private ERS key directly to recover. In a production setting, this key may only exist inside an HSM, and we may want to do the recovery using only the RSA decryption function of the HSM, i.e., without exporting the private ERS key from the HSM. In our open-source ERS library, you can see how recovery can be done using only "black-box" calls to RSA decryptions using the private ERS key. That is, we can recover using only something that implements this interface:

Decrypt(ciphertext, label []byte) (plaintext []byte, err error)

A Complete Example

The following is a complete code example, combining the steps described above:

package main

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

func main() {

	// 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)
		}
	}

	// Generate an ECDSA key

	threshold := 2 // Security threshold to use for this key
	sessionConfig := tsm.NewStaticSessionConfig(tsm.GenerateSessionID(), len(clients))
	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(context.TODO(), sessionConfig, threshold, "secp256k1", "")
			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)

	// Create an ERS key pair and ERS label
	// Here we generate the private key in the clear, but it could also be exported from an HSM

	ersPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		panic(err)
	}
	ersPublicKey := &ersPrivateKey.PublicKey

	ersLabel := []byte("test")

	// Collect the partial recovery data

	sessionConfig = tsm.NewStaticSessionConfig(tsm.GenerateSessionID(), len(clients))
	recoveryDataArray := make([][]byte, 0)
	for _, client := range clients {
		client := client
		eg.Go(func() error {
			recoveryData, err := client.ECDSA().GenerateRecoveryData(context.TODO(), sessionConfig, keyID, ersPublicKey, ersLabel)
			if err != nil {
				return err
			}
			recoveryDataArray = append(recoveryDataArray, recoveryData)
			return nil
		})
	}
	if err := eg.Wait(); err != nil {
		panic(err)
	}

	// Combine the partial recovery data into the final recovery data

	recoveryData, err := tsm.ECDSAFinalizeRecoveryData(recoveryDataArray, &ersPrivateKey.PublicKey, ersLabel)
	if err != nil {
		panic(err)
	}

	// Get the public key

	publicKeys := make([][]byte, len(clients))
	for i, client := range clients {
		var err error
		if publicKeys[i], err = client.ECDSA().PublicKey(context.TODO(), keyID, nil); 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]

	// Validate the recovery data

	if err = tsm.ECDSAValidateRecoveryData(recoveryData, publicKey, ersPublicKey, ersLabel); err != nil {
		panic(err)
	}

	// Recover key using the recovery data and the ERS private key

	recoveredECDSAPrivateKey, err := tsm.ECDSARecoverPrivateKey(recoveryData, ersPrivateKey, ersLabel)
	if err != nil {
		panic(err)
	}

	fmt.Println("Recovered private ECDSA master key:", hex.EncodeToString(recoveredECDSAPrivateKey.PrivateKey))
	fmt.Println("Recovered master chain code:", hex.EncodeToString(recoveredECDSAPrivateKey.MasterChainCode))

}
package com.example;

import com.sepior.tsm.sdkv2.*;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;

public class EcdsaErsExample {

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

        // Create a client for three MPC nodes

        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[] players = {0, 1, 2}; // The key should be secret shared among all three MPC nodes
        final int threshold = 2; // 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 using players " + Arrays.toString(players));
        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);

        // Create an ERS key pair and ERS label
        // Here we generate the private key in the clear, but it could also be exported from an HSM

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(2048);
        KeyPair ersKeyPair = kpg.generateKeyPair();
        byte[] ersLabel = "myERSLabel".getBytes();
        byte[] ersPublicKey = ersKeyPair.getPublic().getEncoded();
        byte[] ersPrivateKey = ersKeyPair.getPrivate().getEncoded();


        // Collect the partial recovery data

        String generateRecoveryDataSessionId = SessionConfig.generateSessionId();
        final SessionConfig generateRecoverySessionConfig = SessionConfig.newSessionConfig(generateRecoveryDataSessionId, players, null);
        System.out.println("Generated recovery data for key " + keyId + " using players " + Arrays.toString(players));
        List<byte[]> partialRecoveryData = runConcurrent(
                () -> clients[0].getEcdsa().generateRecoveryData(generateRecoverySessionConfig, keyId, ersPublicKey, ersLabel),
                () -> clients[1].getEcdsa().generateRecoveryData(generateRecoverySessionConfig, keyId, ersPublicKey, ersLabel),
                () -> clients[2].getEcdsa().generateRecoveryData(generateRecoverySessionConfig, keyId, ersPublicKey, ersLabel));


        // Combine the partial recovery data into the final recovery data

        byte[] recoveryData = com.sepior.tsm.sdkv2.Ecdsa.finalizeRecoveryData(partialRecoveryData.toArray(new byte[][]{}), ersPublicKey, ersLabel);


        // Get the public ECDSA 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 trusting it.


        // Validate the recovery data using the public ECDSA key, the public ERS key and the ERS label

        if (!com.sepior.tsm.sdkv2.Ecdsa.validateRecoveryData(recoveryData, publicKey, ersPublicKey, ersLabel)) {
            throw new RuntimeException("The recovery data was invalid!");
        }
        System.out.println("The recovery data was valid");


        // Recover the private ECDSA key using the recovery data and the private ERS key, and the ERS label

        EcdsaRecoveredPrivateKey recoveredKey = com.sepior.tsm.sdkv2.Ecdsa.recoverPrivateKey(recoveryData, ersPrivateKey, ersLabel);
        System.out.println("Recovered private key : " + bytesToHex(recoveredKey.getPrivateKey()));
        System.out.println("Recovered chain code  : " + bytesToHex(recoveredKey.getMasterChainCode()));

    }


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

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 ECDSA key

  const threshold = 2;
  let sessionConfig = await SessionConfig.newStaticSessionConfig(
    await SessionConfig.GenerateSessionID(),
    clients.length
  );

  const keyIds = ["", "", ""];

  const keyIdsPromises = [];

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

      keyIds[i] = await ecdsaApi.generateKey(
        sessionConfig,
        threshold,
        curves.SECP256K1,
        ""
      );
    };

    keyIdsPromises.push(func());
  }

  await Promise.all(keyIdsPromises);

  // 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}`);

  // Create an ERS key pair and ERS label
  // Here we generate the private key in the clear, but it could also be exported from an HSM

  const { privateKey: ersPrivateKey, publicKey: ersPublicKey } =
    crypto.generateKeyPairSync("rsa", {
      modulusLength: 2048,
      publicKeyEncoding: {
        type: "spki",
        format: "der",
      },
      privateKeyEncoding: {
        type: "pkcs8",
        format: "der",
      },
    });

  const ersLabel = Buffer.from("test");

  // Collect the partial recovery data

  sessionConfig = await SessionConfig.newStaticSessionConfig(
    await SessionConfig.GenerateSessionID(),
    clients.length
  );

  const recoveryDataArray = [];

  const recoveryDataPromises = [];

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

      const recoveryData = await ecdsaApi.generateRecoveryData(
        sessionConfig,
        keyId,
        Buffer.from(ersPublicKey),
        ersLabel
      );

      recoveryDataArray.push(recoveryData);
    };

    recoveryDataPromises.push(func());
  }

  await Promise.all(recoveryDataPromises);

  // Combine the partial recovery data into the final recovery data
  const ecdsaApi = clients[0].ECDSA();

  const recoveryData = await ecdsaApi.finalizeRecoveryData(
    recoveryDataArray,
    Buffer.from(ersPublicKey),
    ersLabel
  );

  // Get the public key

  const publicKeys = [];

  for (const [i, client] of clients.entries()) {
    const ecdsaApi = client.ECDSA();
    publicKeys.push(await ecdsaApi.publicKey(keyId));
  }

  // Validate public keys

  for (let i = 1; i < publicKeys.length; i++) {
    if (!Buffer.from(publicKeys[0]).equals(publicKeys[i])) {
      console.log("Public keys do not match");
      return;
    }
  }

  const publicKey = publicKeys[0];

  // Validate the recovery data

  const result = await ecdsaApi.validateRecoveryData(
    recoveryData,
    publicKey,
    Buffer.from(ersPublicKey),
    ersLabel
  );

  console.log(result);

  // Recover key using the recovery data and the ERS private key

  const recoveredEcdsaPrivateKey = await ecdsaApi.recoverPrivateKey(
    recoveryData,
    Buffer.from(ersPrivateKey),
    ersLabel
  );

  console.log(
    `Recovered private ECDSA master key: ${Buffer.from(
      recoveredEcdsaPrivateKey.privateKey
    ).toString("hex")}`
  );

  console.log(
    `Recovered master chain code: ${Buffer.from(
      recoveredEcdsaPrivateKey.masterChainCode
    ).toString("hex")}`
  );
}

main();

Running the example should generate output similar to this:

Generating key using players [0, 1, 2]
Generated key with ID: MhbR81vWbLBwEZtbc1sE1hQRRHbN
Generated recovery data for key MhbR81vWbLBwEZtbc1sE1hQRRHbN using players [0, 1, 2]
Public key: 0x3056301006072A8648CE3D020106052B8104000A03420004B495BB1E65268C18F907954EACA0574321638E2D00EBD40FDE52B29BDB908F2D1669116C32AE94C564AA75EFD33C59695C743A0BEDD7D5D94FA21F52C149D2A4
The recovery data was valid
Recovered private key : 626B004AF86B8D9970F0056E01047FEE16BB68F1CE6CB66EA967F2CAD4464C81
Recovered chain code  : 76C0675FCC49C8459F9637D507F26FBC1199143D39311AAF187623B9B800E05E