fix ed25519 --> x25519 conversion and usage in did:key

This commit is contained in:
Michael Muré
2025-05-06 13:20:18 +02:00
parent eca71e594d
commit 0718ee1ac2
7 changed files with 127 additions and 54 deletions

View File

@@ -16,6 +16,10 @@ type document struct {
}
func (d document) MarshalJSON() ([]byte, error) {
// It's unclear where the KeyAgreement should be.
// Maybe it doesn't matter, but the spec contradict itself.
// See https://github.com/w3c-ccg/did-key-spec/issues/71
return json.Marshal(struct {
Context []string `json:"@context"`
ID string `json:"id"`
@@ -35,7 +39,7 @@ func (d document) MarshalJSON() ([]byte, error) {
),
ID: d.id.String(),
AlsoKnownAs: nil,
VerificationMethod: []did.VerificationMethod{d.signature, d.keyAgreement},
VerificationMethod: []did.VerificationMethod{d.signature},
Authentication: []string{d.signature.ID()},
AssertionMethod: []string{d.signature.ID()},
KeyAgreement: []did.VerificationMethod{d.keyAgreement},

View File

@@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/verifications/ed25519"
)
func TestDocument(t *testing.T) {
@@ -61,19 +60,5 @@ func TestDocument(t *testing.T) {
require.JSONEq(t, expected, string(bytes))
}
func TestJsonRoundTrip(t *testing.T) {
data := `{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"type": "Ed25519VerificationKey2020",
"controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}`
var vm ed25519.VerificationKey2020
err := json.Unmarshal([]byte(data), &vm)
require.NoError(t, err)
bytes, err := json.Marshal(vm)
require.NoError(t, err)
require.JSONEq(t, data, string(bytes))
}
// TODO: test vectors:
// https://github.com/w3c-ccg/did-key-spec/tree/main/test-vectors

View File

@@ -62,7 +62,8 @@ func Decode(identifier string) (did.DID, error) {
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
d.keyAgreement, err = x25519.NewKeyAgreementKey2020("TODO", xpub, d)
xmsi := x25519.PublicKeyToMultibase(xpub)
d.keyAgreement, err = x25519.NewKeyAgreementKey2020(fmt.Sprintf("did:key:%s#%s", msi, xmsi), xpub, d)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}

View File

@@ -25,7 +25,7 @@ type VerificationKey2020 struct {
}
func NewVerificationKey2020(id string, pubkey PublicKey, controller did.DID) (*VerificationKey2020, error) {
if len(pubkey) != ed25519.PublicKeySize {
if len(pubkey) != PublicKeySize {
return nil, errors.New("invalid ed25519 public key size")
}
@@ -68,7 +68,7 @@ func (v *VerificationKey2020) UnmarshalJSON(bytes []byte) error {
if len(v.id) == 0 {
return errors.New("invalid id")
}
v.pubkey, err = MultibaseToPublicKey(aux.PublicKeyMultibase)
v.pubkey, err = PublicKeyFromMultibase(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}

View File

@@ -1,11 +1,13 @@
package ed25519_test
import (
"encoding/hex"
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
_ "github.com/INFURA/go-did/methods/did-key"
"github.com/INFURA/go-did/verifications/ed25519"
)
@@ -18,27 +20,71 @@ func TestJsonRoundTrip(t *testing.T) {
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}`
var vm ed25519.VerificationKey2020
err := json.Unmarshal([]byte(data), &vm)
var vk ed25519.VerificationKey2020
err := json.Unmarshal([]byte(data), &vk)
require.NoError(t, err)
bytes, err := json.Marshal(vm)
bytes, err := json.Marshal(vk)
require.NoError(t, err)
require.JSONEq(t, data, string(bytes))
}
// func TestSignature(t *testing.T) {
// d, err := didkey.Decode("did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2")
// require.NoError(t, err)
// doc, err := d.Document()
// require.NoError(t, err)
// method := doc.Authentication()[0]
// require.IsType(t, &ed25519.VerificationKey2020{}, method)
//
// require.True(t, method.Verify(
// []byte("node key test"),
// []byte("Tuhz8eG2jqYG4jUbxt14iMd3r2v2eNLftPTfrZfaaFYn5ta7wP3oYfC1rnDVJsLvHAK7j5CmVoXtGoYGL7Lnb5e"),
// ))
//
// // ed25519.NewVerificationKey2020(did, )
// }
func TestSignature(t *testing.T) {
// test vector from https://datatracker.ietf.org/doc/html/rfc8032#section-7.1
pkHex := "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"
pkBytes := must(hex.DecodeString(pkHex))
pk, err := ed25519.PublicKeyFromBytes(pkBytes)
require.NoError(t, err)
contDid := "did:key:" + ed25519.PublicKeyToMultibase(pk)
controller := did.MustParse(contDid)
vk, err := ed25519.NewVerificationKey2020("foo", pk, controller)
require.NoError(t, err)
for _, tc := range []struct {
name string
data []byte
signature []byte
valid bool
}{
{
name: "valid",
data: must(hex.DecodeString("af82")),
signature: must(hex.DecodeString(
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac" +
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
)),
valid: true,
},
{
name: "data changed",
data: must(hex.DecodeString("af8211")),
signature: must(hex.DecodeString(
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac" +
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
)),
valid: false,
},
{
name: "signature changed",
data: must(hex.DecodeString("af82")),
signature: must(hex.DecodeString(
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac" +
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a11",
)),
valid: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.valid, vk.Verify(tc.data, tc.signature))
})
}
}
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}

View File

@@ -12,19 +12,21 @@ import (
type PublicKey = ed25519.PublicKey
type PrivateKey = ed25519.PrivateKey
const PublicKeySize = ed25519.PublicKeySize
func GenerateKeyPair() (PublicKey, PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}
// 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
func PublicKeyFromBytes(b []byte) (PublicKey, error) {
if len(b) != PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
return ed25519.PublicKey(b), nil
}
// MultibaseToPublicKey decodes the public key from its publicKeyMultibase form
func MultibaseToPublicKey(multibase string) (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
@@ -43,8 +45,15 @@ func MultibaseToPublicKey(multibase string) (PublicKey, error) {
if read != 2 {
return nil, fmt.Errorf("unexpected multibase")
}
if len(bytes)-read != ed25519.PublicKeySize {
if len(bytes)-read != PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
return bytes[read:], 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
}

View File

@@ -1,5 +1,7 @@
package x25519
// TODO: use ecdh.PublicKey instead of defining a custom type below?
// type PublicKey ecdh.PublicKey
//
// func (p PublicKey) Equal(x crypto.PublicKey) bool {
@@ -167,7 +169,23 @@ func (priv PrivateKey) Equal(x crypto.PrivateKey) bool {
}
func PublicKeyFromEd25519(pub ed25519.PublicKey) (PublicKey, error) {
y := new(big.Int).SetBytes(pub)
// 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
if len(pub) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
// Make a copy and clear the sign bit (MSB of last byte)
// This is because ed25519 serialize as bytes with 255 bit for Y, and one bit for the sign.
// We only want Y, and the sign is irrelevant for the conversion.
pubCopy := make([]byte, ed25519.PublicKeySize)
copy(pubCopy, pub)
pubCopy[ed25519.PublicKeySize-1] &= 0x7F
// ed25519 are little-endian, but big.Int expect big-endian
// See https://www.rfc-editor.org/rfc/rfc8032
y := new(big.Int).SetBytes(reverseBytes(pubCopy))
one := big.NewInt(1)
negOne := big.NewInt(-1)
@@ -190,16 +208,26 @@ func PublicKeyFromEd25519(pub ed25519.PublicKey) (PublicKey, error) {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xed,
})
// u := new(big.Int).Mul(
// new(big.Int).Add(one, y),
// new(big.Int).ModInverse(new(big.Int).Sub(one, y), p),
// )
onePlusY := new(big.Int).Add(one, y)
oneMinusY := new(big.Int).Sub(one, y)
oneMinusYInv := new(big.Int).ModInverse(oneMinusY, p)
u := new(big.Int).Mul(onePlusY, oneMinusYInv)
u.Mod(u, p)
return u.Bytes(), nil
// make sure we get 32 bytes, pad if necessary
uBytes := u.Bytes()
res := make([]byte, PublicKeySize)
copy(res[PublicKeySize-len(uBytes):], uBytes)
// x25519 are little-endian, but big.Int give us big-endian.
// See https://www.ietf.org/rfc/rfc7748.txt
return reverseBytes(res), nil
}
func reverseBytes(b []byte) []byte {
r := make([]byte, len(b))
for i := 0; i < len(b); i++ {
r[i] = b[len(b)-1-i]
}
return r
}