Key Lifecycle Management
The TSM provides a number of methods to manage the life of a key.
Listing and Deleting Keys
You can get a list of the IDs of all keys for which a given MPC node holds shares:
ctx := context.Background()
keyIDs, err := node.KeyManagement().ListKeys(ctx)
Given a specific key ID, you can delete the corresponding key share on the MPC node like this:
err := node.KeyManagement().DeleteKeyShare(ctx, keyID)
Note that ListKeys
and DeleteKeyShare
operate on a single MPC node. So the fact that a single MPC node returns a key ID does not necessarily mean that the key “exists” in the TSM. Likewise, deleting the key share may not delete the key itself from the TSM. Generally, a key “exists” in the TSM if a sufficient number of MPC nodes (usually t+1, where t is the security threshold of the key) holds shares of the key in order to generate MPC signatures using the key.
Key Resharing
Suppose you have a key with ID keyID
in the TSM. The secret sharing of the key can then be refreshed by running an MPC key resharing session.
First choose the MPC session meta data, that is, the session ID, and the set of nodes to participate. All nodes holding key shares of the key must participate in the resharing.
sessionID := tsm.GenerateSessionID()
players := []int{ 0, 1, 2 } // This assumes the key was generated among Node 0, 1, 2
sessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
Then run the MPC session by calling this method on all SDKs:
ctx := context.Background()
err := node.ECDSA().Reshare(ctx, sessionConfig, keyID)
If the MPC session succeeds, the secret sharing of the key in the TSM will have been replaced by a fresh random secret sharing (of the same key).
Importantly, if the operation fails, for some reason, it should be retried until it succeeds. After this operation is called for a given key, and until it succeeds, other operations involving the same key might fail.
Key Resharing and Key Share Backup
Care must be taken if you use key resharing together with our key share backup 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.
Key Resharing and Presignatures
Any presignatures for a given key is automatically deleted when the key sharing is refreshed.
Here is a full code example, showing how to reshare a key. After resharing, the key will be the same, but it will be shared with a fresh, randomized secret sharing.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v64/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 players", 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]
fmt.Println("Generated key with ID:", keyID)
// Get the public key from one of the nodes
var derivationPath []uint32 = nil // We don't use key derivation in this example
publicKey, err := clients[0].ECDSA().PublicKey(ctx, keyID, derivationPath)
fmt.Println("Public key:", hex.EncodeToString(publicKey))
// Re-randomize the secret sharing of the key
resharePlayers := []int{0, 1, 2} // Must be same set of nodes as used when the key was generated
reshareSessionID := tsm.GenerateSessionID()
reshareSessionConfig := tsm.NewSessionConfig(reshareSessionID, resharePlayers, nil)
for _, client := range clients {
client := client
eg.Go(func() error {
err := client.ECDSA().Reshare(ctx, reshareSessionConfig, keyID)
return err
})
}
if err = eg.Wait(); err != nil {
panic(err)
}
fmt.Println("Completed resharing of key", keyID)
// Test: Create a signature with the re-shared key, and see if it's valid with the original public key
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 players", 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, derivationPath, 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
if err = tsm.ECDSAVerifySignature(publicKey, msgHash[:], signature.ASN1()); err != nil {
panic(err)
}
fmt.Println("Signature is valid")
}
package com.example;
import com.sepior.tsm.sdkv2.*;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
public class EcdsaReshareExample {
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)
String keyGenSessionId = SessionConfig.generateSessionId();
final SessionConfig keyGenSessionConfig = SessionConfig.newSessionConfig(keyGenSessionId, keyGenPlayers, null);
System.out.println("Generating key using players " + Arrays.toString(keyGenPlayers));
List<String> keyGenResults = 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 = keyGenResults.get(0);
System.out.println("Generated key with ID: " + keyId);
// Get the public key from one of the MPC nodes
int[] derivationPath = null; // We don't use key derivation in this example
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 using it, e.g. for creating a cryptocurrency account.
// Re-randomize the secret sharing of the key
String reshareSessionId = SessionConfig.generateSessionId();
int[] resharePlayers = {0, 1, 2}; // Must use the same players as used for the key generation
final SessionConfig reshareSessionConfig = SessionConfig.newSessionConfig(reshareSessionId, resharePlayers, null);
System.out.println("Resharing key " + keyId + " on players " + Arrays.toString(resharePlayers));
runConcurrent(
() -> {
clients[0].getEcdsa().reshare(reshareSessionConfig, keyId);
return null;
},
() -> {
clients[1].getEcdsa().reshare(reshareSessionConfig, keyId);
return null;
},
() -> {
clients[2].getEcdsa().reshare(reshareSessionConfig, keyId);
return null;
});
System.out.println("Completed resharing key " + keyId);
// Test: Create a signature with the re-shared key, and see if it's valid with the original public key
final byte[] msgHash = new byte[32]; // Normally, this is the SHA256 hash of the message.
final int[] signPlayers = {1, 2}; // We sign with threshold + 1 players, in this case MPC node 1, 2
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[1].getEcdsa().sign(signSessionConfig, keyId, derivationPath, msgHash),
() -> clients[2].getEcdsa().sign(signSessionConfig, keyId, derivationPath, msgHash));
byte[][] partialSignatures = {signResults.get(0).getPartialSignature(), signResults.get(1).getPartialSignature()};
EcdsaSignature signature = Ecdsa.finalizeSignature(msgHash, partialSignatures);
boolean valid = Ecdsa.verifySignature(publicKey, msgHash, signature.getSignature());
System.out.println("Validity of signature generated using the reshared key: " + 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 threshold for this key
const keygenPlayers = [0, 1, 2]; // The key should be secret shared among all three MPC nodes
const keygenSessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(keygenPlayers),
{}
);
const keyIds = ["", "", ""];
console.log(`Generating key using players ${keygenPlayers}`);
const generateKeyPromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
const ecdsaApi = client.ECDSA();
keyIds[i] = await ecdsaApi.generateKey(
keygenSessionConfig,
threshold,
curves.SECP256K1,
""
);
};
generateKeyPromises.push(func());
}
await Promise.all(generateKeyPromises);
const keyId = keyIds[0];
console.log(`Generated key with id: ${keyId}`);
// Get the public key from one of the nodes
const derivationPath = new Uint32Array([]); // We don't use key derivation in this example
const ecdsaApi = clients[0].ECDSA();
const publicKey = await ecdsaApi.publicKey(keyId, derivationPath);
console.log(`Public key: ${Buffer.from(publicKey).toString("hex")}`);
// Re-randomize the secret sharing of the key
const resharePlayers = [0, 1, 2]; // Must be same set of nodes as used when the key was generated
const reshareSessionId = await SessionConfig.GenerateSessionID();
const reshareSessionConfig = await SessionConfig.newSessionConfig(
reshareSessionId,
new Uint32Array(resharePlayers),
{}
);
const resharePromises = [];
for (const client of clients) {
const func = async () => {
const ecdsaApi = client.ECDSA();
await ecdsaApi.reshare(reshareSessionConfig, keyId);
};
resharePromises.push(func());
}
await Promise.all(resharePromises);
console.log(`Complete resharing of key ${keyId}`);
// Test: Create a signature with the re-shared key, and see if it's valid with the original public key
const message = "This is a message to be signed";
const messageHash = crypto.createHash("sha256").update(message).digest();
const signPlayers = [0, 1]; // We want to sign with the first two MPC nodes
const sessionId = await SessionConfig.GenerateSessionID();
const signSessionConfig = await SessionConfig.newSessionConfig(
sessionId,
new Uint32Array(signPlayers),
{}
);
console.log(`Creating signature using players ${signPlayers}`);
const partialSignatures = [];
const partialSignaturePromises = [];
for (const playerIdx of signPlayers) {
const func = async () => {
const ecdsaApi = clients[playerIdx].ECDSA();
const partialSignResult = await ecdsaApi.sign(
signSessionConfig,
keyId,
new Uint32Array([]),
messageHash
);
partialSignatures.push(partialSignResult);
};
partialSignaturePromises.push(func());
}
await Promise.all(partialSignaturePromises);
const signature = await ecdsaApi.finalizeSignature(
messageHash,
partialSignatures
);
// Verify the signature relative to the signed message and the public key
try {
const result = await ecdsaApi.verifySignature(
publicKey,
messageHash,
signature.signature
);
console.log(result);
console.log("Signature was valid");
} catch (e) {
console.log(e);
}
}
main()
This produces a result like:
Generating key using players [0, 1, 2]
Generated key with ID: ieRBiR3PL7KCcFAh3KZZzRB18ReB
Public key: 0x3056301006072A8648CE3D020106052B8104000A03420004759A0B9CD513510B364D07EAF836F5BC34B48926A09A1560A6CE95C5ED88F54C7907888E49E74481B353D4C6E77B06E86AF304224A4ECF4FA38F5D8DD2A53C17
Resharing key ieRBiR3PL7KCcFAh3KZZzRB18ReB on players [0, 1, 2]
Completed resharing key ieRBiR3PL7KCcFAh3KZZzRB18ReB
Signing message with players [1, 2]
Validity of signature generated using the reshared key: true
Key Copy
You can create a copy of a key that already exists in the Builder Vault. This is done as follows:
newKeyID, err = client.ECDSA().CopyKey(ctx, sessionConfig, keyID, curveName, newThreshold, desiredKeyID)
This call instructs the MPC node to participate in an MPC session that creates a copy of a key. The copy will represent the same key as the original key, but with a new random and independent secret sharing. The copy will be saved under a new key ID and the existing key will not be affected.
The MPC session may include MPC nodes that do not hold shares of the original key. For these MPC nodes, the key ID must be empty and curveName
must be the curve name for the original key, e.g., secp256k1
, or ED-25519
. MPC nodes that hold shares of the original key must provide keyID
and use an empty curveName
. The desiredKeyID
is optional, and if provided, it will be used as the key ID for the new copy.
The MPC session only succeeds if all MPC nodes agree on keyID
, newThreshold
, and desiredKeyID
. In addition, it will only succeed if threshold + 1 or more MPC nodes, who all hold key shares of the original key, participate in the session.
Use cases:
- If the number of MPC nodes in the key copy session is the same as the original number of MPC nodes that generated or imported the key, and the new threshold is also the same, this will just compute a fresh secret sharing of the same key. This will exist along with the old key, but with a different key ID. If the old key is then deleted, it works as a two-step resharing of the old key, which also changes the key ID.
- The new threshold can be different than the original threshold. This can be used to upgrade or downgrade the threshold security of the key. Note that this is not a security concern, since at least threshold + 1 MPC nodes are needed to complete the key copy. This number of MPC nodes would be able to sign anyway, and they could in principle also just recombine their key shares to gain full control of the key.
- The number of MPC nodes in the key copy session can be greater than or less than the number of MPC nodes that originally generated or imported the key. Given a (3,1) sharing, it is for example possible for two of the original MPC nodes to run a key copy MPC session that creates a (2,1) copy of the key. It is also possible for five MPC, two of which holds the shares of a (2,1) key, to run an MPC session that creates a new (5,2) copy of the key. Again, this is not a security concern since the MPC session requires acknowledgement of threshold+1 of the original key share holders.
The following is a complete example. First, a (2,1) key is generated among two MPC nodes. It is then copied to a (3,2) sharing among these two MPC nodes and a third node.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v64/tsm"
"golang.org/x/sync/errgroup"
"sync"
)
func main() {
// Create a client for each of the MPC 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 a (2,1)-shared ECDSA key among the first two MPC nodes
threshold := 1 // The security threshold for this key
players := []int{0, 1} // The original key should is secret shared among two of the MPC nodes
keyGenSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
fmt.Println("Generating key using players", players, "and threshold", threshold)
ctx := context.Background()
keyIDs := make([]string, len(clients))
var eg errgroup.Group
for i, client := range clients[:2] {
client, i := client, i
eg.Go(func() error {
var err error
keyIDs[i], err = client.ECDSA().GenerateKey(ctx, keyGenSessionConfig, threshold, "secp256k1", "")
return err
})
}
// Make sure none of the nodes aborted during key generation, before you use the key
if err := eg.Wait(); err != nil {
panic(err)
}
keyID := keyIDs[0]
fmt.Println("Generated key with ID:", keyID)
// Get the public key from one of the nodes
var derivationPath []uint32 = nil // We don't use key derivation in this example
publicKey, err := clients[0].ECDSA().PublicKey(ctx, keyID, derivationPath)
if err != nil {
panic(err)
}
fmt.Println("Public key:", hex.EncodeToString(publicKey))
// Copy the key to a (3,2) sharing among all three MPC nodes
newPlayers := []int{0, 1, 2} // The new set of players
newThreshold := 2 // The security threshold for the new copy of the key
keyCopySessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), newPlayers, nil)
fmt.Println("Copying key using players", newPlayers, "and threshold", newThreshold)
newKeyIDs := make([]string, len(clients))
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
var existingKeyID, curveName string
if i == 2 {
existingKeyID = ""
curveName = "secp256k1"
} else {
existingKeyID = keyID
curveName = ""
}
newKeyIDs[i], err = client.ECDSA().CopyKey(ctx, keyCopySessionConfig, existingKeyID, curveName, newThreshold, "")
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
newKeyID := newKeyIDs[0]
fmt.Println("CopyKey completed; new key ID:", newKeyID)
// Test: Create a signature with the new copy of the key, and see if it's valid with the original public key
message := []byte("This is a message to be signed")
msgHash := sha256.Sum256(message)
sessionID := tsm.GenerateSessionID()
signSessionConfig := tsm.NewSessionConfig(sessionID, newPlayers, nil)
fmt.Println("Creating signature using players", newPlayers)
partialSignaturesLock := sync.Mutex{}
var partialSignatures [][]byte
for _, player := range newPlayers {
player := player
eg.Go(func() error {
if partialSignResult, err := clients[player].ECDSA().Sign(ctx, signSessionConfig, newKeyID, derivationPath, 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
if err = tsm.ECDSAVerifySignature(publicKey, msgHash[:], signature.ASN1()); err != nil {
panic(err)
}
fmt.Println("Signature is valid")
}
Updated about 1 month ago