BIP32: Hardened Derivation
The TSM supports hardened key derivation according to BIP32 as follows.
Generating the Initial Seed
Initially, you can generate a BIP32 seed like this:
seedID, err = node.ECDSA().BIP32GenerateSeed(ctx, sessionConfig, threshold)
When this is called on all the SDKs, this will start an MPC session that creates a new, random 256-bit BIP32 seed that is secret shared among the MPC nodes, and it will return a handle for the seed, seedID, to each SDK
Limited Support
Currently, Blockdaemon Builder Vault only offers hardened BIP32 key derivation for ECDSA keys in the DKLS19 protocol, and only for limited security thresholds. Contact Blockdaemon’s support team for more information about these limitations.
Deriving the Master Key
To derive the master key and its master chain code from the seed, use this SDK method:
masterKeyID, err = node.ECDSA().BIP32DeriveFromSeed(ctx, sessionConfig, seedID)
This instructs the MPC nodes to derive secret shares of the master key and master chain code from the shares of the seed. The MPC nodes use general purpose MPC for this, to ensure that no single party at any point gets access to the seed nor the master key or master chain code.
Deriving Hardened Keys
From the master key, you can now continue to derive sub-keys and chain codes, one level at a time. For example, if you want to derive the key corresponding to the chain path m / 44' / 0'
(where 44'
and 0'
refers to hardened derivations), you would then continue as follows:
derivation := 0x8000002C
keyID_44H, err = client.ECDSA().BIP32DeriveFromKey(ctx, sessionConfig, masterKeyID, derivation)
The derivation is set to 0x8000002C, which is a 32-bit integer corresponding to the integer value 44, but with the most significant bit set. Setting the most significant bit is how BIP32 knows that it is a hardened derivation.
To get the key corresponding to the derivation path m / 44' / 0'
we continue with another derivation:
derivation = 0x80000000
keyID_44H_0H, err = client.ECDSA().BIP32DeriveFromKey(ctx, sessionConfig, keyID_44H, derivation)
Now the MPC nodes hold shares of the desired key. But we are not yet ready to use the key for signing. The key still only exists as a special type of secret sharing that is only suitable for further hardened derivation (using general purpose MPC), and is not suitable for signing. To sign using the derived key, we therefore first need to convert the sharing to a standard sharing:
keyID, err = node.ECDSA().BIP32ConvertKey(ctx, sessionConfig, keyID_44H_0H)
The result of this is a standard ECDSA key, just like the random keys you would obtain by calling node.ECDSA().GenerateKey(...)
, but with the only exception that this particular key has been deterministically derived from the master key and not randomly sampled. Just as standard random keys, the derived key can now be used for signing messages.
Note that when signing, you can optionally specify a chain path that is used for further non-hardened derivation. So if you for example wants to produce a signature using the derived key m/44'/0'/2/3
(where the last two elements of the chain path are non-hardened), you can do as follows:
message := []byte("This is a message to be signed with key m/44'/0'")
msgHash := sha256.Sum256(message)
derivationPath := []uint32{2, 3}
partialSignResult, err := node.ECDSA().Sign(ctx, sessionConfig, keyID, derivationPath, msgHash[:])
This returns a partial signature to each SDK. The final signature can be obtained by combining the partial signatures using the tsm.ECDSAFinalizeSignature()
method as described in a previous example.
Performance of Hardened Derivation
Doing hardened BIP32 derivation in a threshold wallet requires the use of so-called general purpose MPC. This is notoriously network intensive. Consequently, doing the initial hardened derivations usually required to set up a BIP44 wallet can easily take several seconds. And if one or more of the MPC nodes are running on mobile devices, it requires a good network, at least 4G or WiFi connection.
On the other hand, BIP44 only requires you to do a few initial hardened derivations when the wallet is created (one for each virtual wallet, and one for each type of asset). The remaining derivations will, according to BIP44, be non-hardened derivations, which can be done much faster with the TSM.
As noted, it requires substantial resources to do hardened derivation. So you may want to save the intermediate hardened keys, in case you need them for future derivations. It is up to you, as a user of the TSM, to decide whether to save the intermediate hardened keys in the TSM. If you decide not to save them, you can delete them individually, like this:
node.KeyManagement().DeleteKeyShare(ctx, keyID)
Exporting a Seed
The derivation operations described above are fully BIP32 compliant. This means that you can export the seed to another BIP32 compliant wallet, where you will then have access to the same set of derived keys as in the TSM.
Care must be taken when exporting a seed, as leaking the seed may compromise all keys in the wallet. We help you handle the exported seed safely in several ways:
- Explicit switch to enable export As with the other MPC operations, the seed will only be exported if all MPC nodes are instructed to do so via the SDK(s). But in addition, in order to be able to export a seed in the first place, the MPC node configuration must enable BIP32 seed export. See this section for more about how to configure the MPC nodes.
- Exporting only shares of the seed Secondly, the seed is not exported directly. Instead, the seed is exported as an xor sharing, where each SDK receives one share. If each MPC node is controlled by its own SDK, this means that no individual SDK user learns the actual seed, unless all users choose to combine their exported shares. This, in turn, allows the SDK users to transport the seed to another wallet (e.g., another TSM instance) via different channels, to obtain a kind of multi-factor security.
- Exported shares are wrapped The exported share is not output in the clear. Instead, each SDK can provide a wrapping key, and the share is wrapped (encrypted) under this key, before it is output. The MPC node can be configured to only accept certain wrapping keys. If the BIP32 seed is transferred to another TSM, the wrapping key can be obtained by calling
node.WrappingKey().WrappingKey()
on the target MPC node in this TSM. - Seed authentication tag As a fourth security measure, in addition to a share of the seed, each SDK user also receives an authentication tag t = SHA512(“Exported Share” || seed). The seed authentication tag enables detection in the case where a malicious party flips bits in his share of the seed while in transit.
- Re-randomized shares Finally, the shares of the seed that are output to the SDK users are re-randomized. This means that in the case where a subset of the exported seed shares are somehow leaked to an attacker, this will not reduce the security of the seed that is still held in the TSM (unless, of course, all seed shares are leaked to the attacker).
To export a seed with the handle seedID, the SDK users first need to agree on a string sessionID
and a session configuration. Then each SDK user must invoke the following method on his SDK:
share, err := client.ECDSA().BIP32ExportSeed(ctx, sessionConfig, seedID, wrappingKey)
The returned share contains two values: share.WrappedSeedShare
is the actual seed share, encrypted under the wrapping key. share.SeedWitness
is the witness, which can be used to later check the integrity of the shares.
Importing a Seed
To import a seed that was exported by a TSM, the SDK users just need to agree on a session ID and session configuration, and input the same shares and witnesses that they received at export:
seedID, err := client.ECDSA().BIP32ImportSeed(ctx, sessionConfig, threshold, seedShare)
If the seed was not exported by a TSM, but comes from elsewhere, you first have to make sure that the seed is secret shared, such that the xor sum of all shares equal the seed. A simple way to do this is to use the seed itself as share for one of the MPC players and shares consisting of zero-bytes for the other players. (But remember that it might be more secure to create the shares such that no single SDK user learns the actual seed.)
You can let all SDK users input nil
as witness, in which case the MPC nodes will not check the authenticity of the seed. To ensure authenticity of the seed, each MPC node must receive the same witness, so the seedShare
provided to an MPC node can be prepared as follows:
seed := []byte{ ... } // the actual seed to import
wrappedSeedShare := []byte{ ... } // an xor-share of the seed for this player
witness := sha512.Sum512(append([]byte("Exported Share"), seed...))
seedShare := BIP32Seed{
WrappedSeedShare: wrappedSeedShare,
SeedWitness: witness,
}
See the code below, for a full example.
Getting Key Information
If you use hardened derivation, you may end up having both seeds, hardened keys, and normal, converted keys in the TSM. To help you administering this, you can call
keyInfo, err := client.ECDSA().BIP32Info(ctx, keyID)
The returned keyInfo
has three fields:
keyInfo.KeyType
keyInfo.ParentKeyID
keyInfo.DerivationPath
If you called BIP32Info
on a hardened BIP32 key (keyType=BIP32Key) then DerivationPath will tell you which derivation path (aka chain path) was used to derive the key, and ParentKeyID
will point to its parent key or the master seed used to derive it. Regular keys, that can be used for signing, will have KeyType=ECKey and the DerivationPath is empty. So is ParentKeyID
, unless the key was converted from a hardened BIP32 key using the BIP32Convert
operation. In this case, the ParentKeyID points to the hardened key that it was converted from.
Code Example: Seed Generation and Derivation
Here is a complete code example showing how to first generate a BIP32 seed in the TSM and then sign a message using the key derived according to the chain path m / 44' / 0' / 1' / 0 / 2
. According to BIP44, this chain path is used to derive the key corresponding to the third external address for the second virtual Bitcoin account in the wallet, as explained here.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"gitlab.com/sepior/go-tsm-sdkv2/v68/tsm"
"golang.org/x/sync/errgroup"
)
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)
}
}
// Step 1: Generate a BIP32 seed
threshold := 2 // The security threshold for this key (note: hardened derivation requires three nodes and threshold = 2)
players := []int{0, 1, 2} // The seed should be secret shared among all three MPC nodes
sessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
ctx := context.Background()
seedIDs := make([]string, len(clients))
var eg errgroup.Group
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
seedIDs[i], err = client.ECDSA().BIP32GenerateSeed(ctx, sessionConfig, threshold)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Here we just use the seed ID provided by one of the MPC nodes;
// you should probably check that all MPC nodes return the same ID before using it.
seedID := seedIDs[0]
fmt.Println("Generated seed with ID:", seedID)
// Step 2: Derive master key from the seed
sessionConfig = tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
bip32MasterKeyIDs := make([]string, len(clients))
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
bip32MasterKeyIDs[i], err = client.ECDSA().BIP32DeriveFromSeed(ctx, sessionConfig, seedID)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Again, you should probably check that all MPC nodes return the same ID before using it.
bip32MasterKeyID := bip32MasterKeyIDs[0]
fmt.Println("Derived master key (m) with ID:", bip32MasterKeyID)
// Step 3: Derive key from master key using hardened chain path 44', i.e, compute the key corresponding to the path m/44'
var derivation_44H uint32 = 0x8000002C // This is hex for 44, but with the most significant bit is set, indicating a hardened BIP32 derivation
sessionConfig = tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
bip32KeyIDs_44H := make([]string, len(clients))
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
bip32KeyIDs_44H[i], err = client.ECDSA().BIP32DeriveFromKey(ctx, sessionConfig, bip32MasterKeyID, derivation_44H)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Again, you should probably check that all MPC nodes return the same ID before using it.
bip32KeyID_44H := bip32KeyIDs_44H[0]
fmt.Println("Derived key from master key (m/44') with ID:", bip32KeyID_44H)
// Step 4: Derive key m/44'/0' from the key m/44'
var derivation_00H uint32 = 0x80000000 // Again, most significant bit is set to indicate hardened derivation
sessionConfig = tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
bip32KeyIDs_44H_0H := make([]string, len(clients))
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
bip32KeyIDs_44H_0H[i], err = client.ECDSA().BIP32DeriveFromKey(ctx, sessionConfig, bip32MasterKeyID, derivation_00H)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Again, you should probably check that all MPC nodes return the same ID before using it.
bip32KeyID_44H_0H := bip32KeyIDs_44H_0H[0]
fmt.Println("Derived key from master key (m/44'/0') with ID:", bip32KeyID_44H_0H)
// Step 5: Derive key m/44'/0'/1' from the key m/44'/0'
var derivation_01H uint32 = 0x80000001 // Again, most significant bit is set to indicate hardened derivation
sessionConfig = tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
bip32KeyIDs_44H_0H_1H := make([]string, len(clients))
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
bip32KeyIDs_44H_0H_1H[i], err = client.ECDSA().BIP32DeriveFromKey(ctx, sessionConfig, bip32MasterKeyID, derivation_01H)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Again, you should probably check that all MPC nodes return the same ID before using it.
bip32KeyID_44H_0H_1H := bip32KeyIDs_44H_0H_1H[0]
fmt.Println("Derived key from master key (m/44'/0'/1') with ID:", bip32KeyID_44H_0H_1H)
// Step 6: Convert m/44'/0'/1' to a format that can be used to sign, using DKLs19
sessionConfig = tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
keyIDs := make([]string, len(clients))
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
keyIDs[i], err = client.ECDSA().BIP32ConvertKey(ctx, sessionConfig, bip32KeyID_44H_0H_1H)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Remember to check that all MPC nodes return the same ID before using it.
keyID := keyIDs[0]
fmt.Println("Converted key (m/44'/0'/1') with ID:", keyID)
// Get the public key for key m/44'/0'/1'/0/2
softDerivationPath := []uint32{0, 2}
// Here we just fetch the public key from MPC node 0. Remember to fetch it from all nodes and check that the
// same key is returned, before using it, e.g. for receiving funds.
publicKey, err := clients[0].ECDSA().PublicKey(ctx, keyID, softDerivationPath)
if err != nil {
panic(err)
}
fmt.Println("Public key for derivation path m/44/0'/1'/0/2:", hex.EncodeToString(publicKey))
// Sign with the derived key, appending a non-hardened chain path
message := []byte("This is a message to be signed with key m/44'/0'/1'/0/2")
msgHash := sha256.Sum256(message)
sessionConfig = tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
partialSignatures := make([][]byte, len(clients))
for i, player := range players {
i, player := i, player
eg.Go(func() error {
partialSignResult, err := clients[player].ECDSA().Sign(ctx, sessionConfig, keyID, softDerivationPath, msgHash[:])
if err != nil {
return err
}
partialSignatures[i] = partialSignResult.PartialSignature
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 using m/44'/0'/1'/0/2:", hex.EncodeToString(signature.ASN1()))
}
package com.example;
import com.sepior.tsm.sdkv2.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
public class EcdsaBip32DeriveExample {
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]),
};
// Step 1: Generate a BIP32 seed
final int threshold = 2; // The security threshold for this key
final int[] players = {0, 1, 2}; // The seed should be secret shared among all three MPC nodes
final SessionConfig sessionConfig1 = SessionConfig.newSessionConfig(SessionConfig.generateSessionId(), players, null);
List<String> results1 = runConcurrent(
() -> clients[0].getEcdsa().bip32GenerateSeed(sessionConfig1, threshold),
() -> clients[1].getEcdsa().bip32GenerateSeed(sessionConfig1, threshold),
() -> clients[2].getEcdsa().bip32GenerateSeed(sessionConfig1, threshold));
// Here we just use the seed ID provided by one of the MPC nodes;
// you should probably check that all MPC nodes return the same ID before using it.
String seedId = results1.get(0);
System.out.println("Generated seed with ID: " + seedId);
// Step 2: Derive master key from the seed
final SessionConfig sessionConfig2 = SessionConfig.newSessionConfig(SessionConfig.generateSessionId(), players, null);
List<String> results2 = runConcurrent(
() -> clients[0].getEcdsa().bip32DeriveFromSeed(sessionConfig2, seedId),
() -> clients[1].getEcdsa().bip32DeriveFromSeed(sessionConfig2, seedId),
() -> clients[2].getEcdsa().bip32DeriveFromSeed(sessionConfig2, seedId));
// Again, consider checking that all MPC nodes return the same ID before using it.
String bip32MasterKeyId = results2.get(0);
System.out.println("Generated master key with ID: " + bip32MasterKeyId);
// Step 3: Derive key from master key using hardened chain path 44', to obtain key m/44'
int derivation44H = 0x8000002C; // // This is hex for 44, with the most significant bit is set to indicate a hardened BIP32 derivation
final SessionConfig sessionConfig3 = SessionConfig.newSessionConfig(SessionConfig.generateSessionId(), players, null);
List<String> results3 = runConcurrent(
() -> clients[0].getEcdsa().bip32DeriveFromKey(sessionConfig3, bip32MasterKeyId, derivation44H),
() -> clients[1].getEcdsa().bip32DeriveFromKey(sessionConfig3, bip32MasterKeyId, derivation44H),
() -> clients[2].getEcdsa().bip32DeriveFromKey(sessionConfig3, bip32MasterKeyId, derivation44H));
String bip32KeyId44H = results3.get(0);
System.out.println("Derived key m/44' with, key ID: " + bip32KeyId44H);
// Step 4: Derive key m/44'/0' from the key m/44'
int derivation44H0H = 0x80000000; // // This is hex for 0', with the most significant bit is set
final SessionConfig sessionConfig4 = SessionConfig.newSessionConfig(SessionConfig.generateSessionId(), players, null);
List<String> results4 = runConcurrent(
() -> clients[0].getEcdsa().bip32DeriveFromKey(sessionConfig4, bip32KeyId44H, derivation44H0H),
() -> clients[1].getEcdsa().bip32DeriveFromKey(sessionConfig4, bip32KeyId44H, derivation44H0H),
() -> clients[2].getEcdsa().bip32DeriveFromKey(sessionConfig4, bip32KeyId44H, derivation44H0H));
String bip32KeyId44H0H = results4.get(0);
System.out.println("Derived key m/44'/0' with, key ID: " + bip32KeyId44H0H);
// Step 5: Derive key m/44'/0'/1' from the key m/44'/0'
int derivation44H0H1H = 0x80000001;
final SessionConfig sessionConfig5 = SessionConfig.newSessionConfig(SessionConfig.generateSessionId(), players, null);
List<String> results5 = runConcurrent(
() -> clients[0].getEcdsa().bip32DeriveFromKey(sessionConfig5, bip32KeyId44H0H, derivation44H0H1H),
() -> clients[1].getEcdsa().bip32DeriveFromKey(sessionConfig5, bip32KeyId44H0H, derivation44H0H1H),
() -> clients[2].getEcdsa().bip32DeriveFromKey(sessionConfig5, bip32KeyId44H0H, derivation44H0H1H));
String bip32KeyId44H0H1H = results5.get(0);
System.out.println("Derived key m/44'/0'/1' with, key ID: " + bip32KeyId44H0H1H);
// Step 6: Convert m/44'/0'/1' to a format that can be used to sign, using DKLs19
final SessionConfig sessionConfig6 = SessionConfig.newSessionConfig(SessionConfig.generateSessionId(), players, null);
List<String> results6 = runConcurrent(
() -> clients[0].getEcdsa().bip32ConvertKey(sessionConfig6, bip32KeyId44H0H1H),
() -> clients[1].getEcdsa().bip32ConvertKey(sessionConfig6, bip32KeyId44H0H1H),
() -> clients[2].getEcdsa().bip32ConvertKey(sessionConfig6, bip32KeyId44H0H1H));
String keyId = results6.get(0);
System.out.println("Converted key m/44'/0'/1' with key ID: " + keyId);
// Get the public key for derived key m/44'/0'/1'/0/2
int[] softDerivationPath = {0, 2};
// Here we just fetch the public key from MPC node 0. Remember to fetch it from all nodes and check that the
// same key is returned, before using it, e.g. for receiving funds.
byte[] publicKey = clients[0].getEcdsa().publicKey(keyId, softDerivationPath);
System.out.println("Public key m/44'/0'/1'/0/2: " + bytesToHex(publicKey));
// Sign with the derived key, appending a non-hardened chain path
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, players, null);
List<EcdsaPartialSignResult> signResults = runConcurrent(
() -> clients[0].getEcdsa().sign(signSessionConfig, keyId, softDerivationPath, msgHash),
() -> clients[1].getEcdsa().sign(signSessionConfig, keyId, softDerivationPath, msgHash),
() -> clients[2].getEcdsa().sign(signSessionConfig, keyId, softDerivationPath, msgHash));
byte[][] partialSignatures = {
signResults.get(0).getPartialSignature(),
signResults.get(1).getPartialSignature(),
signResults.get(2).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 } = 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);
}
// Step 1: Generate a BIP32 seed
const threshold = 2; // The security threshold for this key (note: hardened derivation requires three nodes and threshold = 2)
const keygenPlayers = [0, 1, 2]; // The seed should be secret shared among all three MPC nodes
const sessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(keygenPlayers),
{}
);
const seedIds = ["1", "2", "3"];
const generateSeedPromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
const ecdsaApi = client.ECDSA();
seedIds[i] = await ecdsaApi.bip32GenerateSeed(sessionConfig, threshold);
};
generateSeedPromises.push(func());
}
await Promise.all(generateSeedPromises);
// Here we just use the seed ID provided by one of the MPC nodes;
// you should probably check that all MPC nodes return the same ID before using it.
const seedId = seedIds[0];
console.log(`Generated seed with ID: ${seedId}`);
// Step 2: Derive master key from the seed
const deriveSessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(keygenPlayers),
{}
);
const bip32MasterKeyIds = ["1", "2", "3"];
const generatedMasterKeyIdPromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
const ecdsaApi = client.ECDSA();
bip32MasterKeyIds[i] = await ecdsaApi.bip32DeriveFromSeed(
deriveSessionConfig,
seedId
);
};
generatedMasterKeyIdPromises.push(func());
}
await Promise.all(generatedMasterKeyIdPromises);
// Again, you should probably check that all MPC nodes return the same ID before using it.
const bip32MasterKeyId = bip32MasterKeyIds[0];
console.log(`Derived master key (m) with ID: ${bip32MasterKeyId}`);
// Step 3: Derive key from master key using hardened chain path 44', i.e, compute the key corresponding to the path m/44'
const derivation44H = 0x8000002c; // This is hex for 44, but with the most significant bit is set, indicating a hardened BIP32 derivation
const derivation44HSesisonConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(keygenPlayers),
{}
);
const bip32KeyIds44H = ["1", "2", "3"];
const generatedBip3244HPromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
const ecdsaApi = client.ECDSA();
bip32KeyIds44H[i] = await ecdsaApi.bip32DeriveFromKey(
derivation44HSesisonConfig,
bip32MasterKeyId,
derivation44H
);
};
generatedBip3244HPromises.push(func());
}
await Promise.all(generatedBip3244HPromises);
// Again, you should probably check that all MPC nodes return the same ID before using it.
const bip32KeyId44H = bip32KeyIds44H[0];
console.log(`Derived key from master key (m/44') with id ${bip32KeyId44H}`);
// Step 5: Derive key m/44'/0'/1' from the key m/44'/0'
const derivation01H = 0x80000001; // Again, most significant bit is set to indicate hardened derivation
const derivation01HSesisonConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(keygenPlayers),
{}
);
const bip32KeyIDs_44H_0H_1H = ["1", "2", "3"];
const generatedBip3201HPromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
const ecdsaApi = client.ECDSA();
bip32KeyIDs_44H_0H_1H[i] = await ecdsaApi.bip32DeriveFromKey(
derivation01HSesisonConfig,
bip32MasterKeyId,
derivation01H
);
};
generatedBip3201HPromises.push(func());
}
await Promise.all(generatedBip3201HPromises);
// Again, you should probably check that all MPC nodes return the same ID before using it.
const bip32KeyID_44H_0H_1H = bip32KeyIDs_44H_0H_1H[0];
console.log(
`Derived key from master key (m/44'/0'/1') with ID: ${bip32KeyID_44H_0H_1H}`
);
// Step 6: Convert m/44'/0'/1' to a format that can be used to sign, using DKLs19
const keyIdSessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(keygenPlayers),
{}
);
const keyIds = ["1", "2", "3"];
const generateKeyIdsPromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
const ecdsaApi = client.ECDSA();
keyIds[i] = await ecdsaApi.bip32ConvertKey(
keyIdSessionConfig,
bip32KeyID_44H_0H_1H
);
};
generateKeyIdsPromises.push(func());
}
await Promise.all(generateKeyIdsPromises);
// Remember to check that all MPC nodes return the same ID before using it.
const keyId = keyIds[0];
console.log(`Converted key (m/44'/0'/1') with ID: ${keyId}`);
// Get the public key for key m/44'/0'/1'/0/2
const softDerivationPath = [0, 2];
// Here we just fetch the public key from MPC node 0. Remember to fetch it from all nodes and check that the
// same key is returned, before using it, e.g. for receiving funds.
const ecdsaApi = clients[0].ECDSA();
const publicKey = await ecdsaApi.publicKey(
keyId,
new Uint32Array(softDerivationPath)
);
console.log(
`Public key for derivation path m/44/0'/1'/0/2: ${Buffer.from(
publicKey
).toString("hex")}`
);
// Sign with the derived key m/44'/0'/1'/0/2
const message = "This is a message to be signed with key m/44'/0'/1'/0/2";
const messageHash = crypto.createHash("sha256").update(message).digest();
const signatureSessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(keygenPlayers),
{}
);
const partialSignatures = [];
const partialSignaturePromises = [];
for (const playerIdx of keygenPlayers) {
const func = async () => {
const ecdsaApi = clients[playerIdx].ECDSA();
const partialSignResult = await ecdsaApi.sign(
signatureSessionConfig,
keyId,
new Uint32Array(softDerivationPath),
messageHash
);
partialSignatures.push(partialSignResult);
};
partialSignaturePromises.push(func());
}
await Promise.all(partialSignaturePromises);
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 using m/44'/0'/1'/0/2: ${Buffer.from(
signature.signature
).toString("hex")}`
);
} catch (e) {
console.log(e);
}
}
main().catch((e) => console.log(e));
This would generate output similar to this:
Derived master key (m) with ID: 83JRh9GVxkk6uU85390bOvln0PXX
Derived key from master key (m/44') with ID: 9Y6u5ULTmzFEmCXfrMGZgm0AfqKg
Derived key from master key (m/44'/0') with ID: ljFp0rqFyklYdhn8wHikwYHwEi5d
Derived key from master key (m/44'/0'/1') with ID: ECKPAhgw3DZGoeIdWWo3NBKZE6mO
Converted key (m/44'/0'/1') with ID: 4tJ0ck4A0zS3FCxhHvAHO94ymDgK
Public key for derivation path m/44/0'/1'/0/2: 3056301006072a8648ce3d020106052b8104000a034200045db7283ac39fea82da6982843b3cef302f84fb39f2d8c1888f7efa0e8d7d479009efe1af4bda0a4ba1b39436c03cb6fb36628f058b70bfbd116dd655d9aaef89
Signature using m/44'/0'/1'/0/2: 30440220124ff915d63dcb77d112372b4cd32f9d04253d807b4e571ed432cc3d25f76e0a0220146663a1424b8187409e93b8262636158124b48a91cc6684eea2e492c58224cd
Code Example 2: BIP32 Seed Import
The following is a full example, showing how you can import an existing BIP32 seed into the TSM.
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"fmt"
"gitlab.com/sepior/go-tsm-sdkv2/v68/tsm"
"gitlab.com/sepior/go-tsm-sdkv2/v68/tsm/tsmutils"
"golang.org/x/sync/errgroup"
)
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 a random BIP32 seed. According to the spec, it must be between 128 and 512 bit. We use 256 bit here.
seed := make([]byte, 32)
if _, err := rand.Read(seed); err != nil {
panic(err)
}
// Create a simple xor sharing of the seed.
// Here we use the seed itself as the first share, and 0-byte arrays as the remaining shares.
// This creates a simple xor sharing of the seed. Note that to increase security in some applications, it may make
// sense to use random shares, as long as the logical xor of the shares equals the actual seed.
seedShares := make([][]byte, len(clients))
seedShares[0] = seed
for i := 1; i < len(clients); i++ {
seedShares[i] = make([]byte, len(seed))
}
// Create the seed witness
// We could also just use nil as witness. But using this value makes the TSM verify the integrity of the
// seed when the shares have been combined.
witness := sha512.Sum512(append([]byte("Exported Share"), seed...))
// Import the seed into the TSM
threshold := 2 // The security threshold for this key (note: hardened derivation requires three nodes and threshold = 2)
players := []int{0, 1, 2} // The seed should be secret shared among all three MPC nodes
sessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
ctx := context.Background()
seedIDs := make([]string, len(clients))
var eg errgroup.Group
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
// Wrap the seed share using the MPC node's wrapping key
wrappingKey, err := client.WrappingKey().WrappingKey(ctx)
if err != nil {
return err
}
pub, err1 := x509.ParsePKIXPublicKey(wrappingKey)
if err1 != nil {
return err
}
rsaWrappingKey := pub.(*rsa.PublicKey)
wrappedKey, err := tsmutils.Wrap(rsaWrappingKey, seedShares[i])
seedShare := &tsm.BIP32Seed{
WrappedSeedShare: wrappedKey,
SeedWitness: witness[:],
}
seedIDs[i], err = client.ECDSA().BIP32ImportSeed(ctx, sessionConfig, threshold, seedShare)
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
// Here we just use the seed ID provided by one of the MPC nodes;
// you should probably check that all MPC nodes return the same ID before using it.
seedID := seedIDs[0]
fmt.Println("Imported seed with ID:", seedID)
}
package com.example;
import com.sepior.tsm.sdkv2.*;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
public class EcdsaBip32ImportExample {
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 a random BIP32 seed. According to the spec, it must be between 128 and 512 bit. We use 256 bit here.
byte[] seed = new byte[32];
SecureRandom.getInstanceStrong().nextBytes(seed);
// Create a simple xor sharing of the seed.
// Here we use the seed itself as the first share, and 0-byte arrays as the remaining shares.
// This creates a simple xor sharing of the seed. Note that to increase security in some applications, it may make
// sense to use random shares, as long as the logical xor of the shares equals the actual seed.
byte[][] seedShares = new byte[clients.length][];
seedShares[0] = seed;
for (int i = 1; i < clients.length; i++) {
seedShares[i] = new byte[seed.length];
}
// Create the seed witness
// We could also just use nil as witness. But using this value makes the TSM verify the integrity of the
// seed when the shares have been combined.
byte[] prefix = "Exported Share".getBytes();
byte[] data = new byte[prefix.length + seed.length];
ByteBuffer buff = ByteBuffer.wrap(data);
buff.put(prefix);
buff.put(seed);
byte[] combined = buff.array();
MessageDigest md = MessageDigest.getInstance("SHA-512");
md.update(combined);
byte[] seedWitness = md.digest();
// Get wrapping keys for the MPC nodes in the destination TSM
byte[][] wrappingKeys = {
clients[0].getWrappingKey().getWrappingKey(),
clients[1].getWrappingKey().getWrappingKey(),
clients[2].getWrappingKey().getWrappingKey(),
};
// Encrypt each seed share using the corresponding wrapping key.
byte[][] wrappedSeedShares = {
TsmUtils.wrap(wrappingKeys[0], seedShares[0]),
TsmUtils.wrap(wrappingKeys[1], seedShares[1]),
TsmUtils.wrap(wrappingKeys[2], seedShares[2]),
};
// Import the seed shares
final int threshold = 2; // The security threshold for this key
final int[] players = {0, 1, 2}; // The seed should be secret shared among all three MPC nodes
final SessionConfig sessionConfig = SessionConfig.newSessionConfig(SessionConfig.generateSessionId(), players, null);
List<String> results1 = runConcurrent(
() -> clients[0].getEcdsa().bip32ImportSeed(sessionConfig, threshold, wrappedSeedShares[0], seedWitness),
() -> clients[1].getEcdsa().bip32ImportSeed(sessionConfig, threshold, wrappedSeedShares[1], seedWitness),
() -> clients[2].getEcdsa().bip32ImportSeed(sessionConfig, threshold, wrappedSeedShares[2], seedWitness));
// Here we just use the seed ID provided by one of the MPC nodes;
// you should probably check that all MPC nodes return the same ID before using it.
String seedId = results1.get(0);
System.out.println("Generated seed with ID: " + seedId);
}
@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;
}
}
const { TSMClient, Configuration, SessionConfig } = 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 a random BIP32 seed. According to the spec, it must be between 128 and 512 bit. We use 256 bit here.
const seed = crypto.randomBytes(32);
// Create a simple xor sharing of the seed.
// Here we use the seed itself as the first share, and 0-byte arrays as the remaining shares.
// This creates a simple xor sharing of the seed. Note that to increase security in some applications, it may make
// sense to use random shares, as long as the logical xor of the shares equals the actual seed.
const seedShares = [seed, Buffer.alloc(32), Buffer.alloc(32)];
// Create the seed witness
// We could also just use nil as witness. But using this value makes the TSM verify the integrity of the
// seed when the shares have been combined.
const witness = crypto
.createHash("sha512")
.update(Buffer.concat([Buffer.from("Exported Share"), seed]))
.digest();
// Import the seed into the TSM
const threshold = 2; // The security threshold for this key (note: hardened derivation requires three nodes and threshold = 2)
const players = [0, 1, 2]; // The seed should be secret shared among all three MPC nodes
const sessionConfig = await SessionConfig.newSessionConfig(
await SessionConfig.GenerateSessionID(),
new Uint32Array(players),
{}
);
const seedIds = ["", "", ""];
const seedPromises = [];
for (const [i, client] of clients.entries()) {
const func = async () => {
// Wrap the seed share using the MPC node's wrapping key
const wrappingKeyApi = client.WrappingKey();
const wrappingKey = await wrappingKeyApi.wrappingKey();
const utilsApi = client.Utils();
const wrappedKey = await utilsApi.wrap(wrappingKey, seedShares[i]);
const ecdsaApi = client.ECDSA();
seedIds[i] = await ecdsaApi.bip32ImportSeed(
sessionConfig,
threshold,
wrappedKey,
witness
);
};
seedPromises.push(func());
}
await Promise.all(seedPromises);
// Here we just use the seed ID provided by one of the MPC nodes;
// you should probably check that all MPC nodes return the same ID before using it.
const seedId = seedIds[0];
console.log(`Imported seed with ID: ${seedId}`);
}
main().catch((e) => console.log(e));
Updated 4 days ago