diff --git a/methods/did-key/document.go b/methods/did-key/document.go index 2f2bf6e..b86b6e4 100644 --- a/methods/did-key/document.go +++ b/methods/did-key/document.go @@ -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}, diff --git a/methods/did-key/document_test.go b/methods/did-key/document_test.go index ee7172d..b83c1b7 100644 --- a/methods/did-key/document_test.go +++ b/methods/did-key/document_test.go @@ -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 diff --git a/methods/did-key/key.go b/methods/did-key/key.go index c6bc053..ddf5744 100644 --- a/methods/did-key/key.go +++ b/methods/did-key/key.go @@ -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) } diff --git a/verifications/ed25519/VerificationKey2020.go b/verifications/ed25519/VerificationKey2020.go index 39815a4..7e5c935 100644 --- a/verifications/ed25519/VerificationKey2020.go +++ b/verifications/ed25519/VerificationKey2020.go @@ -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) } diff --git a/verifications/ed25519/VerificationKey2020_test.go b/verifications/ed25519/VerificationKey2020_test.go index 7c15e97..05b3108 100644 --- a/verifications/ed25519/VerificationKey2020_test.go +++ b/verifications/ed25519/VerificationKey2020_test.go @@ -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 +} diff --git a/verifications/ed25519/key.go b/verifications/ed25519/key.go index e02ecd8..189a9c0 100644 --- a/verifications/ed25519/key.go +++ b/verifications/ed25519/key.go @@ -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 +} diff --git a/verifications/x25519/key.go b/verifications/x25519/key.go index b70b8fc..c9bf602 100644 --- a/verifications/x25519/key.go +++ b/verifications/x25519/key.go @@ -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 }