Importing an External Key (EdDSA)

Importing an EdDSA key from an external key store or wallet works much like importing an external ECDSA key.

Importing from a Seed (RFC 8032)

When importing an Ed25519 or Ed448 key, as in the example below, the TSM expects you to start with the raw private key scalar. But some external wallets instead hold an seed, which is then derived into a raw Ed25519 or Ed448 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 example shows how you can use a 3rd party tool like @noble/ed25519 to convert the RFC 8032 seed into a raw Ed25519 key pair. The example illustrates that the same conversion happens internally in tools such as @polkadot-util-crypto .

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 RFC 8032 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());

The raw private key output by this example can be used in the following example.

Code Example

This example shows how to import a raw Ed25519 key into the TSM.

package main

import (
	"bytes"
	"context"
	"crypto/rsa"
	"crypto/x509"
	"encoding/hex"
	"gitlab.com/sepior/go-tsm-sdkv2/ec"
	"gitlab.com/sepior/go-tsm-sdkv2/tsm"
	"gitlab.com/sepior/go-tsm-sdkv2/tsm/tsmutils"
	"golang.org/x/sync/errgroup"
)

func main() {

	// This is the external key and chain code that we want to import into the TSM;
	// e.g. as recovered by ERS.
	// Note: The chain code is optional, and only relevant if you use key derivation.
	// To just import a private EdDSA key, you can set the chain code to nil.

	privateKeyHex := "0dd855e20d7af1575858570d7551d237b9b348455e795763e565d636c5acc5b8"
	chainCodeHex := "7365df71160ca42df2fa3f447fb62f74c90e1996a7cacbd437d41a3638a49809"

	privateKey, err := hex.DecodeString(privateKeyHex)
	if err != nil {
		panic(err)
	}
	chainCode, err := hex.DecodeString(chainCodeHex)
	if err != nil {
		panic(err)
	}

	// We first compute the public key corresponding to the private key.

	curve, err := ec.NewCurve(ec.Edwards25519.Name())
	x, err := curve.Zn().DecodeScalar(privateKey)
	if err != nil {
		panic(err)
	}
	y := curve.G().Multiply(x)
	pkixPubKey, err := tsmutils.ECPointToPKIXPublicKey(curve.Name(), y.Encode())
	if err != nil {
		panic(err)
	}

	// Create clients for three nodes

	configs := []*tsm.Configuration{
		tsm.Configuration{URL: "http://localhost:8500"}.WithAPIKeyAuthentication("Node0LoginPassword"),
		tsm.Configuration{URL: "http://localhost:8501"}.WithAPIKeyAuthentication("Node1LoginPassword"),
		tsm.Configuration{URL: "http://localhost:8502"}.WithAPIKeyAuthentication("Node2LoginPassword"),
	}

	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)
		}
	}

	// Split the private key into a secret sharing.

	threshold := 2 // Can also be set to 1
	players := []int{0, 1, 2}
	ecdsaKeyShares, err := tsmutils.ShamirSecretShare(threshold, players, ec.Edwards25519.Name(), privateKey)
	if err != nil {
		panic(err)
	}

	// Import one secret share into each MPC node, encrypted under that MPC node's public wrapping key.

	sessionID := tsm.GenerateSessionID()
	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

			wrappingKey, err := client.WrappingKey().WrappingKey(context.Background())
			if err != nil {
				return err
			}

			pub, err := x509.ParsePKIXPublicKey(wrappingKey)
			if err != nil {
				return err
			}
			rsaWrappingKey := pub.(*rsa.PublicKey)
			wrappedShare, err := tsmutils.Wrap(rsaWrappingKey, ecdsaKeyShares[i])
			if err != nil {
				return err
			}

			wrappedChainCode, err := tsmutils.Wrap(rsaWrappingKey, chainCode)
			if err != nil {
				return err
			}

			sessionConfig := tsm.NewStaticSessionConfig(sessionID, len(clients))
			keyIDs[i], err = client.Schnorr().ImportKeyShares(context.Background(), sessionConfig, threshold, wrappedShare, wrappedChainCode, pkixPubKey, "")
			return err

		})
	}
	if err = eg.Wait(); err != nil {
		panic(err)
	}

	// Test: All MPC nodes should agree on the new key ID.

	for _, keyID := range keyIDs {
		if keyID != keyIDs[0] {
			panic("keyID disagreement")
		}
	}

	// Test: Once imported, the public key should equal the public key we computed.

	for _, client := range clients {
		pubKey, err := client.Schnorr().PublicKey(context.Background(), keyIDs[0], nil)
		if err != nil {
			panic(err)
		}
		if !bytes.Equal(pkixPubKey, pubKey) {
			panic("public key disagreement")
		}
	}

}