From 2809127a08c24858433249f51bc391161a1fc36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 25 Jun 2025 10:51:13 +0200 Subject: [PATCH] add support for JsonWebKey2020 --- crypto/p256/public.go | 8 + document/document_test.go | 164 ++++++++++++++++-- verifications/json.go | 3 + verifications/jsonwebkey/JsonWebKey2020.go | 106 +++++++++++ .../jsonwebkey/JsonWebKey2020_test.go | 102 +++++++++++ verifications/jsonwebkey/jwk.go | 120 +++++++++++++ verifications/multikey/multikey.go | 8 +- 7 files changed, 493 insertions(+), 18 deletions(-) create mode 100644 verifications/jsonwebkey/JsonWebKey2020.go create mode 100644 verifications/jsonwebkey/JsonWebKey2020_test.go create mode 100644 verifications/jsonwebkey/jwk.go diff --git a/crypto/p256/public.go b/crypto/p256/public.go index 6dcb8d7..57100e8 100644 --- a/crypto/p256/public.go +++ b/crypto/p256/public.go @@ -31,6 +31,14 @@ func PublicKeyFromBytes(b []byte) (*PublicKey, error) { return (*PublicKey)(&ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}), nil } +// PublicKeyFromXY converts x and y coordinates into a PublicKey. +func PublicKeyFromXY(x, y *big.Int) (*PublicKey, error) { + if !elliptic.P256().IsOnCurve(x, y) { + return nil, fmt.Errorf("invalid P-256 public key") + } + return (*PublicKey)(&ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}), nil +} + // PublicKeyFromPublicKeyMultibase decodes the public key from its Multibase form func PublicKeyFromPublicKeyMultibase(multibase string) (*PublicKey, error) { code, bytes, err := helpers.PublicKeyMultibaseDecode(multibase) diff --git a/document/document_test.go b/document/document_test.go index 4f9dd68..e28364b 100644 --- a/document/document_test.go +++ b/document/document_test.go @@ -8,11 +8,57 @@ import ( _ "github.com/INFURA/go-did/methods/did-key" "github.com/INFURA/go-did/verifications/ed25519" + "github.com/INFURA/go-did/verifications/jsonwebkey" "github.com/INFURA/go-did/verifications/x25519" ) func TestRoundTrip(t *testing.T) { - strDoc := ` + for _, tc := range []struct { + name string + strDoc string + assertion func(t *testing.T, doc *Document) + }{ + { + name: "ed25519", + strDoc: ed25519Doc, + assertion: func(t *testing.T, doc *Document) { + require.Equal(t, "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", doc.ID()) + require.Equal(t, ed25519vm.Type, doc.Authentication()[0].Type()) + require.Equal(t, ed25519vm.Type, doc.Assertion()[0].Type()) + require.Equal(t, x25519vm.Type, doc.KeyAgreement()[0].Type()) + require.Equal(t, ed25519vm.Type, doc.CapabilityInvocation()[0].Type()) + require.Equal(t, ed25519vm.Type, doc.CapabilityDelegation()[0].Type()) + }, + }, + { + name: "jsonWebKey", + strDoc: jsonWebKeyDoc, + assertion: func(t *testing.T, doc *Document) { + require.Equal(t, "did:example:123", doc.ID()) + require.Len(t, doc.VerificationMethods(), 6) + require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A"].Type()) + require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A"].Type()) + require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs"].Type()) + require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw"].Type()) + require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY"].Type()) + require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E"].Type()) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + doc, err := FromJsonBytes([]byte(tc.strDoc)) + require.NoError(t, err) + + tc.assertion(t, doc) + + roundtrip, err := json.Marshal(doc) + require.NoError(t, err) + require.JSONEq(t, tc.strDoc, string(roundtrip)) + }) + } +} + +const ed25519Doc = ` { "@context": [ "https://www.w3.org/ns/did/v1", @@ -46,18 +92,108 @@ func TestRoundTrip(t *testing.T) { }] } ` - doc, err := FromJsonBytes([]byte(strDoc)) - require.NoError(t, err) - // basic testing - require.Equal(t, "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", doc.ID()) - require.Equal(t, ed25519vm.Type, doc.Authentication()[0].Type()) - require.Equal(t, ed25519vm.Type, doc.Assertion()[0].Type()) - require.Equal(t, x25519vm.Type, doc.KeyAgreement()[0].Type()) - require.Equal(t, ed25519vm.Type, doc.CapabilityInvocation()[0].Type()) - require.Equal(t, ed25519vm.Type, doc.CapabilityDelegation()[0].Type()) - - roundtrip, err := json.Marshal(doc) - require.NoError(t, err) - require.JSONEq(t, strDoc, string(roundtrip)) +const jsonWebKeyDoc = ` +{ + "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:example:123", + "verificationMethod": [ + { + "id": "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ" + } + }, + { + "id": "did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "Z4Y3NNOxv0J6tCgqOBFnHnaZhJF6LdulT7z8A-2D5_8", + "y": "i5a2NtJoUKXkLm6q8nOEu9WOkso1Ag6FTUT6k_LMnGk" + } + }, + { + "id": "did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "RSA", + "e": "AQAB", + "n": "omwsC1AqEk6whvxyOltCFWheSQvv1MExu5RLCMT4jVk9khJKv8JeMXWe3bWHatjPskdf2dlaGkW5QjtOnUKL742mvr4tCldKS3ULIaT1hJInMHHxj2gcubO6eEegACQ4QSu9LO0H-LM_L3DsRABB7Qja8HecpyuspW1Tu_DbqxcSnwendamwL52V17eKhlO4uXwv2HFlxufFHM0KmCJujIKyAxjD_m3q__IiHUVHD1tDIEvLPhG9Azsn3j95d-saIgZzPLhQFiKluGvsjrSkYU5pXVWIsV-B2jtLeeLC14XcYxWDUJ0qVopxkBvdlERcNtgF4dvW4X00EHj4vCljFw" + } + }, + { + "id": "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-256", + "x": "38M1FDts7Oea7urmseiugGW7tWc3mLpJh6rKe7xINZ8", + "y": "nDQW6XZ7b_u2Sy9slofYLlG03sOEoug3I0aAPQ0exs4" + } + }, + { + "id": "did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-384", + "x": "GnLl6mDti7a2VUIZP5w6pcRX8q5nvEIgB3Q_5RI2p9F_QVsaAlDN7IG68Jn0dS_F", + "y": "jq4QoAHKiIzezDp88s_cxSPXtuXYFliuCGndgU4Qp8l91xzD1spCmFIzQgVjqvcP" + } + }, + { + "id": "did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-521", + "x": "AVlZG23LyXYwlbjbGPMxZbHmJpDSu-IvpuKigEN2pzgWtSo--Rwd-n78nrWnZzeDc187Ln3qHlw5LRGrX4qgLQ-y", + "y": "ANIbFeRdPHf1WYMCUjcPz-ZhecZFybOqLIJjVOlLETH7uPlyG0gEoMWnIZXhQVypPy_HtUiUzdnSEPAylYhHBTX2" + } + } + ], + "authentication": [ + "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A", + "did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs", + "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY", + "did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E" + ], + "assertionMethod": [ + "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A", + "did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs", + "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY", + "did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E" + ], + "capabilityDelegation": [ + "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A", + "did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs", + "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY", + "did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E" + ], + "capabilityInvocation": [ + "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A", + "did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs", + "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY", + "did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E" + ] } +` diff --git a/verifications/json.go b/verifications/json.go index 361fbf5..382b9de 100644 --- a/verifications/json.go +++ b/verifications/json.go @@ -6,6 +6,7 @@ import ( "github.com/INFURA/go-did" "github.com/INFURA/go-did/verifications/ed25519" + "github.com/INFURA/go-did/verifications/jsonwebkey" "github.com/INFURA/go-did/verifications/multikey" "github.com/INFURA/go-did/verifications/x25519" ) @@ -26,6 +27,8 @@ func UnmarshalJSON(data []byte) (did.VerificationMethod, error) { res = &multikey.MultiKey{} case x25519vm.Type: res = &x25519vm.KeyAgreementKey2020{} + case jsonwebkey.Type: + res = &jsonwebkey.JsonWebKey2020{} default: return nil, fmt.Errorf("unknown verification type: %s", aux.Type) } diff --git a/verifications/jsonwebkey/JsonWebKey2020.go b/verifications/jsonwebkey/JsonWebKey2020.go new file mode 100644 index 0000000..f8db1fa --- /dev/null +++ b/verifications/jsonwebkey/JsonWebKey2020.go @@ -0,0 +1,106 @@ +package jsonwebkey + +import ( + "encoding/json" + "errors" + + "github.com/INFURA/go-did" + "github.com/INFURA/go-did/crypto" +) + +// Specification: https://www.w3.org/TR/vc-jws-2020/ + +const ( + JsonLdContext = "https://w3id.org/security/suites/jws-2020/v1" + Type = "JsonWebKey2020" +) + +var _ did.VerificationMethodSignature = &JsonWebKey2020{} +var _ did.VerificationMethodKeyAgreement = &JsonWebKey2020{} + +type JsonWebKey2020 struct { + id string + pubkey crypto.PublicKey + controller string +} + +func NewJsonWebKey2020(id string, pubkey crypto.PublicKey, controller did.DID) *JsonWebKey2020 { + return &JsonWebKey2020{ + id: id, + pubkey: pubkey, + controller: controller.String(), + } +} + +func (j JsonWebKey2020) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKeyJWK jwk `json:"publicKeyJwk"` + }{ + ID: j.ID(), + Type: j.Type(), + Controller: j.Controller(), + PublicKeyJWK: jwk{pubkey: j.pubkey}, + }) +} + +func (j *JsonWebKey2020) UnmarshalJSON(bytes []byte) error { + aux := struct { + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKeyJWK jwk `json:"publicKeyJwk"` + }{} + err := json.Unmarshal(bytes, &aux) + if err != nil { + return err + } + if aux.Type != j.Type() { + return errors.New("invalid type") + } + j.id = aux.ID + if len(j.id) == 0 { + return errors.New("invalid id") + } + j.controller = aux.Controller + if !did.HasValidDIDSyntax(j.controller) { + return errors.New("invalid controller") + } + + j.pubkey = aux.PublicKeyJWK.pubkey + + return nil +} + +func (j JsonWebKey2020) ID() string { + return j.id +} + +func (j JsonWebKey2020) Type() string { + return Type +} + +func (j JsonWebKey2020) Controller() string { + return j.controller +} + +func (j JsonWebKey2020) JsonLdContext() string { + return JsonLdContext +} + +func (j JsonWebKey2020) Verify(data []byte, sig []byte) (bool, error) { + if pub, ok := j.pubkey.(crypto.SigningPublicKey); ok { + return pub.VerifyBytes(data, sig), nil + } + return false, errors.New("not a signing public key") +} + +func (j JsonWebKey2020) PrivateKeyIsCompatible(local crypto.KeyExchangePrivateKey) bool { + return local.PublicKeyIsCompatible(j.pubkey) +} + +func (j JsonWebKey2020) KeyExchange(local crypto.KeyExchangePrivateKey) ([]byte, error) { + return local.KeyExchange(j.pubkey) +} diff --git a/verifications/jsonwebkey/JsonWebKey2020_test.go b/verifications/jsonwebkey/JsonWebKey2020_test.go new file mode 100644 index 0000000..87a7278 --- /dev/null +++ b/verifications/jsonwebkey/JsonWebKey2020_test.go @@ -0,0 +1,102 @@ +package jsonwebkey + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestJsonRoundTrip(t *testing.T) { + for _, tc := range []struct { + name string + str string + }{ + { + name: "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + str: `{ + "id": "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ" + }}`, + }, + { + name: "did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A", + str: `{ + "id": "did:example:123#4SZ-StXrp5Yd4_4rxHVTCYTHyt4zyPfN1fIuYsm6k3A", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "Z4Y3NNOxv0J6tCgqOBFnHnaZhJF6LdulT7z8A-2D5_8", + "y": "i5a2NtJoUKXkLm6q8nOEu9WOkso1Ag6FTUT6k_LMnGk" + }}`, + }, + { + name: "did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs", + str: `{ + "id": "did:example:123#n4cQ-I_WkHMcwXBJa7IHkYu8CMfdNcZKnKsOrnHLpFs", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "RSA", + "e": "AQAB", + "n": "omwsC1AqEk6whvxyOltCFWheSQvv1MExu5RLCMT4jVk9khJKv8JeMXWe3bWHatjPskdf2dlaGkW5QjtOnUKL742mvr4tCldKS3ULIaT1hJInMHHxj2gcubO6eEegACQ4QSu9LO0H-LM_L3DsRABB7Qja8HecpyuspW1Tu_DbqxcSnwendamwL52V17eKhlO4uXwv2HFlxufFHM0KmCJujIKyAxjD_m3q__IiHUVHD1tDIEvLPhG9Azsn3j95d-saIgZzPLhQFiKluGvsjrSkYU5pXVWIsV-B2jtLeeLC14XcYxWDUJ0qVopxkBvdlERcNtgF4dvW4X00EHj4vCljFw" + }}`, + }, + { + name: "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + str: `{ + "id": "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-256", + "x": "38M1FDts7Oea7urmseiugGW7tWc3mLpJh6rKe7xINZ8", + "y": "nDQW6XZ7b_u2Sy9slofYLlG03sOEoug3I0aAPQ0exs4" + }}`, + }, + { + name: "did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY", + str: `{ + "id": "did:example:123#8wgRfY3sWmzoeAL-78-oALNvNj67ZlQxd1ss_NX1hZY", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-384", + "x": "GnLl6mDti7a2VUIZP5w6pcRX8q5nvEIgB3Q_5RI2p9F_QVsaAlDN7IG68Jn0dS_F", + "y": "jq4QoAHKiIzezDp88s_cxSPXtuXYFliuCGndgU4Qp8l91xzD1spCmFIzQgVjqvcP" + }}`, + }, + { + name: "did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E", + str: `{ + "id": "did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-521", + "x": "AVlZG23LyXYwlbjbGPMxZbHmJpDSu-IvpuKigEN2pzgWtSo--Rwd-n78nrWnZzeDc187Ln3qHlw5LRGrX4qgLQ-y", + "y": "ANIbFeRdPHf1WYMCUjcPz-ZhecZFybOqLIJjVOlLETH7uPlyG0gEoMWnIZXhQVypPy_HtUiUzdnSEPAylYhHBTX2" + }}`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var jwk JsonWebKey2020 + err := json.Unmarshal([]byte(tc.str), &jwk) + require.NoError(t, err) + + bytes, err := json.Marshal(jwk) + require.NoError(t, err) + require.JSONEq(t, tc.str, string(bytes)) + }) + } +} diff --git a/verifications/jsonwebkey/jwk.go b/verifications/jsonwebkey/jwk.go new file mode 100644 index 0000000..9d4ea4a --- /dev/null +++ b/verifications/jsonwebkey/jwk.go @@ -0,0 +1,120 @@ +package jsonwebkey + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + + "github.com/INFURA/go-did/crypto" + "github.com/INFURA/go-did/crypto/ed25519" + "github.com/INFURA/go-did/crypto/p256" + "github.com/INFURA/go-did/crypto/x25519" +) + +// Specification: +// - https://www.rfc-editor.org/rfc/rfc7517#section-4 (JWK) +// - https://www.iana.org/assignments/jose/jose.xhtml#web-key-types (key parameters) + +type jwk struct { + pubkey crypto.PublicKey +} + +func (j jwk) MarshalJSON() ([]byte, error) { + switch pubkey := j.pubkey.(type) { + case *p256.PublicKey: + return json.Marshal(struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` + }{ + Kty: "EC", + Crv: "P-256", + X: base64.RawURLEncoding.EncodeToString(pubkey.X.Bytes()), + Y: base64.RawURLEncoding.EncodeToString(pubkey.Y.Bytes()), + }) + case ed25519.PublicKey: + return json.Marshal(struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + X string `json:"x"` + }{ + Kty: "OKP", + Crv: "Ed25519", + X: base64.RawURLEncoding.EncodeToString(pubkey.ToBytes()), + }) + case *x25519.PublicKey: + return json.Marshal(struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + X string `json:"x"` + }{ + Kty: "OKP", + Crv: "X25519", + X: base64.RawURLEncoding.EncodeToString(pubkey.ToBytes()), + }) + + default: + return nil, fmt.Errorf("unsupported key type %T", pubkey) + } +} + +func (j *jwk) UnmarshalJSON(bytes []byte) error { + aux := make(map[string]string) + err := json.Unmarshal(bytes, &aux) + if err != nil { + return err + } + + bigIntBase64Url := func(s string) (*big.Int, error) { + raw, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + return new(big.Int).SetBytes(raw), nil + } + + switch aux["kty"] { + case "EC": // Elliptic curve + x, err := bigIntBase64Url(aux["x"]) + if err != nil { + return fmt.Errorf("invalid x parameter with kty=EC: %w", err) + } + y, err := bigIntBase64Url(aux["y"]) + if err != nil { + return fmt.Errorf("invalid y parameter with kty=EC: %w", err) + } + switch aux["crv"] { + case "P-256": + j.pubkey, err = p256.PublicKeyFromXY(x, y) + return err + + default: + return fmt.Errorf("unsupported Curve %s", aux["crv"]) + } + + case "RSA": + return fmt.Errorf("not implemented") + + case "OKP": // Octet key pair + x, err := base64.RawURLEncoding.DecodeString(aux["x"]) + if err != nil { + return fmt.Errorf("invalid x parameter with kty=OKP: %w", err) + } + switch aux["crv"] { + case "Ed25519": + j.pubkey, err = ed25519.PublicKeyFromBytes(x) + return err + case "X25519": + j.pubkey, err = x25519.PublicKeyFromBytes(x) + return err + + default: + return fmt.Errorf("unsupported Curve %s", aux["crv"]) + } + + default: + return fmt.Errorf("unsupported key type %s", aux["kty"]) + } +} diff --git a/verifications/multikey/multikey.go b/verifications/multikey/multikey.go index 513b723..3bceedf 100644 --- a/verifications/multikey/multikey.go +++ b/verifications/multikey/multikey.go @@ -69,6 +69,10 @@ func (m *MultiKey) UnmarshalJSON(bytes []byte) error { if len(m.id) == 0 { return errors.New("invalid id") } + m.controller = aux.Controller + if !did.HasValidDIDSyntax(m.controller) { + return errors.New("invalid controller") + } code, pubBytes, err := helpers.PublicKeyMultibaseDecode(aux.PublicKeyMultibase) if err != nil { @@ -83,10 +87,6 @@ func (m *MultiKey) UnmarshalJSON(bytes []byte) error { return fmt.Errorf("invalid publicKeyMultibase: %w", err) } - m.controller = aux.Controller - if !did.HasValidDIDSyntax(m.controller) { - return errors.New("invalid controller") - } return nil }