ERS Code Example

Here we show how to create an ERS backup of a key, and how the key can later be recovered from the backup. We will use the TSM SDK to do the recovery, but on request we can also provide an open source Go reference implementation of the recovery and validation.

Creating Recovery Data

Let's say, for example, that our TSM is configured to generate ECDSA signatures using the DKLS19 MPC protocol. Then we must first enable ERS in the TSM configuration files as follows:

[DKLS19]
EnableERSExport = true

We first choose an ERS label and generate the ERS key:

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
)

ersLabel := []byte("exampleLabel")
ersPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
ersPublicKey, err := x509.MarshalPKIXPublicKey(&ersPrivateKey.PublicKey)

In this example we just generate the private ERS key in the clear. In a production setting, the ERS key will instead often be generated inside an HSM, and only the public ERS key will be exported from the HSM.

We will need an instances of the SDK that is connected to one of the MPC nodes in the TSM. See https://docs.sepior.com/docs/tsm-sdk for more about how to create these. Then we can generate an ECDSA key as follows:

sessionID := tsm.GenerateSessionID()
keyID, err := ecdsaClient.KeygenWithSessionID(sessionID, "secp256k1")

For this to work, there must in fact be one SDK for each of the MPC nodes in the TSM, and they must first agree on the sesssion ID and then all make the same call to KeygenWithSessionID. A full, running example of this can be found below.

Once the key is generated, we can now create a backup as follows:

sessionID := tsm.GenerateSessionID()
recoveryInfo := ecdsaClient.PartialRecoveryInfo(sessionID, keyID, ersPublicKey, label)

Again, this has to be done for each MPC node. Assume that we have collected all of the partial recovery data. We can then combine them into the final recovery data:

var recoveryInfos [][]byte // these are collected from all the MPC nodes 
recoveryData, err := tsm.RecoveryInfoCombine(recoveryInfos, ersPublicKey, ersLabel)

The recoveryData now contains all the secret key shares and can be stored and later used for recovery. The recovery data is not sensitive, since the secret key shares are encrypted under the ERS public key.

Validating Recovery Data

Given the public ERS key, the ERS label, and the public ECDSA key, we can later verify that the recovery data is ok:

// Chain path is nil, so we will get the master public key, with no key derivation.
publicKey, err := ecdsaClient.PublicKey(keyID, nil)

err = tsm.RecoveryInfoValidate(recoveryData, ersPublicKey, ersLabel, publicKey)

If the call to RecoveryInfoValidate does not return an error, we know that the recovery data is valid. This means that it contains key shares that can be decrypted with the private ERS key and ERS label, and that the decrypted key shares are indeed shares of the private key that corresponds to the given public key.

Anyone can in fact validate the recovery data. It does not require the ERS private key to validate, and the key shares contained in the recovery data remains encrypted under the ERS public key.

Validation also does not require access to any MPC nodes. The RecoveryInfoValidate() method in the TSM SDK is indeed just a static method. If you don't have access to a TSM SDK, you can also validate the recovery data using our open-source ERS reference implementation or implement the validate method yourself.

Validating the recovery data regularly lets you detect if someone tampered with it or it was otherwise corrupted. Also, if transferring the recovery data from one person to another, the receiving person may want to re-validate the recovery data if he don't trust the sender.

The only really important thing to remember when validating is to use the correct ERS public key and the correct ECDSA public key, as obtained by sdk.PublicKey().

Key Recovery

Given the private ERS key, the ERS label, and the recovery data, you can later recover the private ECDSA key:

ersPrivateKeyBytes := x509.MarshalPKCS1PrivateKey(ersPrivateKey)
curveName, ecdsaPrivateKey, masterChainCode, err := tsm.RecoverKeyECDSA(recoveryData, ersPrivateKey, ersLabel)

Again, the RecoverKeyECDSA() method is available on the SDK only for convenience. You can also recover the ECDSA key using our open-source reference implementation, or implement the recovery code yourself.

In this example we used the private ERS key to recovery. In a production setting, this key may only exist inside an HSM. In our open-source ERS library you can see how recovery can be done using only "black-box" calls to RSA decryptions using the private ERS key. That is, we can recover using only something that implements this interface:

type Decryptor interface {
	Decrypt(ciphertext, label []byte) (plaintext []byte, err error)
}

A Complete Example

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/hex"
	"fmt"
	"log"

	"gitlab.com/sepior/go-tsm-sdk/sdk/tsm"
	"golang.org/x/sync/errgroup"
)

func main() {

	// Configure your TSM here

	const credentials string = `
	{
		"userID": "my-user-id",
		"urls": [ "https://my-tsm-node1.tsm.sepior.net", "https://my-tsm-node2.tsm.sepior.net", "https://my-tsm-node3.tsm.sepior.net" ],
		"passwords": [ "password1", "password2", "password3" ]
	}
	`
	creds, err := tsm.DecodePasswordCredentials(credentials)
	if err != nil {
		log.Fatal(err)
	}

	// Create clients for each player

	playerCount := len(creds.URLs)
	ecdsaClients := make([]tsm.ECDSAClient, 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(credsPlayer)
		if err != nil {
			log.Fatal(err)
		}
		ecdsaClients[player] = tsm.NewECDSAClient(client)
	}

	// Generate an ECDSA key

	sessionID := tsm.GenerateSessionID()
	var keyID string
	var eg errgroup.Group
	for _, ecdsaClient := range ecdsaClients {
		ecdsaClient := ecdsaClient
		eg.Go(func() error {
			var err error
			keyID, err = ecdsaClient.KeygenWithSessionID(sessionID, "secp256k1")
			return err
		})
	}
	if err := eg.Wait(); err != nil {
		log.Fatal(err)
	}

	// Create an ERS key pair and ERS label
	// Here we generate the private key in the clear, but it could also be exported from an HSM

	ersPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		log.Fatal(err)
	}
	ersPrivateKeyBytes := x509.MarshalPKCS1PrivateKey(ersPrivateKey)

	ersPublicKey, err := x509.MarshalPKIXPublicKey(&ersPrivateKey.PublicKey)
	if err != nil {
		log.Fatal(err)
	}

	ersLabel := []byte("test")

	// Collect the partial recovery data

	sessionID = tsm.GenerateSessionID()
	var partialRecoveryData = make([][]byte, len(ecdsaClients))
	for i := range ecdsaClients {
		i := i
		eg.Go(func() error {
			var err error
			r, err := ecdsaClients[i].PartialRecoveryInfo(sessionID, keyID, ersPublicKey, ersLabel)
			partialRecoveryData[i] = r[0]
			return err
		})
	}
	if err := eg.Wait(); err != nil {
		log.Fatal(err)
	}

	// Combine the partial recovery data

	recoveryData, err := tsm.RecoveryInfoCombine(partialRecoveryData, ersPublicKey, ersLabel)
	if err != nil {
		log.Fatal(err)
	}

	// Validate the combined recovery data against the ERS public key and the public ECDSA key

	publicKey, err := ecdsaClients[0].PublicKey(keyID, nil)
	if err != nil {
		log.Fatal(err)
	}

	err = tsm.RecoveryInfoValidate(recoveryData, ersPublicKey, ersLabel, publicKey)
	if err != nil {
		log.Fatal(err)
	}

	// Recover the private ECDSA key

	curveName, privateECDSAKey, masterChainCode, err := tsm.RecoverKeyECDSA(recoveryData, ersPrivateKeyBytes, ersLabel)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Curve:                      ", curveName)
	fmt.Println("Recovered private ECDSA key:", privateECDSAKey)
	fmt.Println("Recovered master chain code:", hex.EncodeToString(masterChainCode))

}