BIP32: Non-Hardened Derivation

Generating a Master Key

Generating a BIP32 master key can be done like this:

curveName := ec.Secp256k1.Name()
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 affect 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 will end up with the correct key share and the correct public key. But the protocol does 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 := ec.Secp256k1.Name()
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 soft 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 (
	"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"
	"sync"
)

func main() {

	// Create clients for three nodes
	
	configs := []*tsm.Configuration{
		tsm.Configuration{URL: "http://localhost:8500"}.WithAPIKeyAuthentication("Node0LoginPassword"),
		tsm.Configuration{URL: "http://localhost:8501"}.WithAPIKeyAuthentication("Node1LoginPassword"),
		tsm.Configuration{URL: "http://localhost:8502"}.WithAPIKeyAuthentication("Node2LoginPassword"),
	}

	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 := 2 // Security threshold for this key
	sessionConfig := tsm.NewStaticSessionConfig(tsm.GenerateSessionID(), len(clients))
	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(), sessionConfig, 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)

	message := []byte("This is a message to be signed")
	msgHash := sha256.Sum256(message)

	partialSignaturesLock := sync.Mutex{}

	// Example 1: Sign message using the generated master key

	var bip32Path1 []uint32 // empty chain path means that we use master key
	var partialSignatures [][]byte
	sessionConfig = tsm.NewStaticSessionConfig(tsm.GenerateSessionID(), len(clients))
	for _, client := range clients {
		client := client
		eg.Go(func() error {
			if partialSignResult, err := client.ECDSA().Sign(context.TODO(), sessionConfig, keyID, bip32Path1, 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)
	}

	signature1, err := tsm.ECDSAFinalizeSignature(msgHash[:], partialSignatures)
	if err != nil {
		panic(err)
	}

	fmt.Println("Signature 1:", hex.EncodeToString(signature1.ASN1()), "; derivation path =", bip32Path1)

	// Example 2: Sign message using the derived key m/1/2/3

	bip32Path2 := []uint32{1, 2, 3}
	partialSignatures = make([][]byte, 0)
	sessionConfig = tsm.NewStaticSessionConfig(tsm.GenerateSessionID(), len(clients))
	for _, client := range clients {
		client := client
		eg.Go(func() error {
			if partialSignResult, err := client.ECDSA().Sign(context.TODO(), sessionConfig, keyID, bip32Path2, 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)
	}

	signature2, err := tsm.ECDSAFinalizeSignature(msgHash[:], partialSignatures)
	if err != nil {
		panic(err)
	}
	fmt.Println("Signature 2:", hex.EncodeToString(signature2.ASN1()), "; derivation path =", bip32Path2)

}

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("Node0LoginPassword");
        configs[1].withApiKeyAuthentication("Node1LoginPassword");
        configs[2].withApiKeyAuthentication("Node2LoginPassword");

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

}

Running this should produce output like this:

Generating master key using players [0 1 2]
Generated master key with ID: MaxRzhhVOX7HVPTaCFHMkeZg7KXC
Public key for derivation path m/42/2/3: 3056301006072a8648ce3d020106052b8104000a034200042eb31ba76a8f528c678029d14432151617f0d48d18659a0fc8e432b1386914c12694399e8a93a6417043c4c988d4eb20c2067d6cb1663c05463060057e783218
Creating signature using derived key m/42/2/3 and players [0 1]
Signature: 3045022100edff78ae037308f69622c8645ab1a21b4d8c3c62344d4de411ab8509dc7fb2e602203eb4783e3bc38c03bca07090def1c37f2f78fe4c7069ad041aeecb490a75bca8