Import/Export of Key Shares

Background

There are different scenarios where import and export are useful to enable access to the same key on different
platforms.

Fail-over

When running in a high availability system, it may be important to have a fail-over site that can be switched to in
case of failure in the primary site. Import and export allows moving key shares from one site to another and keep the same keyID on the new site such that the user does not notice that load has been shifted to an alternate site.

Note that key shares cannot be used across the two sites, so a signature cannot be made using node 1 from the main site and node 2 from the fail-over site. From the TSM perspective it is two separate keys. This is done to ensure that shares are kept secure as long as a single site is not compromised in excess of the security parameters.

Multiple control

Some keys may require that multiple people are in control of them and can use them to sign data independently of each other. In this scenario one share may be stored on an embedded client on a mobile phone while the rest are stored on some shared servers. In this case each instance of the key will have its own keyID as they have to coexist on some servers.

Security notes

When exporting and importing keys, the key shares are re-shared while in transport. This is done such that transport shares that are compromised in transport will not compromise the shares at either the exporting or importing site. This also ensures that the importing and export site are independent.

Configuration

To enable export a couple of configuration entries need to be configured

[Player]
  ExportWhiteList = ["BA3E64=="]

[SEPH18S/DKLS19]
  EnableExport=true

EnableExport will enable endpoints so export can be accessed.

ExportWhiteList is used to control under which public keys the keyShares can be exported under. This is a list of BASE64 encoded RSA public keys. Only keys on the list will be allowed when performing an export operation.

Configuring the WhiteList

If possible, the white list should only contain public keys of trusted servers. For testing, or in the case with very dynamic client lists, a wildcard can be used:

[Player]
  ExportWhiteList = ["*"]

This will accept any keys for export, and should be used only for testing or when other security checks are used to ensure no unauthorized access is allowed.

Fail-over setting

In the fail-over setting, the best way to configure the fail-over sites, is that the WhiteList of the main site only contains the exported wrapping key from the corresponding fail-over site. So the WhiteList of node1 in the main site will contain the wrapping key from node1 of the fail-over site and only this key, and likewise for node2, etc. This will ensure that key shares can only be moved between the two corresponding nodes in the main and fail-over site and nowhere else.

Code Examples

There are two types of functions for export and import. With or without session ID. Without session ID is for when all nodes are controlled by the same entity, and with session ID is for when different nodes are run from different places, e.g. when using embedded nodes on mobile phones or when different people are in control of different nodes.

Without session ID

This example exports and imports on the same client. In a fail-over setup the exported shares need to be moved from the main nodes to the fail-over nodes.

// Error checking have been omitted for simplicity
// Input: client and keyId
ecdsaClient := tsm.NewECDSAClient(client)
keyID, err := ecdsaClient.Keygen(curveName)

// Performed on the client at the importing side, wrapKeys are transported to the exporting nodes
wrapKeys, _, err := ecdsaClient.WrappingKeys()

// Performed on the client at the exporting side, return values are transported to the importing nodes
keyShares, chainCodes, curve, derPublicKey, err := ecdsaClient.ExportWrappedKeyShares(keyID, wrapKeys)

// Performed on the client at the importing side. keyID can be replaced with the empty string in which case a new keyID
// will be generated
importedKeyId, err := ecdsaClient.ImportWrappedKeyShares(curve, keyShares, chainCodes, derPublicKey, keyID)

With session ID

This example is pseudocode written using GO against the mobile interfaces. The mobile interfaces are released in swift and java for iOS and Android respectively.

// Error checking have been omitted for simplicity
// Input: config and keyId

// Importing Client: Prepare wrapping key
importClient, _ := NewEmbeddedTenantClient(config)
wrappingClient := NewWrappingClient(importClient)
importECDSAClient := NewECDSAClient(importClient)

wrappingKey, _ := wrappingClient.WrappingKeys()
encodedKey := base64.StdEncoding.EncodeToString(wrappingKey.SubjectPublicKeyInfo)

// Exporting client: Export key share
exportClient, _ := NewEmbeddedTenantClient(fmt.Sprintf(
	`# Normal configuration plus:\n" +
	"[Player]\n" +
	"ExportWhiteList = ["%s"]\n`, encodedKey))
exportECDSAClient := NewECDSAClient(exportClient)
exportSessionID := GenerateSessionID()
exportedShare, _ := exportECDSAClient.ExportWrappedKeySharesWithSessionID(exportSessionID, keyID, wrappingKey.SubjectPublicKeyInfo)
// Simultaneous to this, the export need to be started on the all the server nodes with the same exportSessionID

