ed25519/x25519: simplify key implementation, add tests

This commit is contained in:
Michael Muré
2025-06-18 12:11:52 +02:00
parent 47fa1645c6
commit 807dd553df
5 changed files with 170 additions and 94 deletions

View File

@@ -5,8 +5,7 @@ import (
"crypto/rand"
"fmt"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/INFURA/go-did/verifications/internal"
)
type PublicKey = ed25519.PublicKey
@@ -25,7 +24,7 @@ func GenerateKeyPair() (PublicKey, PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}
// PublicKeyFromBytes convert a serialized public key to a PublicKey.
// PublicKeyFromBytes converts a serialized public key to a PublicKey.
// It errors if the slice is not the right size.
func PublicKeyFromBytes(b []byte) (PublicKey, error) {
if len(b) != PublicKeySize {
@@ -36,38 +35,25 @@ func PublicKeyFromBytes(b []byte) (PublicKey, error) {
// PublicKeyFromMultibase decodes the public key from its Multibase form
func PublicKeyFromMultibase(multibase string) (PublicKey, error) {
baseCodec, bytes, err := mbase.Decode(multibase)
if err != nil {
return nil, err
}
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return nil, fmt.Errorf("not Base58BTC encoded")
}
code, read, err := varint.FromUvarint(bytes)
code, bytes, err := helpers.MultibaseDecode(multibase)
if err != nil {
return nil, err
}
if code != MultibaseCode {
return nil, fmt.Errorf("invalid code")
}
if read != 2 {
return nil, fmt.Errorf("unexpected multibase")
}
if len(bytes)-read != PublicKeySize {
if len(bytes) != PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
return bytes[read:], nil
return bytes, nil
}
// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase
func PublicKeyToMultibase(pub PublicKey) string {
// can only fail with an invalid encoding, but it's hardcoded
bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub...))
return bytes
return helpers.MultibaseEncode(MultibaseCode, pub)
}
// PrivateKeyFromBytes convert a serialized public key to a PrivateKey.
// PrivateKeyFromBytes converts a serialized public key to a PrivateKey.
// It errors if the slice is not the right size.
func PrivateKeyFromBytes(b []byte) (PrivateKey, error) {
if len(b) != ed25519.PrivateKeySize {

View File

@@ -0,0 +1,27 @@
package ed25519_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/verifications/ed25519"
)
func TestGenerateKey(t *testing.T) {
pub, priv, err := ed25519.GenerateKeyPair()
require.NoError(t, err)
require.NotNil(t, pub)
require.NotNil(t, priv)
require.True(t, pub.Equal(priv.Public()))
}
func TestMultibaseRoundTrip(t *testing.T) {
pub, _, err := ed25519.GenerateKeyPair()
require.NoError(t, err)
mb := ed25519.PublicKeyToMultibase(pub)
rt, err := ed25519.PublicKeyFromMultibase(mb)
require.NoError(t, err)
require.Equal(t, pub, rt)
}

View File

@@ -0,0 +1,35 @@
package helpers
import (
"fmt"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
)
// MultibaseDecode is a helper for decoding multibase public keys.
func MultibaseDecode(multibase string) (uint64, []byte, error) {
baseCodec, bytes, err := mbase.Decode(multibase)
if err != nil {
return 0, nil, err
}
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return 0, nil, fmt.Errorf("not Base58BTC encoded")
}
code, read, err := varint.FromUvarint(bytes)
if err != nil {
return 0, nil, err
}
if read != 2 {
return 0, nil, fmt.Errorf("unexpected multibase")
}
return code, bytes[read:], nil
}
// MultibaseEncode is a helper for encoding multibase public keys.
func MultibaseEncode(code uint64, bytes []byte) string {
// can only fail with an invalid encoding, but it's hardcoded
res, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(code), bytes...))
return res
}

View File

