add support for the Multikey verification method

This commit is contained in:
Michael Muré
2025-06-24 18:10:36 +02:00
parent 4503b2e4b9
commit 875c07db66
17 changed files with 214 additions and 69 deletions

View File

@@ -1,4 +1,4 @@
package helpers
package testsuite
import (
"fmt"

View File

@@ -3,10 +3,10 @@ package ed25519
import (
"testing"
"github.com/INFURA/go-did/crypto/internal"
"github.com/INFURA/go-did/crypto/_testsuite"
)
var harness = helpers.TestHarness[PublicKey, PrivateKey]{
var harness = testsuite.TestHarness[PublicKey, PrivateKey]{
Name: "ed25519",
GenerateKeyPair: GenerateKeyPair,
PublicKeyFromBytes: PublicKeyFromBytes,
@@ -23,9 +23,9 @@ var harness = helpers.TestHarness[PublicKey, PrivateKey]{
}
func TestSuite(t *testing.T) {
helpers.TestSuite(t, harness)
testsuite.TestSuite(t, harness)
}
func BenchmarkSuite(b *testing.B) {
helpers.BenchSuite(b, harness)
testsuite.BenchSuite(b, harness)
}

View File

@@ -10,7 +10,7 @@ import (
"golang.org/x/crypto/cryptobyte"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/internal"
"github.com/INFURA/go-did/crypto/_helpers"
)
var _ crypto.SigningPublicKey = &PublicKey{}

View File

@@ -3,10 +3,10 @@ package p256
import (
"testing"
"github.com/INFURA/go-did/crypto/internal"
"github.com/INFURA/go-did/crypto/_testsuite"
)
var harness = helpers.TestHarness[*PublicKey, *PrivateKey]{
var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
Name: "p256",
GenerateKeyPair: GenerateKeyPair,
PublicKeyFromBytes: PublicKeyFromBytes,
@@ -23,9 +23,9 @@ var harness = helpers.TestHarness[*PublicKey, *PrivateKey]{
}
func TestSuite(t *testing.T) {
helpers.TestSuite(t, harness)
testsuite.TestSuite(t, harness)
}
func BenchmarkSuite(b *testing.B) {
helpers.BenchSuite(b, harness)
testsuite.BenchSuite(b, harness)
}

View File

@@ -10,7 +10,7 @@ import (
"math/big"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/internal"
helpers "github.com/INFURA/go-did/crypto/_helpers"
)
var _ crypto.SigningPublicKey = (*PublicKey)(nil)

View File

@@ -5,11 +5,11 @@ import (
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/crypto/_testsuite"
"github.com/INFURA/go-did/crypto/ed25519"
"github.com/INFURA/go-did/crypto/internal"
)
var harness = helpers.TestHarness[*PublicKey, *PrivateKey]{
var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
Name: "x25519",
GenerateKeyPair: GenerateKeyPair,
PublicKeyFromBytes: PublicKeyFromBytes,
@@ -26,11 +26,11 @@ var harness = helpers.TestHarness[*PublicKey, *PrivateKey]{
}
func TestSuite(t *testing.T) {
helpers.TestSuite(t, harness)
testsuite.TestSuite(t, harness)
}
func BenchmarkSuite(b *testing.B) {
helpers.BenchSuite(b, harness)
testsuite.BenchSuite(b, harness)
}
func TestEd25519ToX25519(t *testing.T) {

View File

@@ -8,8 +8,8 @@ import (
"math/big"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/_helpers"
"github.com/INFURA/go-did/crypto/ed25519"
helpers "github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.PublicKey = (*PublicKey)(nil)

View File

@@ -102,7 +102,7 @@ type VerificationMethodSignature interface {
VerificationMethod
// Verify checks that 'sig' is a valid signature of 'data'.
Verify(data []byte, sig []byte) bool
Verify(data []byte, sig []byte) (bool, error)
}
// VerificationMethodKeyAgreement is a VerificationMethod implementing a shared key agreement.

View File

@@ -4,15 +4,14 @@ import (
"fmt"
"strings"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_helpers"
"github.com/INFURA/go-did/crypto/ed25519"
"github.com/INFURA/go-did/crypto/p256"
"github.com/INFURA/go-did/crypto/x25519"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/multikey"
"github.com/INFURA/go-did/verifications/x25519"
)
@@ -34,66 +33,54 @@ func Decode(identifier string) (did.DID, error) {
const keyPrefix = "did:key:"
if !strings.HasPrefix(identifier, keyPrefix) {
return nil, fmt.Errorf("must start with 'did:key'")
return nil, fmt.Errorf("%w: must start with 'did:key'", did.ErrInvalidDid)
}
msi := identifier[len(keyPrefix):]
baseCodec, bytes, err := mbase.Decode(msi)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return nil, fmt.Errorf("%w: not Base58BTC encoded", did.ErrInvalidDid)
}
code, read, err := varint.FromUvarint(bytes)
code, bytes, err := helpers.PublicKeyMultibaseDecode(msi)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
switch code {
case ed25519.MultibaseCode:
pub, err := ed25519.PublicKeyFromBytes(bytes[read:])
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return FromPublicKey(pub)
case p256.MultibaseCode:
pub, err := p256.PublicKeyFromBytes(bytes[read:])
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return FromPublicKey(pub)
// case Secp256k1: // TODO
// case RSA: // TODO
decoder, ok := map[uint64]func(b []byte) (crypto.PublicKey, error){
ed25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return ed25519.PublicKeyFromBytes(b) },
p256.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p256.PublicKeyFromBytes(b) },
x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) },
}[code]
if !ok {
return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code)
}
return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code)
pub, err := decoder(bytes)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
d, err := FromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return d, nil
}
func FromPublicKey(pub crypto.PublicKey) (did.DID, error) {
var err error
switch pub := pub.(type) {
case ed25519.PublicKey:
d := DidKey{msi: pub.ToPublicKeyMultibase()}
d.signature, err = ed25519vm.NewVerificationKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d)
d.signature = ed25519vm.NewVerificationKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d)
xpub, err := x25519.PublicKeyFromEd25519(pub)
if err != nil {
return nil, err
}
xpub, err := x25519.PublicKeyFromEd25519(pub)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
xmsi := xpub.ToPublicKeyMultibase()
d.keyAgreement, err = x25519vm.NewKeyAgreementKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, xmsi), xpub, d)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
d.keyAgreement = x25519vm.NewKeyAgreementKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, xmsi), xpub, d)
return d, nil
case *p256.PublicKey:
d := DidKey{msi: pub.ToPublicKeyMultibase()}
mk := multikey.NewMultiKey(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d)
d.signature = mk
d.keyAgreement = mk
return d, nil
// case *p256.PublicKey:
// d := DidKey{msi: pub.ToPublicKeyMultibase()}
default:
return nil, fmt.Errorf("unsupported public key: %T", pub)

