ed25519/x25519: simplify key implementation, add tests
This commit is contained in:
@@ -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 {
|
||||
|
||||
27
verifications/ed25519/key_test.go
Normal file
27
verifications/ed25519/key_test.go
Normal 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)
|
||||
}
|
||||
35
verifications/internal/multibase.go
Normal file
35
verifications/internal/multibase.go
Normal 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
|
||||
}
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user