// Importing Client: Import key share. Note an empty string is used for keyID which will result in a new keyID being generated.
importSessionID := GenerateSessionID()
importECDSAClient.ImportWrappedKeySharesWithSessionID(importSessionID, exportedShare.Curve, exportedShare.KeyShare, exportedShare.ChainCode, exportedShare.PublicKey, "")
// Simultaneous to this, the import should be started on the server node with the same importSessionID

Import of an External Key

Sometimes a key exist outside the TSM and need to be imported into the TSM. This can be done both with and without session ID

ECDSA keys

The below example shows how to import an external private ECDSA key into the TSM without session ID. It could easily be modified to using session ID by adapting the changes in the with session ID section.

// Error checking have been omitted for simplicity
ecdsaClient := tsm.NewECDSAClient(n, t, client)

keyShares, _ := tsm.ECDSASecretShare(n, t, curveName, privateKey)

publicKeys, _, _ := ecdsaClient.WrappingKeys()
wrappedKeyShares := wrapKeyShares(publicKeys, keyShares)

// Wrapped chain codes can be encrypted like the key shares
ecdsaClient.ImportWrappedKeyShares(curveName, wrappedKeyShares, nil, publicKey, "")

The code uses the following two utility methods:

// Error checking have been omitted for simplicity
func wrapKeyShares(keys [][]byte, blobs [][]byte) [][]byte {
    length := len(keys)

    encryptions := make([][]byte, length)
    for i := 0; i < length; i++ {
         encryptions[i] = oaepEncrypt(keys[i], blobs[i])
    }

    return encryptions
}

func oaepEncrypt(key []byte, data []byte) []byte {
    pkInterface, _ := x509.ParsePKIXPublicKey(key)
    pk, _ := pkInterface.(*rsa.PublicKey)

    cipherText, _ := rsa.EncryptOAEP(crypto.SHA256.New(), rand.Reader, pk, data, nil)

    return cipherText
}

Encoding of public key

The (DER) public key supplied to the import call needs to be a ASN.1 DER encoded SubjectPublicKeyInfo. This is what is exported from the TSM, but for keys that are imported from an external system it may be required to encode a raw ECC point to an SPKI. A short example showing how to encode a raw secp256k1 ECC point to a SPKI:

// No errors are checked, so add error tests on all err return values.
oidPublicKeyECDSA := asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}

secp256k1OID := asn1.ObjectIdentifier{1, 3, 132, 0, 10}
oidBytes, err := asn1.Marshal(secp256k1OID)

publicKeyBytes, err := hex.DecodeString("045514f5ebcd1850c9e76cd201b385ad81fac5f2a183841cd7adee7c8bfc34bff2c73921fd29d7088bd085df353d65197ec884ab7c168e8cf242c078fcfaac77ba")
publicKeyBitString := asn1.BitString{
	Bytes:     publicKeyBytes,
	BitLength: 8 * len(publicKeyBytes),
}

var algorithmIdentifier pkix.AlgorithmIdentifier
algorithmIdentifier.Algorithm = oidPublicKeyECDSA
algorithmIdentifier.Parameters.FullBytes = oidBytes

spki := subjectPublicKeyInfo{
	Algorithm: algorithmIdentifier,
	PublicKey: publicKeyBitString,
}
spkiBytes, err := asn1.Marshal(spki)

spkiStr := hex.EncodeToString(spkiBytes)
fmt.Println(spkiStr)

EdDSA Keys

Here is an example showing how to import a raw private Ed25519 key into the TSM:

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/hex"
	"fmt"
	"gitlab.com/sepior/go-tsm-sdk/sdk/tsm"
	"golang.org/x/sync/errgroup"
	"log"
	"os"
)

