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 := client.ECDSA().BackupKeyShare(ctx, keyID)
The returned backup is a byte array that contains the key share, along with some additional metadata, including the key ID. You can later restore the key share in the MPC node like this:
keyID, err := client.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
andRestoreKeyShare
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, making 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/Blockdaemon/go-tsm-sdkv2/v68/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, "secp256k1", "")
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
Updated 7 days ago