ERS for EdDSA Keys

In the previous example we generated an emergency backup of an ECDSA key. It also works for Ed25519 and Ed448 keys:

[SEPD19S]
EnableERSExport = true
sessionID := tsm.GenerateSessionID()
r1, err := eddsaClient1.PartialRecoveryInfo(sessionID, keyID, ersPublicKey, ersLabel)
// handle error

partialRecoveryData = [][]byte{r1, r2, r3}
recoveryData, err := tsm.RecoveryInfoCombine(partialRecoveryData, ersPublicKey, ersLabel)
// handle error

publicKey, err := ecdsaClients[0].PublicKey(keyID, nil)
// handle error

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

curveName, privateEdDSAKey, masterChainCode, err := tsm.RecoverKeyEdDSA(recoveryData, ersPrivateKeyBytes, ersLabel)
// handle error

Like with ECDSA this lets you recover the private key privateEdDSAKey corresponding to the public key publicKey.

Randomized Signing

If you try to use the recovered private key pair with an external library, it may not work. This is because many EdDSA signing libraries, such as Noble, use 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.

In our case, during key generation we instead directly generate a secret sharing of the raw private key. Then, when signing, we use a fresh random nonce for each signature generation.

Consequently, to use the recovered key pair, you need a library that allows to sign using the "raw" private key.

Here is an example that shows how a recovered Ed25519 key can be used to sign messages:

package main

import (
	"crypto/ed25519"
	"crypto/sha512"
	"encoding/hex"
	"filippo.io/edwards25519"
	"fmt"
	"log"
)

func main() {

	// This is an example of a private key recovered from our ERS system
	privateKeyBytes, err := hex.DecodeString("08687f8a741cad9b34a6d2ccb232f5729e92d871e56a8f2e488404c1ed4525ac")
	if err != nil {
		log.Fatal(err)
	}

	// ERS recovers key in big endian, but filippo.io/edwards25519 expects little endian, so we convert here
	reverseSlice(privateKeyBytes)

	privateKey, err := edwards25519.NewScalar().SetCanonicalBytes(privateKeyBytes)
	if err != nil {
		log.Fatal(err)
	}

	// Public key is g^{privateKey} where g is the Ed25519 base point.
	publicKey := edwards25519.NewGeneratorPoint().ScalarBaseMult(privateKey)
	publicKeyBytes := publicKey.Bytes()

	message := []byte("hello world")

	signature, err := sign(publicKeyBytes, privateKeyBytes, message)
	if err != nil {
		log.Fatal(err)
	}

	// Verify works as usual; we can use the Golang standard lib method
	verified := ed25519.Verify(publicKeyBytes, message, signature)

	fmt.Println("private key..:", hex.EncodeToString(privateKeyBytes))
	fmt.Println("public key...:", hex.EncodeToString(publicKeyBytes))
	fmt.Println("message......:", hex.EncodeToString(message))
	fmt.Println("signature....:", hex.EncodeToString(signature))
	fmt.Println("verified.....:", verified)
}

// This is how we sign in the TSM (except that in the TSM the private key is secret shared throughout).
// It differs from the Golang standard lib ed25519.Sign() method (RFC-8032) in that we interpret the priavte key
// as a raw point, whereas RFC-8032 interprets it as a seed from which it derives the private key.
func sign(publicKey, privateKey, message []byte) (signature []byte, err error) {
	signature = make([]byte, 64)

	// The Golang ed25519.Sign() follows RFC80432 like this:
	// seed, publicKey := privateKey[:SeedSize], privateKey[SeedSize:]
	// h := sha512.Sum512(seed)
	// s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32])
	// prefix := h[32:]

	// We instead use the private key directly
	s, err := edwards25519.NewScalar().SetCanonicalBytes(privateKey)
	if err != nil {
		return nil, err
	}

	mh := sha512.New()

	// In the Golang std lib (RFC8032) the prefix is written here, but we use the actual public key
	// mh.Write(prefix)
	mh.Write(publicKey)

	mh.Write(message)
	messageDigest := make([]byte, 0, sha512.Size)
	messageDigest = mh.Sum(messageDigest)
	r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest)
	if err != nil {
		panic("ed25519: internal error: setting scalar failed")
	}

	R := (&edwards25519.Point{}).ScalarBaseMult(r)

	kh := sha512.New()
	kh.Write(R.Bytes())
	kh.Write(publicKey)
	kh.Write(message)
	hramDigest := make([]byte, 0, sha512.Size)
	hramDigest = kh.Sum(hramDigest)
	k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest)
	if err != nil {
		panic("ed25519: internal error: setting scalar failed")
	}

	S := edwards25519.NewScalar().MultiplyAdd(k, s, r)

	copy(signature[:32], R.Bytes())
	copy(signature[32:], S.Bytes())

	return signature, nil
}

func reverseSlice(b []byte) {
	l := len(b)
	for i := 0; i < l/2; i++ {
		tt := b[i]
		b[i] = b[l-1-i]
		b[l-1-i] = tt
	}
}