EdDSA Key Derivation

The TSM also supports non-hardened key derivation for EdDSA keys (Ed25519 and Ed448).

๐Ÿ“˜

RFC 8032, BIP32, and SLIP10

According to RFC 8032 Section 5.1.5, a private EdDSA key is a seed from which the raw private scalar value is derived. Due to this extra derivation, EdDSA private keys (i.e., seeds) are not compatible with non-hardened BIP32 key derivation. For this reason, SLIP10 (which generalizes BIP32 to other curves) only defines hardened derivation for EdDSA.

When you generate an EdDSA key in the TSM, on the other hand, there is no seed. Instead, the TSM generates a random sharing of the raw private scalar value, as well as a random master chain code. In addition, for each signature, the TSM samples a uniformly random nonce. The fact that the TSM uses raw EdDSA keys like this, allows us to support non-hardened EdDSA key derivation.

The fact that the TSM generates the private EdDSA key as a raw scalar value has consequences if you want to import an EdDSA seed from another wallet into the TSM which uses RFC 8032 derivation. See this section for more about key import.

Code Example

The following shows how to generate derived public keys and sign using derived private keys according to a non-hardened derivation path.

package main

import (
	"bytes"
	"context"
	"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 two of the nodes

	configs := []*tsm.Configuration{
		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 an EdDSA master key

	threshold := 1         // The security threshold to use for this key
	players := []int{1, 2} // We want to use MPC node 1 and 2
	keyGenSessionID := tsm.GenerateSessionID()
	keyGenSessionConfig := tsm.NewSessionConfig(keyGenSessionID, players, nil)
	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.Schnorr().GenerateKey(context.TODO(), keyGenSessionConfig, threshold, ec.Edwards25519.Name(), "")
			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, "on MPC nodes", players)

	// 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
		if publicKeys[i], err = client.Schnorr().PublicKey(context.TODO(), masterKeyID, derivationPath); err != nil {
			panic(err)
		}
	}

	// Validate derived 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", derivationPath, ":", hex.EncodeToString(publicKey))

	// We can now sign with the created key

	message := []byte("The message to be signed")

	fmt.Println("Creating signature using key derived according to path", derivationPath, " and players", players)
	partialSignaturesLock := sync.Mutex{}
	partialSignatures := make([][]byte, 0)
	signSessionID := tsm.GenerateSessionID()
	signSessionConfig := tsm.NewSessionConfig(signSessionID, players, nil)
	for _, client := range clients {
		client := client
		eg.Go(func() error {
			partialSignResult, err := client.Schnorr().Sign(context.TODO(), signSessionConfig, masterKeyID, derivationPath, message)
			if err != nil {
				return err
			}
			partialSignaturesLock.Lock()
			partialSignatures = append(partialSignatures, partialSignResult.PartialSignature)
			partialSignaturesLock.Unlock()
			return nil
		})
	}

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

	signature, err := tsm.SchnorrFinalizeSignature(message, partialSignatures)
	if err != nil {
		panic(err)
	}

	// Verify the signature relative to the signed message and the public key

	err = tsm.SchnorrVerifySignature(publicKey, message, signature)
	if err != nil {
		panic(err)
	}

	fmt.Println("Signature:", hex.EncodeToString(signature))
}
package com.example;

import com.sepior.tsm.sdkv2.*;

import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;

public class SchnorrSignDerivedKeyExample {

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

        Client[] clients = {
            new Client(configs[0]),
            new Client(configs[1]),
        };


        // Generate an ECDSA key

        final int[] keyGenPlayers = {0, 1 }; // The key should be secret shared among the two MPC nodes
        final int threshold = 1; // The security threshold for this key
        final String curveName = "ED-25519"; // We want the key to be a Ed25519 key (e.g., for Polkadot)
        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].getSchnorr().generateKey(keyGenSessionConfig, threshold, curveName, null),
                () -> clients[1].getSchnorr().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].getSchnorr().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 = {0, 1}; // We sign with threshold + 1 players, in this case MPC node 0, 1
        final byte[] message = new byte[117];  // This is the message to be signed

        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<SchnorrPartialSignResult> signResults = runConcurrent(
                () -> clients[0].getSchnorr().sign(signSessionConfig, keyId, derivationPath, message),
                () -> clients[1].getSchnorr().sign(signSessionConfig, keyId, derivationPath, message));

        byte[][] partialSignatures = {signResults.get(0).getPartialSignature(), signResults.get(1).getPartialSignature()};
        byte[] signature = Schnorr.finalizeSignature(message, partialSignatures);
        System.out.println("Signature: 0x" + bytesToHex(signature));

        // Validate the signature

        boolean valid = Schnorr.verifySignature(publicKey, message, signature);
        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 the example produces output like this:

Generated master key with ID bc6nzTNEGDsS6jh9LMQtcCcFzX5n on MPC nodes [1 2]
Public key for derivation path [42 2 3] : 302a300506032b657003210017767c78e569d339fc0b1d06c7ec5ac31669444669dc110467ea6038c2b3fcd3
Creating signature using key derived according to path [42 2 3]  and players [1 2]
Signature: 34d431e74abf04880ff903aff0804be2f368f98593b20091de90a15d22ba1958a985b358f1f83b7814087fc1390b7413d29e30679a62c129b3e976dca2345b0c