@@ -3,13 +3,12 @@ package x25519
import (
"crypto/ecdh"
"crypto/rand"
"crypto/sha512"
"fmt"
"math/big"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/INFURA/go-did/verifications/ed25519"
helpers "github.com/INFURA/go-did/verifications/internal"
)
type PublicKey = *ecdh.PublicKey
@@ -32,12 +31,16 @@ func GenerateKeyPair() (PublicKey, PrivateKey, error) {
return priv.Public().(PublicKey), priv, nil
}
// PublicKeyFromBytes convert a serialized public key to a PublicKey.
// PublicKeyFromBytes converts a serialized public key to a PublicKey.
// It errors if the slice is not the right size.
func PublicKeyFromBytes(b []byte) (PublicKey, error) {
return ecdh.X25519().NewPublicKey(b)
}
// PublicKeyFromEd25519 converts an ed25519 public key to a x25519 public key.
// It errors if the slice is not the right size.
//
// This function is based on the algorithm described in https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm#name-curve25519
func PublicKeyFromEd25519(pub ed25519.PublicKey) (PublicKey, error) {
// Conversion formula is u = (1 + y) / (1 - y) (mod p)
// See https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm#name-ecdh-with-montgomery-coordi
@@ -53,7 +56,7 @@ func PublicKeyFromEd25519(pub ed25519.PublicKey) (PublicKey, error) {
copy(pubCopy, pub)
pubCopy[ed25519.PublicKeySize-1] &= 0x7F
// ed25519 are little-endian, but big.Int expect big-endian
// ed25519 are little-endian, but big.Int expects big-endian
// See https://www.rfc-editor.org/rfc/rfc8032
y := new(big.Int).SetBytes(reverseBytes(pubCopy))
one := big.NewInt(1)
@@ -89,47 +92,56 @@ func PublicKeyFromEd25519(pub ed25519.PublicKey) (PublicKey, error) {
res := make([]byte, PublicKeySize)
copy(res[PublicKeySize-len(uBytes):], uBytes)
// x25519 are little-endian, but big.Int give us big-endian.
// x25519 are little-endian, but big.Int gives us big-endian.
// See https://www.ietf.org/rfc/rfc7748.txt
return ecdh.X25519().NewPublicKey(reverseBytes(res))
}
// PublicKeyFromMultibase decodes the public key from its Multibase form
func PublicKeyFromMultibase(multibase string) (PublicKey, error) {
baseCodec, bytes, err := mbase.Decode(multibase)
if err != nil {
return nil, err
}
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return nil, fmt.Errorf("not Base58BTC encoded")
}
code, read, err := varint.FromUvarint(bytes)
code, bytes, err := helpers.MultibaseDecode(multibase)
if err != nil {
return nil, err
}
if code != MultibaseCode {
return nil, fmt.Errorf("invalid code")
}
if read != 2 {
return nil, fmt.Errorf("unexpected multibase")
}
return ecdh.X25519().NewPublicKey(bytes[read:])
return ecdh.X25519().NewPublicKey(bytes)
}
// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase
func PublicKeyToMultibase(pub PublicKey) string {
// can only fail with an invalid encoding, but it's hardcoded
bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub.Bytes()...))
return bytes
return helpers.MultibaseEncode(MultibaseCode, pub.Bytes())
}
// PrivateKeyFromBytes convert a serialized public key to a PrivateKey.
// PrivateKeyFromBytes converts a serialized public key to a PrivateKey.
// It errors if len(privateKey) is not [PrivateKeySize].
func PrivateKeyFromBytes(b []byte) (PrivateKey, error) {
return ecdh.X25519().NewPrivateKey(b)
}
// PrivateKeyFromEd25519 converts an ed25519 private key to a x25519 private key.
// It errors if the slice is not the right size.
//
// This function is based on the algorithm described in https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm#name-curve25519
func PrivateKeyFromEd25519(priv ed25519.PrivateKey) (PrivateKey, error) {
if len(priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid ed25519 private key size")
}
// get the 32-byte seed (first half of the private key)
seed := priv.Seed()
h := sha512.Sum512(seed)
// clamp as per the X25519 spec
h[0] &= 248
h[31] &= 127
h[31] |= 64
return ecdh.X25519().NewPrivateKey(h[:32])
}
func reverseBytes(b []byte) []byte {
r := make([]byte, len(b))
for i := 0; i < len(b); i++ {

View File

@@ -1,52 +1,68 @@
package x25519_test
//
// import (
// "encoding/hex"
// "testing"
//
// "github.com/stretchr/testify/require"
//
// "github.com/INFURA/go-did/verifications/x25519"
// )
//
// func TestGenerateKey(t *testing.T) {
// t.Run("x25519.GenerateKey()", func(t *testing.T) {
// _, _, err := x25519.GenerateKey()
// require.NoError(t, err, `x25519.GenerateKey should work`)
// })
// t.Run("x25519.NewKeyFromSeed(wrongSeedLength)", func(t *testing.T) {
// dummy := make([]byte, x25519.SeedSize-1)
// _, err := x25519.NewKeyFromSeed(dummy)
// require.Error(t, err, `wrong seed size should result in error`)
// })
// }
//
// func TestNewKeyFromSeed(t *testing.T) {
// // These test vectors are from RFC7748 Section 6.1
// const alicePrivHex = `77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a`
// const alicePubHex = `8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a`
// const bobPrivHex = `5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb`
// const bobPubHex = `de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f`
//
// alicePrivSeed, err := hex.DecodeString(alicePrivHex)
// require.NoError(t, err, `alice seed decoded`)
// alicePriv, err := x25519.NewKeyFromSeed(alicePrivSeed)
// require.NoError(t, err, `alice private key`)
//
// alicePub := alicePriv.Public().(x25519.PublicKey)
// require.Equal(t, hex.EncodeToString(alicePub), alicePubHex, `alice public key`)
//
// bobPrivSeed, err := hex.DecodeString(bobPrivHex)
// require.NoError(t, err, `bob seed decoded`)
// bobPriv, err := x25519.NewKeyFromSeed(bobPrivSeed)
// require.NoError(t, err, `bob private key`)
//
// bobPub := bobPriv.Public().(x25519.PublicKey)
// require.Equal(t, hex.EncodeToString(bobPub), bobPubHex, `bob public key`)
//
// require.True(t, bobPriv.Equal(bobPriv), `bobPriv should equal bobPriv`)
// require.True(t, bobPub.Equal(bobPub), `bobPub should equal bobPub`)
// require.False(t, bobPriv.Equal(bobPub), `bobPriv should NOT equal bobPub`)
// require.False(t, bobPub.Equal(bobPriv), `bobPub should NOT equal bobPriv`)
// }
import (
"crypto/ecdh"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/x25519"
)
func TestGenerateKey(t *testing.T) {
pub, priv, err := x25519.GenerateKeyPair()
require.NoError(t, err)
require.NotNil(t, pub)
require.NotNil(t, priv)
require.Equal(t, ecdh.X25519(), pub.Curve())
require.Equal(t, ecdh.X25519(), priv.Curve())
require.True(t, pub.Equal(priv.Public()))
}
func TestMultibaseRoundTrip(t *testing.T) {
pub, _, err := x25519.GenerateKeyPair()
require.NoError(t, err)
mb := x25519.PublicKeyToMultibase(pub)
rt, err := x25519.PublicKeyFromMultibase(mb)
require.NoError(t, err)
require.Equal(t, pub, rt)
}
func TestEd25519ToX25519(t *testing.T) {
// Known pubkey ed25519 --> x25519
for _, tc := range []struct {
pubEdMultibase string
pubXMultibase string
}{
{
// From https://w3c-ccg.github.io/did-key-spec/#ed25519-with-x25519
pubEdMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
pubXMultibase: "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p",
},
} {
t.Run(tc.pubEdMultibase, func(t *testing.T) {
pubEd, err := ed25519.PublicKeyFromMultibase(tc.pubEdMultibase)
require.NoError(t, err)
pubX, err := x25519.PublicKeyFromEd25519(pubEd)
require.NoError(t, err)
require.Equal(t, tc.pubXMultibase, x25519.PublicKeyToMultibase(pubX))
})
}
// Check that ed25519 --> x25519 match for pubkeys and privkeys
t.Run("ed25519 --> x25519 priv+pub are matching", func(t *testing.T) {
for i := 0; i < 10; i++ {
pubEd, privEd, err := ed25519.GenerateKeyPair()
require.NoError(t, err)
pubX, err := x25519.PublicKeyFromEd25519(pubEd)
require.NoError(t, err)
privX, err := x25519.PrivateKeyFromEd25519(privEd)
require.NoError(t, err)
require.True(t, pubX.Equal(privX.PublicKey()))
}
})
}