func main() {

	playerCount := 3
	threshold := 2

	// Create EdDSA clients for each of the MPC nodes (You probably create the clients in a different way)

	eddsaClients := getClients(threshold)

	// We start with a raw private Ed25519 key that we want to import:

	privateKeyHex := "05260212f2c6e3537c1acb7d03918aba97efe5f3a9130922cd98d901f2d20528" // x
	publicKeyHex := "00ef13fae121e0a848290f960a2a6dedcc3592060e56d18b7ba3e50ce44cf339"  // xG
	privateKey, err := hex.DecodeString(privateKeyHex)
	if err != nil {
		panic(err)
	}
	pubKey, err := hex.DecodeString(publicKeyHex)
	if err != nil {
		panic(err)
	}

	// We then secret share the raw private key; here we use a static helper method on the TSM SDK

	keyShares, err := tsm.ECDSASecretShare(playerCount, threshold, "ED-25519", privateKey)
	if err != nil {
		panic(err)
	}

	// Import one wrapped key share to each of the MPC nodes 

	sessionID := tsm.GenerateSessionID()
	eg := errgroup.Group{}
	for i := 0; i < playerCount; i++ {
		i := i
		eg.Go(func() error {

			// Get wrapping key from MPC node and use it to encrypt the key share for that node

			publicKeys, _, err := eddsaClients[i].WrappingKeys()
			if err != nil {
				panic(err)
			}
			wrappingKey, err := x509.ParsePKIXPublicKey(publicKeys[0])
			if err != nil {
				panic(err)
			}
			wrappingRSAKey, ok := wrappingKey.(*rsa.PublicKey)
			if !ok {
				panic(err)
			}
			wrappedKeyShare, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, wrappingRSAKey, keyShares[i], nil)
			if err != nil {
				panic(err)
			}

			// Import wrapped key share into the MPC node

			keyID, err := eddsaClients[i].ImportWrappedKeySharesWithSessionID(sessionID, "ED-25519", [][]byte{wrappedKeyShare}, nil, pubKey, "")
			if err != nil {
				panic(err)
			}

			fmt.Println("key id for imported key", keyID)

			return err
		})
	}
	err = eg.Wait()
	if err != nil {
		log.Fatal(err)
	}

}

func getClients(threshold int) []tsm.EDDSAClient {
	credsJSON, err := os.ReadFile("../../user.json")
	if err != nil {
		panic(err)
	}
	creds, err := tsm.DecodePasswordCredentials(string(credsJSON))
	if err != nil {
		panic(err)
	}

	playerCount := len(creds.URLs)
	eddsaClients := make([]tsm.EDDSAClient, playerCount)
	for player := 0; player < playerCount; player++ {
		credsPlayer := tsm.PasswordCredentials{
			UserID:    creds.UserID,
			URLs:      []string{creds.URLs[player]},
			Passwords: []string{creds.Passwords[player]},
		}
		client, err := tsm.NewPasswordClientFromCredentials(playerCount, threshold, credsPlayer)
		if err != nil {
			panic(err)
		}
		eddsaClients[player] = tsm.NewEDDSAClient(client)
	}

	return eddsaClients
}

Import from an Ed25519 seed (RFC 8032)

When importing an Ed25519 key, as in the example above, the TSM expects you to start with the raw private key scalar. But many external wallets instead holds a key which is then derived into a raw Ed25519 key pair according to RFC 8032 Section 5.1.5. If you start out with a seed like this, you first need to convert it into a raw private key. The following shows how you can convert the seed into the raw Ed25519 key pair.

const { hexToU8a, u8aToHex } = require('@polkadot/util');
const { ed25519PairFromSeed } = require('@polkadot/util-crypto');
const ed  = require('@noble/ed25519');  // npm install @noble/[email protected] (2.0 doesn't support import via 'require')

const Example = async () => {

  // The Ed25519 seed to import

  const seed = hexToU8a("b6b3dd3021cffe5fdaaccd9c2fa2543ea97584ad1da01e3bd12fe0656f1bf4b6")

  // Derive the raw Ed25519 key pair from the seed according to RFC-8032 (Section 5.1.5)

  const hash = await ed.utils.sha512(seed)	
  var left = hash.slice(0,32)
  left[0] &= 248;
  left[31] &= 127;
  left[31] |= 64;

  const privateKey = modlLE(left);
  const publicKey = ed.Point.BASE.multiply(privateKey);

  // We can use polkadot-js to test that we have done it correctly. It also derives according to
  // RFC-8032, so we should get the same public key from the seed:

  const keyPairPolkaJS = ed25519PairFromSeed(seed);

  console.log("private key               :", privateKey.toString(16).padStart(64, '0'));
  console.log("public key                :", u8aToHex(publicKey.toRawBytes()));
  console.log("public key (polkadot-js)  :", u8aToHex(keyPairPolkaJS.publicKey));

}


function modlLE(uint8a) {
  const bytesLE = Uint8Array.from(uint8a).reverse();
  const hex = Buffer.from(bytesLE).toString('hex').padStart(64, "0");
  let scalar = BigInt('0x' + hex) % ed.CURVE.l;
  return scalar >= BigInt(0) ? scalar : ed.CURVE.l + scalar;
}


Example().catch(console.error).finally(() => process.exit());