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
Updated 4 days ago