EdDSA Key Derivation

We currently only support non-hardened key derivation for EdDSA (Ed25519 and Ed448).

Note that our EdDSA key derivation scheme is not SLIP10 compliant. SLIP10 only defines hardened key derivation for EdDSA. This is because SLIP10 assumes deterministic signing as specified in RFC-8032. In deterministic signing, the "raw" private key as well as a prefix is derived from a seed. And when signing, the signing nonce is computed as a hash of the public key, the message, and the prefix. This approach is not compatible with non-hardened derivation.

During key generation we instead directly generate a random secret sharing of the raw private key. Then, when signing, we use a fresh random nonce for each signature generation. This approach lets us do non-hardened derivation in much the same way as in BIP32. As for BIP32, non-hardened derivation for EdDSA keys does not affect security in our case, since the derived keys are protected in the same way (by secret sharing) as the master key.

For the non-hardened EdDSA derivation we use multiplicative offset, contrary to BIP32 which uses an additive offset for the derived keys. See this for more information about the difference between additive and multiplicative offsets in key derivation.

Code Example

package main

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

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 one SDK for each MPC node in the TSM

	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(credsPlayer)
		if err != nil {
			log.Fatal(err)
		}
		eddsaClients[player] = tsm.NewEDDSAClient(client)
	}

	// Generate an Ed25519 master key

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

	// Get the public key for a given soft derivation path

	chainPath := []uint32{1, 2, 34}
	publicKey, err := eddsaClients[0].PublicKey(keyID, chainPath)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Public Key:", hex.EncodeToString(publicKey))

	// Sign using the given chain path

	partialSignatures := make([][]byte, playerCount)
	lock := sync.Mutex{}
	message := []byte{4, 3, 2, 5}
	sessionID = tsm.GenerateSessionID()
	for i, eddsaClient := range eddsaClients {
		i := i
		eddsaClient := eddsaClient
		eg.Go(func() error {
			var err error
			partialSignature, err := eddsaClient.PartialSign(sessionID, keyID, chainPath, message)
			if err != nil {
				return err
			}

			lock.Lock() // avoid concurrent updates to the map
			partialSignatures[i] = partialSignature
			lock.Unlock()

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

	// Combine partial signatures into the final signature

	signature, err := tsm.EDDSAFinalize(partialSignatures...)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Signature:", hex.EncodeToString(signature))

	// Validate signature

	err = tsm.EDDSAVerify25519(publicKey, message, signature)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Signature is valid")

}