Key Share Backup

You can create a backup of a key share from an MPC node in the TSM using this:

ctx := context.Background()
backup, err := node.ECDSA().BackupKeyShare(ctx, keyID)

The returned backup is a byte array that contains the key share, along with some additional meta, including the key ID. You can later restore the key share in the MPC node like this:

keyID, err := node.ECDSA().RestoreKeyShare(ctx, backup)

The key will get the same key ID when restored.

πŸ“˜

Protecting the Backup

The returned backup is an internal format that contains the key share. The key share is not encrypted, so it is important that the backup is stored securely. The BackupKeyShare and RestoreKeyShare methods only work if key share backup has been enabled in the MPC node configuration.

There are several ways to back up a TSM, as explained here. But using BackupKeyShare and RestoreKeyShare, as explained here, is a simple way to back up an individual share of a key, without the need to run an MPC session involving the other MPC nodes.

Use Cases

The fact that BackupKeyShare and RestoreKeyShare allows you to back up an individual key share without interacting with the other MPC nodes in the TSM, makes it well suited for backup of key shares on a mobile device.

The additional metadata stored in the backup increases the size of the backup. There are several ways to handle the backup; a couple of them are:

  • Create a QR code containing the backup data to be saved securely outside the phone. Restoring is just a matter of scanning the QR code. This will require access to printers from the phone to get the QR code printed, and the printout should not be left in the printer unattended as it contains the key share.
  • Create a symmetric AES key (16-32 bytes) and encrypt the backed-up data. The user can then store or memorize the AES key, e.g., using BIP39, or write down the hex or base64 encoding. The encryption of the backed-up data can then be stored in some central server, as the only one who can decrypt it will be the user who knows the AES key.

These are just a couple of examples of how the backup can be handled safely for inspiration, but many other solutions will solve the problem just as well.

πŸ“˜

Key Share Backup and Key Resharing

Care must be taken if you use key share backup together with our key resharing 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.

Code Example

package main

import (
	"context"
	"crypto/sha256"
	"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 nodes", 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]

	// Back up the key share of an MPC node

	backup, err := clients[0].ECDSA().BackupKeyShare(ctx, keyID)
	if err != nil {
		panic(err)
	}

	// Delete the key share from the MPC node, to simulate that it is lost.

	fmt.Println("Deleting key share for key", keyID, "on MPC node 0")
	if err = clients[0].KeyManagement().DeleteKeyShare(ctx, keyID); err != nil {
		panic(err)
	}

	// Restore the lost key share from the backup

	restoredKeyID, err := clients[0].ECDSA().RestoreKeyShare(ctx, backup)
	if err != nil {
		panic(err)
	}
	fmt.Println("Key share for key", restoredKeyID, "restored from backup on MPC node 0")

	if restoredKeyID != keyID {
		panic("different key id after restoration of key share")
	}

	// Test that we can sign with the restored key share

	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 nodes", 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, nil, 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

	publicKey, err := clients[0].ECDSA().PublicKey(ctx, keyID, nil)
	if err != nil {
		panic(err)
	}

	if err = tsm.ECDSAVerifySignature(publicKey, msgHash[:], signature.ASN1()); err != nil {
		panic(err)
	}

	fmt.Println("Signature was valid")
}

package com.example;

import com.sepior.tsm.sdkv2.*;

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

