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
}
}
Updated almost 2 years ago