Key Export

The Builder Vault supports export of keys. This can be done by running an MPC session, where each SDK calls the following method:

wrappedKeyShare, err := client.ECDSA().ExportKeyShares(ctx, sessionConfig, keyID, derivationPath, wrappingKey)

This returns to each SDK a share of the key, encrypted under the provided wrapping key. The wrapping key must be the SubjectPublicKeyInfo in an ASN.1 DER encoding.

The key export MPC session will only work if all MPC nodes are configured to allow key share export, and if the provided wrapping keys are whitelisted in the MPC nodes' configuration.

While the original key shares remain unchanged in the Builder Vault, the exported key shares are actually shares of a new, independent, re-sharing of the key. This ensures that the security of the original key sharing remains unaffected if a few of the exported key shares should leak to an attacker (as long as the attacker sees t or less shares, where t is the security threshold of the key).

If derivationPath is provided, the exported key shares will be shares of a key derived from the original key defined by keyID, using the provided derivation path.

The idea of exporting the key shares individually, and encrypted under wrapping keys, is to make it easier to transport the key securely to a destination, such as another Builder Vault instance or an HSM, without having to reconstruct the key in the clear while it in transit. To accomplish this, unwrapping and recombination of the secret shares should only happen in the destination. An example of this, where the destination is another Builder Vault instance, see this tutorial.

📘

Exporting Keys with No Single Point of Trust

In the following, we show how to unwrap the shares and recombine them into the secret key. This can be useful, if you just want to recover the key in the clear. This does, however, introduce a single point of trust, which is what the Builder Vault otherwise avoids.

To achieve the highest level of security, you should consider not to introduce a single point of trust while the key is in transit, by moving the wrapped key shares to the destination (e.g., another wallet, a HSM, or another Builder Vault instance) and only unwrap and recombine the key inside this destination. An example of moving the key to another Builder Vault instance without introducing any single point of trust is given here.

To unwrap a key share, you can do the following:

keyShare, err = tsmutils.Unwrap(unwrappingKey, wrappedKeyShare.WrappedKeyShare)

Finally, if you have enough unwrapped key shares, you can recombine them into the secret key as follows:

exportedKey, err := tsmutils.ShamirRecombine(threshold, exportedKeyShares, curveName)

It is not only the key share, but also the corresponding public key and the chain code that is exported. The chain code is also encrypted with the wrapping key. You can access the public key and chain code as follows:

exportedPublicKey := wrappedKeyShare.PKIXPublicKey
exportedChainCode, err = tsmutils.Unwrap(unwrappingKey, wrappedKeyShare.WrappedChainCode)

Comparing Key Export and Emergency Recovery

The key export feature in this section has some similarity with the emergency recovery feature. Both lets you export a secret from the Builder Vault. The difference is that key export only exports one share to each SDK, whereas the RecoveryData method which is part of the the emergency recovery procedure, outputs all key shares to one SDK, encrypted under an external RSA key, and accompanied by a zero-knowledge proof. So the regular key export feature is better suited, if you want to migrate the key sharing to another Builder Vault instance, without reducing the threshold security.

Code Example

The following complete example shows how a private key can be exported from the Builder Vault.

package main

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/hex"
	"fmt"
	"gitlab.com/Blockdaemon/go-tsm-sdkv2/v68/tsm"
	"gitlab.com/Blockdaemon/go-tsm-sdkv2/v68/tsm/tsmutils"
	"golang.org/x/sync/errgroup"
)

// This example shows how to export a key from the Builder Vault.
func main() {

	// Create clients for the Builder Vault MPC 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)
		}
	}

	// We first generate a random key in the Builder Vault

	players := []int{0, 1, 2} // Generate the key as a sharing among these players
	threshold := 1            // The security threshold for this key
	keyGenSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
	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(), keyGenSessionConfig, threshold, "secp256k1", "")
			return err
		})
	}
	if err := eg.Wait(); err != nil {
		panic(err)
	}
	keyID := keyIDs[0]
	fmt.Println("Generated key", keyID, "using players", players)

	// We then export the key shares, wrapped in random wrapping keys

	// Note: For this to work, the MPC nodes must be configured with "EnableExport = true", and the wrapping keys
	// must be whitelisted. When using random wrapping keys like in this example, the MPC node config files must contain
	// the statement ExportWhiteList = ["*"], which should only be used for test, and not in production.

	wrappingPubKeys, wrappingPrivKeys := generateWrappingKeys(clients)

	fmt.Println("Exporting wrapped key sharing from players", players)
	var derivationPath []uint32 = nil // We want to export the key itself, not a derivation of the key.
	wrappedKeyShares := make([]tsm.ECDSAWrappedKeyShare, len(clients))
	exportSessionConfig := tsm.NewSessionConfig(tsm.GenerateSessionID(), players, nil)
	for i, client := range clients {
		client := client
		i := i
		eg.Go(func() error {
			wrappedKeyShare, err := client.ECDSA().ExportKeyShares(context.TODO(), exportSessionConfig, keyID, derivationPath, wrappingPubKeys[i])
			if err != nil {
				return err
			}
			wrappedKeyShares[i] = *wrappedKeyShare
			return nil
		})
	}
	err := eg.Wait()
	if err != nil {
		panic(err)
	}

	// Unwrap exported key shares

	fmt.Println("Unwrapping exported key shares")
	exportedKeyShares := make(map[int][]byte, len(clients))
	for i, wrappedKeyShare := range wrappedKeyShares {
		exportedKeyShares[i], err = tsmutils.Unwrap(wrappingPrivKeys[i], wrappedKeyShare.WrappedKeyShare)
		if err != nil {
			panic(err)
		}
	}

	// Recombine exported key shares into the key itself

	exportedKeyBytes, err := tsmutils.ShamirRecombine(threshold, exportedKeyShares, "secp256k1")
	if err != nil {
		panic(err)
	}
	fmt.Println("Exported private key:", hex.EncodeToString(exportedKeyBytes))

}

func generateWrappingKeys(clients []*tsm.Client) (wrappingPubKeys [][]byte, wrappingPrivKeys []*rsa.PrivateKey) {
	wrappingPrivKeys = make([]*rsa.PrivateKey, len(clients))
	wrappingPubKeys = make([][]byte, len(clients))
	var err error
	for i := range clients {
		wrappingPrivKeys[i], err = rsa.GenerateKey(rand.Reader, 2048)
		if err != nil {
			panic(err)
		}

		rsaPublicKey, ok := wrappingPrivKeys[i].Public().(*rsa.PublicKey)
		if !ok {
			panic("failed to cast public key")
		}

		wrappingPubKeys[i], err = x509.MarshalPKIXPublicKey(rsaPublicKey)
		if err != nil {
			panic(err)
		}

	}
	return wrappingPubKeys, wrappingPrivKeys
}

Running this example should produce output like this:

Generated key gdNbl6mEQEBGjmx30hb9O6A08a7v using players [0 1 2]
Exporting wrapped key sharing from players [0 1 2]
Unwrapping exported key shares
Exported private key: 6dfa37b2925b38db13328bcf2785fc0c91aaeac06877d01ef0214c50c7f191e5