BIP32: Non-Hardened Derivation

Generating a Master Key

Generating a BIP32 master key can be done like this:

curveName := "secp256k1"
masterKeyID, err := client.ECDSA().GenerateKey(context, sessionConfig, threshold, curveName, "")

As explained in the previous section, each SDK must be called in order for the MPC key generation session to take place. As a result, each MPC node will hold one key share of the master key. In addition, a random master chain code is generated and stored at each MPC node.

The above call generates an extended key in the TSM, as specified in BIP-32. The extended key consists of the actual key, which is secret shared among the MPC nodes, and a chain code. Each MPC node holds a copy of the chain code.

Instead of generating a master key in the TSM, it is also possible to import a master key.

📘

Note

According to the BIP32 standard, the master key and chain code are both derived from a master seed (or a BIP39 passphrase). When calling GenerateKey the master key and the chain code are sampled as two independent values. The master key only exists as a secret sharing and is never assembled at a single MPC node. On the other hand, each of the MPC nodes will hold a copy of the master chain code. This may require special care when importing master keys from external wallets.

Obtaining a Derived Public Key

Once the master key has been generated, you can obtain a derived public key as follows. Suppose you want the public key derived according to the derivation path m/42/2/3 . Here m stands for the master key and /42/2/3 is the actual derivation path, see BIP32 for more on derivation paths. Then you can proceed like this:

derivationPath := []uint32{42,2,3}
pkixPublicKey, err := ecdsaClient.PublicKey(masterKeyID, derivationPath)

The returned encoded public key is a SubjectPublicKeyInfo structure, see RFC 5280, Section 4.1.

Each MPC node uses its master key share and master chain code and the provided derivation path to compute a share of the derived key. Each MPC node can do this locally, due to the mathematical properties of non-hardened key derivation. The derived key shares are computed on-the-fly and cached in the memory of each MPC node.

📘

When to Trust the Public Key

The key generation protocol ensures that honest players, whose operation succeeds, will end up with the correct key share and the correct public key. But the protocol does not guarantee that all honest players will succeed. In fact, all but one honest player may experience a session abort.

Since the call to PublicKey() simply fetches the public key from a single MPC node, it is important that this public key is only used, e.g., for creating a wallet address, when it is known that all players succeeded the key generation session.

In addition, unless you really trust the MPC node to which the SDK is connected, it may be necessary to retrieve the public key from the other MPC nodes and ensure that the public keys returned are all equal, before the public key is used. This prevents a single corrupt MPC node from returning a false public key where only the corrupt MPC node knows the private key.

Signing with a Derived Key

Suppose you want to sign a message using a key derived from a master key according to the derivation path m/42/2/3. Then you can do as follows:

derivationPath := []uint32{42, 2, 3}
curveName := "secp256k1"
partialSignResult, err := client.ECDSA().Sign(context, sessionConfig, masterKeyID, derivationPath, msgHash)

This will instruct each MPC node to locally derive a share of the derived key m/42/2/3 and use that key share to sign the message via an interactive MPC session with the other MPC nodes.

Note that there is no key ID for the derived key. Instead, you use the master key ID and the derivation path each time you want to sign with the derived key.

Signing with a derived key, as shown, is quite efficient. This is because non-hardened BIP-32 derivation lets each MPC node compute its share of the derived key locally, based only on its share of the master key and the given chain path. The MPC nodes keep their derived key shares safe, just like the master key shares.

If you want to get the master public key, or sign directly with the master key, you can simply use an empty derivation path, like this:

derivationPath := []uint32{} // empty path corresponds to master key
partialSignResult, err := client.ECDSA().Sign(context, sessionConfig, masterKeyID, nil, msgHash)

Code Example

The following is a self-contained example, showing how to generate an ECDSA master key, and then generate signatures and public keys for keys derived from the master key.