View File

@@ -11,7 +11,7 @@ import (
// If no method verifies the signature, it returns false and nil.
func TryAllVerify(methods []VerificationMethodSignature, data []byte, sig []byte) (bool, VerificationMethodSignature) {
for _, method := range methods {
if method.Verify(data, sig) {
if valid, err := method.Verify(data, sig); err == nil && valid {
return true, method
}
}

View File

@@ -24,12 +24,12 @@ type VerificationKey2020 struct {
controller string
}
func NewVerificationKey2020(id string, pubkey ed25519.PublicKey, controller did.DID) (*VerificationKey2020, error) {
func NewVerificationKey2020(id string, pubkey ed25519.PublicKey, controller did.DID) *VerificationKey2020 {
return &VerificationKey2020{
id: id,
pubkey: pubkey,
controller: controller.String(),
}, nil
}
}
func (v VerificationKey2020) MarshalJSON() ([]byte, error) {
@@ -91,6 +91,6 @@ func (v VerificationKey2020) JsonLdContext() string {
return JsonLdContext
}
func (v VerificationKey2020) Verify(data []byte, sig []byte) bool {
return v.pubkey.VerifyBytes(data, sig)
func (v VerificationKey2020) Verify(data []byte, sig []byte) (bool, error) {
return v.pubkey.VerifyBytes(data, sig), nil
}

View File

@@ -40,8 +40,7 @@ func TestSignature(t *testing.T) {
contDid := "did:key:" + pk.ToPublicKeyMultibase()
controller := did.MustParse(contDid)
vk, err := ed25519vm.NewVerificationKey2020("foo", pk, controller)
require.NoError(t, err)
vk := ed25519vm.NewVerificationKey2020("foo", pk, controller)
for _, tc := range []struct {
name string
@@ -78,7 +77,9 @@ func TestSignature(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.valid, vk.Verify(tc.data, tc.signature))
valid, err := vk.Verify(tc.data, tc.signature)
require.NoError(t, err)
require.Equal(t, tc.valid, valid)
})
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/multikey"
"github.com/INFURA/go-did/verifications/x25519"
)
@@ -21,6 +22,8 @@ func UnmarshalJSON(data []byte) (did.VerificationMethod, error) {
switch aux.Type {
case ed25519vm.Type:
res = &ed25519vm.VerificationKey2020{}
case multikey.Type:
res = &multikey.MultiKey{}
case x25519vm.Type:
res = &x25519vm.KeyAgreementKey2020{}
default:

View File

@@ -0,0 +1,126 @@
package multikey
import (
"encoding/json"
"errors"
"fmt"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/_helpers"
"github.com/INFURA/go-did/crypto/ed25519"
"github.com/INFURA/go-did/crypto/p256"
"github.com/INFURA/go-did/crypto/x25519"
)
// Specification: https://www.w3.org/TR/cid-1.0/#Multikey
const (
JsonLdContext = "https://www.w3.org/ns/cid/v1"
Type = "Multikey"
)
var _ did.VerificationMethodSignature = &MultiKey{}
var _ did.VerificationMethodKeyAgreement = &MultiKey{}
type MultiKey struct {
id string
pubkey crypto.PublicKey
controller string
}
func NewMultiKey(id string, pubkey crypto.PublicKey, controller did.DID) *MultiKey {
return &MultiKey{
id: id,
pubkey: pubkey,
controller: controller.String(),
}
}
func (m MultiKey) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}{
ID: m.ID(),
Type: m.Type(),
Controller: m.Controller(),
PublicKeyMultibase: m.pubkey.ToPublicKeyMultibase(),
})
}
func (m *MultiKey) UnmarshalJSON(bytes []byte) error {
aux := struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}{}
err := json.Unmarshal(bytes, &aux)
if err != nil {
return err
}
if aux.Type != m.Type() {
return errors.New("invalid type")
}
m.id = aux.ID
if len(m.id) == 0 {
return errors.New("invalid id")
}
code, pubBytes, err := helpers.PublicKeyMultibaseDecode(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
decoder, ok := map[uint64]func(b []byte) (crypto.PublicKey, error){
ed25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return ed25519.PublicKeyFromBytes(b) },
p256.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p256.PublicKeyFromBytes(b) },
x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) },
}[code]
if !ok {
return fmt.Errorf("unsupported publicKeyMultibase code: %d", code)
}
m.pubkey, err = decoder(pubBytes)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
m.controller = aux.Controller
if !did.HasValidDIDSyntax(m.controller) {
return errors.New("invalid controller")
}
return nil
}
func (m MultiKey) ID() string {
return m.id
}
func (m MultiKey) Type() string {
return Type
}
func (m MultiKey) Controller() string {
return m.controller
}
func (m MultiKey) JsonLdContext() string {
return JsonLdContext
}
func (m MultiKey) Verify(data []byte, sig []byte) (bool, error) {
if pub, ok := m.pubkey.(crypto.SigningPublicKey); ok {
return pub.VerifyBytes(data, sig), nil
}
return false, errors.New("not a signing public key")
}
func (m MultiKey) PrivateKeyIsCompatible(local crypto.KeyExchangePrivateKey) bool {
return local.PublicKeyIsCompatible(m.pubkey)
}
func (m MultiKey) KeyExchange(local crypto.KeyExchangePrivateKey) ([]byte, error) {
return local.KeyExchange(m.pubkey)
}

View File

@@ -0,0 +1,28 @@
package multikey_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
_ "github.com/INFURA/go-did/methods/did-key"
"github.com/INFURA/go-did/verifications/multikey"
)
func TestJsonRoundTrip(t *testing.T) {
data := `{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"type": "Multikey",
"controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}`
var mk multikey.MultiKey
err := json.Unmarshal([]byte(data), &mk)
require.NoError(t, err)
bytes, err := json.Marshal(mk)
require.NoError(t, err)
require.JSONEq(t, data, string(bytes))
}

View File

@@ -25,12 +25,12 @@ type KeyAgreementKey2020 struct {
controller string
}
func NewKeyAgreementKey2020(id string, pubkey *x25519.PublicKey, controller did.DID) (*KeyAgreementKey2020, error) {
func NewKeyAgreementKey2020(id string, pubkey *x25519.PublicKey, controller did.DID) *KeyAgreementKey2020 {
return &KeyAgreementKey2020{
id: id,
pubkey: pubkey,
controller: controller.String(),
}, nil
}
}
func (k KeyAgreementKey2020) MarshalJSON() ([]byte, error) {