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 and DKLS23 protocols, 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 BIP44 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 1: Seed Generation and Derivation

You can find complete code examples showing how to first generate a BIP32 seed in Builder Vault and then sign a message using the key derived according to the chain path m / 44' / 0' / 1' / 0 / 2 in our demo repository (Go, Java, node.js). 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.

Code Example 2: BIP32 Seed Import

Another code example in the demo repository shows how you can import an existing BIP32 seed into a Builder Vault instance (Go, Java, node.js).