package main

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"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 master 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 master key using players", keyGenPlayers)
	ctx := context.Background()
	masterKeyIDs := make([]string, len(clients))
	var eg errgroup.Group
	for i, client := range clients {
		client, i := client, i
		eg.Go(func() error {
			var err error
			masterKeyIDs[i], err = client.ECDSA().GenerateKey(ctx, keyGenSessionConfig, threshold, "secp256k1", "")
			return err
		})
	}

	if err := eg.Wait(); err != nil {
		panic(err)
	}

	// Validate key IDs

	for i := 1; i < len(masterKeyIDs); i++ {
		if masterKeyIDs[0] != masterKeyIDs[i] {
			panic("key IDs do not match")
		}
	}
	masterKeyID := masterKeyIDs[0]
	fmt.Println("Generated master key with ID:", masterKeyID)

	// Get derived public key

	// In this example we will obtain the non-hardened derived key m/42/2/3
	derivationPath := []uint32{42, 2, 3}

	publicKeys := make([][]byte, len(clients))
	for i, client := range clients {
		var err error
		publicKeys[i], err = client.ECDSA().PublicKey(ctx, masterKeyID, derivationPath)
		if err != nil {
			panic(err)
		}
	}

	// Validate public keys

	for i := 1; i < len(publicKeys); i++ {
		if bytes.Compare(publicKeys[0], publicKeys[i]) != 0 {
			panic("public keys do not match")
		}
	}
	publicKey := publicKeys[0]
	fmt.Println("Public key for derivation path m/42/2/3:", hex.EncodeToString(publicKey))

	// We can now sign with the derived 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 derived key m/42/2/3 and 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, masterKeyID, 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:", 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 EcdsaSignDerivedKeyExample {

    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 = {42, 2, 3}; // In this example we will obtain the non-hardened derived key m/42/2/3

        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);


        // Get the public key

        byte[] publicKey = clients[0].getEcdsa().publicKey(keyId, derivationPath);
        System.out.println("Public key for path m/42/2/3: 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.


        // Sign a message using the private key

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

        String signSessionId = SessionConfig.generateSessionId();
        final SessionConfig signSessionConfig = SessionConfig.newSessionConfig(signSessionId, signPlayers, null);

        System.out.println("Creating signature using derived key m/42/2/3 and players " + Arrays.toString(signPlayers));
        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);
        System.out.println("Signature: 0x" + bytesToHex(signature.getSignature()));

        // Validate the signature

        boolean valid = Ecdsa.verifySignature(publicKey, msgHash, signature.getSignature());
        System.out.println("Signature validity: " + 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 masterKeyIds = ["", "", ""];

  console.log(`Generating key using players ${keygenPlayers}`);

  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);

  // Validate key IDs

  for (let i = 1; i < masterKeyIds.length; i++) {
    if (masterKeyIds[0] !== masterKeyIds[i]) {
      console.log("Key ids do not match");
      return;
    }
  }

  const masterKeyId = masterKeyIds[0];

  console.log(`Generated master key with id: ${masterKeyId}`);

  // Get derived public key

  // In this example we will obtain the non-hardened derived key m/42/2/3
  const derivationPath = new Uint32Array([42, 2, 3]);

  const publickeys = [];

  for (const client of clients) {
    const ecdsaApi = client.ECDSA();
    publickeys.push(await ecdsaApi.publicKey(masterKeyId, derivationPath));
  }

  // Validate public keys

  for (let i = 1; i < publickeys.length; i++) {
    if (!Buffer.from(publickeys[0]).equals(publickeys[i])) {
      console.log("Public keys does not match");
      return;
    }
  }

  const publicKey = publickeys[0];

  console.log(
    `Public key for derivation path m/42/2/3: ${Buffer.from(publicKey).toString(
      "hex"
    )}`
  );

  // We can now sign with the created 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 derived key m/42/2/3 and 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,
        masterKeyId,
        derivationPath,
        messageHash
      );

      partialSignatures.push(partialSignResult);
    };

    partialSignaturePromises.push(func());
  }

  await Promise.all(partialSignaturePromises);

  const ecdsaApi = clients[0].ECDSA();

  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: ${Buffer.from(signature.signature).toString("hex")}`
    );
  } catch (e) {
    console.log(e);
  }
}

main().catch((e) => console.log(e));

Running this should produce output like this:

Generating key using players 0,1,2
Generated master key with id: zX1e4DxkweVmvu63TvZD3fNDqkrG
Public key for derivation path m/42/2/3: 3056301006072a8648ce3d020106052b8104000a03420004d3fa8db3961b9d777760a95d772a07700d0df42ae6333511c2de00aaa3716c05b8c84ba86bbb8c52c6575e7c068e042732bc73abdac6994d3ce8970f5d277621
Creating signature using derived key m/42/2/3 and players 0,1
signature verified
Signature: 304402207de0574e7afbe53a3cc870edd095189f77c36768b39d86345fea3b572b836a510220046026647aed7987f00f200dc08e4b1a2dc5d3d1a6728fa29fa5e38cb616a30b