Merge pull request #1 from INFURA/x25519
fix ed25519 --> x25519 conversion and usage in did:key
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user