public class EcdsaBackupKeyShareExample {

    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)
        final int[] derivationPath = null; // In this example we do not use key derivation
        
        String keyGenSessionId = SessionConfig.generateSessionId();
        final SessionConfig keyGenSessionConfig = SessionConfig.newSessionConfig(keyGenSessionId, keyGenPlayers, null);
        System.out.println("Generating key using players " + Arrays.toString(keyGenPlayers));
        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);


        // Back up the key share of an MPC node

        byte[] backup = clients[0].getEcdsa().backupKeyShare(keyId);


        // Delete the key share from the MPC node, to simulate that it is lost.

        System.out.println("Deleting key share for key " + keyId + "on MPC node 0");
        clients[0].getKeyManagement().deleteKeyShare(keyId);


        // Restore the lost key share from the backup

        String restoredKeyId = clients[0].getEcdsa().restoreKeyShare(backup);
        System.out.println("Key share for key " + restoredKeyId + " restored from backup on MPC node 0");


        // Test that we can sign a message after restoring key share

        final int[] signPlayers = {0, 2}; // We sign with threshold + 1 players, in this case MPC node 1, 2
        final byte[] messageHash = new byte[32];  // Normally, this is the SHA256 hash of the message.

        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[0].getEcdsa().sign(signSessionConfig, keyId, derivationPath, messageHash),
                () -> clients[2].getEcdsa().sign(signSessionConfig, keyId, derivationPath, messageHash));
        byte[][] partialSignatures = {signResults.get(0).getPartialSignature(), signResults.get(1).getPartialSignature()};
        // When we provide the message to `finalizeSignature`, the method also validates the signature
        Ecdsa.finalizeSignature(messageHash, partialSignatures);
        System.out.println("Signature generated after restoring share was 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
  const keygenPlayers = [0, 1, 2];
  const keygenSessionConfig = await SessionConfig.newSessionConfig(
    await SessionConfig.GenerateSessionID(),
    new Uint32Array(keygenPlayers),
    {}
  );

  const masterKeyIds = ["1", "2", "3"];

  console.log("Generating key using nodes 0, 1, and 2");

  const generateKeyPromises = [];

  for (const [i, client] of clients.entries()) {
    const func = async () => {
      const ecdsaApi = client.ECDSA();
      masterKeyIds[i] = await ecdsaApi.generateKey(
        keygenSessionConfig,
        threshold,
        curves.SECP256K1,
        ""
      );
    };
    generateKeyPromises.push(func());
  }

  await Promise.all(generateKeyPromises);

  // Backup the key share of an MPC node

  const firstClient = clients[0];
  const firstEcdsaApi = firstClient.ECDSA();
  const firstKeyId = masterKeyIds[0];

  const backup = await firstEcdsaApi.backupKeyShare(firstKeyId);

  // Delete the key share from the MPC node, to simulate that it is lost.
  console.log(`Deleting key share for key ${firstKeyId} on MPC node 0`);

  const keyManagement = firstClient.KeyManagement();
  await keyManagement.deleteKeyShare(firstKeyId);

  const restoredKeyId = await firstEcdsaApi.restoreKeyShare(backup);

  console.log(
    `Key share for key ${restoredKeyId} restored from backup on MPC Node 0`
  );

  if (restoredKeyId !== firstKeyId) {
    console.log("Different key id after restoration of key share");
    return;
  }

  // Test that we can sign with the restored key share

  const message = "This is a message to be signed";
  const messageHash = crypto.createHash("sha256").update(message).digest();

  const signPlayers = new Uint32Array([0, 1]); // We want to sign with the first two MPC nodes
  const sessionId = await SessionConfig.GenerateSessionID();
  const signSessionConfig = await SessionConfig.newSessionConfig(
    sessionId,
    signPlayers,
    {}
  );

  console.log(`Creating signature using nodes 0 and 1`);

  const partialSignatures = [];

  const partialSignaturePromises = [];

  for (const playerIdx of signPlayers) {
    const func = async () => {
      const ecdsaApi = clients[playerIdx].ECDSA();

      const partialSignResult = await ecdsaApi.sign(
        signSessionConfig,
        firstKeyId,
        new Uint32Array([]),
        messageHash
      );

      partialSignatures.push(partialSignResult);
    };

    partialSignaturePromises.push(func());
  }

  await Promise.all(partialSignaturePromises);

  const signature = await firstEcdsaApi.finalizeSignature(
    messageHash,
    partialSignatures
  );

  // Verify the signature relative to the signed message and the public key

  const publicKey = await firstEcdsaApi.publicKey(
    firstKeyId,
    new Uint32Array([])
  );

  try {
    await firstEcdsaApi.verifySignature(
      publicKey,
      messageHash,
      signature.signature
    );
    console.log("Signature was valid");
  } catch (e) {
    console.log(e);
  }
}

main()

Running the example produces output similar to this:

Generating key using players [0, 1, 2]
Generated key with ID: pOsLzjLPYOmJU3bNMCI0UL1e9r0S
Deleting key share for key pOsLzjLPYOmJU3bNMCI0UL1e9r0Son MPC node 0
Key share for key pOsLzjLPYOmJU3bNMCI0UL1e9r0S restored from backup on MPC node 0
Signing message with players [0, 2]
Signature generated after restoring share was valid