Presignatures
Builder Vault allows you to split signature generation into two steps:
- Presignature Generation This step consists of an MPC session between the nodes that results in the generation of one or more presignatures. Each presignature consists of secret sharings of a number of intermediate values (including a secret sharing of the random signing nonce) that are used to later generate the final signature. The presignature generation is the main bulk of work, including several rounds of communication between the MPC nodes. But importantly, presignatures can be computed in advance, before knowing the messages to sign.
- Non-Interactive Signing Given a presignature and a message to be signed, the final signature can be generated efficiently. In particular, this does not involve an MPC session with interaction between the MPC nodes.
If, for example, you already used Builder Vault to generate an EdDSA key identified by keyID
, the SDK call for requesting an MPC session generating presignatures for that key, would look like this:
presignatureIDs, err := client.EdDSA().GeneratePresignatures(ctx, sessionConfig, keyID, presigCount)
The sessionConfig
should include the same MPC nodes as was used when generating the key.
Presignature generation can be done in batches, and presigCount
specifies the number of presignatures to generate. The SDK returns an array presignaureIDs
that contains IDs for each presignature in this batch.
The presignatures can then be used for signing non-interactively as follows:
presignatureID := presignatureIDs[0] // Use the first presignature in the batch
partialSignResult, err := client.EdDSA().SignWithPresignature(ctx, keyID, presignatureID, nil, message)
This will instruct the MPC node to generate a partial signature based on the presignature and the provided message. Note that this does not require an MPC session. Instead, it just involves a single roundtrip between the SDK and its MPC node. In order to produce a valid signature, the MPC nodes must agree on the presignature ID and the message.
As an alternative to keeping track of the presignature ID yourself, you can also call one of the SDKs without providing a presignature ID. The MPC node will then use a random presignature among the set of presignatures that it holds, and it will return the presignature ID as part of the sign result. The returned presignature ID can then be provided when calling SignWithPresignature()
on the remaining SDKs.
partialSignResult, err := client.EdDSA().SignWithPresignature(ctx, keyID, nil, derivationPath, message)
presignatureID := partialSignResult.PresignatureID
Just as with regular interactive signing the resulting partial signatures can be combined into the final signature:
partialSignatures := [][]byte{ partialSignResult1.partialSignature, ...}
signature, err := tsm.EDDSAFinalizeSignature(message, partialSignatures)
Preventing Presignature Reuse
A presignature essentially consists of a sharing of the random signing nonce. Generally, with ECDSA and Schnorr signatures (such as EdDSA) it holds that if the same nonce is used to sign two different messages, the key leaks right away. It is therefore important that a presignature is only used for a single signature.
An MPC node ensures this by deleting a presignature in the same transaction in which it uses its presignature share for signing. Trying to sign another message with the same presignature ID will then simply result in an error message.
One caveat to this: Make sure to delete all presignatures either right before you take a backup of the MPC node database, or when you restore the database from the backup. Otherwise the MPC node has no way of knowing that the presignature shares in its database were already used in an earlier signing session.
Deleting and Counting Presignatures
You can delete all presignatures for a given ECDSA or Schnorr key as follows. As noted above, this is important to do in connection with regular database backup.
// Delete all the player's presignature shares for a key.
err := client.KeyManagement().DeletePresignatures(ctx, keyID)
You can also count the number of presignatures:
// CountPresignatures returns a count of the presignatures available for a key.
presigCount, err := client.KeyManagement().CountPresignatures(ctx, keyID)
These calls do not require you to create an MPC session. They only affect the presig shares stored at the MPC node controlled by the given SDK.
Use Cases
Presignatures may be useful in two cases:
- Cold Signing Presignatures are useful when one or more MPC nodes are disconnected from the network. In that case, normal interactive signing would be impractical because it requires several rounds of communication between the disconnected MPC nodes and the remaining system. But if presignatures have been prepared (e.g., as part of the key generation), then a single roundtrip to each of the disconnected MPC nodes is sufficient.
- Asynchronous Signing In some applications, even without cold nodes, it may be beneficial that the signing nodes do not have to be online at the same time, but can instead produce partial signatures independently of each other.
- Performance Using presignatures, the major part of the work can be performed at any time (because presignatures do not depend on the messages to sign). This allows us to “even out” the load on the system and prepare presignatures for throughput of signature generation during peak hours. In cases where the MPC nodes are located worldwide, generating a single signature may take a second or more because several rounds of computation are required between the MPC nodes. By preparing a presignature in advance, the latency experienced by the user providing the message to be signed is significantly reduced.
2-out-of-3 Signing with Presignatures
Builder Vault allows normal, interactive, signing with a subset of MPC nodes, usually with threshold+1 MPC nodes. If the key was for example generated as a secret sharing among three MPC (n=3) nodes with a security threshold of one (t=1), then we can do 2-of-3 signing with any two of the three MPC nodes.
But with presignatures, we must take extra care. Consider for example a Builder Vault instance with three MPC nodes, P1, P2, P3. Suppose we generate a key among the three MPC nodes, with security threshold t=1. In this case, it would be insecure if we allowed the generation of a (3,1) presignature sharing that could be used to do non-interactive online signing with any subset of the three players. To see this, suppose one of them, say P1, is corrupt (which is exactly what the MPC is supposed to protect against). Then P1 could just request 2-of-3 non-interactive online signing with P2 using one message, and 2-of-3 non-interactive online signing with P3 using another message, but using the same presignature. That would give the attacker signatures of two different messages, signed using the same nonce. And from that he can extract the signing key.
Since signing with presignatures is non-interactive, the two honest nodes P2 and P3 have no chance of knowing that they are signing different messages using the same presignature. This is not an issue with normal (interactive) signing, since the two honest players interact and will detect that they are about to use the same presignature to sign two different messages, and they will then just abort.
For this reason, the Builder VAult always produces (n, n-1) presignatures, meaning that all MPC nodes must participate in the online signing.
Builder Vault can also support 2-out-of-3 signing non-interactively, but this requires a bit more work. Specifically, you will need to do as follows:
- First generate a key with all three nodes (P1, P2, P3) using threshold t=1.
- Then run a key copy operation with P1 and P2 in order to create a copy of the key, secret shared among only P1 and P2. Repeat this, to also create key copies among P1 and P3 and among P2 and P3. After this, you may want to delete the original (3,1) sharing of the key. As a result, all pairs of MPC nodes now hold their own independent (2,1) secret sharing of the original key. The keys will have different key IDs, but will be sharings of the same key.
- Now, for each of the new (2,1) sharings, you can generate a number of presignatures. This completes the interactive part of the process.
- Finally, if you for example want to generate a signature non-interactively with P2 and P3, you can use the key ID for the copy of the key held by these two MPC nodes, as well as one of the presignatures that you prepared for this pair of nodes.
This approach works for more players as well. But it doesn't scale to large number of nodes, at least not if you want to be able to sign non-interactively with any subset, since this will require too many copies of the key.
Code Example
The following is a self-contained code example, showing how to generate a batch of presignatures and use them for non-interactive online signing.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v70/tsm"
"golang.org/x/sync/errgroup"
)
func main() {
// Create clients for two of the nodes
configs := []*tsm.Configuration{
tsm.Configuration{URL: "http://localhost:8500"}.WithAPIKeyAuthentication("apikey0"),
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 ECDSA key
threshold := 1 // Security threshold for this key
players := []int{0, 2} // We want to use MPC node 0 and 2
sessionID := tsm.GenerateSessionID()
keyGenSessionConfig := tsm.NewSessionConfig(sessionID, players, nil)
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(), keyGenSessionConfig, 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, "on MPC nodes", players)
// Generate ten presignatures
presigSessionID := tsm.GenerateSessionID()
presigSessionConfig := tsm.NewSessionConfig(presigSessionID, players, nil)
presignatureIDs := make([][]string, len(players))
for i, client := range clients {
i, client := i, client
eg.Go(func() error {
var err error
presignatureIDs[i], err = client.ECDSA().GeneratePresignatures(context.TODO(), presigSessionConfig, keyID, 10)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Validate presignature IDs
for i := 1; i < len(presignatureIDs); i++ {
if len(presignatureIDs[0]) != len(presignatureIDs[i]) {
panic(fmt.Sprintf("size mismatch between index 0 (%d) and %d (%d)", len(presignatureIDs[0]), i, len(presignatureIDs[i])))
}
for j := 0; j < len(presignatureIDs[i]); j++ {
if presignatureIDs[0][j] != presignatureIDs[i][j] {
panic(fmt.Sprintf("values mismatch between index 0,%d (%s) and %d,%d (%s)", j, presignatureIDs[0][j], i, j, presignatureIDs[i][j]))
}
}
}
// We can now sign with the created key and one of the presignatures ;
// this does not require MPC nodes to interact, so we don't have to do this concurrently.
message := []byte("This is the message to be signed")
msgHash := sha256.Sum256(message)
partialSignatures := make([][]byte, 0)
for _, client := range clients {
client := client
partialSignResult, err := client.ECDSA().SignWithPresignature(context.TODO(), keyID, presignatureIDs[0][0], nil, msgHash[:])
if err != nil {
panic(err)
}
partialSignatures = append(partialSignatures, partialSignResult.PartialSignature)
}
// Combine partial signatures from each of the MPC nodes into the final signature
signature, err := tsm.ECDSAFinalizeSignature(msgHash[:], partialSignatures)
if err != nil {
panic(err)
}
fmt.Println("Signature:", hex.EncodeToString(signature.ASN1()))
}
package com.example;
import com.sepior.tsm.sdkv2.*;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
public class EcdsaSignWithPresigExample {
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);
// Generate ten presignatures for the key
int[] presigGenPlayers = {0, 1, 2}; // We must use same set of players as when we generated the key
int presigCount = 10;
System.out.println("Generating " + presigCount + " presignatures using players " + Arrays.toString(presigGenPlayers));
String presigGenSessionId = SessionConfig.generateSessionId();
SessionConfig presigGenSessionConfig = SessionConfig.newSessionConfig(presigGenSessionId, presigGenPlayers, null);
List<String[]> presigGenResults = runConcurrent(
() -> clients[0].getEcdsa().generatePresignatures(presigGenSessionConfig, keyId, presigCount),
() -> clients[1].getEcdsa().generatePresignatures(presigGenSessionConfig, keyId, presigCount),
() -> clients[2].getEcdsa().generatePresignatures(presigGenSessionConfig, keyId, presigCount));
String[] presigIds = presigGenResults.get(0);
// We can now sign a message with the created key and one of the presignatures ;
// this does not require MPC nodes to interact, so we don't have to do this concurrently.
byte[] messageHash = new byte[32]; // Normally, this is the SHA256 hash of the message.
int[] signPlayers = {0, 1, 2}; // All players needed for signing when using presignatures
int[] derivationPath = null; // In this example we do not use key derivation
System.out.println("Signing message with players " + Arrays.toString(signPlayers) + " and using presig " + presigIds[0]);
// No MPC node interaction required; no need to do this in parallel
byte[][] partialSignatures = {
clients[0].getEcdsa().signWithPresignature(keyId, presigIds[0], derivationPath, messageHash).getPartialSignature(),
clients[1].getEcdsa().signWithPresignature(keyId, presigIds[0], derivationPath, messageHash).getPartialSignature(),
clients[2].getEcdsa().signWithPresignature(keyId, presigIds[0], derivationPath, messageHash).getPartialSignature(),
};
// Combine partial signatures from each of the MPC nodes into the final signature and validate it against the message
EcdsaSignature signature = com.sepior.tsm.sdkv2.Ecdsa.finalizeSignature(messageHash, partialSignatures);
System.out.println("Signature: 0x" + bytesToHex(signature.getSignature()));
}
@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 two of the nodes
const configs = [
{
url: "http://localhost:8500",
apiKey: "apikey0",
},
{
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
const threshold = 1; // The security threshold for this key
const players = [0, 2]; // We want to use MPC node 0 and 2
const keygenSessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(players),
{}
);
const keyIds = ["", ""];
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);
// 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} on MPC nodes ${players}`);
// Generate ten presignatures
const presigSessionId = await SessionConfig.GenerateSessionID();
const presigSessionConfig = await SessionConfig.newSessionConfig(
presigSessionId,
new Uint32Array(players),
{}
);
const presignatureIds = [[], []];
const presignaturePromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
const ecdsaApi = client.ECDSA();
presignatureIds[i] = await ecdsaApi.generatePresignatures(
presigSessionConfig,
keyId,
10
);
};
presignaturePromises.push(func());
}
await Promise.all(presignaturePromises);
// Validate presignature IDs
for (let i = 1; i < presignatureIds; i++) {
if (presignatureIds[0].length !== presignatureIds[1].length) {
console.log(
`Size mismatch between index 0 (${presignatureIds[0].length}) and ${i} (${presignatureIds[i].length})`
);
return;
}
for (let j = 0; j < presignatureIds[i]; j++) {
if (presignatureIds[0][j] !== presignatureIds[i][j]) {
console.log(
`Value mismatch between index 0,${j} (${presignatureIds[0][j]}) and ${i},${j} (${presignatureIds[i][j]})`
);
return;
}
}
}
// We can now sign with the created key and one of the presignatures ;
// this does not require MPC nodes to interact, so we don't have to do this concurrently.
const message = "This is a message to be signed";
const messageHash = crypto.createHash("sha256").update(message).digest();
const partialSignatures = [];
for (const client of clients) {
const ecdsaApi = client.ECDSA();
const partialSignResult = await ecdsaApi.signWithPresignature(
keyId,
presignatureIds[0][0],
new Uint32Array([]),
messageHash
);
partialSignatures.push(partialSignResult.partialSignature);
}
// Combine partial signatures from each of the MPC nodes into the final signature
const ecdsaApi = clients[0].ECDSA();
const signature = await ecdsaApi.finalizeSignature(
messageHash,
partialSignatures
);
console.log(`Signature: ${Buffer.from(signature.signature).toString("hex")}`);
}
main()
This will generate output like this:
Generating key using players [0, 1, 2]
Generated key with ID: TtqCRe0IZOBPJUyA0k4tKRCQlKhv
Generating 10 presignatures using players [0, 1, 2]
Signing message with players [0, 1, 2] and using presig 2ygmejczidMcuPcAGJp3sW000000
Signature: 0x304402201556793A53443C3A5129C32DE390E60D695E79D71B1E8462426CEF9EDBD1B93C02207FD3606973A5E856651D0E23140BE2E898D01396F8A8323DE6E730F0895448AE
Updated 2 months ago