Presignatures

The TSM allows you to split signature generation into two steps:

  1. 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.
  2. 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 generated an EdDSA key in the TSM identified by keyID, the SDK call for requesting an MPC session generating presignatures for that key, looks 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 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 request 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 shaing 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 therefor important that a presignature is only used for a single signature.

The MPC nodes ensure this by deleting a presignature when it is used for signing. Trying to sign another message with the same presignature ID will then simply result in an error message.

Use Cases

Presignatures may be useful in two cases:

  • 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 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 world-wide, generating a single signature may take a second or more, because several rounds of computation is 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.
  • Cold Signing Presignatures are also useful in the case where one or more of the 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.

2-out-of-3 Signing with Presignatures

The TSM 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 TSM 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 him signatures of two different messages, signed using the same nonce. And from that he can extract the signing key.

If it is really 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 TSM always produces (n, n-1) presignatures, meaning that all parties must participate in the online signing.

If you really want to be able to do non-interactive online signing with any subset of two out of three MPC nodes, this can be done by generating independent presignature sharings for each subset of players. In other words, after generating the key among P1, P2, P3, you can continue and generate separate presigantures for the key, among the subsets (P1, P2), (P1, P3), and (P2, P3). Then, to sign, non-interactively with for example P2 and P3, you use the corresponding presignature. This would require an excessive amount of presignatures in the general case, with many MPC nodes. But in the case of 3-5 nodes, it should work well.

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 "C"
import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"gitlab.com/sepior/go-tsm-sdkv2/ec"
	"gitlab.com/sepior/go-tsm-sdkv2/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, ec.Secp256k1.Name(), "")
			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