From 3048ca8ef373ed1723ff3f0e7c7acd5f0487cb5b Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Sat, 10 Feb 2024 04:11:02 -0500 Subject: [PATCH] Refactor `@web5/dids` to adopt Web5 Spec API changes (#406) * Break out BearerDid/PortableDid and add DidRegistrationResult * Refactor DIDs package to new API design * Bump @web5/dids to 0.4.0 --------- Signed-off-by: Frank Hinek Co-authored-by: nitro-neal <5314059+nitro-neal@users.noreply.github.com> --- package-lock.json | 458 +++++++------ packages/dids/package.json | 2 +- packages/dids/src/bearer-did.ts | 307 +++++++++ packages/dids/src/index.ts | 9 +- packages/dids/src/methods/did-dht.ts | 535 +++++++-------- packages/dids/src/methods/did-ion.ts | 398 ++++++----- packages/dids/src/methods/did-jwk.ts | 211 +++--- packages/dids/src/methods/did-key.ts | 329 ++++----- packages/dids/src/methods/did-method.ts | 389 ++--------- packages/dids/src/types/did-core.ts | 17 +- packages/dids/src/types/multibase.ts | 29 + packages/dids/src/types/portable-did.ts | 64 ++ packages/dids/src/utils.ts | 164 +++++ packages/dids/tests/bearer-did.spec.ts | 447 +++++++++++++ .../fixtures/test-vectors/did-ion/to-keys.ts | 81 --- packages/dids/tests/methods/did-dht.spec.ts | 621 +++++------------ packages/dids/tests/methods/did-ion.spec.ts | 349 ++++++---- packages/dids/tests/methods/did-jwk.spec.ts | 532 +++++---------- packages/dids/tests/methods/did-key.spec.ts | 622 ++++++------------ .../dids/tests/methods/did-method.spec.ts | 293 +-------- .../dids/tests/resolver/did-resolver.spec.ts | 2 +- packages/dids/tests/utils.spec.ts | 207 +++++- 22 files changed, 2991 insertions(+), 3075 deletions(-) create mode 100644 packages/dids/src/bearer-did.ts create mode 100644 packages/dids/src/types/multibase.ts create mode 100644 packages/dids/src/types/portable-did.ts create mode 100644 packages/dids/tests/bearer-did.spec.ts delete mode 100644 packages/dids/tests/fixtures/test-vectors/did-ion/to-keys.ts diff --git a/package-lock.json b/package-lock.json index 87dd6ef81..a94595314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2006,9 +2006,9 @@ } }, "node_modules/@smithy/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.1.tgz", - "integrity": "sha512-tf+NIu9FkOh312b6M9G4D68is4Xr7qptzaZGZUREELF8ysE1yLKphqt7nsomjKZVwW7WE5pDDex9idowNGRQ/Q==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.2.tgz", + "integrity": "sha512-tYDmTp0f2TZVE18jAOH1PnmkngLQ+dOGUlMd1u67s87ieueNeyqhja6z/Z4MxhybEiXKOWFOmGjfTZWFxljwJw==", "dependencies": { "@smithy/middleware-endpoint": "^2.4.1", "@smithy/middleware-retry": "^2.1.1", @@ -2394,9 +2394,9 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.1.1.tgz", - "integrity": "sha512-tYVrc+w+jSBfBd267KDnvSGOh4NMz+wVH7v4CClDbkdPfnjvImBZsOURncT5jsFwR9KCuDyPoSZq4Pa6+eCUrA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.0.tgz", + "integrity": "sha512-iFJp/N4EtkanFpBUtSrrIbtOIBf69KNuve03ic1afhJ9/korDxdM0c6cCH4Ehj/smI9pDCfVv+bqT3xZjF2WaA==", "dependencies": { "@smithy/config-resolver": "^2.1.1", "@smithy/credential-provider-imds": "^2.2.1", @@ -2519,9 +2519,9 @@ } }, "node_modules/@sphereon/pex-models": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@sphereon/pex-models/-/pex-models-2.1.5.tgz", - "integrity": "sha512-7THexvdYUK/Dh8olBB46ErT9q/RnecnMdb5r2iwZ6be0Dt4vQLAUN7QU80H0HZBok4jRTb8ydt12x0raBSTHOg==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@sphereon/pex-models/-/pex-models-2.2.0.tgz", + "integrity": "sha512-dGDRdoxJj+P0TRqu0R8R0/IdIzrCya1MsnxIFbcmSW3rjPsbwXbV0EojEfxXGD5LhqsUJiuAffMtyE2dtVI/XQ==" }, "node_modules/@sphereon/ssi-types": { "version": "0.13.0", @@ -2609,9 +2609,9 @@ "dev": true }, "node_modules/@tbd54566975/dwn-sql-store/node_modules/cborg": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.0.8.tgz", - "integrity": "sha512-/6QDK0Hw//cV4YNWZZjdIUMFNw0DZmb56jdVGJPwXP7874gSN0AMYqM07mVKpAm+6Nn7U8lvYFzPgBGatC+5xw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.0.9.tgz", + "integrity": "sha512-xAuZbCDUOZxCe/ZJuIrnlG1Bk1R0qhwCXdnPYxVmqBSqm9M3BeE3G6Qoj5Zq+8epas36bT3vjiInDTJ6BVH6Rg==", "dev": true, "bin": { "cborg": "lib/bin.js" @@ -2786,9 +2786,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.42", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz", - "integrity": "sha512-ckM3jm2bf/MfB3+spLPWYPUH573plBFwpOhqQ2WottxYV85j1HQFlxmnTq57X1yHY9awZPig06hL/cLMgNWHIQ==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dev": true, "dependencies": { "@types/node": "*", @@ -2889,9 +2889,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.13.tgz", - "integrity": "sha512-5G4zQwdiQBSWYTDAH1ctw2eidqdhMJaNsiIDKHFr55ihz5Trl2qqR8fdrT732yPBho5gkNxXm67OxWFBqX9aPg==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3028,16 +3028,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", - "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { @@ -3057,14 +3057,14 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", - "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3075,13 +3075,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", - "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -3190,9 +3190,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", - "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "peer": true, "engines": { @@ -3204,14 +3204,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", - "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3233,13 +3233,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", - "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -4093,12 +4093,15 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4120,16 +4123,17 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -4219,9 +4223,9 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", + "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", "engines": { "node": ">= 0.4" }, @@ -4230,9 +4234,9 @@ } }, "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -4240,6 +4244,13 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.0.tgz", + "integrity": "sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg==", + "dev": true, + "optional": true + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -4815,13 +4826,17 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", + "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.3", + "set-function-length": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4849,9 +4864,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001581", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", - "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", + "version": "1.0.30001585", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz", + "integrity": "sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==", "dev": true, "funding": [ { @@ -5049,16 +5064,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5071,6 +5080,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -5781,13 +5793,14 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", + "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", "dependencies": { - "get-intrinsic": "^1.2.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.2", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -6012,9 +6025,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.651", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.651.tgz", - "integrity": "sha512-jjks7Xx+4I7dslwsbaFocSwqBbGHQmuXBJUK9QBZTIrzPq3pzn6Uf2szFSP728FtLYE3ldiccmlkOM/zhGKCpA==", + "version": "1.4.665", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.665.tgz", + "integrity": "sha512-UpyCWObBoD+nSZgOC2ToaIdZB0r9GhqT2WahPKiSki6ckkSuKhQNso8V2PrFcHBMleI/eqbKgVQgVC4Wni4ilw==", "dev": true, "peer": true }, @@ -6140,6 +6153,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", @@ -6213,9 +6234,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -6856,9 +6877,9 @@ } }, "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -7228,15 +7249,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7254,12 +7279,13 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -7420,16 +7446,25 @@ "dev": true }, "node_modules/hamt-sharding": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/hamt-sharding/-/hamt-sharding-3.0.2.tgz", - "integrity": "sha512-f0DzBD2tSmLFdFsLAvOflIBqFPjerbA7BfmwO8mVho/5hXwgyyYhv+ijIzidQf/DpDX3bRjAQvhGoBFj+DBvPw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/hamt-sharding/-/hamt-sharding-3.0.6.tgz", + "integrity": "sha512-nZeamxfymIWLpVcAN0CRrb7uVq3hCOGj9IcL6NMA6VVCVWqj+h9Jo/SmaWuS92AEDf1thmHsM5D5c70hM3j2Tg==", "dependencies": { "sparse-array": "^1.3.1", - "uint8arrays": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" + "uint8arrays": "^5.0.1" + } + }, + "node_modules/hamt-sharding/node_modules/multiformats": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.0.1.tgz", + "integrity": "sha512-bt3R5iXe2O8xpp3wkmQhC73b/lC4S2ihU8Dndwcsysqbydqb8N+bpP116qMcClZ17g58iSIwtXUTcg2zT4sniA==" + }, + "node_modules/hamt-sharding/node_modules/uint8arrays": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.0.2.tgz", + "integrity": "sha512-S0GaeR+orZt7LaqzTRs4ZP8QqzAauJ+0d4xvP2lJTA99jIkKsE2FgDs4tGF/K/z5O9I/2W5Yvrh7IuqNeYH+0Q==", + "dependencies": { + "multiformats": "^13.0.0" } }, "node_modules/has-bigints": { @@ -7483,11 +7518,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -7735,9 +7770,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -7821,11 +7856,11 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -7930,13 +7965,15 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8261,11 +8298,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -9547,6 +9584,33 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/mocha/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -11000,9 +11064,9 @@ "dev": true }, "node_modules/protons-runtime": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-5.3.0.tgz", - "integrity": "sha512-RySXxx+jvz4mi/rr2VsnvgWvC6dFP2pVyWpVRFYb/jxmwih862ZjXJLUkjd+nOtXBx0Lwk2xeuY2f2fmd3FT9w==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-5.4.0.tgz", + "integrity": "sha512-XfA++W/WlQOSyjUyuF5lgYBfXZUEMP01Oh1C2dSwZAlF2e/ZrMRPfWonXj6BGM+o8Xciv7w0tsRMKYwYEuQvaw==", "dependencies": { "uint8-varint": "^2.0.2", "uint8arraylist": "^2.4.3", @@ -11015,9 +11079,9 @@ "integrity": "sha512-bt3R5iXe2O8xpp3wkmQhC73b/lC4S2ihU8Dndwcsysqbydqb8N+bpP116qMcClZ17g58iSIwtXUTcg2zT4sniA==" }, "node_modules/protons-runtime/node_modules/uint8arrays": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.0.1.tgz", - "integrity": "sha512-ND5RpJAnPgHmZT7hWD/2T4BwRp04j8NLKvMKC/7bhiEwEjUMkQ4kvBKiH6hOqbljd6qJ2xS8reL3vl1e33grOQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.0.2.tgz", + "integrity": "sha512-S0GaeR+orZt7LaqzTRs4ZP8QqzAauJ+0d4xvP2lJTA99jIkKsE2FgDs4tGF/K/z5O9I/2W5Yvrh7IuqNeYH+0Q==", "dependencies": { "multiformats": "^13.0.0" } @@ -11768,12 +11832,12 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -11848,9 +11912,9 @@ "peer": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -11944,13 +12008,14 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.1" }, @@ -12054,13 +12119,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12288,9 +12357,9 @@ } }, "node_modules/sodium-native": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.6.tgz", - "integrity": "sha512-uYsyycwcz9kYDwpXxJmL2YZosynsxcP6RPySbARVJdC9uNDa2CMjzJ7/WsMMvThKgvAYsBWdZc7L/WSVj9lTcA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.8.tgz", + "integrity": "sha512-2MOwB92RlCF0Y+teiTOgRMaKvcgXbXvwmSlyHaY5Gy8d4W3Bm++9cMv+hB/gf8INdMUxo69DbmGcvJ7HzLSL9w==", "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.6.0" @@ -12415,9 +12484,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, "node_modules/split2": { @@ -12600,13 +12669,16 @@ } }, "node_modules/streamx": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", - "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", + "version": "2.15.8", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.8.tgz", + "integrity": "sha512-6pwMeMY/SuISiRsuS8TeIrAzyFbG5gGPHFQsYjUr/pbBadaL1PCWmzKw+CHZSwainfvcF6Si6cVLq4XTEwswFQ==", "dev": true, "dependencies": { "fast-fifo": "^1.1.0", "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -13150,12 +13222,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -13240,13 +13312,13 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz", + "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -13345,9 +13417,9 @@ "integrity": "sha512-bt3R5iXe2O8xpp3wkmQhC73b/lC4S2ihU8Dndwcsysqbydqb8N+bpP116qMcClZ17g58iSIwtXUTcg2zT4sniA==" }, "node_modules/uint8-varint/node_modules/uint8arrays": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.0.1.tgz", - "integrity": "sha512-ND5RpJAnPgHmZT7hWD/2T4BwRp04j8NLKvMKC/7bhiEwEjUMkQ4kvBKiH6hOqbljd6qJ2xS8reL3vl1e33grOQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.0.2.tgz", + "integrity": "sha512-S0GaeR+orZt7LaqzTRs4ZP8QqzAauJ+0d4xvP2lJTA99jIkKsE2FgDs4tGF/K/z5O9I/2W5Yvrh7IuqNeYH+0Q==", "dependencies": { "multiformats": "^13.0.0" } @@ -13366,9 +13438,9 @@ "integrity": "sha512-bt3R5iXe2O8xpp3wkmQhC73b/lC4S2ihU8Dndwcsysqbydqb8N+bpP116qMcClZ17g58iSIwtXUTcg2zT4sniA==" }, "node_modules/uint8arraylist/node_modules/uint8arrays": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.0.1.tgz", - "integrity": "sha512-ND5RpJAnPgHmZT7hWD/2T4BwRp04j8NLKvMKC/7bhiEwEjUMkQ4kvBKiH6hOqbljd6qJ2xS8reL3vl1e33grOQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.0.2.tgz", + "integrity": "sha512-S0GaeR+orZt7LaqzTRs4ZP8QqzAauJ+0d4xvP2lJTA99jIkKsE2FgDs4tGF/K/z5O9I/2W5Yvrh7IuqNeYH+0Q==", "dependencies": { "multiformats": "^13.0.0" } @@ -13659,9 +13731,9 @@ } }, "node_modules/webpack": { - "version": "5.90.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.0.tgz", - "integrity": "sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w==", + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", + "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", "dev": true, "peer": true, "dependencies": { @@ -13791,15 +13863,15 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -16245,7 +16317,7 @@ }, "packages/dids": { "name": "@web5/dids", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@decentralized-identity/ion-sdk": "1.0.1", diff --git a/packages/dids/package.json b/packages/dids/package.json index 41226316d..5f1318fd0 100644 --- a/packages/dids/package.json +++ b/packages/dids/package.json @@ -1,6 +1,6 @@ { "name": "@web5/dids", - "version": "0.3.0", + "version": "0.4.0", "description": "TBD DIDs library", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/dids/src/bearer-did.ts b/packages/dids/src/bearer-did.ts new file mode 100644 index 000000000..96d49bd22 --- /dev/null +++ b/packages/dids/src/bearer-did.ts @@ -0,0 +1,307 @@ +import { LocalKeyManager, type CryptoApi, type EnclosedSignParams, type EnclosedVerifyParams, type Jwk, type KeyIdentifier, type KeyImporterExporter, type KmsExportKeyParams, type KmsImportKeyParams, type Signer } from '@web5/crypto'; + +import type { DidDocument } from './types/did-core.js'; +import type { DidMetadata, PortableDid } from './types/portable-did.js'; + +import { DidError, DidErrorCode } from './did-error.js'; +import { extractDidFragment, getVerificationMethods } from './utils.js'; + +/** + * A `BearerDidSigner` extends the {@link Signer} interface to include specific properties for + * signing with a Decentralized Identifier (DID). It encapsulates the algorithm and key identifier, + * which are often needed when signing JWTs, JWSs, JWEs, and other data structures. + * + * Typically, the algorithm and key identifier are used to populate the `alg` and `kid` fields of a + * JWT or JWS header. + */ +export interface BearerDidSigner extends Signer { + /** + * The cryptographic algorithm identifier used for signing operations. + * + * Typically, this value is used to populate the `alg` field of a JWT or JWS header. The + * registered algorithm names are defined in the + * {@link https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms | IANA JSON Web Signature and Encryption Algorithms registry}. + * + * @example + * "ES256" // ECDSA using P-256 and SHA-256 + */ + algorithm: string; + + /** + * The unique identifier of the key within the DID document that is used for signing and + * verification operations. + * + * This identifier must be a DID URI with a fragment (e.g., did:method:123#key-0) that references + * a specific verification method in the DID document. It allows users of a `BearerDidSigner` to + * determine the DID and key that will be used for signing and verification operations. + * + * @example + * "did:dht:123#key-1" // A fragment identifier referring to a key in the DID document + */ + keyId: string; +} + +/** + * Represents a Decentralized Identifier (DID) along with its DID document, key manager, metadata, + * and convenience functions. + */ +export class BearerDid { + /** {@inheritDoc Did#uri} */ + uri: string; + + /** + * The DID document associated with this DID. + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, ยง DID Document} + */ + document: DidDocument; + + /** {@inheritDoc DidMetadata} */ + metadata: DidMetadata; + + /** + * Key Management System (KMS) used to manage the DIDs keys and sign data. + * + * Each DID method requires at least one key be present in the provided `keyManager`. + */ + keyManager: CryptoApi; + + constructor({ uri, document, metadata, keyManager }: { + uri: string, + document: DidDocument, + metadata: DidMetadata, + keyManager: CryptoApi + }) { + this.uri = uri; + this.document = document; + this.metadata = metadata; + this.keyManager = keyManager; + } + + /** + * Converts a `BearerDid` object to a portable format containing the URI and verification methods + * associated with the DID. + * + * This method is useful when you need to represent the key material and metadata associated with + * a DID in format that can be used independently of the specific DID method implementation. It + * extracts both public and private keys from the DID's key manager and organizes them into a + * `PortableDid` structure. + * + * @remarks + * This method requires that the DID's key manager supports the `exportKey` operation. If the DID + * document does not contain any verification methods, or if the key manager does not support key + * export, an error is thrown. + * + * The resulting `PortableDid` will contain the same number of verification methods as the DID + * document, each with its associated public and private keys and the purposes for which the key + * can be used. + * + * @example + * ```ts + * // Assuming `did` is an instance of BearerDid + * const portableDid = await did.export(); + * // portableDid now contains the DID URI, document, metadata, and optionally, private keys. + * ``` + * + * @returns A `PortableDid` containing the URI, DID document, metadata, and optionally private + * keys associated with the `BearerDid`. + * @throws An error if the DID document does not contain any verification methods or the keys for + * any verification method are missing in the key manager. + */ + public async export(): Promise { + // Verify the DID document contains at least one verification method. + if (!(Array.isArray(this.document.verificationMethod) && this.document.verificationMethod.length > 0)) { + throw new Error(`DID document for '${this.uri}' is missing verification methods`); + } + + // Create a new `PortableDid` object to store the exported data. + let portableDid: PortableDid = { + uri : this.uri, + document : this.document, + metadata : this.metadata + }; + + // If the BearerDid's key manager supports exporting private keys, add them to the portable DID. + if ('exportKey' in this.keyManager && typeof this.keyManager.exportKey === 'function') { + const privateKeys: Jwk[] = []; + for (let vm of this.document.verificationMethod) { + if (!vm.publicKeyJwk) { + throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`); + } + + // Compute the key URI of the verification method's public key. + const keyUri = await this.keyManager.getKeyUri({ key: vm.publicKeyJwk }); + + // Retrieve the private key from the key manager. + const privateKey = await this.keyManager.exportKey({ keyUri }) as Jwk; + + // Add the verification method to the key set. + privateKeys.push({ ...privateKey }); + } + portableDid.privateKeys = privateKeys; + } + + return portableDid; + } + + /** + * Return a {@link Signer} that can be used to sign messages, credentials, or arbitrary data. + * + * If given, the `methodId` parameter is used to select a key from the verification methods + * present in the DID Document. + * + * If `methodID` is not given, the first verification method intended for signing claims is used. + * + * @param params - The parameters for the `getSigner` operation. + * @param params.methodId - ID of the verification method key that will be used for sign and + * verify operations. Optional. + * @returns An instantiated {@link Signer} that can be used to sign and verify data. + */ + public async getSigner(params?: { methodId: string }): Promise { + // Attempt to find a verification method that matches the given method ID, or if not given, + // find the first verification method intended for signing claims. + const verificationMethod = this.document.verificationMethod?.find( + vm => extractDidFragment(vm.id) === (extractDidFragment(params?.methodId) ?? extractDidFragment(this.document.assertionMethod?.[0])) + ); + + if (!(verificationMethod && verificationMethod.publicKeyJwk)) { + throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document'); + } + + // Compute the expected key URI of the signing key. + const keyUri = await this.keyManager.getKeyUri({ key: verificationMethod.publicKeyJwk }); + + // Get the public key to be used for verify operations, which also verifies that the key is + // present in the key manager's store. + const publicKey = await this.keyManager.getPublicKey({ keyUri }); + + // Bind the DID's key manager to the signer. + const keyManager = this.keyManager; + + // Determine the signing algorithm. + const algorithm = BearerDid.getAlgorithmFromPublicKey(publicKey); + + return { + algorithm : algorithm, + keyId : verificationMethod.id, + + async sign({ data }: EnclosedSignParams): Promise { + const signature = await keyManager.sign({ data, keyUri: keyUri! }); // `keyUri` is guaranteed to be defined at this point. + return signature; + }, + + async verify({ data, signature }: EnclosedVerifyParams): Promise { + const isValid = await keyManager.verify({ data, key: publicKey!, signature }); // `publicKey` is guaranteed to be defined at this point. + return isValid; + } + }; + } + + /** + * Instantiates a {@link BearerDid} object for the DID DHT method from a given {@link PortableDid}. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @example + * ```ts + * // Export an existing BearerDid to PortableDid format. + * const portableDid = await did.export(); + * // Reconstruct a BearerDid object from the PortableDid. + * const did = await DidDht.import({ portableDid }); + * ``` + * + * @param params - The parameters for the import operation. + * @param params.portableDid - The PortableDid object to import. + * @param params.keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * {@link LocalKeyManager} instance will be created and + * used. + * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the + * provided PortableDid. + * @throws An error if the PortableDid document does not contain any verification methods or the + * keys for any verification method are missing in the key manager. + */ + public static async import({ portableDid, keyManager = new LocalKeyManager() }: { + keyManager?: CryptoApi & KeyImporterExporter; + portableDid: PortableDid; + }): Promise { + // Get all verification methods from the given DID document, including embedded methods. + const verificationMethods = getVerificationMethods({ didDocument: portableDid.document }); + + // Validate that the DID document contains at least one verification method. + if (verificationMethods.length === 0) { + throw new DidError(DidErrorCode.InvalidDidDocument, `At least one verification method is required but 0 were given`); + } + + // If given, import the private key material into the key manager. + for (let key of portableDid.privateKeys ?? []) { + await keyManager.importKey({ key }); + } + + // Validate that the key material for every verification method in the DID document is present + // in the key manager. + for (let vm of verificationMethods) { + if (!vm.publicKeyJwk) { + throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`); + } + + // Compute the key URI of the verification method's public key. + const keyUri = await keyManager.getKeyUri({ key: vm.publicKeyJwk }); + + // Verify that the key is present in the key manager. If not, an error is thrown. + await keyManager.getPublicKey({ keyUri }); + } + + // Use the given PortableDid to construct the BearerDid object. + const did = new BearerDid({ + uri : portableDid.uri, + document : portableDid.document, + metadata : portableDid.metadata, + keyManager + }); + + return did; + } + + /** + * Determines the name of the algorithm based on the key's curve property. + * + * @remarks + * This method facilitates the identification of the correct algorithm for cryptographic + * operations based on the `alg` or `crv` properties of a {@link Jwk | JWK}. + * + * @example + * ```ts + * const publicKey = { ... }; // Public key in JWK format + * const algorithm = BearerDid.getAlgorithmFromPublicKey({ key: publicKey }); + * ``` + * + * @param publicKey - A JWK containing the `alg` and/or `crv` properties. + * + * @returns The name of the algorithm associated with the key. + * + * @throws Error if the algorithm cannot be determined from the provided input. + */ + private static getAlgorithmFromPublicKey(publicKey: Jwk): string { + const registeredSigningAlgorithms: Record = { + 'Ed25519' : 'EdDSA', + 'P-256' : 'ES256', + 'P-384' : 'ES384', + 'P-521' : 'ES512', + 'secp256k1' : 'ES256K', + }; + + // If the key contains an `alg` property, return its value. + if (publicKey.alg) { + return publicKey.alg; + } + + // If the key contains a `crv` property, return the corresponding algorithm. + if (publicKey.crv && Object.keys(registeredSigningAlgorithms).includes(publicKey.crv)) { + return registeredSigningAlgorithms[publicKey.crv]; + } + + throw new Error(`Unable to determine algorithm based on provided input: alg=${publicKey.alg}, crv=${publicKey.crv}`); + } +} \ No newline at end of file diff --git a/packages/dids/src/index.ts b/packages/dids/src/index.ts index 83333c9d9..f4dc3af74 100644 --- a/packages/dids/src/index.ts +++ b/packages/dids/src/index.ts @@ -1,5 +1,10 @@ +export * from './types/did-core.js'; +export type * from './types/multibase.js'; +export type * from './types/portable-did.js'; + export * from './did.js'; export * from './did-error.js'; +export * from './bearer-did.js'; export * from './methods/did-dht.js'; export * from './methods/did-ion.js'; @@ -12,6 +17,4 @@ export * from './resolver/did-resolver.js'; export * from './resolver/resolver-cache-level.js'; export * from './resolver/resolver-cache-noop.js'; -export * as utils from './utils.js'; - -export * from './types/did-core.js'; \ No newline at end of file +export * as utils from './utils.js'; \ No newline at end of file diff --git a/packages/dids/src/methods/did-dht.ts b/packages/dids/src/methods/did-dht.ts index aaa17335a..b6091bea6 100644 --- a/packages/dids/src/methods/did-dht.ts +++ b/packages/dids/src/methods/did-dht.ts @@ -2,6 +2,7 @@ import type { Packet, TxtAnswer, TxtData } from '@dnsquery/dns-packet'; import type { Jwk, Signer, + CryptoApi, KeyIdentifier, KmsExportKeyParams, KmsImportKeyParams, @@ -11,10 +12,11 @@ import type { import bencode from 'bencode'; import { Convert } from '@web5/common'; -import { CryptoApi, computeJwkThumbprint, Ed25519, LocalKeyManager, Secp256k1, Secp256r1 } from '@web5/crypto'; +import { computeJwkThumbprint, Ed25519, LocalKeyManager, Secp256k1, Secp256r1 } from '@web5/crypto'; import { AUTHORITATIVE_ANSWER, decode as dnsPacketDecode, encode as dnsPacketEncode } from '@dnsquery/dns-packet'; -import type { BearerDid, DidCreateOptions, DidCreateVerificationMethod, DidMetadata, PortableDid } from './did-method.js'; +import type { DidMetadata, PortableDid } from '../types/portable-did.js'; +import type { DidCreateOptions, DidCreateVerificationMethod, DidRegistrationResult } from './did-method.js'; import type { DidService, DidDocument, @@ -25,6 +27,8 @@ import type { import { Did } from '../did.js'; import { DidMethod } from './did-method.js'; +import { BearerDid } from '../bearer-did.js'; +import { extractDidFragment } from '../utils.js'; import { DidError, DidErrorCode } from '../did-error.js'; import { DidVerificationRelationship } from '../types/did-core.js'; import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; @@ -39,7 +43,7 @@ import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; * * @see {@link https://www.bittorrent.org/beps/bep_0044.html | BEP44} */ -interface Bep44Message { +export interface Bep44Message { /** * The public key bytes of the Identity Key, which serves as the identifier in the DHT network for * the corresponding BEP44 message. @@ -112,8 +116,8 @@ export interface DidDhtCreateOptions extends DidCreateOptions { /** * Optional. The URI of a server involved in executing DID method operations. In the context of - * DID creation, the endpoint is expected to be a Pkarr relay. If not specified, a default gateway - * node is used. + * DID creation, the endpoint is expected to be a DID DHT Gateway or Pkarr relay. If not + * specified, a default gateway node is used. */ gatewayUri?: string; @@ -203,9 +207,10 @@ export interface DidDhtCreateOptions extends DidCreateOptions { } /** - * The default Pkarr relay server to use when publishing and resolving DID documents. + * The default DID DHT Gateway or Pkarr Relay server to use when publishing and resolving DID + * documents. */ -const DEFAULT_PKARR_RELAY = 'https://diddht.tbddev.org'; +const DEFAULT_GATEWAY_URI = 'https://diddht.tbddev.org'; /** * The version of the DID DHT specification that is implemented by this library. @@ -431,37 +436,13 @@ const AlgorithmToKeyTypeMap = { * const signature = await signer.sign({ data: new TextEncoder().encode('Message') }); * const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature }); * - * // Key Management + * // Import / Export * - * // Instantiate a DID object for a published DID with existing keys in a KMS - * const did = await DidDht.fromKeyManager({ - * didUri: 'did:dht:cf69rrqpanddbhkqecuwia314hfawfua9yr6zx433jmgm39ez57y', - * keyManager - * }); + * // Export a BearerDid object to the PortableDid format. + * const portableDid = await did.export(); * - * // Instantiate a DID object from an existing verification method key - * const did = await DidDht.fromKeys({ - * verificationMethods: [{ - * publicKeyJwk : { - * crv : 'Ed25519', - * kty : 'OKP', - * x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0' - * }, - * privateKeyJwk: { - * crv : 'Ed25519', - * d : 'hdSIwbQwVD-fNOVEgt-k3mMl44Ip1iPi58Ex6VDGxqY', - * kty : 'OKP', - * x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0' - * }, - * purposes: ['authentication', 'assertionMethod' ], - * }] - * }); - * - * // Convert a DID object to a portable format - * const portableDid = await DidDht.toKeys({ did }); - * - * // Reconstruct a DID object from a portable format - * const did = await DidDht.fromKeys(portableDid); + * // Reconstruct a BearerDid object from a PortableDid + * const did = await DidDht.import(portableDid); * ``` */ export class DidDht extends DidMethod { @@ -514,25 +495,101 @@ export class DidDht extends DidMethod { throw new Error('One or more verification method algorithms are not supported'); } - // Check 2: Validate that the required properties for any given services are present. + // Check 2: Validate that the ID for any given verification method is unique. + const methodIds = options.verificationMethods?.filter(vm => 'id' in vm).map(vm => vm.id); + if (methodIds && methodIds.length !== new Set(methodIds).size) { + throw new Error('One or more verification method IDs are not unique'); + } + + // Check 3: Validate that the required properties for any given services are present. if (options.services?.some(s => !s.id || !s.type || !s.serviceEndpoint)) { throw new Error('One or more services are missing required properties'); } + // Generate random key material for the Identity Key. + const identityKeyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); + const identityKey = await keyManager.getPublicKey({ keyUri: identityKeyUri }); + + // Compute the DID URI from the Identity Key. + const didUri = await DidDhtUtils.identityKeyToIdentifier({ identityKey }); + + // Begin constructing the DID Document. + const document: DidDocument = { + id: didUri, + ...options.alsoKnownAs && { alsoKnownAs: options.alsoKnownAs }, + ...options.controllers && { controller: options.controllers } + }; + + // If the given verification methods do not contain an Identity Key, add one. + const verificationMethodsToAdd = [...options.verificationMethods ?? []]; + if (!verificationMethodsToAdd?.some(vm => vm.id?.split('#').pop() === '0')) { + // Add the Identity Key to the beginning of the key set. + verificationMethodsToAdd.unshift({ + algorithm : 'Ed25519' as any, + id : '0', + purposes : ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation'] + }); + } + // Generate random key material for the Identity Key and any additional verification methods. - const keySet = await DidDht.generateKeys({ - keyManager, - verificationMethods: options.verificationMethods ?? [] + // Add verification methods to the DID document. + for (const vm of verificationMethodsToAdd) { + // Generate a random key for the verification method, or if its the Identity Key's + // verification method (`id` is 0) use the key previously generated. + const keyUri = (vm.id && vm.id.split('#').pop() === '0') + ? identityKeyUri + : await keyManager.generateKey({ algorithm: vm.algorithm }); + + const publicKey = await keyManager.getPublicKey({ keyUri }); + + // Use the given ID, the key's ID, or the key's thumbprint as the verification method ID. + let methodId = vm.id ?? publicKey.kid ?? await computeJwkThumbprint({ jwk: publicKey }); + methodId = `${didUri}#${extractDidFragment(methodId)}`; // Remove fragment prefix, if any. + + // Initialize the `verificationMethod` array if it does not already exist. + document.verificationMethod ??= []; + + // Add the verification method to the DID document. + document.verificationMethod.push({ + id : methodId, + type : 'JsonWebKey', + controller : vm.controller ?? didUri, + publicKeyJwk : publicKey, + }); + + // Add the verification method to the specified purpose properties of the DID document. + for (const purpose of vm.purposes ?? []) { + // Initialize the purpose property if it does not already exist. + if (!document[purpose]) document[purpose] = []; + // Add the verification method to the purpose property. + document[purpose]!.push(methodId); + } + } + + // Add services, if any, to the DID document. + options.services?.forEach(service => { + document.service ??= []; + service.id = `${didUri}#${service.id.split('#').pop()}`; // Remove fragment prefix, if any. + document.service.push(service); }); - // Create the DID object from the generated key material, including DID document, metadata, - // signer convenience function, and URI. - const did = await DidDht.fromPublicKeys({ keyManager, options, ...keySet }); + // Create the BearerDid object, including the registered DID types (if any), and specify that + // the DID has not yet been published. + const did = new BearerDid({ + uri : didUri, + document, + metadata : { + published: false, + ...options.types && { types: options.types } + }, + keyManager + }); - // By default, publish the DID document to a DHT operations endpoint unless explicitly disabled. - did.metadata.published = options.publish ?? true - ? await this.publish({ did, gatewayUri: options.gatewayUri }) - : false; + // By default, publish the DID document to a DHT Gateway unless explicitly disabled. + if (options.publish ?? true) { + const registrationResult = await DidDht.publish({ did, gatewayUri: options.gatewayUri }); + did.metadata = registrationResult.didDocumentMetadata; + } return did; } @@ -540,76 +597,47 @@ export class DidDht extends DidMethod { /** * Instantiates a {@link BearerDid} object for the DID DHT method from a given {@link PortableDid}. * - * This method allows for the creation of a `BearerDid` object using pre-existing key material, - * encapsulated within the `verificationMethods` array of the `PortableDid`. This is particularly - * useful when the key material is already available and you want to construct a `BearerDid` - * object based on these keys, instead of generating new keys. - * - * @remarks - * The key material (both public and private keys) should be provided in JWK format. The method - * handles the inclusion of these keys in the DID Document and specified verification - * relationships. + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. * * @example * ```ts - * // Example with an existing key in JWK format. - * const verificationMethods = [{ - * publicKeyJwk: { id: 0, // public key in JWK format }, - * privateKeyJwk: { id: 0, // private key in JWK format }, - * purposes: ['authentication'] - * }]; - * const did = await DidDht.fromKeys({ verificationMethods }); + * // Export an existing BearerDid to PortableDid format. + * const portableDid = await did.export(); + * // Reconstruct a BearerDid object from the PortableDid. + * const did = await DidDht.import({ portableDid }); * ``` * - * @param params - The parameters for the `fromKeys` operation. + * @param params - The parameters for the import operation. + * @param params.portableDid - The PortableDid object to import. * @param params.keyManager - Optionally specify an external Key Management System (KMS) used to * generate keys and sign data. If not given, a new - * {@link @web5/crypto#LocalKeyManager} instance will be created and used. - * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the provided keys. - * @throws An error if the `verificationMethods` array does not contain any keys, lacks an - * Identity Key, or any verification method is missing a public or private key. + * {@link LocalKeyManager} instance will be created and + * used. + * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the + * provided PortableDid. + * @throws An error if the PortableDid document does not contain any verification methods, lacks + * an Identity Key, or the keys for any verification method are missing in the key + * manager. */ - public static async fromKeys({ - keyManager = new LocalKeyManager(), - uri, - verificationMethods, - options = {} - }: { + public static async import({ portableDid, keyManager = new LocalKeyManager() }: { keyManager?: CryptoApi & KeyImporterExporter; - options?: DidDhtCreateOptions; - } & PortableDid): Promise { - if (!(verificationMethods && Array.isArray(verificationMethods) && verificationMethods.length > 0)) { - throw new Error(`At least one verification method is required but 0 were given`); - } - - if (!verificationMethods?.some(vm => vm.id?.split('#').pop() === '0')) { - throw new Error(`Given verification methods are missing an Identity Key`); + portableDid: PortableDid; + }): Promise { + // Verify the DID method is supported. + const parsedDid = Did.parse(portableDid.uri); + if (parsedDid?.method !== DidDht.methodName) { + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`); } - if (!verificationMethods?.some(vm => vm.privateKeyJwk && vm.publicKeyJwk)) { - throw new Error(`All verification methods must contain a public and private key in JWK format`); - } + const did = await BearerDid.import({ portableDid, keyManager }); - // Import the private key material for every verification method into the key manager. - for (let vm of verificationMethods) { - await keyManager.importKey({ key: vm.privateKeyJwk! }); + // Validate that the given verification methods contain an Identity Key. + if (!did.document.verificationMethod?.some(vm => vm.id?.split('#').pop() === '0')) { + throw new DidError(DidErrorCode.InvalidDidDocument, `DID document must contain an Identity Key`); } - // If the DID URI is provided, resolve the DID document and metadata from the DHT network and - // use it to construct the DID object. - if (uri) { - return await DidDht.fromKeyManager({ didUri: uri, keyManager }); - } else { - // Otherwise, use the given key material and options to construct the DID object. - const did = await DidDht.fromPublicKeys({ keyManager, verificationMethods, options }); - - // By default, the DID document will NOT be published unless explicitly enabled. - did.metadata.published = options.publish - ? await this.publish({ did, gatewayUri: options.gatewayUri }) - : false; - - return did; - } + return did; } /** @@ -626,14 +654,22 @@ export class DidDht extends DidMethod { public static async getSigningMethod({ didDocument, methodId = '#0' }: { didDocument: DidDocument; methodId?: string; - }): Promise { - + }): Promise { + // Verify the DID method is supported. const parsedDid = Did.parse(didDocument.id); if (parsedDid && parsedDid.method !== this.methodName) { - throw new Error(`Method not supported: ${parsedDid.method}`); + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`); } - const verificationMethod = didDocument.verificationMethod?.find(vm => vm.id.endsWith(methodId)); + // Attempt to find a verification method that matches the given method ID, or if not given, + // find the first verification method intended for signing claims. + const verificationMethod = didDocument.verificationMethod?.find( + vm => extractDidFragment(vm.id) === (extractDidFragment(methodId) ?? extractDidFragment(didDocument.assertionMethod?.[0])) + ); + + if (!(verificationMethod && verificationMethod.publicKeyJwk)) { + throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document'); + } return verificationMethod; } @@ -651,30 +687,31 @@ export class DidDht extends DidMethod { * - For existing, unpublished DIDs, it can be used to publish the DID Document to Mainline DHT. * - The method relies on the specified Pkarr relay server to interface with the DHT network. * - * @param params - The parameters for the `publish` operation. - * @param params.did - The `BearerDid` object representing the DID to be published. - * @param params.gatewayUri - Optional. The URI of a server involved in executing DID - * method operations. In the context of publishing, the - * endpoint is expected to be a Pkarr relay. If not specified, - * a default relay server is used. - * @returns A Promise resolving to a boolean indicating whether the publication was successful. - * * @example * ```ts * // Generate a new DID and keys but explicitly disable publishing. * const did = await DidDht.create({ options: { publish: false } }); * // Publish the DID to the DHT. - * const isPublished = await DidDht.publish({ did }); - * // `isPublished` is true if the DID was successfully published. + * const registrationResult = await DidDht.publish({ did }); + * // `registrationResult.didDocumentMetadata.published` is true if the DID was successfully published. * ``` + * + * @param params - The parameters for the `publish` operation. + * @param params.did - The `BearerDid` object representing the DID to be published. + * @param params.gatewayUri - Optional. The URI of a server involved in executing DID method + * operations. In the context of publishing, the endpoint is expected + * to be a DID DHT Gateway or Pkarr Relay. If not specified, a default + * gateway node is used. + * @returns A promise that resolves to a {@link DidRegistrationResult} object that contains + * the result of registering the DID with a DID DHT Gateway or Pkarr relay. */ - public static async publish({ did, gatewayUri = DEFAULT_PKARR_RELAY }: { + public static async publish({ did, gatewayUri = DEFAULT_GATEWAY_URI }: { did: BearerDid; gatewayUri?: string; - }): Promise { - const isPublished = await DidDhtDocument.put({ did, relay: gatewayUri }); + }): Promise { + const registrationResult = await DidDhtDocument.put({ did, gatewayUri }); - return isPublished; + return registrationResult; } /** @@ -697,24 +734,25 @@ export class DidDht extends DidMethod { * * @param didUri - The DID to be resolved. * @param options - Optional parameters for resolving the DID. Unused by this DID method. - * @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution. + * @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of + * the resolution. */ public static async resolve(didUri: string, options: DidResolutionOptions = {}): Promise { - // To execute the read method operation, use the given gateway URI or a default Pkarr relay. - const relay = options?.gatewayUri ?? DEFAULT_PKARR_RELAY; + // To execute the read method operation, use the given gateway URI or a default. + const gatewayUri = options?.gatewayUri ?? DEFAULT_GATEWAY_URI; try { // Attempt to decode the z-base-32-encoded identifier. await DidDhtUtils.identifierToIdentityKey({ didUri }); // Attempt to retrieve the DID document and metadata from the DHT network. - const { didDocument, didMetadata } = await DidDhtDocument.get({ didUri, relay }); + const { didDocument, didDocumentMetadata } = await DidDhtDocument.get({ didUri, gatewayUri }); // If the DID document was retrieved successfully, return it. return { ...EMPTY_DID_RESOLUTION_RESULT, didDocument, - didDocumentMetadata: { published: true, ...didMetadata } + didDocumentMetadata }; } catch (error: any) { @@ -731,142 +769,6 @@ export class DidDht extends DidMethod { }; } } - - /** - * Instantiates a `BearerDid` object for the DID DHT method using an array of public keys. - * - * This method is used to create a `BearerDid` object from a set of public keys, typically after - * these keys have been generated or provided. It constructs the DID document, metadata, and - * other necessary components for the DID based on the provided public keys and any additional - * options specified. - * - * @param params - The parameters for the DID object creation. - * @param params.keyManager - The Key Management System to manage keys. - * @param params.options - Additional options for DID creation. - * @returns A Promise resolving to a `BearerDid` object. - */ - private static async fromPublicKeys({ keyManager, verificationMethods, options }: { - keyManager: CryptoApi; - options: DidDhtCreateOptions; - } & PortableDid): Promise { - // Validate that the given verification methods contain an Identity Key. - const identityKey = verificationMethods?.find(vm => vm.id?.split('#').pop() === '0')?.publicKeyJwk; - if (!identityKey) { - throw new Error('Identity Key not found in verification methods'); - } - - // Compute the DID identifier from the Identity Key. - const id = await DidDhtUtils.identityKeyToIdentifier({ identityKey }); - - // Begin constructing the DID Document. - const didDocument: DidDocument = { - id, - ...options.alsoKnownAs && { alsoKnownAs: options.alsoKnownAs }, - ...options.controllers && { controller: options.controllers } - }; - - // Add verification methods to the DID document. - for (const vm of verificationMethods) { - if (!vm.publicKeyJwk) { - throw new Error(`Verification method does not contain a public key in JWK format`); - } - - // Use the given ID, the key's ID, or the key's thumbprint as the verification method ID. - let methodId = vm.id ?? vm.publicKeyJwk.kid ?? await computeJwkThumbprint({ jwk: vm.publicKeyJwk }); - methodId = `${id}#${methodId.split('#').pop()}`; // Remove fragment prefix, if any. - - // Initialize the `verificationMethod` array if it does not already exist. - didDocument.verificationMethod ??= []; - - // Add the verification method to the DID document. - didDocument.verificationMethod.push({ - id : methodId, - type : 'JsonWebKey', - controller : vm.controller ?? id, - publicKeyJwk : vm.publicKeyJwk, - }); - - // Add the verification method to the specified purpose properties of the DID document. - for (const purpose of vm.purposes ?? []) { - // Initialize the purpose property if it does not already exist. - if (!didDocument[purpose]) didDocument[purpose] = []; - // Add the verification method to the purpose property. - didDocument[purpose]!.push(methodId); - } - } - - // Add services, if any, to the DID document. - options.services?.forEach(service => { - didDocument.service ??= []; - service.id = `${id}#${service.id.split('#').pop()}`; // Remove fragment prefix, if any. - didDocument.service.push(service); - }); - - // Define DID Metadata, including the registered DID types (if any). - const metadata: DidMetadata = { - ...options.types && { types: options.types } - }; - - // Define a function that returns a signer for the DID. - const getSigner = async (params?: { keyUri?: string }) => await DidDht.getSigner({ - didDocument, - keyManager, - keyUri: params?.keyUri - }); - - return { didDocument, getSigner, keyManager, metadata, uri: id }; - } - - /** - * Generates a set of keys for use in creating a `BearerDid` object for the `did:dht` method. - * - * This method is responsible for generating the cryptographic keys necessary for the DID. It - * supports generating keys for the specified verification methods. - * - * @param params - The parameters for key generation. - * @param params.keyManager - The Key Management System used for generating keys. - * @param params.verificationMethods - Optional array of methods specifying key generation details. - * @returns A Promise resolving to a `PortableDid` object containing the generated keys. - */ - private static async generateKeys({ - keyManager, - verificationMethods - }: { - keyManager: CryptoApi; - verificationMethods: DidCreateVerificationMethod[]; - }): Promise { - let portableDid: PortableDid = { - verificationMethods: [] - }; - - // If the given verification methods do not contain an Identity Key, add one. - if (!verificationMethods?.some(vm => vm.id?.split('#').pop() === '0')) { - // Add the Identity Key to the beginning of the key set. - verificationMethods.unshift({ - algorithm : 'Ed25519' as any, - id : '0', - purposes : ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation'] - }); - } - - // Generate keys and add verification methods to the key set. - for (const vm of verificationMethods) { - // Generate a random key for the verification method. - const keyUri = await keyManager.generateKey({ algorithm: vm.algorithm }); - const publicKey = await keyManager.getPublicKey({ keyUri }); - - // Add the verification method to the `PortableDid`. - portableDid.verificationMethods.push({ - id : vm.id, - type : 'JsonWebKey', - controller : vm.controller, - publicKeyJwk : publicKey, - purposes : vm.purposes - }); - } - - return portableDid; - } } /** @@ -874,34 +776,38 @@ export class DidDht extends DidMethod { * Mainline DHT in support of DID DHT method create, resolve, update, and deactivate operations. * * This class includes methods for retrieving and publishing DID documents to and from the DHT, - * using DNS packet encoding and Pkarr relay servers. + * using DNS packet encoding and DID DHT Gateway or Pkarr Relay servers. */ -class DidDhtDocument { +export class DidDhtDocument { /** * Retrieves a DID document and its metadata from the DHT network. * * @param params - The parameters for the get operation. * @param params.didUri - The DID URI containing the Identity Key. - * @param params.relay - The Pkarr relay server URL. - * @returns A promise resolving to an object containing the DID document and its metadata. + * @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI. + * @returns A Promise resolving to a {@link DidResolutionResult} object containing the DID + * document and its metadata. */ - public static async get({ didUri, relay }: { + public static async get({ didUri, gatewayUri }: { didUri: string; - relay: string; - }): Promise<{ didDocument: DidDocument, didMetadata: DidMetadata }> { + gatewayUri: string; + }): Promise { // Decode the z-base-32 DID identifier to public key as a byte array. const publicKeyBytes = DidDhtUtils.identifierToIdentityKeyBytes({ didUri }); - // Retrieve the signed BEP44 message from a Pkarr relay. - const bep44Message = await DidDhtDocument.pkarrGet({ relay, publicKeyBytes }); + // Retrieve the signed BEP44 message from a DID DHT Gateway or Pkarr relay. + const bep44Message = await DidDhtDocument.pkarrGet({ gatewayUri, publicKeyBytes }); // Verify the signature of the BEP44 message and parse the value to a DNS packet. const dnsPacket = await DidDhtUtils.parseBep44GetMessage({ bep44Message }); - // Convert the DNS packet to a DID document and DID metadata. - const { didDocument, didMetadata } = await DidDhtDocument.fromDnsPacket({ didUri, dnsPacket }); + // Convert the DNS packet to a DID document and metadata. + const resolutionResult = await DidDhtDocument.fromDnsPacket({ didUri, dnsPacket }); - return { didDocument, didMetadata }; + // Set the version ID of the DID document metadata to the sequence number of the BEP44 message. + resolutionResult.didDocumentMetadata.versionId = bep44Message.seq.toString(); + + return resolutionResult; } /** @@ -909,17 +815,17 @@ class DidDhtDocument { * * @param params - The parameters to use when publishing the DID document to the DHT network. * @param params.did - The DID object whose DID document will be published. - * @param params.relay - The Pkarr relay to use when publishing the DID document. - * @returns A promise that resolves to `true` if the DID document was published successfully. - * If publishing fails, `false` is returned. + * @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI. + * @returns A promise that resolves to a {@link DidRegistrationResult} object that contains + * the result of registering the DID with a DID DHT Gateway or Pkarr relay. */ - public static async put({ did, relay }: { + public static async put({ did, gatewayUri }: { did: BearerDid; - relay: string; - }): Promise { + gatewayUri: string; + }): Promise { // Convert the DID document and DID metadata (such as DID types) to a DNS packet. const dnsPacket = await DidDhtDocument.toDnsPacket({ - didDocument : did.didDocument, + didDocument : did.document, didMetadata : did.metadata }); @@ -927,35 +833,46 @@ class DidDhtDocument { const bep44Message = await DidDhtUtils.createBep44PutMessage({ dnsPacket, publicKeyBytes : DidDhtUtils.identifierToIdentityKeyBytes({ didUri: did.uri }), - signer : await did.getSigner() + signer : await did.getSigner({ methodId: '0' }) }); // Publish the DNS packet to the DHT network. - const putResult = await DidDhtDocument.pkarrPut({ relay, bep44Message }); - - return putResult; + const putResult = await DidDhtDocument.pkarrPut({ gatewayUri, bep44Message }); + + // Return the result of processing the PUT operation, including the updated DID metadata with + // the version ID and the publishing result. + return { + didDocument : did.document, + didDocumentMetadata : { + ...did.metadata, + published : putResult, + versionId : bep44Message.seq.toString() + }, + didRegistrationMetadata: {} + }; } /** - * Retrieves a signed BEP44 message from a Pkarr relay server. + * Retrieves a signed BEP44 message from a DID DHT Gateway or Pkarr Relay server. * * @see {@link https://github.com/Nuhvi/pkarr/blob/main/design/relays.md | Pkarr Relay design} * - * @param relay - The Pkarr relay server URL. - * @param publicKeyBytes - The public key bytes of the Identity Key, z-base-32 encoded. + * @param params + * @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI. + * @param params.publicKeyBytes - The public key bytes of the Identity Key, z-base-32 encoded. * @returns A promise resolving to a BEP44 message containing the signed DNS packet. */ - private static async pkarrGet({ relay, publicKeyBytes }: { + private static async pkarrGet({ gatewayUri, publicKeyBytes }: { publicKeyBytes: Uint8Array; - relay: string; + gatewayUri: string; }): Promise { // The identifier (key in the DHT) is the z-base-32 encoding of the Identity Key. const identifier = Convert.uint8Array(publicKeyBytes).toBase32Z(); - // Concatenate the Pkarr relay URL with the identifier to form the full URL. - const url = new URL(identifier, relay).href; + // Concatenate the gateway URI with the identifier to form the full URL. + const url = new URL(identifier, gatewayUri).href; - // Transmit the Get request to the Pkarr relay and get the response. + // Transmit the Get request to the DID DHT Gateway or Pkarr Relay and get the response. let response: Response; try { response = await fetch(url, { method: 'GET' }); @@ -992,23 +909,24 @@ class DidDhtDocument { } /** - * Publishes a signed BEP44 message to a Pkarr relay server. + * Publishes a signed BEP44 message to a DID DHT Gateway or Pkarr Relay server. * * @see {@link https://github.com/Nuhvi/pkarr/blob/main/design/relays.md | Pkarr Relay design} * - * @param relay - The Pkarr relay server URL. - * @param bep44Message - The BEP44 message to be published, containing the signed DNS packet. + * @param params - The parameters to use when publishing a signed BEP44 message to a Pkarr relay server. + * @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI. + * @param params.bep44Message - The BEP44 message to be published, containing the signed DNS packet. * @returns A promise resolving to `true` if the message was successfully published, otherwise `false`. */ - private static async pkarrPut({ relay, bep44Message }: { + private static async pkarrPut({ gatewayUri, bep44Message }: { bep44Message: Bep44Message; - relay: string; + gatewayUri: string; }): Promise { // The identifier (key in the DHT) is the z-base-32 encoding of the Identity Key. const identifier = Convert.uint8Array(bep44Message.k).toBase32Z(); - // Concatenate the Pkarr relay URL with the identifier to form the full URL. - const url = new URL(identifier, relay).href; + // Concatenate the gateway URI with the identifier to form the full URL. + const url = new URL(identifier, gatewayUri).href; // Construct the body of the request according to the Pkarr relay specification. const body = new Uint8Array(bep44Message.v.length + 72); @@ -1041,15 +959,20 @@ class DidDhtDocument { * @param params - The parameters to use when converting a DNS packet to a DID document. * @param params.didUri - The DID URI of the DID document. * @param params.dnsPacket - The DNS packet to convert to a DID document. - * @returns A promise that resolves to a DID document. + * @returns A Promise resolving to a {@link DidResolutionResult} object containing the DID + * document and its metadata. */ private static async fromDnsPacket({ didUri, dnsPacket }: { didUri: string; dnsPacket: Packet; - }): Promise<{ didDocument: DidDocument, didMetadata: DidMetadata }> { + }): Promise { // Begin constructing the DID Document. const didDocument: DidDocument = { id: didUri }; - const didMetadata: DidMetadata = {}; + + // Since the DID document is being retrieved from the DHT, it is considered published. + const didDocumentMetadata: DidMetadata = { + published: true + }; const idLookup = new Map(); @@ -1147,7 +1070,7 @@ class DidDhtDocument { const { id: types } = DidDhtUtils.parseTxtDataToObject(answer.data); // Add the DID DHT Registered DID Types represented as numbers to DID metadata. - didMetadata.types = types.split(VALUE_SEPARATOR).map(typeInteger => Number(typeInteger)); + didDocumentMetadata.types = types.split(VALUE_SEPARATOR).map(typeInteger => Number(typeInteger)); break; } @@ -1175,7 +1098,7 @@ class DidDhtDocument { } } - return { didDocument, didMetadata }; + return { didDocument, didDocumentMetadata, didResolutionMetadata: {} }; } /** @@ -1349,7 +1272,7 @@ class DidDhtDocument { * This includes functions for creating and parsing BEP44 messages, handling identity keys, and * converting between different formats and representations. */ -class DidDhtUtils { +export class DidDhtUtils { /** * Creates a BEP44 put message, which is used to publish a DID document to the DHT network. * diff --git a/packages/dids/src/methods/did-ion.ts b/packages/dids/src/methods/did-ion.ts index 1e26ba1c9..8b0e4e192 100644 --- a/packages/dids/src/methods/did-ion.ts +++ b/packages/dids/src/methods/did-ion.ts @@ -1,4 +1,4 @@ -import type { CryptoApi, Jwk } from '@web5/crypto'; +import type { CryptoApi, Jwk, KeyIdentifier, KeyImporterExporter, KmsExportKeyParams, KmsImportKeyParams } from '@web5/crypto'; import type { JwkEs256k, IonDocumentModel, @@ -9,25 +9,22 @@ import type { import { IonDid, IonRequest } from '@decentralized-identity/ion-sdk'; import { LocalKeyManager, computeJwkThumbprint } from '@web5/crypto'; +import type { PortableDid } from '../types/portable-did.js'; +import type { DidCreateOptions, DidCreateVerificationMethod, DidRegistrationResult } from '../methods/did-method.js'; import type { DidService, DidDocument, DidResolutionResult, DidResolutionOptions, DidVerificationMethod, + DidVerificationRelationship, } from '../types/did-core.js'; -import type { - BearerDid, - DidMetadata, - PortableDid, - DidCreateOptions, - DidCreateVerificationMethod, - PortableDidVerificationMethod, -} from '../methods/did-method.js'; import { Did } from '../did.js'; +import { BearerDid } from '../bearer-did.js'; import { DidMethod } from '../methods/did-method.js'; import { DidError, DidErrorCode } from '../did-error.js'; +import { getVerificationRelationshipsById } from '../utils.js'; import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; /** @@ -146,6 +143,88 @@ export interface DidIonCreateRequest { } } +/** + * Represents a {@link DidVerificationMethod | DID verification method} in the context of DID ION + * create, update, deactivate, and resolve operations. + * + * Unlike the DID Core standard {@link DidVerificationMethod} interface, this type is specific to + * the ION method operations and only includes the `id`, `publicKeyJwk`, and `purposes` properties: + * - The `id` property is optional and specifies the identifier fragment of the verification method. + * - The `publicKeyJwk` property is required and represents the public key in JWK format. + * - The `purposes` property is required and specifies the purposes for which the verification + * method can be used. + * + * @example + * ```ts + * const verificationMethod: DidIonVerificationMethod = { + * id : 'sig', + * publicKeyJwk : { + * kty : 'OKP', + * crv : 'Ed25519', + * x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo', + * kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ', + * }, + * purposes: ['authentication', 'assertionMethod'] + * }; + * ``` + */ +export interface DidIonVerificationMethod { + /** + * Optionally specify the identifier fragment of the verification method. + * + * If not specified, the method's ID will be generated from the key's ID or thumbprint. + * + * @example + * ```ts + * const verificationMethod: DidIonVerificationMethod = { + * id: 'sig', + * ... + * }; + * ``` + */ + id?: string; + + /** + * A public key in JWK format. + * + * A JSON Web Key (JWK) that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}. + * + * @example + * ```ts + * const verificationMethod: DidIonVerificationMethod = { + * publicKeyJwk: { + * kty : "OKP", + * crv : "X25519", + * x : "7XdJtNmJ9pV_O_3mxWdn6YjiHJ-HhNkdYQARzVU_mwY", + * kid : "xtsuKULPh6VN9fuJMRwj66cDfQyLaxuXHkMlmAe_v6I" + * }, + * ... + * }; + * ``` + */ + publicKeyJwk: Jwk; + + /** + * Specify the purposes for which a verification method is intended to be used in a DID document. + * + * The `purposes` property defines the specific + * {@link DidVerificationRelationship | verification relationships} between the DID subject and + * the verification method. This enables the verification method to be utilized for distinct + * actions such as authentication, assertion, key agreement, capability delegation, and others. It + * is important for verifiers to recognize that a verification method must be associated with the + * relevant purpose in the DID document to be valid for that specific use case. + * + * @example + * ```ts + * const verificationMethod: DidIonVerificationMethod = { + * purposes: ['authentication', 'assertionMethod'], + * ... + * }; + * ``` + */ + purposes: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[]; +} + /** * `IonPortableDid` interface extends the {@link PortableDid} interface. * @@ -219,7 +298,8 @@ const DEFAULT_GATEWAY_URI = 'https://ion.tbd.engineering'; * - DID Key Management: Instantiate a DID object from an existing key in a Key Management System * (KMS). If supported by the KMS, a DID's key can be exported to a portable * DID format. - * - DID Resolution: Resolve a `did:ion` to its corresponding DID Document stored in the DHT network. + * - DID Resolution: Resolve a `did:ion` to its corresponding DID Document stored in the Sidetree + * network. * - Signature Operations: Sign and verify messages using keys associated with a DID. * * @see {@link https://identity.foundation/sidetree/spec/ | Sidetree Protocol Specification} @@ -300,26 +380,80 @@ export class DidIon extends DidMethod { throw new Error('One or more verification method algorithms are not supported'); } - // Check 2: Validate that the required properties for any given services are present. + // Check 2: Validate that the ID for any given verification method is unique. + const methodIds = options.verificationMethods?.filter(vm => 'id' in vm).map(vm => vm.id); + if (methodIds && methodIds.length !== new Set(methodIds).size) { + throw new Error('One or more verification method IDs are not unique'); + } + + // Check 3: Validate that the required properties for any given services are present. if (options.services?.some(s => !s.id || !s.type || !s.serviceEndpoint)) { throw new Error('One or more services are missing required properties'); } - // Generate random key material for the ION Recovery Key, ION Update Key, and any additional - // verification methods. - const keySet = await DidIon.generateKeys({ - keyManager, - verificationMethods: options.verificationMethods ?? [] + // If no verification methods were specified, generate a default Ed25519 verification method. + const defaultVerificationMethod: DidCreateVerificationMethod = { + algorithm : 'Ed25519' as any, + purposes : ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation'] + }; + + const verificationMethodsToAdd: DidIonVerificationMethod[] = []; + + // Generate random key material for additional verification methods, if any. + for (const vm of options.verificationMethods ?? [defaultVerificationMethod]) { + // Generate a random key for the verification method. + const keyUri = await keyManager.generateKey({ algorithm: vm.algorithm }); + const publicKey = await keyManager.getPublicKey({ keyUri }); + + // Add the verification method to the DID document. + verificationMethodsToAdd.push({ + id : vm.id, + publicKeyJwk : publicKey, + purposes : vm.purposes ?? ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation'] + }); + } + + // Generate a random key for the ION Recovery Key. Sidetree requires secp256k1 recovery keys. + const recoveryKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 }); + const recoveryKey = await keyManager.getPublicKey({ keyUri: recoveryKeyUri }); + + // Generate a random key for the ION Update Key. Sidetree requires secp256k1 update keys. + const updateKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 }); + const updateKey = await keyManager.getPublicKey({ keyUri: updateKeyUri }); + + // Compute the Long Form DID URI from the keys and services, if any. + const longFormDidUri = await DidIonUtils.computeLongFormDidUri({ + recoveryKey, + updateKey, + services : options.services ?? [], + verificationMethods : verificationMethodsToAdd }); - // Create the DID object from the generated key material, including DID document, metadata, - // signer convenience function, and URI. - const did = await DidIon.fromPublicKeys({ keyManager, options, ...keySet }); + // Expand the DID URI string to a DID document. + const { didDocument, didResolutionMetadata } = await DidIon.resolve(longFormDidUri, { gatewayUri: options.gatewayUri }); + if (didDocument === null) { + throw new Error(`Unable to resolve DID during creation: ${didResolutionMetadata?.error}`); + } + + // Create the BearerDid object, including the "Short Form" of the DID URI, the ION update and + // recovery keys, and specifying that the DID has not yet been published. + const did = new BearerDid({ + uri : longFormDidUri, + document : didDocument, + metadata : { + published : false, + canonicalId : longFormDidUri.split(':', 3).join(':'), + recoveryKey, + updateKey + }, + keyManager + }); // By default, publish the DID document to a Sidetree node unless explicitly disabled. - did.metadata.published = options.publish ?? true - ? await this.publish({ did, gatewayUri: options.gatewayUri }) - : false; + if (options.publish ?? true) { + const registrationResult = await DidIon.publish({ did, gatewayUri: options.gatewayUri }); + did.metadata = registrationResult.didDocumentMetadata; + } return did; } @@ -338,24 +472,65 @@ export class DidIon extends DidMethod { public static async getSigningMethod({ didDocument, methodId }: { didDocument: DidDocument; methodId?: string; - }): Promise { + }): Promise { // Verify the DID method is supported. const parsedDid = Did.parse(didDocument.id); if (parsedDid && parsedDid.method !== this.methodName) { throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`); } - // Get the ID of the first verification method intended for signing. - const [ firstAuthMethodId ] = didDocument.authentication || []; - - // Get the verification method with either the specified ID or the first authentication method. + // Get the verification method with either the specified ID or the first assertion method. const verificationMethod = didDocument.verificationMethod?.find( - vm => vm.id === (methodId ?? firstAuthMethodId) + vm => vm.id === (methodId ?? didDocument.assertionMethod?.[0]) ); + if (!(verificationMethod && verificationMethod.publicKeyJwk)) { + throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document'); + } + return verificationMethod; } + /** + * Instantiates a {@link BearerDid} object for the DID ION method from a given {@link PortableDid}. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @example + * ```ts + * // Export an existing BearerDid to PortableDid format. + * const portableDid = await did.export(); + * // Reconstruct a BearerDid object from the PortableDid. + * const did = await DidIon.import({ portableDid }); + * ``` + * + * @param params - The parameters for the import operation. + * @param params.portableDid - The PortableDid object to import. + * @param params.keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * {@link LocalKeyManager} instance will be created and + * used. + * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the + * provided PortableDid. + * @throws An error if the DID document does not contain any verification methods or the keys for + * any verification method are missing in the key manager. + */ + public static async import({ portableDid, keyManager = new LocalKeyManager() }: { + keyManager?: CryptoApi & KeyImporterExporter; + portableDid: PortableDid; + }): Promise { + // Verify the DID method is supported. + const parsedDid = Did.parse(portableDid.uri); + if (parsedDid?.method !== DidIon.methodName) { + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`); + } + + const did = await BearerDid.import({ portableDid, keyManager }); + + return did; + } + /** * Publishes a DID to a Sidetree node, making it publicly discoverable and resolvable. * @@ -388,11 +563,21 @@ export class DidIon extends DidMethod { public static async publish({ did, gatewayUri = DEFAULT_GATEWAY_URI }: { did: BearerDid; gatewayUri?: string; - }): Promise { + }): Promise { + // Construct an ION verification method made up of the id, public key, and purposes from each + // verification method in the DID document. + const verificationMethods: DidIonVerificationMethod[] = did.document.verificationMethod?.map( + vm => ({ + id : vm.id, + publicKeyJwk : vm.publicKeyJwk!, + purposes : getVerificationRelationshipsById({ didDocument: did.document, methodId: vm.id }) + }) + ) ?? []; + // Create the ION document. const ionDocument = await DidIonUtils.createIonDocument({ - services : did.didDocument.service ?? [], - verificationMethods : did.didDocument.verificationMethod ?? [] + services: did.document.service ?? [], + verificationMethods }); // Construct the ION Create Operation request. @@ -417,11 +602,28 @@ export class DidIon extends DidMethod { body : JSON.stringify(createOperation) }); - // Return true if the Create Operation was processed successfully; false otherwise. - return response.ok; + // Return the result of processing the Create operation, including the updated DID metadata + // with the publishing result. + return { + didDocument : did.document, + didDocumentMetadata : { + ...did.metadata, + published: response.ok, + }, + didRegistrationMetadata: {} + }; } catch (error: any) { - throw new DidError(DidErrorCode.InternalError, `Failed to publish DID document for: ${did.uri}`); + return { + didDocument : null, + didDocumentMetadata : { + published: false, + }, + didRegistrationMetadata: { + error : DidErrorCode.InternalError, + errorMessage : `Failed to publish DID document for: ${did.uri}` + } + }; } } @@ -510,128 +712,12 @@ export class DidIon extends DidMethod { }; } } - - /** - * Instantiates a `BearerDid` object for the DID ION method using an array of public keys. - * - * This method is used to create a `BearerDid` object from a set of public keys, typically after - * these keys have been generated or provided. It constructs the DID document, metadata, and - * other necessary components for the DID based on the provided public keys and any additional - * options specified. - * - * @param params - The parameters for the DID object creation. - * @param params.keyManager - The Key Management System to manage keys. - * @param params.options - Additional options for DID creation. - * @returns A Promise resolving to a `BearerDid` object. - */ - private static async fromPublicKeys({ keyManager, recoveryKey, updateKey, verificationMethods, options }: { - keyManager: CryptoApi; - options: DidIonCreateOptions; - } & IonPortableDid): Promise { - // Validate an ION Recovery Key was generated or provided. - if (!recoveryKey) { - throw new Error('Missing required input: ION Recovery Key'); - } - - // Validate an ION Update Key was generated or provided. - if (!updateKey) { - throw new Error('Missing required input: ION Update Key'); - } - - // Compute the Long Form DID URI from the keys and services, if any. - const id = await DidIonUtils.computeLongFormDidUri({ - recoveryKey, - updateKey, - verificationMethods, - services: options.services ?? [] - }); - - // Expand the DID URI string to a DID document. - const { didDocument, didResolutionMetadata } = await DidIon.resolve(id, { gatewayUri: options.gatewayUri }); - if (didDocument === null) { - throw new Error(`Unable to resolve DID during creation: ${didResolutionMetadata?.error}`); - } - - // Define DID Metadata, including the "Short Form" of the DID URI. - const metadata: DidMetadata = { - canonicalId: id.split(':', 3).join(':'), - recoveryKey, - updateKey - }; - - // Define a function that returns a signer for the DID. - const getSigner = async (params?: { keyUri?: string }) => await DidIon.getSigner({ - didDocument, - keyManager, - keyUri: params?.keyUri - }); - - return { didDocument, getSigner, keyManager, metadata, uri: id }; - } - - /** - * Generates a set of keys for use in creating a `BearerDid` object for the DID ION method. - * - * This method is responsible for generating the cryptographic keys necessary for the DID, - * including the ION Update and Recovery keys and any verification methods specified in the - * `verificationMethods` parameter. - * - * @param params - The parameters for key generation. - * @param params.keyManager - The Key Management System used for generating keys. - * @param params.verificationMethods - Optional array of methods specifying key generation details. - * @returns A Promise resolving to a `PortableDid` object containing the generated keys. - */ - private static async generateKeys({ - keyManager, - verificationMethods - }: { - keyManager: CryptoApi; - verificationMethods: DidCreateVerificationMethod[]; - }): Promise { - let portableDid: PortableDid = { - verificationMethods: [] - }; - - // If no verification methods were specified, generate a default Ed25519 verification method. - if (verificationMethods.length === 0) { - verificationMethods = [{ - algorithm : 'Ed25519', - purposes : ['authentication', 'assertionMethod'] - }]; - } - - // Generate keys and add verification methods to the key set. - for (const vm of verificationMethods) { - // Generate a random key for the verification method. - const keyUri = await keyManager.generateKey({ algorithm: vm.algorithm }); - const publicKey = await keyManager.getPublicKey({ keyUri }); - - // Add the verification method to the `PortableDid`. - portableDid.verificationMethods.push({ - id : vm.id, - type : 'JsonWebKey', - controller : vm.controller, - publicKeyJwk : publicKey, - purposes : vm.purposes - }); - } - - // Generate a random key for the ION Recovery Key. Sidetree requires secp256k1 recovery keys. - const recoveryKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 }); - const recoveryKey = await keyManager.getPublicKey({ keyUri: recoveryKeyUri }); - - // Generate a random key for the ION Update Key. Sidetree requires secp256k1 update keys. - const updateKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 }); - const updateKey = await keyManager.getPublicKey({ keyUri: updateKeyUri }); - - return { ...portableDid, recoveryKey, updateKey }; - } } /** * The `DidIonUtils` class provides utility functions to support operations in the DID ION method. */ -class DidIonUtils { +export class DidIonUtils { /** * Appends a specified path to a base URL, ensuring proper formatting of the resulting URL. * @@ -668,9 +754,9 @@ class DidIonUtils { */ public static async computeLongFormDidUri({ recoveryKey, updateKey, services, verificationMethods }: { recoveryKey: Jwk; - services: DidService[]; updateKey: Jwk; - verificationMethods: PortableDidVerificationMethod[]; + services: DidService[]; + verificationMethods: DidIonVerificationMethod[]; }): Promise { // Create the ION document. const ionDocument = await DidIonUtils.createIonDocument({ services, verificationMethods }); @@ -732,7 +818,7 @@ class DidIonUtils { */ public static async createIonDocument({ services, verificationMethods }: { services: DidService[]; - verificationMethods: PortableDidVerificationMethod[] + verificationMethods: DidIonVerificationMethod[] }): Promise { /** * STEP 1: Convert verification methods to ION SDK format. @@ -740,10 +826,6 @@ class DidIonUtils { const ionPublicKeys: IonPublicKeyModel[] = []; for (const vm of verificationMethods) { - if (!vm.publicKeyJwk) { - throw new Error(`Verification method does not contain a public key in JWK format`); - } - // Use the given ID, the key's ID, or the key's thumbprint as the verification method ID. let methodId = vm.id ?? vm.publicKeyJwk.kid ?? await computeJwkThumbprint({ jwk: vm.publicKeyJwk }); methodId = `${methodId.split('#').pop()}`; // Remove fragment prefix, if any. diff --git a/packages/dids/src/methods/did-jwk.ts b/packages/dids/src/methods/did-jwk.ts index 70d9e0a6c..88c0f56fd 100644 --- a/packages/dids/src/methods/did-jwk.ts +++ b/packages/dids/src/methods/did-jwk.ts @@ -11,11 +11,13 @@ import type { import { Convert } from '@web5/common'; import { LocalKeyManager } from '@web5/crypto'; -import type { BearerDid, DidCreateOptions, DidCreateVerificationMethod, DidMetadata, PortableDid } from './did-method.js'; +import type { PortableDid } from '../types/portable-did.js'; +import type { DidCreateOptions, DidCreateVerificationMethod } from './did-method.js'; import type { DidDocument, DidResolutionOptions, DidResolutionResult, DidVerificationMethod } from '../types/did-core.js'; import { Did } from '../did.js'; import { DidMethod } from './did-method.js'; +import { BearerDid } from '../bearer-did.js'; import { DidError, DidErrorCode } from '../did-error.js'; import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; @@ -32,7 +34,9 @@ import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; * * @example * ```ts - * // By default, when no options are given, a new Ed25519 key will be generated. + * // DID Creation + * + * // By default, when no options are given, a new Ed25519 key will be generated. * const did = await DidJwk.create(); * * // The algorithm to use for key generation can be specified as a top-level option. @@ -46,6 +50,26 @@ import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; * verificationMethods: [{ algorithm = 'ES256K' }] * } * }); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidJwk.create({ keyManager }); + * + * // DID Resolution + * const resolutionResult = await DidJwk.resolve({ did: did.uri }); + * + * // Signature Operations + * const signer = await did.getSigner(); + * const signature = await signer.sign({ data: new TextEncoder().encode('Message') }); + * const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature }); + * + * // Import / Export + * + * // Export a BearerDid object to the PortableDid format. + * const portableDid = await did.export(); + * + * // Reconstruct a BearerDid object from a PortableDid + * const did = await DidJwk.import(portableDid); * ``` */ export interface DidJwkCreateOptions extends DidCreateOptions { @@ -176,10 +200,20 @@ export class DidJwk extends DidMethod { keyManager?: TKms; options?: DidJwkCreateOptions; } = {}): Promise { + // Before processing the create operation, validate DID-method-specific requirements to prevent + // keys from being generated unnecessarily. + + // Check 1: Validate that `algorithm` or `verificationMethods` options are not both given. if (options.algorithm && options.verificationMethods) { throw new Error(`The 'algorithm' and 'verificationMethods' options are mutually exclusive`); } + // Check 2: If `verificationMethods` is given, it must contain exactly one entry since DID JWK + // only supports a single verification method. + if (options.verificationMethods && options.verificationMethods.length !== 1) { + throw new Error(`The 'verificationMethods' option must contain exactly one entry`); + } + // Default to Ed25519 key generation if an algorithm is not given. const algorithm = options.algorithm ?? options.verificationMethods?.[0]?.algorithm ?? 'Ed25519'; @@ -187,70 +221,23 @@ export class DidJwk extends DidMethod { const keyUri = await keyManager.generateKey({ algorithm }); const publicKey = await keyManager.getPublicKey({ keyUri }); - // Create the DID object from the generated key material, including DID document, metadata, - // signer convenience function, and URI. - const did = await DidJwk.fromPublicKey({ keyManager, publicKey }); - - return did; - } + // Compute the DID identifier from the public key by serializing the JWK to a UTF-8 string and + // encoding in Base64URL format. + const identifier = Convert.object(publicKey).toBase64Url(); - /** - * Instantiates a {@link BearerDid} object for the `did:jwk` method from a given - * {@link PortableDid}. - * - * This method allows for the creation of a `BearerDid` object using pre-existing key material, - * encapsulated within the `verificationMethods` array of the `PortableDid`. This is particularly - * useful when the key material is already available and you want to construct a `BearerDid` - * object based on these keys, instead of generating new keys. - * - * @remarks - * The `verificationMethods` array must contain exactly one key since the `did:jwk` method only - * supports a single verification method. - * - * The key material (both public and private keys) should be provided in JWK format. The method - * handles the inclusion of these keys in the DID Document and sets up the necessary verification - * relationships. - * - * @example - * ```ts - * // Example with an existing key in JWK format. - * const verificationMethods = [{ - * publicKeyJwk: { // public key in JWK format }, - * privateKeyJwk: { // private key in JWK format } - * }]; - * const did = await DidJwk.fromKeys({ verificationMethods }); - * ``` - * - * @param params - The parameters for the `fromKeys` operation. - * @param params.keyManager - Optionally specify an external Key Management System (KMS) used to - * generate keys and sign data. If not given, a new - * {@link @web5/crypto#LocalKeyManager} instance will be created and used. - * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the provided keys. - * @throws An error if the `verificationMethods` array does not contain exactly one entry. - */ - public static async fromKeys({ - keyManager = new LocalKeyManager(), - verificationMethods - }: { - keyManager?: CryptoApi & KeyImporterExporter; - options?: unknown; - } & PortableDid): Promise { - if (!verificationMethods || verificationMethods.length !== 1) { - throw new Error(`Only one verification method can be specified but ${verificationMethods?.length ?? 0} were given`); - } - - if (!(verificationMethods[0].privateKeyJwk && verificationMethods[0].publicKeyJwk)) { - throw new Error(`Verification method does not contain a public and private key in JWK format`); - } - - // Store the private key in the key manager. - await keyManager.importKey({ key: verificationMethods[0].privateKeyJwk }); + // Attach the prefix `did:jwk` to form the complete DID URI. + const didUri = `did:${DidJwk.methodName}:${identifier}`; - // Create the DID object from the given key material, including DID document, metadata, - // signer convenience function, and URI. - const did = await DidJwk.fromPublicKey({ - keyManager, - publicKey: verificationMethods[0].publicKeyJwk + // Expand the DID URI string to a DID document. + const didResolutionResult = await DidJwk.resolve(didUri); + const document = didResolutionResult.didDocument as DidDocument; + + // Create the BearerDid object from the generated key material. + const did = new BearerDid({ + uri : didUri, + document, + metadata : {}, + keyManager }); return did; @@ -270,10 +257,10 @@ export class DidJwk extends DidMethod { * @param params.methodId - ID of the verification method to use for signing. * @returns Verification method to use for signing. */ - public static async getSigningMethod({ didDocument, methodId = '#0' }: { + public static async getSigningMethod({ didDocument }: { didDocument: DidDocument; methodId?: string; - }): Promise { + }): Promise { // Verify the DID method is supported. const parsedDid = Did.parse(didDocument.id); if (parsedDid && parsedDid.method !== this.methodName) { @@ -281,11 +268,65 @@ export class DidJwk extends DidMethod { } // Attempt to find the verification method in the DID Document. - const verificationMethod = didDocument.verificationMethod?.find(vm => vm.id.endsWith(methodId)); + const [ verificationMethod ] = didDocument.verificationMethod ?? []; + + if (!(verificationMethod && verificationMethod.publicKeyJwk)) { + throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document'); + } return verificationMethod; } + /** + * Instantiates a {@link BearerDid} object for the DID JWK method from a given {@link PortableDid}. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @remarks + * The `verificationMethod` array of the DID document must contain exactly one key since the + * `did:jwk` method only supports a single verification method. + * + * @example + * ```ts + * // Export an existing BearerDid to PortableDid format. + * const portableDid = await did.export(); + * // Reconstruct a BearerDid object from the PortableDid. + * const did = await DidJwk.import({ portableDid }); + * ``` + * + * @param params - The parameters for the import operation. + * @param params.portableDid - The PortableDid object to import. + * @param params.keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * {@link LocalKeyManager} instance will be created and + * used. + * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the provided keys. + * @throws An error if the DID document does not contain exactly one verification method. + */ + public static async import({ portableDid, keyManager = new LocalKeyManager() }: { + keyManager?: CryptoApi & KeyImporterExporter; + portableDid: PortableDid; + }): Promise { + // Verify the DID method is supported. + const parsedDid = Did.parse(portableDid.uri); + if (parsedDid?.method !== DidJwk.methodName) { + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`); + } + + // Use the given PortableDid to construct the BearerDid object. + const did = await BearerDid.import({ portableDid, keyManager }); + + // Validate that the given DID document contains exactly one verification method. + // Note: The non-undefined assertion is necessary because the type system cannot infer that + // the `verificationMethod` property is defined -- which is checked by `BearerDid.import()`. + if (did.document.verificationMethod!.length !== 1) { + throw new DidError(DidErrorCode.InvalidDidDocument, `DID document must contain exactly one verification method`); + } + + return did; + } + /** * Resolves a `did:jwk` identifier to a DID Document. * @@ -367,40 +408,4 @@ export class DidJwk extends DidMethod { didDocument, }; } - - /** - * Instantiates a {@link BearerDid} object for the DID JWK method from a given public key. - * - * @param params - The parameters for the operation. - * @param params.keyManager - A Key Management System (KMS) instance for managing keys and - * performing cryptographic operations. - * @param params.publicKey - The public key of the DID in JWK format. - * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the provided public key. - */ - private static async fromPublicKey({ keyManager, publicKey }: { - keyManager: CryptoApi; - publicKey: Jwk; - }): Promise { - // Serialize the public key JWK to a UTF-8 string and encode to Base64URL format. - const base64UrlEncoded = Convert.object(publicKey).toBase64Url(); - - // Attach the prefix `did:jwk` to form the complete DID URI. - const didUri = `did:${DidJwk.methodName}:${base64UrlEncoded}`; - - // Expand the DID URI string to a DID document. - const didResolutionResult = await DidJwk.resolve(didUri); - const didDocument = didResolutionResult.didDocument as DidDocument; - - // DID Metadata is initially empty for this DID method. - const metadata: DidMetadata = {}; - - // Define a function that returns a signer for the DID. - const getSigner = async (params?: { keyUri?: string }) => await DidJwk.getSigner({ - didDocument, - keyManager, - keyUri: params?.keyUri - }); - - return { didDocument, getSigner, keyManager, metadata, uri: didUri }; - } } \ No newline at end of file diff --git a/packages/dids/src/methods/did-key.ts b/packages/dids/src/methods/did-key.ts index 5180bddbf..06540face 100644 --- a/packages/dids/src/methods/did-key.ts +++ b/packages/dids/src/methods/did-key.ts @@ -11,7 +11,7 @@ import type { InferKeyGeneratorAlgorithm, } from '@web5/crypto'; -import { Convert, Multicodec, universalTypeOf } from '@web5/common'; +import { Multicodec, universalTypeOf } from '@web5/common'; import { X25519, Ed25519, @@ -20,44 +20,22 @@ import { LocalKeyManager, } from '@web5/crypto'; -import type { BearerDid, DidCreateOptions, DidCreateVerificationMethod, DidMetadata, PortableDid } from './did-method.js'; -import type { DidDocument, DidResolutionOptions, DidResolutionResult, DidVerificationMethod } from '../types/did-core.js'; +import type { PortableDid } from '../types/portable-did.js'; +import type { DidCreateOptions, DidCreateVerificationMethod } from './did-method.js'; +import type { + DidDocument, + DidResolutionOptions, + DidResolutionResult, + DidVerificationMethod, +} from '../types/did-core.js'; import { Did } from '../did.js'; import { DidMethod } from './did-method.js'; +import { BearerDid } from '../bearer-did.js'; import { DidError, DidErrorCode } from '../did-error.js'; -import { getVerificationMethodTypes } from '../utils.js'; +import { KeyWithMulticodec } from '../types/multibase.js'; import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; - -/** - * Represents a cryptographic key with associated multicodec metadata. - * - * The `KeyWithMulticodec` type encapsulates a cryptographic key along with optional multicodec - * information. It is primarily used in functions that convert between cryptographic keys and their - * string representations, ensuring that the key's format and encoding are preserved and understood - * across different systems and applications. - */ -export type KeyWithMulticodec = { - /** - * A `Uint8Array` representing the raw bytes of the cryptographic key. This is the primary data of - * the type and is essential for cryptographic operations. - */ - keyBytes: Uint8Array, - - /** - * An optional number representing the multicodec code. This code uniquely identifies the encoding - * format or protocol associated with the key. The presence of this code is crucial for decoding - * the key correctly in different contexts. - */ - multicodecCode?: number, - - /** - * An optional string representing the human-readable name of the multicodec. This name provides - * an easier way to identify the encoding format or protocol of the key, especially when the - * numerical code is not immediately recognizable. - */ - multicodecName?: string -}; +import { getVerificationMethodTypes, keyBytesToMultibaseId, multibaseIdToKeyBytes } from '../utils.js'; /** * Defines the set of options available when creating a new Decentralized Identifier (DID) with the @@ -86,6 +64,26 @@ export type KeyWithMulticodec = { * verificationMethods: [{ algorithm = 'secp256k1' }] * } * }); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidKey.create({ keyManager }); + * + * // DID Resolution + * const resolutionResult = await DidKey.resolve({ did: did.uri }); + * + * // Signature Operations + * const signer = await did.getSigner(); + * const signature = await signer.sign({ data: new TextEncoder().encode('Message') }); + * const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature }); + * + * // Import / Export + * + * // Export a BearerDid object to the PortableDid format. + * const portableDid = await did.export(); + * + * // Reconstruct a BearerDid object from a PortableDid + * const did = await DidKey.import(portableDid); * ``` */ export interface DidKeyCreateOptions extends DidCreateOptions { @@ -345,10 +343,20 @@ export class DidKey extends DidMethod { keyManager?: TKms; options?: DidKeyCreateOptions; } = {}): Promise { + // Before processing the create operation, validate DID-method-specific requirements to prevent + // keys from being generated unnecessarily. + + // Check 1: Validate that `algorithm` or `verificationMethods` options are not both given. if (options.algorithm && options.verificationMethods) { throw new Error(`The 'algorithm' and 'verificationMethods' options are mutually exclusive`); } + // Check 2: If `verificationMethods` is given, it must contain exactly one entry since DID Key + // only supports a single verification method. + if (options.verificationMethods && options.verificationMethods.length !== 1) { + throw new Error(`The 'verificationMethods' option must contain exactly one entry`); + } + // Default to Ed25519 key generation if an algorithm is not given. const algorithm = options.algorithm ?? options.verificationMethods?.[0]?.algorithm ?? 'Ed25519'; @@ -356,72 +364,23 @@ export class DidKey extends DidMethod { const keyUri = await keyManager.generateKey({ algorithm }); const publicKey = await keyManager.getPublicKey({ keyUri }); - // Create the DID object from the generated key material, including DID document, metadata, - // signer convenience function, and URI. - const did = await DidKey.fromPublicKey({ keyManager, publicKey, options }); - - return did; - } - - /** - * Instantiates a {@link BearerDid} object for the `did:jwk` method from a given - * {@link PortableDid}. - * - * This method allows for the creation of a `BearerDid` object using pre-existing key material, - * encapsulated within the `verificationMethods` array of the `PortableDid`. This is particularly - * useful when the key material is already available and you want to construct a `BearerDid` - * object based on these keys, instead of generating new keys. - * - * @remarks - * The `verificationMethods` array must contain exactly one key since the `did:jwk` method only - * supports a single verification method. - * - * The key material (both public and private keys) should be provided in JWK format. The method - * handles the inclusion of these keys in the DID Document and sets up the necessary verification - * relationships. - * - * @example - * ```ts - * // Example with an existing key in JWK format. - * const verificationMethods = [{ - * publicKeyJwk: { // public key in JWK format }, - * privateKeyJwk: { // private key in JWK format } - * }]; - * const did = await DidKey.fromKeys({ verificationMethods }); - * ``` - * - * @param params - The parameters for the `fromKeys` operation. - * @param params.keyManager - Optionally specify an external Key Management System (KMS) used to - * generate keys and sign data. If not given, a new - * {@link @web5/crypto#LocalKeyManager} instance will be created and used. - * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the provided keys. - * @throws An error if the `verificationMethods` array does not contain exactly one entry. - */ - public static async fromKeys({ - keyManager = new LocalKeyManager(), - verificationMethods, - options = {} - }: { - keyManager?: CryptoApi & KeyImporterExporter; - options?: DidKeyCreateOptions; - } & PortableDid): Promise { - if (!verificationMethods || verificationMethods.length !== 1) { - throw new Error(`Only one verification method can be specified but ${verificationMethods?.length ?? 0} were given`); - } - - if (!(verificationMethods[0].privateKeyJwk && verificationMethods[0].publicKeyJwk)) { - throw new Error(`Verification method does not contain a public and private key in JWK format`); - } + // Compute the DID identifier from the public key by converting the JWK to a multibase-encoded + // multicodec value. + const identifier = await DidKeyUtils.publicKeyToMultibaseId({ publicKey }); - // Store the private key in the key manager. - await keyManager.importKey({ key: verificationMethods[0].privateKeyJwk }); + // Attach the prefix `did:key` to form the complete DID URI. + const didUri = `did:${DidKey.methodName}:${identifier}`; - // Create the DID object from the given key material, including DID document, metadata, - // signer convenience function, and URI. - const did = await DidKey.fromPublicKey({ - keyManager, - publicKey: verificationMethods[0].publicKeyJwk, - options + // Expand the DID URI string to a DID document. + const didResolutionResult = await DidKey.resolve(didUri, options); + const document = didResolutionResult.didDocument as DidDocument; + + // Create the BearerDid object from the generated key material. + const did = new BearerDid({ + uri : didUri, + document, + metadata : {}, + keyManager }); return did; @@ -444,22 +403,74 @@ export class DidKey extends DidMethod { public static async getSigningMethod({ didDocument }: { didDocument: DidDocument; methodId?: string; - }): Promise { + }): Promise { // Verify the DID method is supported. const parsedDid = Did.parse(didDocument.id); if (parsedDid && parsedDid.method !== this.methodName) { throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`); } - // Get the ID of the first verification method intended for signing. - const [ methodId ] = didDocument.authentication || []; - - // Get the verification method with the specified ID. + // Attempt to ge the first verification method intended for signing claims. + const [ methodId ] = didDocument.assertionMethod || []; const verificationMethod = didDocument.verificationMethod?.find(vm => vm.id === methodId); + if (!(verificationMethod && verificationMethod.publicKeyJwk)) { + throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document'); + } + return verificationMethod; } + /** + * Instantiates a {@link BearerDid} object for the DID Key method from a given {@link PortableDid}. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @remarks + * The `verificationMethod` array of the DID document must contain exactly one key since the + * `did:key` method only supports a single verification method. + * + * @example + * ```ts + * // Export an existing BearerDid to PortableDid format. + * const portableDid = await did.export(); + * // Reconstruct a BearerDid object from the PortableDid. + * const did = await DidKey.import({ portableDid }); + * ``` + * + * @param params - The parameters for the import operation. + * @param params.portableDid - The PortableDid object to import. + * @param params.keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * {@link LocalKeyManager} instance will be created and + * used. + * @returns A Promise resolving to a `BearerDid` object representing the DID formed from the provided keys. + * @throws An error if the DID document does not contain exactly one verification method. + */ + public static async import({ portableDid, keyManager = new LocalKeyManager() }: { + keyManager?: CryptoApi & KeyImporterExporter; + portableDid: PortableDid; + }): Promise { + // Verify the DID method is supported. + const parsedDid = Did.parse(portableDid.uri); + if (parsedDid?.method !== DidKey.methodName) { + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`); + } + + // Use the given PortableDid to construct the BearerDid object. + const did = await BearerDid.import({ portableDid, keyManager }); + + // Validate that the given DID document contains exactly one verification method. + // Note: The non-undefined assertion is necessary because the type system cannot infer that + // the `verificationMethod` property is defined -- which is checked by `BearerDid.import()`. + if (did.document.verificationMethod!.length !== 1) { + throw new DidError(DidErrorCode.InvalidDidDocument, `DID document must contain exactly one verification method`); + } + + return did; + } + /** * Resolves a `did:key` identifier to a DID Document. * @@ -694,7 +705,7 @@ export class DidKey extends DidMethod { * base58-btc encoding of the concatenation of the multicodecValue and * the raw publicKeyBytes. */ - const kemMultibaseValue = DidKeyUtils.keyBytesToMultibaseId({ + const kemMultibaseValue = keyBytesToMultibaseId({ keyBytes : publicKeyBytes, multicodecCode : multicodecValue }); @@ -793,7 +804,7 @@ export class DidKey extends DidMethod { keyBytes: publicKeyBytes, multicodecCode: multicodecValue, multicodecName - } = DidKeyUtils.multibaseIdToKeyBytes({ multibaseKeyId: multibaseValue }); + } = multibaseIdToKeyBytes({ multibaseKeyId: multibaseValue }); /** * 3. Ensure the proper key length of publicKeyBytes based on the multicodecValue @@ -929,7 +940,7 @@ export class DidKey extends DidMethod { const { keyBytes: publicKeyBytes, multicodecCode: multicodecValue - } = DidKeyUtils.multibaseIdToKeyBytes({ multibaseKeyId: multibaseValue }); + } = multibaseIdToKeyBytes({ multibaseKeyId: multibaseValue }); /** * 4. If the multicodecValue is 0xed (Ed25519 public key), derive a public X25519 encryption key @@ -965,38 +976,6 @@ export class DidKey extends DidMethod { return publicEncryptionKey; } - /** - * Creates a new DID using the DID Key method formed from a public key. - * - */ - private static async fromPublicKey({ keyManager, publicKey, options }: { - keyManager: CryptoApi; - publicKey: Jwk; - options: DidKeyCreateOptions; - }): Promise { - // Convert the public key to a byte array and encode to Base64URL format. - const multibaseId = await DidKeyUtils.publicKeyToMultibaseId({ publicKey }); - - // Attach the prefix `did:jwk` to form the complete DID URI. - const didUri = `did:${DidKey.methodName}:${multibaseId}`; - - // Expand the DID URI string to a DID document. - const didResolutionResult = await DidKey.resolve(didUri, options); - const didDocument = didResolutionResult.didDocument as DidDocument; - - // DID Metadata is initially empty for this DID method. - const metadata: DidMetadata = {}; - - // Define a function that returns a signer for the DID. - const getSigner = async (params?: { keyUri?: string }) => await DidKey.getSigner({ - didDocument, - keyManager, - keyUri: params?.keyUri - }); - - return { didDocument, getSigner, keyManager, metadata, uri: didUri }; - } - /** * Validates the structure and components of a DID URI against the `did:key` method specification. * @@ -1101,7 +1080,7 @@ export class DidKeyUtils { * @example * ```ts * const jwk: Jwk = { crv: 'Ed25519', kty: 'OKP', x: '...' }; - * const { code, name } = await Jose.jwkToMulticodec({ jwk }); + * const { code, name } = await DidKeyUtils.jwkToMulticodec({ jwk }); * ``` * * @param params - The parameters for the conversion. @@ -1134,38 +1113,6 @@ export class DidKeyUtils { return { code, name }; } - /** - * Converts a cryptographic key to a multibase identifier. - * - * @remarks - * This method provides a way to represent a cryptographic key as a multibase identifier. - * It takes a `Uint8Array` representing the key, and either the multicodec code or multicodec name - * as input. The method first adds the multicodec prefix to the key, then encodes it into Base58 - * format. Finally, it converts the Base58 encoded key into a multibase identifier. - * - * @example - * ```ts - * const key = new Uint8Array([...]); // Cryptographic key as Uint8Array - * const multibaseId = keyBytesToMultibaseId({ key, multicodecName: 'ed25519-pub' }); - * ``` - * - * @param params - The parameters for the conversion. - * @returns The multibase identifier as a string. - */ - public static keyBytesToMultibaseId({ keyBytes, multicodecCode, multicodecName }: - RequireOnly - ): string { - const prefixedKey = Multicodec.addPrefix({ - code : multicodecCode, - data : keyBytes, - name : multicodecName - }); - const prefixedKeyB58 = Convert.uint8Array(prefixedKey).toBase58Btc(); - const multibaseKeyId = Convert.base58Btc(prefixedKeyB58).toMultibase(); - - return multibaseKeyId; - } - /** * Returns the appropriate public key compressor for the specified cryptographic curve. * @@ -1209,46 +1156,12 @@ export class DidKeyUtils { return converter; } - /** - * Converts a multibase identifier to a cryptographic key. - * - * @remarks - * This function decodes a multibase identifier back into a cryptographic key. It first decodes the - * identifier from multibase format into Base58 format, and then converts it into a `Uint8Array`. - * Afterward, it removes the multicodec prefix, extracting the raw key data along with the - * multicodec code and name. - * - * @example - * ```ts - * const multibaseKeyId = '...'; // Multibase identifier of the key - * const { key, multicodecCode, multicodecName } = multibaseIdToKey({ multibaseKeyId }); - * ``` - * - * @param params - The parameters for the conversion. - * @param params.multibaseKeyId - The multibase identifier string of the key. - * @returns An object containing the key as a `Uint8Array` and its multicodec code and name. - * @throws `DidError` if the multibase identifier is invalid. - */ - public static multibaseIdToKeyBytes({ multibaseKeyId }: { - multibaseKeyId: string - }): Required { - try { - const prefixedKeyB58 = Convert.multibase(multibaseKeyId).toBase58Btc(); - const prefixedKey = Convert.base58Btc(prefixedKeyB58).toUint8Array(); - const { code, data, name } = Multicodec.removePrefix({ prefixedData: prefixedKey }); - - return { keyBytes: data, multicodecCode: code, multicodecName: name }; - } catch (error: any) { - throw new DidError(DidErrorCode.InvalidDid, `Invalid multibase identifier: ${multibaseKeyId}`); - } - } - /** * Converts a Multicodec code or name to parial JWK (JSON Web Key). * * @example * ```ts - * const partialJwk = await Jose.multicodecToJwk({ name: 'ed25519-pub' }); + * const partialJwk = await DidKeyUtils.multicodecToJwk({ name: 'ed25519-pub' }); * ``` * * @param params - The parameters for the conversion. @@ -1299,7 +1212,7 @@ export class DidKeyUtils { * @example * ```ts * const publicKey = { crv: 'Ed25519', kty: 'OKP', x: '...' }; - * const multibaseId = await Jose.publicKeyToMultibaseId({ publicKey }); + * const multibaseId = await DidKeyUtils.publicKeyToMultibaseId({ publicKey }); * ``` * * @param params - The parameters for the conversion. @@ -1325,7 +1238,7 @@ export class DidKeyUtils { const { name: multicodecName } = await DidKeyUtils.jwkToMulticodec({ jwk: publicKey }); // Compute the multibase identifier based on the provided key. - const multibaseId = DidKeyUtils.keyBytesToMultibaseId({ + const multibaseId = keyBytesToMultibaseId({ keyBytes: publicKeyBytes, multicodecName }); diff --git a/packages/dids/src/methods/did-method.ts b/packages/dids/src/methods/did-method.ts index 08a884626..1418965ac 100644 --- a/packages/dids/src/methods/did-method.ts +++ b/packages/dids/src/methods/did-method.ts @@ -1,13 +1,11 @@ import type { - Jwk, - Signer, CryptoApi, LocalKeyManager, - EnclosedSignParams, - EnclosedVerifyParams, InferKeyGeneratorAlgorithm, } from '@web5/crypto'; +import type { BearerDid } from '../bearer-did.js'; +import type { DidMetadata } from '../types/portable-did.js'; import type { DidDocument, DidResolutionResult, @@ -15,47 +13,7 @@ import type { DidVerificationMethod, } from '../types/did-core.js'; -import { getVerificationMethodByKey } from '../utils.js'; import { DidVerificationRelationship } from '../types/did-core.js'; -import { DidError, DidErrorCode } from '../did-error.js'; - -/** - * Represents a Decentralized Identifier (DID) along with its DID document, key manager, metadata, - * and convenience functions. - */ -export interface BearerDid { - /** - * The DID document associated with this DID. - */ - didDocument: DidDocument; - - /** - * Returns a {@link @web5/crypto#Signer} that can be used to sign messages, credentials, or - * arbitrary data. - * - * If given, the `keyUri` parameter is used to select a key from the verification methods present - * in the DID Document. If `keyUri` is not given, each DID method implementation will select a - * default verification method key from the DID Document. - * - * @param params - The parameters for the `getSigner` operation. - * @param params.keyUri - Key URI of the key that will be used for sign and verify operations. Optional. - * @returns An instantiated {@link Signer} that can be used to sign and verify data. - */ - getSigner: (params?: { keyUri?: string }) => Promise; - - /** - * Key Management System (KMS) used to manage a DIDs keys and sign data. - * - * Each DID method requires at least one key be present in the provided `keyManager`. - */ - keyManager: CryptoApi; - - /** {@inheritDoc DidMetadata} */ - metadata: DidMetadata; - - /** {@inheritDoc Did#uri} */ - uri: string; -} /** * Represents options during the creation of a Decentralized Identifier (DID). @@ -116,14 +74,6 @@ export interface DidCreateVerificationMethod extends Pick { /** - * Express the private key in JWK format. + * Metadata about the DID Document. + * + * This structure contains information about the DID Document like creation and update timestamps, + * deactivation status, versioning information, and other details relevant to the DID Document. * - * (Optional) A JSON Web Key that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}. + * @see {@link https://www.w3.org/TR/did-core/#dfn-diddocumentmetadata | DID Core Specification, ยง DID Document Metadata} */ - privateKeyJwk?: Jwk; + didDocumentMetadata: DidMetadata; /** - * Optionally specify the purposes for which a verification method is intended to be used in a DID - * document. - * - * The `purposes` property defines the specific - * {@link DidVerificationRelationship | verification relationships} between the DID subject and - * the verification method. This enables the verification method to be utilized for distinct - * actions such as authentication, assertion, key agreement, capability delegation, and others. It - * is important for verifiers to recognize that a verification method must be associated with the - * relevant purpose in the DID document to be valid for that specific use case. + * A metadata structure consisting of values relating to the results of the DID registration + * process. * - * @example - * ```ts - * const verificationMethod: PortableDidVerificationMethod = { - * publicKeyJwk: { - * kty: "OKP", - * crv: "X25519", - * x: "7XdJtNmJ9pV_O_3mxWdn6YjiHJ-HhNkdYQARzVU_mwY", - * kid: "xtsuKULPh6VN9fuJMRwj66cDfQyLaxuXHkMlmAe_v6I" - * }, - * privateKeyJwk: { - * kty: "OKP", - * crv: "X25519", - * d: "qM1E646TMZwFcLwRAFwOAYnTT_AvbBd3NBGtGRKTyU8", - * x: "7XdJtNmJ9pV_O_3mxWdn6YjiHJ-HhNkdYQARzVU_mwY", - * kid: "xtsuKULPh6VN9fuJMRwj66cDfQyLaxuXHkMlmAe_v6I" - * }, - * purposes: ['authentication', 'assertionMethod'] - * }; - * ``` + * This structure is REQUIRED, and in the case of an error in the registration process, + * this MUST NOT be empty. If the registration is not successful, this structure MUST contain an + * `error` property describing the error. */ - purposes?: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[]; + didRegistrationMetadata: DidRegistrationMetadata; } +/** + * Represents metadata related to the result of a DID registration operation. + * + * This type includes fields that provide information about the outcome of a DID registration + * process (e.g., create, update, deactivate), including any errors that occurred. + * + * This metadata typically changes between invocations of the `create`, `update`, and `deactivate` + * functions, as it represents data about the registration process itself. + */ +export type DidRegistrationMetadata = { + /** + * An error code indicating issues encountered during the DID registration process. + * + * While the DID Core specification does not define a specific set of error codes for the result + * returned by the `create`, `update`, or `deactivate` functions, it is recommended to use the + * error codes defined in the DID Specification Registries for + * {@link https://www.w3.org/TR/did-spec-registries/#error | DID Resolution Metadata }. + * + * Recommended error codes include: + * - `internalError`: An unexpected error occurred during DID registration process. + * - `invalidDid`: The provided DID is invalid. + * - `invalidDidDocument`: The provided DID document does not conform to valid syntax. + * - `invalidDidDocumentLength`: The byte length of the provided DID document does not match the expected value. + * - `invalidSignature`: Verification of a signature failed. + * - `methodNotSupported`: The DID method specified is not supported. + * - Custom error codes can also be provided as strings. + */ + error?: string; + + // Additional output metadata generated during DID registration. + [key: string]: any; +}; + /** * Base abstraction for all Decentralized Identifier (DID) method implementations. * @@ -287,139 +221,6 @@ export interface PortableDidVerificationMethod extends Partial { - // Resolve the DID URI to a DID document and document metadata. - const { didDocument, didDocumentMetadata, didResolutionMetadata } = await this.resolve(didUri); - - // Verify the DID method is supported. - if (didResolutionMetadata.error === DidErrorCode.MethodNotSupported) { - throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`); - } - - // Verify the DID Resolution Result includes a DID document containing verification methods. - if (!(didDocument && Array.isArray(didDocument.verificationMethod) && didDocument.verificationMethod.length > 0)) { - throw new Error(`DID document for '${didUri}' is missing verification methods`); - } - - // Validate that the key material for every verification method in the DID document is present - // in the provided key manager. - for (let vm of didDocument.verificationMethod) { - if (!vm.publicKeyJwk) { - throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`); - } - - // Compute the key URI of the verification method's public key. - const keyUri = await keyManager.getKeyUri({ key: vm.publicKeyJwk }); - - // Verify that the key is present in the key manager. If not, an error is thrown. - await keyManager.getPublicKey({ keyUri }); - } - - const metadata: DidMetadata = didDocumentMetadata; - - // Define a function that returns a signer for the DID. - const getSigner = async (params?: { keyUri?: string }) => await this.getSigner({ - didDocument, - keyManager, - keyUri: params?.keyUri - }); - - return { didDocument, getSigner, keyManager, metadata, uri: didUri }; - } - - /** - * Given a W3C DID Document, return a {@link Signer} that can be used to sign messages, - * credentials, or arbitrary data. - * - * If given, the `keyUri` parameter is used to select a key from the verification methods present - * in the DID Document. - * - * If `keyUri` is not given, the first (or DID method specific default) verification method in the - * DID document is used. - * - * @param params - The parameters for the `getSigner` operation. - * @param params.didDocument - DID Document of the DID whose keys will be used to construct the {@link Signer}. - * @param params.keyManager - Web5 Crypto API used to sign and verify data. - * @param params.keyUri - Key URI of the key that will be used for sign and verify operations. Optional. - * @returns An instantiated {@link Signer} that can be used to sign and verify data. - */ - public static async getSigner({ didDocument, keyManager, keyUri }: { - didDocument: DidDocument; - keyManager: CryptoApi; - keyUri?: string; - }): Promise { - let publicKey: Jwk | undefined; - - // If a key URI is given use the referenced key for sign and verify operations. - if (keyUri) { - // Get the public key from the key store, which also verifies that the key is present. - publicKey = await keyManager.getPublicKey({ keyUri }); - // Verify the public key exists in the DID Document. - if (!(await getVerificationMethodByKey({ didDocument, publicKeyJwk: publicKey }))) { - throw new Error(`Key referenced by '${keyUri}' is not present in the provided DID Document for '${didDocument.id}'`); - } - - } else { - // If a key URI is not given, use the key associated with the verification method that is used - // by default for sign and verify operations. The default verification method is determined by - // the DID method implementation. - ({ publicKeyJwk: publicKey } = await this.getSigningMethod({ didDocument }) ?? {}); - if (publicKey === undefined) { - throw new Error(`No verification methods found in the provided DID Document for '${didDocument.id}'`); - } - // Compute the expected key URI of the signing key. - keyUri = await keyManager.getKeyUri({ key: publicKey }); - } - - // Both the `keyUri` and `publicKey` must be known before returning a signer. - if (!(keyUri && publicKey)) { - throw new Error(`Failed to determine the keys needed to create a signer`); - } - - return { - async sign({ data }: EnclosedSignParams): Promise { - const signature = await keyManager.sign({ data, keyUri: keyUri! }); // `keyUri` is guaranteed to be defined at this point. - return signature; - }, - - async verify({ data, signature }: EnclosedVerifyParams): Promise { - const isValid = await keyManager.verify({ data, key: publicKey!, signature }); // `publicKey` is guaranteed to be defined at this point. - return isValid; - } - }; - } - /** * MUST be implemented by all DID method implementations that extend {@link DidMethod}. * @@ -452,80 +253,4 @@ export class DidMethod { public static async resolve(_didUri: string, _options?: DidResolutionOptions): Promise { throw new Error(`Not implemented: Classes extending DidMethod must implement resolve()`); } - - /** - * Converts a `BearerDid` object to a portable format containing the URI and verification methods - * associated with the DID. - * - * This method is useful when you need to represent the key material and metadata associated with - * a DID in format that can be used independently of the specific DID method implementation. It - * extracts both public and private keys from the DID's key manager and organizes them into a - * `PortableDid` structure. - * - * @remarks - * This method requires that the DID's key manager supports the `exportKey` operation. If the DID - * document does not contain any verification methods, or if the key manager does not support key - * export, an error is thrown. - * - * The resulting `PortableDid` will contain the same number of verification methods as the DID - * document, each with its associated public and private keys and the purposes for which the key - * can be used. - * - * @example - * ```ts - * // Assuming `did` is an instance of BearerDid - * const portableDid = await DidMethod.toKeys({ did }); - * // portableDid now contains the verification methods and their associated keys. - * ``` - * - * @param params - The parameters for the convert operation. - * @param params.did - The `BearerDid` object to convert to a portable format. - * @returns A `PortableDid` containing the URI and verification methods associated with the DID. - * @throws An error if the key manager does not support key export or if the DID document - * is missing verification methods. - */ - public static async toKeys({ did }: { did: BearerDid }): Promise { - // First, confirm that the DID's key manager supports exporting keys. - if (!('exportKey' in did.keyManager && typeof did.keyManager.exportKey === 'function')) { - throw new Error(`The key manager of the given DID does not support exporting keys`); - } - - // Verify the DID document contains at least one verification method. - if (!(Array.isArray(did.didDocument.verificationMethod) && did.didDocument.verificationMethod.length > 0)) { - throw new Error(`DID document for '${did.uri}' is missing verification methods`); - } - - let portableDid: PortableDid = { - uri : did.uri, - verificationMethods : [] - }; - - // Retrieve the key material for every verification method in the DID document from the key - // manager. - for (let vm of did.didDocument.verificationMethod) { - if (!vm.publicKeyJwk) { - throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`); - } - - // Compute the key URI of the verification method's public key. - const keyUri = await did.keyManager.getKeyUri({ key: vm.publicKeyJwk }); - - // Retrieve the public and private keys from the key manager. - const privateKey = await did.keyManager.exportKey({ keyUri }); - - // Collect the purposes associated with this verification method from the DID document. - const purposes = Object - .keys(DidVerificationRelationship) - .filter((purpose) => (did.didDocument[purpose as keyof DidDocument] as any[])?.includes(vm.id)) as DidVerificationRelationship[]; - - // Add the verification method to the key set. - portableDid.verificationMethods.push({ - ...vm, - privateKeyJwk: privateKey, - purposes - }); - } - - return portableDid; - } } \ No newline at end of file diff --git a/packages/dids/src/types/did-core.ts b/packages/dids/src/types/did-core.ts index fbd8c893b..1e82db6c7 100644 --- a/packages/dids/src/types/did-core.ts +++ b/packages/dids/src/types/did-core.ts @@ -215,7 +215,7 @@ export interface DidDocument { * * @see {@link https://www.w3.org/TR/did-core/#did-document-metadata | DID Core Specification, ยง DID Document Metadata} */ -export type DidDocumentMetadata = { +export interface DidDocumentMetadata { /** * Timestamp of the Create operation. * @@ -294,7 +294,7 @@ export type DidDocumentMetadata = { // Additional output metadata generated during DID Resolution. [key: string]: any; -}; +} /** * Represents metadata related to the result of a DID resolution operation. @@ -510,19 +510,18 @@ export interface DidVerificationMethod { controller: string; /** - * Express the public key in JWK format. + * (Optional) A public key in JWK format. * - * (Optional) A JSON Web Key that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}. + * A JSON Web Key (JWK) that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}. */ publicKeyJwk?: Jwk; /** - * (Optional) A public key encoded with a Multibase-prefix, conforming to the draft Multibase - * specification (https://datatracker.ietf.org/doc/draft-multiformats-multibase/). Typically used - * for expressing keys in formats like base58. + * (Optional) A public key in Multibase format. + * + * A multibase key that conforms to the draft + * {@link https://datatracker.ietf.org/doc/draft-multiformats-multibase/ | Multibase specification}. */ - // an encoded (e.g, base58) key with a Multibase-prefix that conforms to - // https://datatracker.ietf.org/doc/draft-multiformats-multibase/ publicKeyMultibase?: string; } diff --git a/packages/dids/src/types/multibase.ts b/packages/dids/src/types/multibase.ts new file mode 100644 index 000000000..2d1198358 --- /dev/null +++ b/packages/dids/src/types/multibase.ts @@ -0,0 +1,29 @@ +/** + * Represents a cryptographic key with associated multicodec metadata. + * + * The `KeyWithMulticodec` type encapsulates a cryptographic key along with optional multicodec + * information. It is primarily used in functions that convert between cryptographic keys and their + * string representations, ensuring that the key's format and encoding are preserved and understood + * across different systems and applications. + */ +export type KeyWithMulticodec = { + /** + * A `Uint8Array` representing the raw bytes of the cryptographic key. This is the primary data of + * the type and is essential for cryptographic operations. + */ + keyBytes: Uint8Array, + + /** + * An optional number representing the multicodec code. This code uniquely identifies the encoding + * format or protocol associated with the key. The presence of this code is crucial for decoding + * the key correctly in different contexts. + */ + multicodecCode?: number, + + /** + * An optional string representing the human-readable name of the multicodec. This name provides + * an easier way to identify the encoding format or protocol of the key, especially when the + * numerical code is not immediately recognizable. + */ + multicodecName?: string +}; \ No newline at end of file diff --git a/packages/dids/src/types/portable-did.ts b/packages/dids/src/types/portable-did.ts new file mode 100644 index 000000000..e65b19fa6 --- /dev/null +++ b/packages/dids/src/types/portable-did.ts @@ -0,0 +1,64 @@ +import type { Jwk } from '@web5/crypto'; + +import type { DidDocument, DidDocumentMetadata } from './did-core.js'; + +/** + * Represents metadata about a DID resulting from create, update, or deactivate operations. + */ +export interface DidMetadata extends DidDocumentMetadata { + /** + * For DID methods that support publishing, the `published` property indicates whether the DID + * document has been published to the respective network. + * + * A `true` value signifies that the DID document is publicly accessible on the network (e.g., + * Mainline DHT), allowing it to be resolved by others. A `false` value implies the DID document + * is not published, limiting its visibility to public resolution. Absence of this property + * indicates that the DID method does not support publishing. + */ + published?: boolean; +} + +/** + * Format to document a DID identifier, along with its associated data, which can be exported, + * saved to a file, or imported. The intent is bundle all of the necessary metadata to enable usage + * of the DID in different contexts. + */ +/** + * Format that documents the key material and metadata of a Decentralized Identifier (DID) to enable + * usage of the DID in different contexts. + * + * This format is useful for exporting, saving to a file, or importing a DID across process + * boundaries or between different DID method implementations. + * + * @example + * ```ts + * // Generate a new DID. + * const did = await DidExample.create(); + * + * // Export to a PortableDid. + * const portableDid = await did.export(); + * + * // Instantiate a BearerDid object from a PortableDid. + * const importedDid = await DidExample.import(portableDid); + * // The `importedDid` object should be equivalent to the original `did` object. + * ``` + */ +export interface PortableDid { + /** {@inheritDoc Did#uri} */ + uri: string; + + /** + * The DID document associated with this DID. + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, ยง DID Document} + */ + document: DidDocument; + + /** {@inheritDoc DidMetadata} */ + metadata: DidMetadata; + + /** + * An optional array of private keys associated with the DID document's verification methods. + */ + privateKeys?: Jwk[]; +} \ No newline at end of file diff --git a/packages/dids/src/utils.ts b/packages/dids/src/utils.ts index fb9f3f54d..a73b67761 100644 --- a/packages/dids/src/utils.ts +++ b/packages/dids/src/utils.ts @@ -1,7 +1,12 @@ import type { Jwk } from '@web5/crypto'; +import type { RequireOnly } from '@web5/common'; +import { Convert, Multicodec } from '@web5/common'; import { computeJwkThumbprint } from '@web5/crypto'; +import type { KeyWithMulticodec } from './types/multibase.js'; + +import { DidError, DidErrorCode } from './did-error.js'; import { DidService, DidDocument, @@ -47,6 +52,35 @@ export interface DwnDidService extends DidService { sig: string | string[]; } +/** + * Extracts the fragment part of a Decentralized Identifier (DID) verification method identifier. + * + * This function takes any input and aims to return only the fragment of a DID identifier, + * which comes after the '#' symbol in a DID string. It's designed specifically for handling + * DID verification method identifiers. The function returns undefined for non-string inputs, inputs + * that do not contain a '#', or complex data structures like objects or arrays, ensuring that only + * the fragment part of a DID string is extracted when present. + * + * @example + * ```ts + * console.log(extractDidFragment("did:example:123#key-1")); // Output: "key-1" + * console.log(extractDidFragment("did:example:123")); // Output: undefined + * console.log(extractDidFragment({ id: "did:example:123#0", type: "JsonWebKey" })); // Output: undefined + * console.log(extractDidFragment(undefined)); // Output: undefined + * ``` + * + * @param input - The input to be processed. Can be of any type, but the function is designed + * to work with strings that represent DID verification method identifiers. + * @returns The fragment part of the DID identifier if the input is a string containing a '#'. + * Returns an empty string for all other inputs, including non-string types, strings + * without a '#', and complex data structures. + */ +export function extractDidFragment(input: unknown): string | undefined { + if (typeof input !== 'string') return undefined; + if (input.length === 0) return undefined; + return input.split('#').pop(); +} + /** * Retrieves services from a given DID document, optionally filtered by `id` or `type`. * @@ -231,6 +265,70 @@ export function getVerificationMethodTypes({ didDocument }: { return [...new Set(types)]; // Return only unique types. } +/** + * Retrieves a list of DID verification relationships by a specific method ID from a DID document. + * + * This function examines the specified DID document to identify any verification relationships + * (e.g., `authentication`, `assertionMethod`) that reference a verification method by its method ID + * or contain an embedded verification method matching the method ID. The method ID is typically a + * fragment of a DID (e.g., `did:example:123#key-1`) that uniquely identifies a verification method + * within the DID document. + * + * The search considers both direct references to verification methods by their IDs and verification + * methods embedded within the verification relationship arrays. It returns an array of + * `DidVerificationRelationship` enums corresponding to the verification relationships that contain + * the specified method ID. + * + * @param params - An object containing input parameters for retrieving verification relationships. + * @param params.didDocument - The DID document to search for verification relationships. + * @param params.methodId - The method ID to search for within the verification relationships. + * @returns An array of `DidVerificationRelationship` enums representing the types of verification + * relationships that reference the specified method ID. + * + * @example + * ```ts + * const didDocument: DidDocument = { + * // ...contents of a DID document... + * }; + * + * const relationships = getVerificationRelationshipsById({ + * didDocument, + * methodId: 'key-1' + * }); + * console.log(relationships); + * // Output might include ['authentication', 'assertionMethod'] if those relationships + * // reference or contain the specified method ID. + * ``` + */ +export function getVerificationRelationshipsById({ didDocument, methodId }: { + didDocument: DidDocument; + methodId: string; +}): DidVerificationRelationship[] { + const relationships: DidVerificationRelationship[] = []; + + Object.keys(DidVerificationRelationship).forEach((relationship) => { + if (Array.isArray(didDocument[relationship as keyof DidDocument])) { + const relationshipMethods = didDocument[relationship as keyof DidDocument] as (string | DidVerificationMethod)[]; + + const methodIdFragment = extractDidFragment(methodId); + + // Check if the verification relationship property contains a matching method ID either + // directly referenced or as an embedded verification method. + const containsMethodId = relationshipMethods.some(method => { + const isByReferenceMatch = extractDidFragment(method) === methodIdFragment; + const isEmbeddedMethodMatch = isDidVerificationMethod(method) && extractDidFragment(method.id) === methodIdFragment; + return isByReferenceMatch || isEmbeddedMethodMatch; + }); + + if (containsMethodId) { + relationships.push(relationship as DidVerificationRelationship); + } + } + }); + + return relationships; +} + /** * Checks if a given object is a {@link DidService}. * @@ -365,4 +463,70 @@ export function isDidVerificationMethod(obj: unknown): obj is DidVerificationMet if (typeof obj.controller !== 'string') return false; return true; +} + +/** + * Converts a cryptographic key to a multibase identifier. + * + * @remarks + * This method provides a way to represent a cryptographic key as a multibase identifier. + * It takes a `Uint8Array` representing the key, and either the multicodec code or multicodec name + * as input. The method first adds the multicodec prefix to the key, then encodes it into Base58 + * format. Finally, it converts the Base58 encoded key into a multibase identifier. + * + * @example + * ```ts + * const key = new Uint8Array([...]); // Cryptographic key as Uint8Array + * const multibaseId = keyBytesToMultibaseId({ key, multicodecName: 'ed25519-pub' }); + * ``` + * + * @param params - The parameters for the conversion. + * @returns The multibase identifier as a string. + */ +export function keyBytesToMultibaseId({ keyBytes, multicodecCode, multicodecName }: + RequireOnly +): string { + const prefixedKey = Multicodec.addPrefix({ + code : multicodecCode, + data : keyBytes, + name : multicodecName + }); + const prefixedKeyB58 = Convert.uint8Array(prefixedKey).toBase58Btc(); + const multibaseKeyId = Convert.base58Btc(prefixedKeyB58).toMultibase(); + + return multibaseKeyId; +} + +/** + * Converts a multibase identifier to a cryptographic key. + * + * @remarks + * This function decodes a multibase identifier back into a cryptographic key. It first decodes the + * identifier from multibase format into Base58 format, and then converts it into a `Uint8Array`. + * Afterward, it removes the multicodec prefix, extracting the raw key data along with the + * multicodec code and name. + * + * @example + * ```ts + * const multibaseKeyId = '...'; // Multibase identifier of the key + * const { key, multicodecCode, multicodecName } = multibaseIdToKey({ multibaseKeyId }); + * ``` + * + * @param params - The parameters for the conversion. + * @param params.multibaseKeyId - The multibase identifier string of the key. + * @returns An object containing the key as a `Uint8Array` and its multicodec code and name. + * @throws `DidError` if the multibase identifier is invalid. + */ +export function multibaseIdToKeyBytes({ multibaseKeyId }: { + multibaseKeyId: string +}): Required { + try { + const prefixedKeyB58 = Convert.multibase(multibaseKeyId).toBase58Btc(); + const prefixedKey = Convert.base58Btc(prefixedKeyB58).toUint8Array(); + const { code, data, name } = Multicodec.removePrefix({ prefixedData: prefixedKey }); + + return { keyBytes: data, multicodecCode: code, multicodecName: name }; + } catch (error: any) { + throw new DidError(DidErrorCode.InvalidDid, `Invalid multibase identifier: ${multibaseKeyId}`); + } } \ No newline at end of file diff --git a/packages/dids/tests/bearer-did.spec.ts b/packages/dids/tests/bearer-did.spec.ts new file mode 100644 index 000000000..01b914ee0 --- /dev/null +++ b/packages/dids/tests/bearer-did.spec.ts @@ -0,0 +1,447 @@ +import type { CryptoApi } from '@web5/crypto'; + +import sinon from 'sinon'; +import { expect } from 'chai'; +import { LocalKeyManager } from '@web5/crypto'; + +import type { PortableDid } from '../src/types/portable-did.js'; + +import { BearerDid } from '../src/bearer-did.js'; + +describe('BearerDid', () => { + let portableDid: PortableDid; + + beforeEach(() => { + portableDid = { + uri : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + document : { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1', + ], + id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + verificationMethod : [ + { + id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + type : 'JsonWebKey2020', + controller : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo', + kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ', + alg : 'EdDSA', + }, + }, + ], + authentication: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + assertionMethod: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + capabilityInvocation: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + capabilityDelegation: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + keyAgreement: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + }, + metadata: { + }, + privateKeys: [ + { + crv : 'Ed25519', + d : '628WwXicdWc0BULN1JG_ybSrhwWWnz9NFwxbG09Ecr0', + kty : 'OKP', + x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo', + kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ', + alg : 'EdDSA', + }, + ], + }; + }); + + describe('export()', () => { + it('returns a PortableDid', async () => { + // Create a DID to use for the test. + const did = await BearerDid.import({ portableDid }); + + const exportedPortableDid = await did.export(); + + expect(exportedPortableDid).to.have.property('uri', portableDid.uri); + expect(exportedPortableDid).to.have.property('document'); + expect(exportedPortableDid).to.have.property('metadata'); + expect(exportedPortableDid).to.have.property('privateKeys'); + + expect(exportedPortableDid.document.verificationMethod).to.have.length(1); + expect(exportedPortableDid.document).to.deep.equal(portableDid.document); + }); + + it('exported PortableDid does not include private keys if the key manager does not support exporting keys', async () => { + // Create a key manager that does not support exporting keys. + const keyManagerWithoutExport: CryptoApi = { + digest : sinon.stub(), + generateKey : sinon.stub(), + getKeyUri : sinon.stub(), + getPublicKey : sinon.stub(), + sign : sinon.stub(), + verify : sinon.stub(), + }; + + const did = await BearerDid.import({ portableDid }); + did.keyManager = keyManagerWithoutExport; + + const exportedPortableDid = await did.export(); + + expect(exportedPortableDid).to.not.have.property('privateKeys'); + }); + + it('throws an error if the DID document lacks any verification methods', async () => { + const did = await BearerDid.import({ portableDid }); + + // Delete the verification method property from the DID document. + delete did.document.verificationMethod; + + try { + await did.export(); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('is missing verification methods'); + } + }); + + it('throws an error if verification methods lack a public key', async () => { + const did = await BearerDid.import({ portableDid }); + + // Delete the verification method property from the DID document. + delete did.document.verificationMethod![0].publicKeyJwk; + + try { + await did.export(); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('does not contain a public key'); + } + }); + }); + + describe('getSigner()', () => { + let keyManagerMock: any; + + beforeEach(() => { + // Mock for CryptoApi + keyManagerMock = { + digest : sinon.stub(), + generateKey : sinon.stub(), + getKeyUri : sinon.stub(), + getPublicKey : sinon.stub(), + importKey : sinon.stub(), + sign : sinon.stub(), + verify : sinon.stub(), + }; + + keyManagerMock.getKeyUri.resolves(`urn:jwk${portableDid.document.verificationMethod![0].publicKeyJwk!.kid}`); // Mock key URI retrieval + keyManagerMock.getPublicKey.resolves(portableDid.document.verificationMethod![0].publicKeyJwk!); // Mock public key retrieval + keyManagerMock.importKey.resolves(`urn:jwk${portableDid.document.verificationMethod![0].publicKeyJwk!.kid}`); // Mock import key + keyManagerMock.sign.resolves(new Uint8Array(64).fill(0)); // Mock signature creation + keyManagerMock.verify.resolves(true); // Mock verification result + }); + + it('returns a signer with sign and verify functions', async () => { + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + const signer = await did.getSigner(); + + expect(signer).to.be.an('object'); + expect(signer).to.have.property('sign').that.is.a('function'); + expect(signer).to.have.property('verify').that.is.a('function'); + }); + + it('handles public keys that do not contain an "alg" property', async () => { + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + + const { alg, ...publicKeyWithoutAlg } = portableDid.document.verificationMethod![0].publicKeyJwk!; + keyManagerMock.getPublicKey.resolves(publicKeyWithoutAlg); + + const signer = await did.getSigner(); + + expect(signer).to.be.have.property('algorithm', 'EdDSA'); + }); + + it('sign function should call keyManager.sign with correct parameters', async () => { + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + const signer = await did.getSigner(); + const dataToSign = new Uint8Array([0x00, 0x01]); + + await signer.sign({ data: dataToSign }); + + expect(keyManagerMock.sign.calledOnce).to.be.true; + expect(keyManagerMock.sign.calledWith(sinon.match({ data: dataToSign }))).to.be.true; + }); + + it('verify function should call keyManager.verify with correct parameters', async () => { + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + const signer = await did.getSigner(); + const dataToVerify = new Uint8Array([0x00, 0x01]); + const signature = new Uint8Array([0x01, 0x02]); + + await signer.verify({ data: dataToVerify, signature }); + + expect(keyManagerMock.verify.calledOnce).to.be.true; + expect(keyManagerMock.verify.calledWith(sinon.match({ data: dataToVerify, signature }))).to.be.true; + }); + + it('uses the provided methodId to fetch the public key', async () => { + const methodId = '0'; + const publicKey = portableDid.document.verificationMethod![0].publicKeyJwk!; + keyManagerMock.getKeyUri.withArgs({ key: publicKey }).resolves(publicKey); + + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + const signer = await did.getSigner({ methodId }); + + expect(signer).to.be.an('object'); + expect(keyManagerMock.getKeyUri.calledWith({ key: publicKey })).to.be.true; + }); + + it('handles undefined params', async function () { + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + + // Simulate the creation of a signer with undefined params + // @ts-expect-error - Testing the method with undefined params + const signer = await did.getSigner({ }); + + // Note: Since this test does not interact with an actual keyManager, it primarily ensures + // that the method doesn't break with undefined params. + expect(signer).to.have.property('sign'); + expect(signer).to.have.property('verify'); + }); + + it('throws an error if the public key contains an unknown "crv" property', async () => { + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + + const { alg, ...publicKeyWithoutAlg } = portableDid.document.verificationMethod![0].publicKeyJwk!; + publicKeyWithoutAlg.crv = 'unknown-crv'; + keyManagerMock.getPublicKey.resolves(publicKeyWithoutAlg); + + try { + await did.getSigner(); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('crv=unknown-crv'); + expect(error.message).to.include('Unable to determine algorithm'); + } + }); + + it('throws an error if the methodId does not match any verification method in the DID Document', async () => { + const methodId = 'nonexistent-id'; + + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + + try { + await did.getSigner({ methodId }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('method intended for signing could not be determined'); + } + }); + + it('throws an error if the DID Document does not contain an assertionMethod property', async () => { + delete portableDid.document.assertionMethod; + + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + + try { + await did.getSigner(); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('method intended for signing could not be determined'); + } + }); + + it('throws an error if the DID Document does not any verification methods', async () => { + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + + did.document.verificationMethod = undefined; + + try { + await did.getSigner(); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('method intended for signing could not be determined'); + } + }); + + it('throws an error if the DID Document contains an embedded assertionMethod verification method', async () => { + portableDid.document.assertionMethod = [ + { + 'type' : 'JsonWebKey2020', + 'id' : 'did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0', + 'controller' : 'did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0', + 'publicKeyJwk' : { + 'kty' : 'EC', + 'use' : 'sig', + 'crv' : 'secp256k1', + 'kid' : 'i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg', + 'x' : 'vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U', + 'y' : 'VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU', + 'alg' : 'ES256K' + } + } + ]; + + const did = await BearerDid.import({ + portableDid, + keyManager: keyManagerMock + }); + + try { + await did.getSigner(); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('method intended for signing could not be determined'); + } + }); + + it('throws an error if the key is missing in the key manager', async function () { + const did = await BearerDid.import({ portableDid }); + + // Replace the key manager with one that does not contain the keys for the DID. + did.keyManager = new LocalKeyManager(); + + try { + await did.getSigner(); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('Key not found'); + } + }); + }); + + describe('import()', () => { + let portableDid: PortableDid; + + beforeEach(() => { + // Define a DID to use for the test. + portableDid = { + uri : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + document : { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1', + ], + id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + verificationMethod : [ + { + id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + type : 'JsonWebKey2020', + controller : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo', + kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ', + alg : 'EdDSA', + }, + }, + ], + authentication: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + assertionMethod: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + capabilityInvocation: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + capabilityDelegation: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + keyAgreement: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + }, + metadata: { + }, + privateKeys: [ + { + crv : 'Ed25519', + d : '628WwXicdWc0BULN1JG_ybSrhwWWnz9NFwxbG09Ecr0', + kty : 'OKP', + x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo', + kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ', + alg : 'EdDSA', + }, + ], + }; + }); + + it('throws an error if the DID document lacks any verification methods', async () => { + // Delete the verification method property from the DID document. + delete portableDid.document.verificationMethod; + + try { + await BearerDid.import({ portableDid }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('verification method is required but 0 were given'); + } + }); + + it('throws an error if the DID document does not contain a public key', async () => { + // Delete the public key from the DID document. + delete portableDid.document.verificationMethod![0].publicKeyJwk; + + try { + await BearerDid.import({ portableDid }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('does not contain a public key'); + } + }); + + it('throws an error if no private keys are given and the key manager does not contain the keys', async () => { + // Delete the private keys from the portable DID to trigger the error. + delete portableDid.privateKeys; + + try { + await BearerDid.import({ portableDid }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('Key not found'); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-ion/to-keys.ts b/packages/dids/tests/fixtures/test-vectors/did-ion/to-keys.ts deleted file mode 100644 index b69eeace1..000000000 --- a/packages/dids/tests/fixtures/test-vectors/did-ion/to-keys.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Jwk, LocalKeyManager } from '@web5/crypto'; - -import sinon from 'sinon'; - -import type { BearerDid } from '../../../../src/methods/did-method.js'; - -type TestVector = { - [key: string]: { - did: BearerDid; - privateKey: Jwk[]; - }; -}; - -export const vectors: TestVector = { - oneMethodNoServices: { - did: { - didDocument: { - id : 'did:ion:EiAXe1c857XIc7F3tvrxV_tsmn2zMqrgILwvrMkEgfuuSQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJyV3hhU2ZWWlVfZWJsWjAzRFk1RXo4TkxkRlA4c200cFVYenJNRjR2d0xVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4Ijoid0t6MUg3SnNqbmlhV0dka1I0akcxT19pWVlnWDFyV29TRVZSXy1sS1VZRSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpQ1IwcDk3UGZHYW9LMV9fdlV4ZlhLcW0xN29RY0RtSEM4dk1WeFFZWUhzTlEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNxSjlyMEtTUmVsUHFNTXE2Q0gwRm13SUtiWkVEUjhuWmVzNGllTW03X1J3IiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlELVUyRDh1bE1VUjVkSWduWkY3YnJCNUpvWkdlY29HS2FpNGNuQ1gzSnNlZyJ9fQ', - '@context' : [ - 'https://www.w3.org/ns/did/v1', - { - '@base': 'did:ion:EiAXe1c857XIc7F3tvrxV_tsmn2zMqrgILwvrMkEgfuuSQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJyV3hhU2ZWWlVfZWJsWjAzRFk1RXo4TkxkRlA4c200cFVYenJNRjR2d0xVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4Ijoid0t6MUg3SnNqbmlhV0dka1I0akcxT19pWVlnWDFyV29TRVZSXy1sS1VZRSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpQ1IwcDk3UGZHYW9LMV9fdlV4ZlhLcW0xN29RY0RtSEM4dk1WeFFZWUhzTlEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNxSjlyMEtTUmVsUHFNTXE2Q0gwRm13SUtiWkVEUjhuWmVzNGllTW03X1J3IiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlELVUyRDh1bE1VUjVkSWduWkY3YnJCNUpvWkdlY29HS2FpNGNuQ1gzSnNlZyJ9fQ', - }, - ], - service: [ - ], - verificationMethod: [ - { - id : '#rWxaSfVZU_eblZ03DY5Ez8NLdFP8sm4pUXzrMF4vwLU', - controller : 'did:ion:EiAXe1c857XIc7F3tvrxV_tsmn2zMqrgILwvrMkEgfuuSQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJyV3hhU2ZWWlVfZWJsWjAzRFk1RXo4TkxkRlA4c200cFVYenJNRjR2d0xVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4Ijoid0t6MUg3SnNqbmlhV0dka1I0akcxT19pWVlnWDFyV29TRVZSXy1sS1VZRSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpQ1IwcDk3UGZHYW9LMV9fdlV4ZlhLcW0xN29RY0RtSEM4dk1WeFFZWUhzTlEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNxSjlyMEtTUmVsUHFNTXE2Q0gwRm13SUtiWkVEUjhuWmVzNGllTW03X1J3IiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlELVUyRDh1bE1VUjVkSWduWkY3YnJCNUpvWkdlY29HS2FpNGNuQ1gzSnNlZyJ9fQ', - type : 'JsonWebKey2020', - publicKeyJwk : { - crv : 'Ed25519', - kty : 'OKP', - x : 'wKz1H7JsjniaWGdkR4jG1O_iYYgX1rWoSEVR_-lKUYE', - }, - }, - ], - authentication: [ - '#rWxaSfVZU_eblZ03DY5Ez8NLdFP8sm4pUXzrMF4vwLU', - ], - assertionMethod: [ - '#rWxaSfVZU_eblZ03DY5Ez8NLdFP8sm4pUXzrMF4vwLU', - ], - }, - getSigner : sinon.stub(), - keyManager : sinon.stub() as unknown as LocalKeyManager, - metadata : { - canonicalId : 'did:ion:EiAXe1c857XIc7F3tvrxV_tsmn2zMqrgILwvrMkEgfuuSQ', - recoveryKey : { - kty : 'EC', - crv : 'secp256k1', - x : 'EdmqCQJjJycUhxz52kCxLR7v1cIpWnbgVOVXDn73sMI', - y : 'a4kbkoG7t5yYYzUqSuSLv9gp8Rumw4wPmCDsQWaLKQQ', - kid : 'cv5f7CxO3H8FVqDuU5b48WP1Y8vfhkcmjOAOFhQDByU', - alg : 'ES256K', - }, - updateKey: { - kty : 'EC', - crv : 'secp256k1', - x : 'p1edwKgrvFZ4XxXcqM8j_ZStWZ0DuWzdhr8JUI42BmA', - y : 'KK_s4WG6vyDeJ4kMyDaAygU3G-Fiixi6Hf7cgFe-HcM', - kid : 'mKJbR4wHIJIeyUITRhddkPFqL-jbpGtSnu4MB6WrYzg', - alg : 'ES256K', - }, - published: true, - }, - uri: 'did:ion:EiAXe1c857XIc7F3tvrxV_tsmn2zMqrgILwvrMkEgfuuSQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJyV3hhU2ZWWlVfZWJsWjAzRFk1RXo4TkxkRlA4c200cFVYenJNRjR2d0xVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4Ijoid0t6MUg3SnNqbmlhV0dka1I0akcxT19pWVlnWDFyV29TRVZSXy1sS1VZRSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpQ1IwcDk3UGZHYW9LMV9fdlV4ZlhLcW0xN29RY0RtSEM4dk1WeFFZWUhzTlEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNxSjlyMEtTUmVsUHFNTXE2Q0gwRm13SUtiWkVEUjhuWmVzNGllTW03X1J3IiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlELVUyRDh1bE1VUjVkSWduWkY3YnJCNUpvWkdlY29HS2FpNGNuQ1gzSnNlZyJ9fQ', - }, - privateKey: [ - { - crv : 'Ed25519', - d : 'tMxiGuJDL1dukJT8xfMwanLHv3ScDTVJH1jtS01Xm-g', - kty : 'OKP', - x : 'wKz1H7JsjniaWGdkR4jG1O_iYYgX1rWoSEVR_-lKUYE', - kid : 'rWxaSfVZU_eblZ03DY5Ez8NLdFP8sm4pUXzrMF4vwLU', - alg : 'EdDSA', - } - ], - }, -}; \ No newline at end of file diff --git a/packages/dids/tests/methods/did-dht.spec.ts b/packages/dids/tests/methods/did-dht.spec.ts index 5e4088a28..d8e7ec967 100644 --- a/packages/dids/tests/methods/did-dht.spec.ts +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -1,12 +1,8 @@ -import type { Jwk } from '@web5/crypto'; - import sinon from 'sinon'; import { expect } from 'chai'; import { Convert } from '@web5/common'; -import { LocalKeyManager } from '@web5/crypto'; -import type { DidResolutionResult } from '../../src/index.js'; -import type { PortableDid } from '../../src/methods/did-method.js'; +import type { PortableDid } from '../../src/types/portable-did.js'; import { DidErrorCode } from '../../src/did-error.js'; import { DidDht, DidDhtRegisteredDidType } from '../../src/methods/did-dht.js'; @@ -42,18 +38,18 @@ describe('DidDht', () => { fetchStub.restore(); }); - describe('create', () => { + describe('create()', () => { it('creates a DID with a single verification method, by default', async () => { const did = await DidDht.create(); - expect(did).to.have.property('didDocument'); + expect(did).to.have.property('document'); expect(did).to.have.property('getSigner'); expect(did).to.have.property('keyManager'); expect(did).to.have.property('metadata'); expect(did).to.have.property('uri'); - expect(did.didDocument).to.have.property('verificationMethod'); - expect(did.didDocument.verificationMethod).to.have.length(1); + expect(did.document).to.have.property('verificationMethod'); + expect(did.document.verificationMethod).to.have.length(1); }); it('handles creating DIDs with additional Ed25519 verification methods', async () => { @@ -68,8 +64,8 @@ describe('DidDht', () => { } }); - expect(did.didDocument.verificationMethod).to.have.length(2); - expect(did.didDocument.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'Ed25519'); + expect(did.document.verificationMethod).to.have.length(2); + expect(did.document.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'Ed25519'); }); it('handles creating DIDs with additional secp256k1 verification methods', async () => { @@ -84,8 +80,8 @@ describe('DidDht', () => { } }); - expect(did.didDocument.verificationMethod).to.have.length(2); - expect(did.didDocument.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'secp256k1'); + expect(did.document.verificationMethod).to.have.length(2); + expect(did.document.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'secp256k1'); }); it('handles creating DIDs with additional secp256r1 verification methods', async () => { @@ -100,8 +96,8 @@ describe('DidDht', () => { } }); - expect(did.didDocument.verificationMethod).to.have.length(2); - expect(did.didDocument.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'P-256'); + expect(did.document.verificationMethod).to.have.length(2); + expect(did.document.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'P-256'); }); it('allows one or more DID controller identifiers to be specified', async () => { @@ -111,7 +107,7 @@ describe('DidDht', () => { } }); - expect(did.didDocument).to.have.property('controller', 'did:example:1234'); + expect(did.document).to.have.property('controller', 'did:example:1234'); did = await DidDht.create({ options: { @@ -119,7 +115,7 @@ describe('DidDht', () => { } }); - expect(did.didDocument.controller).to.deep.equal(['did:example:1234', 'did:example:5678']); + expect(did.document.controller).to.deep.equal(['did:example:1234', 'did:example:5678']); }); it('allows one or more Also Known As identifiers to be specified', async () => { @@ -129,7 +125,7 @@ describe('DidDht', () => { } }); - expect(did.didDocument.alsoKnownAs).to.deep.equal(['did:example:1234']); + expect(did.document.alsoKnownAs).to.deep.equal(['did:example:1234']); did = await DidDht.create({ options: { @@ -137,7 +133,7 @@ describe('DidDht', () => { } }); - expect(did.didDocument.alsoKnownAs).to.deep.equal(['did:example:1234', 'did:example:5678']); + expect(did.document.alsoKnownAs).to.deep.equal(['did:example:1234', 'did:example:5678']); }); it('handles creating DIDs with additional verification methods', async () => { @@ -152,13 +148,13 @@ describe('DidDht', () => { } }); - expect(did.didDocument.verificationMethod).to.have.length(2); + expect(did.document.verificationMethod).to.have.length(2); }); it('assigns 0 as the ID of the Identity Key verification method ', async () => { const did = await DidDht.create(); - expect(did.didDocument.verificationMethod?.[0].id).to.include('#0'); + expect(did.document.verificationMethod?.[0].id).to.include('#0'); }); it('uses the JWK thumbprint as the ID for additional verification methods, by default', async () => { @@ -173,7 +169,7 @@ describe('DidDht', () => { } }); - expect(did.didDocument.verificationMethod?.[1].id).to.include(`#${did?.didDocument?.verificationMethod?.[1]?.publicKeyJwk?.kid}`); + expect(did.document.verificationMethod?.[1].id).to.include(`#${did?.document?.verificationMethod?.[1]?.publicKeyJwk?.kid}`); }); it('allows a custom ID to be specified for additional verification methods', async () => { @@ -189,7 +185,7 @@ describe('DidDht', () => { } }); - expect(did.didDocument.verificationMethod?.[1]).to.have.property('id', `${did.uri}#1`); + expect(did.document.verificationMethod?.[1]).to.have.property('id', `${did.uri}#1`); }); it('handles creating DIDs with one service', async () => { @@ -205,10 +201,10 @@ describe('DidDht', () => { } }); - expect(did.didDocument.service).to.have.length(1); - expect(did.didDocument.service?.[0]).to.have.property('id', `${did.uri}#dwn`); - expect(did.didDocument.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); - expect(did.didDocument.service?.[0]).to.have.property('serviceEndpoint', 'https://example.com/dwn'); + expect(did.document.service).to.have.length(1); + expect(did.document.service?.[0]).to.have.property('id', `${did.uri}#dwn`); + expect(did.document.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); + expect(did.document.service?.[0]).to.have.property('serviceEndpoint', 'https://example.com/dwn'); }); it('handles creating DIDs with multiple services', async () => { @@ -229,9 +225,9 @@ describe('DidDht', () => { } }); - expect(did.didDocument.service).to.have.length(2); - expect(did.didDocument.service?.[0]).to.have.property('id', `${did.uri}#dwn`); - expect(did.didDocument.service?.[1]).to.have.property('id', `${did.uri}#oid4vci`); + expect(did.document.service).to.have.length(2); + expect(did.document.service?.[0]).to.have.property('id', `${did.uri}#dwn`); + expect(did.document.service?.[1]).to.have.property('id', `${did.uri}#oid4vci`); }); it('accepts a custom controller for the Identity Key verification method', async () => { @@ -247,7 +243,7 @@ describe('DidDht', () => { } }); - const identityKeyVerificationMethod = did.didDocument?.verificationMethod?.find( + const identityKeyVerificationMethod = did.document?.verificationMethod?.find( (method) => method.id.endsWith('#0') ); expect(identityKeyVerificationMethod).to.have.property('controller', 'did:example:1234'); @@ -280,15 +276,15 @@ describe('DidDht', () => { } }); - expect(did.didDocument.verificationMethod).to.have.length(3); - expect(did.didDocument.verificationMethod?.[1]).to.have.property('id', `${did.uri}#sig`); - expect(did.didDocument.verificationMethod?.[2]).to.have.property('id', `${did.uri}#enc`); - expect(did.didDocument.service).to.have.length(1); - expect(did.didDocument.service?.[0]).to.have.property('id', `${did.uri}#dwn`); - expect(did.didDocument.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); - expect(did.didDocument.service?.[0]).to.have.property('serviceEndpoint', 'https://example.com/dwn'); - expect(did.didDocument.service?.[0]).to.have.property('enc', '#enc'); - expect(did.didDocument.service?.[0]).to.have.property('sig', '#sig'); + expect(did.document.verificationMethod).to.have.length(3); + expect(did.document.verificationMethod?.[1]).to.have.property('id', `${did.uri}#sig`); + expect(did.document.verificationMethod?.[2]).to.have.property('id', `${did.uri}#enc`); + expect(did.document.service).to.have.length(1); + expect(did.document.service?.[0]).to.have.property('id', `${did.uri}#dwn`); + expect(did.document.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); + expect(did.document.service?.[0]).to.have.property('serviceEndpoint', 'https://example.com/dwn'); + expect(did.document.service?.[0]).to.have.property('enc', '#enc'); + expect(did.document.service?.[0]).to.have.property('sig', '#sig'); }); it('accepts one or more DID DHT registered types', async () => { @@ -318,6 +314,17 @@ describe('DidDht', () => { expect(fetchStub.called).to.be.false; }); + it('returns a version ID in DID metadata when published', async () => { + const did = await DidDht.create(); + expect(did.metadata).to.have.property('versionId'); + expect(did.metadata.versionId).to.be.a.string; + }); + + it('does not return a version ID in DID metadata when not published', async () => { + const did = await DidDht.create({ options: { publish: false } }); + expect(did.metadata).to.not.have.property('versionId'); + }); + it('returns a DID with a getSigner function that can sign and verify data', async () => { const did = await DidDht.create(); @@ -330,6 +337,31 @@ describe('DidDht', () => { expect(isValid).to.be.true; }); + it('throws an error if duplicate verification method IDs are given', async () => { + try { + await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + id : '0', + purposes : ['authentication', 'assertionMethod'] + }, + { + algorithm : 'secp256k1', + id : '0', + purposes : ['keyAgreement'] + } + ] + } + }); + + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('verification method IDs are not unique'); + } + }); + it('throws an error if publishing fails', async () => { // Simulate a network error when attempting to publish the DID. fetchStub.rejects(new Error('Network error')); @@ -417,308 +449,40 @@ describe('DidDht', () => { }); }); - describe('fromKeyManager()', () => { - let didUri: string; - let keyManager: LocalKeyManager; - let privateKey: Jwk; - - before(() => { - keyManager = new LocalKeyManager(); - }); - - beforeEach(() => { - didUri = 'did:dht:cf69rrqpanddbhkqecuwia314hfawfua9yr6zx433jmgm39ez57y'; - - privateKey = { - crv : 'Ed25519', - d : 'PISwJgl1nOlURuaqo144O1eXuGDWggYo7XX1X8oxPJs', - kty : 'OKP', - x : 'YX3yEc3AhjDxTkMnSuMy1wuKFnj4Ceu_WcpWZefovvo', - kid : 'un6C53LHsjSmjFmZsEKZKwrz0gO_LBg2nSV3a54CNoo' - }; - }); - - it('returns a DID from existing keys present in a key manager', async () => { - // Mock the response from the Pkarr relay rather than calling over the network. - fetchStub.resolves(fetchOkResponse( - Convert.hex('28a37dbbf9692e2930696ade738f85a757a508442a9a454946e9a6e11a4ccd6d47e4f1839791' + - 'e7085d836e343d71726ed77fbad48760128e8a749cd61fcf3d0d0000000065b14cbe00008400' + - '0000000200000000035f6b30045f646964346366363972727170616e646462686b7165637577' + - '696133313468666177667561397972367a783433336a6d676d3339657a353779000010000100' + - '001c2000373669643d303b743d303b6b3d5958337945633341686a4478546b4d6e53754d7931' + - '77754b466e6a344365755f576370575a65666f76766f045f646964346366363972727170616e' + - '646462686b7165637577696133313468666177667561397972367a783433336a6d676d333965' + - '7a353779000010000100001c20002726763d303b766d3d6b303b617574683d6b303b61736d3d' + - '6b303b64656c3d6b303b696e763d6b30').toArrayBuffer() - )); - - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: privateKey }); - - const did = await DidDht.fromKeyManager({ didUri, keyManager }); - - expect(did).to.have.property('didDocument'); - expect(did).to.have.property('getSigner'); - expect(did).to.have.property('keyManager'); - expect(did).to.have.property('metadata'); - expect(did).to.have.property('uri', 'did:dht:cf69rrqpanddbhkqecuwia314hfawfua9yr6zx433jmgm39ez57y'); - }); - - it('returns a DID from existing keys present in a key manager, with types', async () => { - const portableDid: PortableDid = { - uri : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', - verificationMethods : [ - { - id : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0', - type : 'JsonWebKey', - controller : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', - publicKeyJwk : { - crv : 'Ed25519', - kty : 'OKP', - x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0', - kid : 'cyvOypa6k-4ffsRWcza37s5XVOh1kO9ICUeo1ZxHVM8', - alg : 'EdDSA', - }, - privateKeyJwk: { - crv : 'Ed25519', - d : 'hdSIwbQwVD-fNOVEgt-k3mMl44Ip1iPi58Ex6VDGxqY', - kty : 'OKP', - x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0', - kid : 'cyvOypa6k-4ffsRWcza37s5XVOh1kO9ICUeo1ZxHVM8', - alg : 'EdDSA', - }, - purposes: [ - 'authentication', - 'assertionMethod', - 'capabilityDelegation', - 'capabilityInvocation', - ], - }, - ], - }; - - const mockDidResolutionResult: DidResolutionResult = { - didDocument: { - id : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', - verificationMethod : [ - { - id : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0', - type : 'JsonWebKey', - controller : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', - publicKeyJwk : { - crv : 'Ed25519', - kty : 'OKP', - x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0', - kid : 'cyvOypa6k-4ffsRWcza37s5XVOh1kO9ICUeo1ZxHVM8', - alg : 'EdDSA' - }, - }, - ], - authentication: [ - 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0' - ], - assertionMethod: [ - 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0' - ], - capabilityDelegation: [ - 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0' - ], - capabilityInvocation: [ - 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0' - ], - }, - didDocumentMetadata: { - types: [6, 7] - }, - didResolutionMetadata: {} - }; - - // Stub the DID resolve method to return the expected DID document and metadata. - const resolveStub = sinon.stub(DidDht, 'resolve').returns(Promise.resolve(mockDidResolutionResult)); - - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: portableDid.verificationMethods![0].privateKeyJwk! }); - - const did = await DidDht.fromKeyManager({ didUri: portableDid.uri!, keyManager }); - - expect(did.uri).to.equal(portableDid.uri); - expect(did.didDocument).to.deep.equal(mockDidResolutionResult.didDocument); - expect(did.metadata).to.have.property('types'); - expect(did.metadata.types).to.deep.equal([6, 7]); - - resolveStub.restore(); - }); - - it('returns a DID with a getSigner function that can sign and verify data', async () => { - const keyManager = new LocalKeyManager(); - - // Create a DID to use for the test. - const testDid = await DidDht.create({ keyManager }); - - // Stub the DID resolve method to return the expected DID document and metadata. - const resolveStub = sinon.stub(DidDht, 'resolve').returns(Promise.resolve({ - didDocument : testDid.didDocument, - didDocumentMetadata : testDid.metadata, - didResolutionMetadata : {} - })); - - const did = await DidDht.fromKeyManager({ didUri: testDid.uri, keyManager }); - - const signer = await did.getSigner(); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - - resolveStub.restore(); - }); - - it('returns a DID with a getSigner function that accepts a specific keyUri', async () => { - const keyManager = new LocalKeyManager(); - - // Create a DID to use for the test. - const testDid = await DidDht.create({ keyManager }); - - // Stub the DID resolve method to return the expected DID document and metadata. - const resolveStub = sinon.stub(DidDht, 'resolve').returns(Promise.resolve({ - didDocument : testDid.didDocument, - didDocumentMetadata : testDid.metadata, - didResolutionMetadata : {} - })); - - const did = await DidDht.fromKeyManager({ didUri: testDid.uri, keyManager }); - - // Retrieve the key URI of the verification method's public key. - const keyUri = await did.keyManager.getKeyUri({ - key: testDid.didDocument.verificationMethod![0].publicKeyJwk! - }); - - const signer = await did.getSigner({ keyUri }); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - - resolveStub.restore(); - }); - - it('throws an error if an unsupported DID method is given', async () => { + describe('getSigningMethod()', () => { + it('returns an error if the DID method is not supported', async () => { try { - await DidDht.fromKeyManager({ didUri: 'did:example:1234', keyManager }); + await DidDht.getSigningMethod({ didDocument: { id: 'did:method:123' } }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('Method not supported'); expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('Method not supported'); } }); - it('throws an error if the resolved DID document lacks any verification methods', async () => { - // Stub the DID resolve method to return a DID document without a verificationMethod property. - sinon.stub(DidDht, 'resolve').returns(Promise.resolve({ - didDocument : { id: 'did:dht:...' }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); - - const didUri = 'did:dht:...'; - try { - await DidDht.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('missing verification methods'); - } finally { - sinon.restore(); - } - - // Stub the DID resolve method to return a DID document an empty verificationMethod property. - sinon.stub(DidDht, 'resolve').returns(Promise.resolve({ - didDocument : { id: 'did:dht:...', verificationMethod: [] }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); - - try { - await DidDht.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('missing verification methods'); - } finally { - sinon.restore(); - } - }); - - it('throws an error if the resolved DID document is missing a public key', async () => { - // Stub the DID resolution method to return a DID document with no verification methods. - sinon.stub(DidDht, 'resolve').returns(Promise.resolve({ - didDocument: { - id : 'did:dht:...', - verificationMethod : [{ - id : 'did:dht:...#0', - type : 'JsonWebKey', - controller : 'did:dht:...' - }], - }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); - - const didUri = 'did:dht:...'; + it('throws an error if the DID Document does not any verification methods', async () => { try { - await DidDht.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); + await DidDht.getSigningMethod({ + didDocument: { + id : 'did:dht:123', + verificationMethod : [] + } + }); + expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('does not contain a public key'); - } finally { - sinon.restore(); + expect(error.message).to.include('method intended for signing could not be determined'); } }); }); - describe('fromKeys', () => { + describe('import()', () => { let portableDid: PortableDid; beforeEach(() => { // Define a DID to use for the test. portableDid = { - uri : 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo', - verificationMethods : [ - { - id : 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#0', - type : 'JsonWebKey', - controller : 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo', - publicKeyJwk : { - crv : 'Ed25519', - kty : 'OKP', - x : 'mRDzqCLKKBGRLs-gEuSNMdMILu2cjB0wquJygGgfK40', - kid : 'FuIkkMgnsq-XRX8gWp3HJpqwoIbyNNsx4Uk-tdDSqbE', - alg : 'EdDSA' - }, - privateKeyJwk: { - crv : 'Ed25519', - d : '3OQkejC7rNiGQSPAugN8CFrIjHGemZh5hbtgD8GXUVw', - kty : 'OKP', - x : 'mRDzqCLKKBGRLs-gEuSNMdMILu2cjB0wquJygGgfK40', - kid : 'FuIkkMgnsq-XRX8gWp3HJpqwoIbyNNsx4Uk-tdDSqbE', - alg : 'EdDSA' - }, - purposes: [ - 'authentication', - 'assertionMethod', - 'capabilityDelegation', - 'capabilityInvocation' - ], - }, - ], - }; - }); - - it('returns a previously created DID from the URI and imported key material', async () => { - const mockDidResolutionResult: DidResolutionResult = { - didDocument: { + uri : 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo', + document : { id : 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo', verificationMethod : [ { @@ -747,59 +511,34 @@ describe('DidDht', () => { 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#0', ], }, - didDocumentMetadata : {}, - didResolutionMetadata : {} + metadata : {}, + privateKeys : [ + { + crv : 'Ed25519', + d : '3OQkejC7rNiGQSPAugN8CFrIjHGemZh5hbtgD8GXUVw', + kty : 'OKP', + x : 'mRDzqCLKKBGRLs-gEuSNMdMILu2cjB0wquJygGgfK40', + kid : 'FuIkkMgnsq-XRX8gWp3HJpqwoIbyNNsx4Uk-tdDSqbE', + alg : 'EdDSA' + } + ] }; + }); - // Stub the DID resolve method to return the expected DID document and metadata. - const resolveStub = sinon.stub(DidDht, 'resolve').returns(Promise.resolve(mockDidResolutionResult)); - - const did = await DidDht.fromKeys(portableDid); + it('returns a previously created DID from the URI and imported key material', async () => { + const did = await DidDht.import({ portableDid }); - expect(did).to.have.property('didDocument'); + expect(did).to.have.property('document'); expect(did).to.have.property('getSigner'); expect(did).to.have.property('keyManager'); expect(did).to.have.property('metadata'); expect(did).to.have.property('uri', portableDid.uri); - - resolveStub.restore(); }); it('returns a previously created DID from the URI and imported key material, with types', async () => { portableDid = { - uri : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', - verificationMethods : [ - { - id : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0', - type : 'JsonWebKey', - controller : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', - publicKeyJwk : { - crv : 'Ed25519', - kty : 'OKP', - x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0', - kid : 'cyvOypa6k-4ffsRWcza37s5XVOh1kO9ICUeo1ZxHVM8', - alg : 'EdDSA', - }, - privateKeyJwk: { - crv : 'Ed25519', - d : 'hdSIwbQwVD-fNOVEgt-k3mMl44Ip1iPi58Ex6VDGxqY', - kty : 'OKP', - x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0', - kid : 'cyvOypa6k-4ffsRWcza37s5XVOh1kO9ICUeo1ZxHVM8', - alg : 'EdDSA', - }, - purposes: [ - 'authentication', - 'assertionMethod', - 'capabilityDelegation', - 'capabilityInvocation', - ], - }, - ], - }; - - const mockDidResolutionResult: DidResolutionResult = { - didDocument: { + uri : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', + document : { id : 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo', verificationMethod : [ { @@ -828,91 +567,67 @@ describe('DidDht', () => { 'did:dht:ksbkpsjytbm7kh6hnt3xi91t6to98zndtrrxzsqz9y87m5qztyqo#0' ], }, - didDocumentMetadata: { + metadata: { types: [6, 7] }, - didResolutionMetadata: {} + privateKeys: [ + { + crv : 'Ed25519', + d : 'hdSIwbQwVD-fNOVEgt-k3mMl44Ip1iPi58Ex6VDGxqY', + kty : 'OKP', + x : 'VYKm2SCIV9Vz3BRy-v5R9GHz3EOJCPvZ1_gP1e3XiB0', + kid : 'cyvOypa6k-4ffsRWcza37s5XVOh1kO9ICUeo1ZxHVM8', + alg : 'EdDSA', + } + ] }; - // Stub the DID resolve method to return the expected DID document and metadata. - const resolveStub = sinon.stub(DidDht, 'resolve').returns(Promise.resolve(mockDidResolutionResult)); - - const did = await DidDht.fromKeys(portableDid); + const did = await DidDht.import({ portableDid }); expect(did.metadata).to.deep.equal({ types: [6, 7] }); - - resolveStub.restore(); - }); - - it('returns a new DID created from the given verification material', async () => { - // Remove the URI from the test portable DID so that fromKeys() creates the DID object - // using only the key material. - const { uri, ...didWithOnlyKeys } = portableDid; - - const did = await DidDht.fromKeys(didWithOnlyKeys); - - expect(did).to.have.property('didDocument'); - expect(did).to.have.property('getSigner'); - expect(did).to.have.property('keyManager'); - expect(did).to.have.property('metadata'); - expect(did).to.have.property('uri', uri); }); - it('throws an error if no verification methods are given', async () => { - try { - // @ts-expect-error - Testing invalid argument. - await DidDht.fromKeys({}); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('one verification method'); - } - }); + it('can import exported PortableDid', async () => { + // Create a DID to use for the test. + const did = await DidDht.create(); - it('throws an error if the given key set is empty', async () => { - try { - await DidDht.fromKeys({ verificationMethods: [] }); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('one verification method'); - } - }); + // Export the BearerDid to a portable format. + const portableDid = await did.export(); - it('throws an error if the given key set is missing a public key', async () => { - delete portableDid.verificationMethods![0].publicKeyJwk; + // Create a DID object from the portable format. + const didFromPortable = await DidDht.import({ portableDid }); - try { - await DidDht.fromKeys(portableDid); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('must contain a public and private key'); - } + expect(didFromPortable.document).to.deep.equal(did.document); + expect(didFromPortable.metadata).to.deep.equal(did.metadata); }); - it('throws an error if the given key set is missing a private key', async () => { - delete portableDid.verificationMethods![0].privateKeyJwk; + it('throws an error if the DID method is not supported', async () => { + // Change the method to something other than 'dht'. + portableDid.uri = 'did:unknown:abc123'; try { - await DidDht.fromKeys(portableDid); + await DidDht.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('must contain a public and private key'); + expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('Method not supported'); } }); it('throws an error if an Identity Key is not included in the given verification methods', async () => { // Change the ID of the verification method to something other than 0. - portableDid.verificationMethods![0].id = 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#1'; + portableDid.document.verificationMethod![0].id = 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#1'; try { - await DidDht.fromKeys(portableDid); + await DidDht.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('missing an Identity Key'); + expect(error.message).to.include('must contain an Identity Key'); } }); }); - describe('resolve', () => { + describe('resolve()', () => { it('resolves a published DID with a single verification method', async () => { // Mock the response from the Pkarr relay rather than calling over the network. fetchStub.resolves(fetchOkResponse( @@ -1030,6 +745,29 @@ describe('DidDht', () => { expect(didResolutionResult.didDocumentMetadata.types).to.include(DidDhtRegisteredDidType.WebApp); }); + it('returns a version ID in DID document metadata', async () => { + // Mock the response from the Pkarr relay rather than calling over the network. + fetchStub.resolves(fetchOkResponse( + Convert.hex('ea33e704f3a48a3392f54b28744cdfb4e24780699f92ba7df62fd486d2a2cda3f263e1c6bcbd' + + '75d438be7316e5d6e94b13e98151f599cfecefad0b37432bd90a0000000065b0ed1600008400' + + '0000000300000000035f6b30045f6469643439746a6f6f773435656631686b736f6f3936626d' + + '7a6b777779336d686d653935643766736933657a6a796a67686d70373571796f000010000100' + + '001c2000373669643d303b743d303b6b3d5f464d49553174425a63566145502d437536715542' + + '6c66466f5f73665332726c4630675362693239323445045f747970045f6469643439746a6f6f' + + '773435656631686b736f6f3936626d7a6b777779336d686d653935643766736933657a6a796a' + + '67686d70373571796f000010000100001c2000070669643d372c36045f6469643439746a6f6f' + + '773435656631686b736f6f3936626d7a6b777779336d686d653935643766736933657a6a796a' + + '67686d70373571796f000010000100001c20002726763d303b766d3d6b303b617574683d6b30' + + '3b61736d3d6b303b64656c3d6b303b696e763d6b30').toArrayBuffer() + )); + + const did = 'did:dht:9tjoow45ef1hksoo96bmzkwwy3mhme95d7fsi3ezjyjghmp75qyo'; + const didResolutionResult = await DidDht.resolve(did); + + expect(didResolutionResult.didDocumentMetadata).to.have.property('versionId'); + expect(didResolutionResult.didDocumentMetadata.versionId).to.be.a.string; + }); + it('returns a notFound error if the DID is not published', async () => { // Mock the response from the Pkarr relay rather than calling over the network. fetchStub.resolves(fetchNotFoundResponse()); @@ -1064,45 +802,4 @@ describe('DidDht', () => { expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'invalidDidDocumentLength'); }); }); - - describe('toKeys()', () => { - it('returns a single verification method for a DID, by default', async () => { - // Create a DID to use for the test. - const did = await DidDht.create(); - - const portableDid = await DidDht.toKeys({ did }); - - expect(portableDid).to.have.property('verificationMethods'); - expect(portableDid.verificationMethods).to.have.length(1); - expect(portableDid.verificationMethods[0]).to.have.property('publicKeyJwk'); - expect(portableDid.verificationMethods[0]).to.have.property('privateKeyJwk'); - expect(portableDid.verificationMethods[0]).to.have.property('purposes'); - expect(portableDid.verificationMethods[0]).to.have.property('type'); - expect(portableDid.verificationMethods[0]).to.have.property('id'); - expect(portableDid.verificationMethods[0]).to.have.property('controller'); - }); - - it('output can be used to instantiate a DID object', async () => { - // Create a DID to use for the test. - const did = await DidDht.create(); - - // Convert the DID to a portable format. - const portableDid = await DidDht.toKeys({ did }); - - // Stub the DID resolve method to return the expected DID document and metadata. - const resolveStub = sinon.stub(DidDht, 'resolve').returns(Promise.resolve({ - didDocument : did.didDocument, - didDocumentMetadata : did.metadata, - didResolutionMetadata : {} - })); - - // Create a DID object from the portable format. - const didFromPortable = await DidDht.fromKeys(portableDid); - - expect(didFromPortable.didDocument).to.deep.equal(did.didDocument); - expect(didFromPortable.metadata).to.deep.equal(did.metadata); - - resolveStub.restore(); - }); - }); }); \ No newline at end of file diff --git a/packages/dids/tests/methods/did-ion.spec.ts b/packages/dids/tests/methods/did-ion.spec.ts index e1f73dc9b..ffcdf5574 100644 --- a/packages/dids/tests/methods/did-ion.spec.ts +++ b/packages/dids/tests/methods/did-ion.spec.ts @@ -2,13 +2,13 @@ import type { Jwk } from '@web5/crypto'; import sinon from 'sinon'; import { expect } from 'chai'; -import { LocalKeyManager, computeJwkThumbprint } from '@web5/crypto'; +import { computeJwkThumbprint } from '@web5/crypto'; import type { DidDocument } from '../../src/types/did-core.js'; +import type { PortableDid } from '../../src/types/portable-did.js'; import { DidIon } from '../../src/methods/did-ion.js'; import { vectors as CreateTestVector } from '../fixtures/test-vectors/did-ion/create.js'; -import { vectors as ToKeysTestVector } from '../fixtures/test-vectors/did-ion/to-keys.js'; import { vectors as ResolveTestVector } from '../fixtures/test-vectors/did-ion/resolve.js'; // Helper function to create a mocked fetch response that fails and returns a 404 Not Found. @@ -54,7 +54,7 @@ describe('DidIon', () => { const did = await DidIon.create(); - expect(did).to.have.property('didDocument'); + expect(did).to.have.property('document'); expect(did).to.have.property('getSigner'); expect(did).to.have.property('keyManager'); expect(did).to.have.property('metadata'); @@ -62,8 +62,8 @@ describe('DidIon', () => { expect(fetchStub.calledTwice).to.be.true; - expect(did.didDocument).to.have.property('verificationMethod'); - expect(did.didDocument.verificationMethod).to.have.length(1); + expect(did.document).to.have.property('verificationMethod'); + expect(did.document.verificationMethod).to.have.length(1); expect(did.metadata).to.have.property('canonicalId'); }); @@ -91,9 +91,9 @@ describe('DidIon', () => { } }); - expect(did.didDocument.verificationMethod).to.have.length(2); - expect(did.didDocument.verificationMethod?.[0].publicKeyJwk).to.have.property('crv', 'Ed25519'); - expect(did.didDocument.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'secp256k1'); + expect(did.document.verificationMethod).to.have.length(2); + expect(did.document.verificationMethod?.[0].publicKeyJwk).to.have.property('crv', 'Ed25519'); + expect(did.document.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'secp256k1'); }); it('uses the JWK thumbprint as the ID for verification methods, by default', async () => { @@ -107,8 +107,8 @@ describe('DidIon', () => { const did = await DidIon.create(); - const expectedKeyId = await computeJwkThumbprint({ jwk: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); - expect(did.didDocument.verificationMethod?.[0].id).to.include(expectedKeyId); + const expectedKeyId = await computeJwkThumbprint({ jwk: did.document.verificationMethod![0]!.publicKeyJwk! }); + expect(did.document.verificationMethod?.[0].id).to.include(expectedKeyId); }); it('allows a custom ID to be specified for additional verification methods', async () => { @@ -132,7 +132,7 @@ describe('DidIon', () => { } }); - expect(did.didDocument.verificationMethod?.[0]).to.have.property('id', '#1'); + expect(did.document.verificationMethod?.[0]).to.have.property('id', '#1'); }); it('retains only the ID fragment if verification method IDs contain a prefix before the hash symbol (#)', async () => { @@ -156,7 +156,7 @@ describe('DidIon', () => { } }); - expect(did.didDocument.verificationMethod?.[0]).to.have.property('id', '#1'); + expect(did.document.verificationMethod?.[0]).to.have.property('id', '#1'); }); it('handles creating DIDs with one service', async () => { @@ -180,10 +180,10 @@ describe('DidIon', () => { } }); - expect(did.didDocument.service).to.have.length(1); - expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); - expect(did.didDocument.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); - expect(did.didDocument.service?.[0]).to.have.property('serviceEndpoint', 'https://example.com/dwn'); + expect(did.document.service).to.have.length(1); + expect(did.document.service?.[0]).to.have.property('id', `#dwn`); + expect(did.document.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); + expect(did.document.service?.[0]).to.have.property('serviceEndpoint', 'https://example.com/dwn'); }); it('handles creating DIDs with multiple services', async () => { @@ -212,9 +212,9 @@ describe('DidIon', () => { } }); - expect(did.didDocument.service).to.have.length(2); - expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); - expect(did.didDocument.service?.[1]).to.have.property('id', `#oid4vci`); + expect(did.document.service).to.have.length(2); + expect(did.document.service?.[0]).to.have.property('id', `#dwn`); + expect(did.document.service?.[1]).to.have.property('id', `#oid4vci`); }); it('given service IDs are automatically prefixed with hash symbol (#) in DID document', async () => { @@ -238,8 +238,8 @@ describe('DidIon', () => { } }); - expect(did.didDocument.service).to.have.length(1); - expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); + expect(did.document.service).to.have.length(1); + expect(did.document.service?.[0]).to.have.property('id', `#dwn`); }); it('accepts service IDs that start with a hash symbol (#)', async () => { @@ -263,8 +263,8 @@ describe('DidIon', () => { } }); - expect(did.didDocument.service).to.have.length(1); - expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); + expect(did.document.service).to.have.length(1); + expect(did.document.service?.[0]).to.have.property('id', `#dwn`); }); it('retains only the ID fragment if service IDs contain a prefix before the hash symbol (#)', async () => { @@ -288,8 +288,8 @@ describe('DidIon', () => { } }); - expect(did.didDocument.service).to.have.length(1); - expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); + expect(did.document.service).to.have.length(1); + expect(did.document.service?.[0]).to.have.property('id', `#dwn`); }); it('accepts custom properties for services', async () => { @@ -329,16 +329,16 @@ describe('DidIon', () => { } }); - expect(did.didDocument.verificationMethod).to.have.length(2); - expect(did.didDocument.verificationMethod?.[0]).to.have.property('id', `#sig`); - expect(did.didDocument.verificationMethod?.[1]).to.have.property('id', `#enc`); - expect(did.didDocument.service).to.have.length(1); - expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); - expect(did.didDocument.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); - expect(did.didDocument.service?.[0]).to.have.property('serviceEndpoint'); - expect(did.didDocument.service?.[0]?.serviceEndpoint).to.have.property('nodes'); - expect(did.didDocument.service?.[0]?.serviceEndpoint).to.have.property('encryptionKeys'); - expect(did.didDocument.service?.[0]?.serviceEndpoint).to.have.property('signingKeys'); + expect(did.document.verificationMethod).to.have.length(2); + expect(did.document.verificationMethod?.[0]).to.have.property('id', `#sig`); + expect(did.document.verificationMethod?.[1]).to.have.property('id', `#enc`); + expect(did.document.service).to.have.length(1); + expect(did.document.service?.[0]).to.have.property('id', `#dwn`); + expect(did.document.service?.[0]).to.have.property('type', 'DecentralizedWebNode'); + expect(did.document.service?.[0]).to.have.property('serviceEndpoint'); + expect(did.document.service?.[0]?.serviceEndpoint).to.have.property('nodes'); + expect(did.document.service?.[0]?.serviceEndpoint).to.have.property('encryptionKeys'); + expect(did.document.service?.[0]?.serviceEndpoint).to.have.property('signingKeys'); }); it('publishes DIDs, by default', async () => { @@ -421,34 +421,8 @@ describe('DidIon', () => { }); }); - describe('fromKeyManager()', () => { - let keyManager: LocalKeyManager; - - before(() => { - keyManager = new LocalKeyManager(); - }); - - it('returns a DID from existing keys present in a key manager', async () => { - // Stub the DID resolution method to return a DID document with no verification methods. - sinon.stub(DidIon, 'resolve').returns(Promise.resolve(CreateTestVector.oneMethodNoServices.didResolutionResult)); - - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: CreateTestVector.oneMethodNoServices.privateKey[0] }); - - const did = await DidIon.fromKeyManager({ didUri: CreateTestVector.oneMethodNoServices.didUri, keyManager }); - - expect(did).to.have.property('didDocument'); - expect(did).to.have.property('getSigner'); - expect(did).to.have.property('keyManager'); - expect(did).to.have.property('metadata'); - expect(did).to.have.property('uri', CreateTestVector.oneMethodNoServices.didUri); - - sinon.restore(); - }); - }); - describe('getSigningMethod()', () => { - it('returns the first authentication verification method', async function () { + it('returns the first assertionMethod verification method', async function () { const verificationMethod = await DidIon.getSigningMethod({ didDocument: { id : 'did:ion:123', @@ -460,7 +434,7 @@ describe('DidIon', () => { publicKeyJwk : {} as Jwk } ], - authentication: ['did:ion:123#0'] + assertionMethod: ['did:ion:123#0'] } }); @@ -468,61 +442,70 @@ describe('DidIon', () => { expect(verificationMethod).to.have.property('id', 'did:ion:123#0'); }); - it('returns undefined if there is no authentication verification method', async function () { - const verificationMethod = await DidIon.getSigningMethod({ - didDocument: { - id : 'did:ion:123', - verificationMethod : [ - { - id : 'did:ion:123#0', - type : 'JsonWebKey2020', - controller : 'did:ion:123', - publicKeyJwk : {} as Jwk - } - ], - assertionMethod: ['did:ion:123#0'] - } - }); - - expect(verificationMethod).to.not.exist; + it('throws an error if the DID document is missing verification methods', async function () { + try { + await DidIon.getSigningMethod({ + didDocument: { id: 'did:ion:123' } + }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('verification method intended for signing could not be determined'); + } }); - it('returns undefined if the only authentication method is embedded', async function () { - const verificationMethod = await DidIon.getSigningMethod({ - didDocument: { - id : 'did:ion:123', - verificationMethod : [ - { - id : 'did:ion:123#0', - type : 'JsonWebKey2020', - controller : 'did:ion:123', - publicKeyJwk : {} as Jwk - } - ], - authentication: [ - { - id : 'did:ion:123#1', - type : 'JsonWebKey2020', - controller : 'did:ion:123', - publicKeyJwk : {} as Jwk - } - ], - assertionMethod: ['did:ion:123#0'] - } - }); - - expect(verificationMethod).to.not.exist; + it('throws an error if there is no assertionMethod verification method', async function () { + try { + await DidIon.getSigningMethod({ + didDocument: { + id : 'did:ion:123', + verificationMethod : [ + { + id : 'did:ion:123#0', + type : 'JsonWebKey2020', + controller : 'did:ion:123', + publicKeyJwk : {} as Jwk + } + ], + authentication: ['did:ion:123#0'] + } + }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('verification method intended for signing could not be determined'); + } }); - it('handles didDocuments missing verification methods', async function () { - const result = await DidIon.getSigningMethod({ - didDocument: { id: 'did:ion:123' } - }); - - expect(result).to.be.undefined; + it('throws an error if the only assertionMethod method is embedded', async function () { + try { + await DidIon.getSigningMethod({ + didDocument: { + id : 'did:ion:123', + verificationMethod : [ + { + id : 'did:ion:123#0', + type : 'JsonWebKey2020', + controller : 'did:ion:123', + publicKeyJwk : {} as Jwk + } + ], + assertionMethod: [ + { + id : 'did:ion:123#1', + type : 'JsonWebKey2020', + controller : 'did:ion:123', + publicKeyJwk : {} as Jwk + } + ], + authentication: ['did:ion:123#0'] + } + }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.include('verification method intended for signing could not be determined'); + } }); - it('throws an error if a non-key method is used', async function () { + it('throws an error if a non-ion method is used', async function () { // Example DID Document with a non-key method const didDocument: DidDocument = { '@context' : 'https://www.w3.org/ns/did/v1', @@ -546,6 +529,92 @@ describe('DidIon', () => { }); }); + describe('import()', () => { + let portableDid: PortableDid; + + beforeEach(() => { + // Define a DID to use for the test. + portableDid = { + uri : 'did:ion:EiB82xs9NseP908Y4amd7oW3jstZuTBQwk2q1ZhdLU9-Sg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJ3N0tPN1hCMTB5VDZ2RFRTVEh5UWtGaG5VcEZmcVd6eGtkNzB3ZHdDY1ZnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOHJXb0xxR1lyLWxjOUZXUC1peWdDbHZ4R1lNRHJBOEF3NVAwR3ZuOC05RSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCIsImNhcGFiaWxpdHlEZWxlZ2F0aW9uIiwiY2FwYWJpbGl0eUludm9jYXRpb24iXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUFDdWppZ084N3oyOUJ0N2pjRlViMUdXeUJBTlNuSlA2NF9QS0ctVzVwc19RIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDN2haUmh3elBTQlE0bkxnbm5TcmRuWE5FWGRZYnk2VUQ1VXNzTkhNSG9rQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQU5UXzdkbVBFbklQMUlUNERqaUQxeVJ2VDVrMlg2V3owcVRNZ1k3TU9vRGcifX0', + document : { + id : 'did:ion:EiB82xs9NseP908Y4amd7oW3jstZuTBQwk2q1ZhdLU9-Sg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJ3N0tPN1hCMTB5VDZ2RFRTVEh5UWtGaG5VcEZmcVd6eGtkNzB3ZHdDY1ZnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOHJXb0xxR1lyLWxjOUZXUC1peWdDbHZ4R1lNRHJBOEF3NVAwR3ZuOC05RSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCIsImNhcGFiaWxpdHlEZWxlZ2F0aW9uIiwiY2FwYWJpbGl0eUludm9jYXRpb24iXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUFDdWppZ084N3oyOUJ0N2pjRlViMUdXeUJBTlNuSlA2NF9QS0ctVzVwc19RIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDN2haUmh3elBTQlE0bkxnbm5TcmRuWE5FWGRZYnk2VUQ1VXNzTkhNSG9rQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQU5UXzdkbVBFbklQMUlUNERqaUQxeVJ2VDVrMlg2V3owcVRNZ1k3TU9vRGcifX0', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiB82xs9NseP908Y4amd7oW3jstZuTBQwk2q1ZhdLU9-Sg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJ3N0tPN1hCMTB5VDZ2RFRTVEh5UWtGaG5VcEZmcVd6eGtkNzB3ZHdDY1ZnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOHJXb0xxR1lyLWxjOUZXUC1peWdDbHZ4R1lNRHJBOEF3NVAwR3ZuOC05RSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCIsImNhcGFiaWxpdHlEZWxlZ2F0aW9uIiwiY2FwYWJpbGl0eUludm9jYXRpb24iXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUFDdWppZ084N3oyOUJ0N2pjRlViMUdXeUJBTlNuSlA2NF9QS0ctVzVwc19RIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDN2haUmh3elBTQlE0bkxnbm5TcmRuWE5FWGRZYnk2VUQ1VXNzTkhNSG9rQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQU5UXzdkbVBFbklQMUlUNERqaUQxeVJ2VDVrMlg2V3owcVRNZ1k3TU9vRGcifX0', + }, + ], + service: [ + ], + verificationMethod: [ + { + id : '#w7KO7XB10yT6vDTSTHyQkFhnUpFfqWzxkd70wdwCcVg', + controller : 'did:ion:EiB82xs9NseP908Y4amd7oW3jstZuTBQwk2q1ZhdLU9-Sg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJ3N0tPN1hCMTB5VDZ2RFRTVEh5UWtGaG5VcEZmcVd6eGtkNzB3ZHdDY1ZnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOHJXb0xxR1lyLWxjOUZXUC1peWdDbHZ4R1lNRHJBOEF3NVAwR3ZuOC05RSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCIsImNhcGFiaWxpdHlEZWxlZ2F0aW9uIiwiY2FwYWJpbGl0eUludm9jYXRpb24iXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUFDdWppZ084N3oyOUJ0N2pjRlViMUdXeUJBTlNuSlA2NF9QS0ctVzVwc19RIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDN2haUmh3elBTQlE0bkxnbm5TcmRuWE5FWGRZYnk2VUQ1VXNzTkhNSG9rQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQU5UXzdkbVBFbklQMUlUNERqaUQxeVJ2VDVrMlg2V3owcVRNZ1k3TU9vRGcifX0', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : '8rWoLqGYr-lc9FWP-iygClvxGYMDrA8Aw5P0Gvn8-9E', + }, + }, + ], + authentication: [ + '#w7KO7XB10yT6vDTSTHyQkFhnUpFfqWzxkd70wdwCcVg', + ], + assertionMethod: [ + '#w7KO7XB10yT6vDTSTHyQkFhnUpFfqWzxkd70wdwCcVg', + ], + capabilityDelegation: [ + '#w7KO7XB10yT6vDTSTHyQkFhnUpFfqWzxkd70wdwCcVg', + ], + capabilityInvocation: [ + '#w7KO7XB10yT6vDTSTHyQkFhnUpFfqWzxkd70wdwCcVg', + ], + }, + metadata: { + published : true, + canonicalId : 'did:ion:EiB82xs9NseP908Y4amd7oW3jstZuTBQwk2q1ZhdLU9-Sg', + recoveryKey : { + kty : 'EC', + crv : 'secp256k1', + x : 'QksyL3a7KSJiP3wBDKE5y6eJfLB-zhrwzogMaBKTJWE', + y : 'UBB51L3h9WtZO-H1DPa14NL0Nprl9QhZqzT-yeE_-Rc', + kid : 'HjpYhxsUEVbp3rJMmP4JZ6I6QoyBwLReEN4LRUm1mbM', + alg : 'ES256K', + }, + updateKey: { + kty : 'EC', + crv : 'secp256k1', + x : 'gr57k7ktS7YtWv1lrqML6bSUIANlnGIOoxbo19hPSyw', + y : 'XeIPR96BI3Q-HTDW5_pF0wNeNw1Q-2wcNx_1IpllFmc', + kid : 'DImcjX7RGtpcmDPKADfYKNEukweZzfP1NZHQH5RW6AM', + alg : 'ES256K', + }, + }, + privateKeys: [ + { + crv : 'Ed25519', + d : 'cmMpyVm6LdGCOW0mk9NWn4RRhTqs_GYz5Oys_0aQNtM', + kty : 'OKP', + x : '8rWoLqGYr-lc9FWP-iygClvxGYMDrA8Aw5P0Gvn8-9E', + kid : 'w7KO7XB10yT6vDTSTHyQkFhnUpFfqWzxkd70wdwCcVg', + alg : 'EdDSA', + }, + ], + }; + }); + + it('returns a previously created DID from the URI and imported key material', async () => { + const did = await DidIon.import({ portableDid }); + + expect(did).to.have.property('document'); + expect(did).to.have.property('getSigner'); + expect(did).to.have.property('keyManager'); + expect(did).to.have.property('metadata'); + expect(did).to.have.property('uri', portableDid.uri); + }); + }); + describe('resolve()', () => { it('resolves published short form ION DIDs', async() => { fetchStub.returns(Promise.resolve(fetchOkResponse(ResolveTestVector.publishedDid.didResolutionResult))); @@ -610,32 +679,32 @@ describe('DidIon', () => { }); }); - describe('toKeys()', () => { - let keyManager: LocalKeyManager; - - before(() => { - keyManager = new LocalKeyManager(); - }); - - it('returns a single verification method for a DID, by default', async () => { - // Import the test DID's key into the key manager. - await keyManager.importKey({ key: ToKeysTestVector.oneMethodNoServices.privateKey[0] }); - - // Use the DID object from the test vector but with the instantiated key manager. - const did = ToKeysTestVector.oneMethodNoServices.did; - did.keyManager = keyManager; - - // Convert the DID to a portable format. - const portableDid = await DidIon.toKeys({ did }); - - expect(portableDid).to.have.property('verificationMethods'); - expect(portableDid.verificationMethods).to.have.length(1); - expect(portableDid.verificationMethods[0]).to.have.property('publicKeyJwk'); - expect(portableDid.verificationMethods[0]).to.have.property('privateKeyJwk'); - expect(portableDid.verificationMethods[0]).to.have.property('purposes'); - expect(portableDid.verificationMethods[0]).to.have.property('type'); - expect(portableDid.verificationMethods[0]).to.have.property('id'); - expect(portableDid.verificationMethods[0]).to.have.property('controller'); - }); - }); + // describe('toKeys()', () => { + // let keyManager: LocalKeyManager; + + // before(() => { + // keyManager = new LocalKeyManager(); + // }); + + // it('returns a single verification method for a DID, by default', async () => { + // // Import the test DID's key into the key manager. + // await keyManager.importKey({ key: ToKeysTestVector.oneMethodNoServices.privateKey[0] }); + + // // Use the DID object from the test vector but with the instantiated key manager. + // const did = ToKeysTestVector.oneMethodNoServices.did; + // did.keyManager = keyManager; + + // // Convert the DID to a portable format. + // const portableDid = await DidIon.toKeys({ did }); + + // expect(portableDid).to.have.property('verificationMethods'); + // expect(portableDid.verificationMethods).to.have.length(1); + // expect(portableDid.verificationMethods[0]).to.have.property('publicKeyJwk'); + // expect(portableDid.verificationMethods[0]).to.have.property('privateKeyJwk'); + // expect(portableDid.verificationMethods[0]).to.have.property('purposes'); + // expect(portableDid.verificationMethods[0]).to.have.property('type'); + // expect(portableDid.verificationMethods[0]).to.have.property('id'); + // expect(portableDid.verificationMethods[0]).to.have.property('controller'); + // }); + // }); }); \ No newline at end of file diff --git a/packages/dids/tests/methods/did-jwk.spec.ts b/packages/dids/tests/methods/did-jwk.spec.ts index fc8c78c04..5142344e7 100644 --- a/packages/dids/tests/methods/did-jwk.spec.ts +++ b/packages/dids/tests/methods/did-jwk.spec.ts @@ -1,12 +1,11 @@ import type { Jwk } from '@web5/crypto'; import type { UnwrapPromise } from '@web5/common'; -import sinon from 'sinon'; import { expect } from 'chai'; import { LocalKeyManager } from '@web5/crypto'; import type { DidDocument } from '../../src/types/did-core.js'; -import type { PortableDid, PortableDidVerificationMethod } from '../../src/methods/did-method.js'; +import type { PortableDid } from '../../src/types/portable-did.js'; import { DidErrorCode } from '../../src/did-error.js'; import { DidJwk } from '../../src/methods/did-jwk.js'; @@ -21,15 +20,15 @@ describe('DidJwk', () => { describe('create()', () => { it('creates a did:jwk DID', async () => { - const did = await DidJwk.create({ keyManager, options: { algorithm: 'Ed25519' } }); + const did = await DidJwk.create({ keyManager, options: { algorithm: 'secp256k1' } }); - expect(did).to.have.property('didDocument'); + expect(did).to.have.property('document'); expect(did).to.have.property('getSigner'); expect(did).to.have.property('keyManager'); expect(did).to.have.property('metadata'); expect(did).to.have.property('uri'); expect(did.uri.startsWith('did:jwk:')).to.be.true; - expect(did.didDocument.verificationMethod).to.have.length(1); + expect(did.document.verificationMethod).to.have.length(1); }); it('uses a default key manager and key generation algorithm if neither is given', async () => { @@ -50,7 +49,7 @@ describe('DidJwk', () => { const did = await DidJwk.create({ keyManager, options: { algorithm: 'secp256k1' } }); // Retrieve the public key from the key manager. - const keyUri = await keyManager.getKeyUri({ key: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); + const keyUri = await keyManager.getKeyUri({ key: did.document.verificationMethod![0]!.publicKeyJwk! }); const publicKey = await keyManager.getPublicKey({ keyUri }); // Verify the public key is an secp256k1 key. @@ -61,7 +60,7 @@ describe('DidJwk', () => { const did = await DidJwk.create({ keyManager, options: { verificationMethods: [{ algorithm: 'secp256k1' }] } }); // Retrieve the public key from the key manager. - const keyUri = await keyManager.getKeyUri({ key: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); + const keyUri = await keyManager.getKeyUri({ key: did.document.verificationMethod![0]!.publicKeyJwk! }); const publicKey = await keyManager.getPublicKey({ keyUri }); // Verify the public key is an secp256k1 key. @@ -72,7 +71,7 @@ describe('DidJwk', () => { const did = await DidJwk.create({ keyManager }); // Retrieve the public key from the key manager. - const keyUri = await keyManager.getKeyUri({ key: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); + const keyUri = await keyManager.getKeyUri({ key: did.document.verificationMethod![0]!.publicKeyJwk! }); const publicKey = await keyManager.getPublicKey({ keyUri }); // Verify the public key is an Ed25519 key. @@ -89,31 +88,6 @@ describe('DidJwk', () => { ).to.have.property('uri'); }); - it('returns a getSigner() function that creates valid signatures that can be verified', async () => { - const did = await DidJwk.create({ keyManager, options: { algorithm: 'Ed25519' } }); - - const signer = await did.getSigner(); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - }); - - it('returns a getSigner() function handles undefined params', async function () { - // Create a `did:jwk` DID. - const did = await DidJwk.create({ keyManager, options: { algorithm: 'Ed25519' } }); - - // Simulate the creation of a signer with undefined params - const signer = await did.getSigner({ }); - - // Note: Since this test does not interact with an actual keyManager, it primarily ensures - // that the method doesn't break with undefined params. - expect(signer).to.have.property('sign'); - expect(signer).to.have.property('verify'); - }); - it('throws an error if both algorithm and verificationMethods are provided', async () => { try { await DidJwk.create({ @@ -128,241 +102,170 @@ describe('DidJwk', () => { expect(error.message).to.include('options are mutually exclusive'); } }); - }); - - describe('fromKeyManager()', () => { - let didUri: string; - let keyManager: LocalKeyManager; - let privateKey: Jwk; - - before(() => { - keyManager = new LocalKeyManager(); - }); - - beforeEach(() => { - didUri = 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjNFQmFfRUxvczJhbHZMb2pxSVZjcmJLcGlyVlhqNmNqVkQ1djJWaHdMejgifQ'; - - privateKey = { - kty : 'OKP', - crv : 'Ed25519', - x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8', - d : 'hMqv-FAvhVWz2nxobesO7TzI0-GN0kvzkUGYdnZt_TA' - }; - }); - - it('returns a DID JWK from existing keys present in a key manager', async () => { - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: privateKey }); - - const did = await DidJwk.fromKeyManager({ didUri, keyManager }); - expect(did).to.have.property('didDocument'); - expect(did).to.have.property('getSigner'); - expect(did).to.have.property('keyManager'); - expect(did).to.have.property('metadata'); - expect(did).to.have.property('uri', didUri); - }); - - it('returns a DID with a getSigner function that can sign and verify data', async () => { - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: privateKey }); - - const did = await DidJwk.fromKeyManager({ didUri, keyManager }); - - const signer = await did.getSigner(); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - }); - - it('returns a DID with a getSigner function that accepts a specific keyUri', async () => { - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: privateKey }); - - const did = await DidJwk.fromKeyManager({ didUri, keyManager }); - - // Retrieve the key URI of the verification method's public key. - const { d, ...publicKey } = privateKey; // Remove the private key component - const keyUri = await did.keyManager.getKeyUri({ key: publicKey }); - - const signer = await did.getSigner({ keyUri }); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - }); - - it(`does not include the 'keyAgreement' relationship when JWK use is 'sig'`, async () => { - // Add the `sig` key use property to the test DID's private key. - privateKey.use = 'sig'; - - // Redefine the DID URI that is based on inclusion of the `use: 'sig'` property. - didUri = 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjNFQmFfRUxvczJhbHZMb2pxSVZjcmJLcGlyVlhqNmNqVkQ1djJWaHdMejgiLCJ1c2UiOiJzaWcifQ'; - - // Import the private key into the key manager. - await keyManager.importKey({ key: privateKey }); - - // Instantiate the DID object using the existing key. - let did = await DidJwk.fromKeyManager({ didUri, keyManager }); - - // Verify the DID document does not contain the `keyAgreement` relationship. - expect(did.didDocument).to.not.have.property('keyAgreement'); - }); - - it(`only specifies 'keyAgreement' relationship when JWK use is 'enc'`, async () => { - // Redefine the test DID's private key to be a secp256k1 key with the `enc` key use property. - privateKey = { - kty : 'EC', - crv : 'secp256k1', - d : 'WJPT7YKR12IulMa2cCQIoQXEK3YL3K4bBDmd684gnEY', - x : 'ORyV-OYLFV0C7Vv9ky-j90Yi4nDTkaYdF2-hObR71SM', - y : 'D2EyTbAkVfaBa9khVngdqwLfSy6hnIYAz3lLxdvJmEc', - kid : '_BuKVglXMSv5OLbiRABKQPXDwmDoHucVPpwdnhdUwEU', - alg : 'ES256K', - use : 'enc', - }; - - // Redefine the DID URI that is based on inclusion of the `use: 'enc'` property. - didUri = 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJPUnlWLU9ZTEZWMEM3VnY5a3ktajkwWWk0bkRUa2FZZEYyLWhPYlI3MVNNIiwieSI6IkQyRXlUYkFrVmZhQmE5a2hWbmdkcXdMZlN5NmhuSVlBejNsTHhkdkptRWMiLCJraWQiOiJfQnVLVmdsWE1TdjVPTGJpUkFCS1FQWER3bURvSHVjVlBwd2RuaGRVd0VVIiwiYWxnIjoiRVMyNTZLIiwidXNlIjoiZW5jIn0'; - - // Import the private key into the key manager. - await keyManager.importKey({ key: privateKey }); - - // Instantiate the DID object using the existing key. - const did = await DidJwk.fromKeyManager({ didUri, keyManager }); - - // Verrify the DID document does not contain any verification relationships other than - // `keyAgreement`. - expect(did.didDocument).to.have.property('keyAgreement'); - expect(did.didDocument).to.not.have.property('assertionMethod'); - expect(did.didDocument).to.not.have.property('authentication'); - expect(did.didDocument).to.not.have.property('capabilityDelegation'); - expect(did.didDocument).to.not.have.property('capabilityInvocation'); - }); - - it('throws an error if the given DID URI cannot be resolved', async () => { - const didUri = 'did:jwk:...'; + it('throws an error if zero verificationMethods are given', async () => { try { - await DidJwk.fromKeyManager({ didUri, keyManager }); + // @ts-expect-error - Test case where verificationMethods is undefined. + await DidJwk.create({ keyManager, options: { verificationMethods: [] } }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('missing verification methods'); + expect(error.message).to.include('must contain exactly one entry'); } }); - it('throws an error if an unsupported DID method is given', async () => { + it('throws an error if two or more verificationMethods are given', async () => { try { - await DidJwk.fromKeyManager({ didUri: 'did:example:e30', keyManager }); + await DidJwk.create({ + keyManager, + // @ts-expect-error - Test case where verificationMethods has too many entries. + options: { verificationMethods: [{ algorithm: 'secp256k1' }, { algorithm: 'Ed25519' }] } + }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('Method not supported'); - expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('must contain exactly one entry'); } }); + }); - it('throws an error if the resolved DID document lacks any verification methods', async () => { - // Stub the DID resolve method to return a DID document without a verificationMethod property. - sinon.stub(DidJwk, 'resolve').returns(Promise.resolve({ - didDocument : { id: 'did:jwk:...' }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); + describe('export()', () => { + it('returns a single verification method for a DID', async () => { + // Create a DID to use for the test. + const did = await DidJwk.create(); - const didUri = 'did:jwk:...'; - try { - await DidJwk.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('missing verification methods'); - } finally { - sinon.restore(); - } + const portableDid = await did.export(); - // Stub the DID resolve method to return a DID document an empty verificationMethod property. - sinon.stub(DidJwk, 'resolve').returns(Promise.resolve({ - didDocument : { id: 'did:jwk:...', verificationMethod: [] }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); + expect(portableDid.document).to.have.property('verificationMethod'); + expect(portableDid.document.verificationMethod).to.have.length(1); + expect(portableDid.document.verificationMethod![0]).to.have.property('publicKeyJwk'); + expect(portableDid.document.verificationMethod![0]).to.have.property('type'); + expect(portableDid.document.verificationMethod![0]).to.have.property('id'); + expect(portableDid.document.verificationMethod![0]).to.have.property('controller'); + expect(portableDid.privateKeys).to.have.length(1); + expect(portableDid.privateKeys![0]).to.have.property('crv'); + expect(portableDid.privateKeys![0]).to.have.property('x'); + expect(portableDid.privateKeys![0]).to.have.property('d'); + }); + }); + describe('getSigningMethod()', () => { + it('returns the signing method for a DID', async () => { + // Create a DID to use for the test. + const did = await DidJwk.create(); + + const signingMethod = await DidJwk.getSigningMethod({ didDocument: did.document }); + + expect(signingMethod).to.have.property('publicKeyJwk'); + expect(signingMethod).to.have.property('type', 'JsonWebKey2020'); + expect(signingMethod).to.have.property('id', `${did.uri}#0`); + expect(signingMethod).to.have.property('controller', did.uri); + }); + + it('throws an error if the DID document is missing verification methods', async function () { try { - await DidJwk.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); + await DidJwk.getSigningMethod({ + didDocument: { id: 'did:jwk:123' } + }); + expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('missing verification methods'); - } finally { - sinon.restore(); + expect(error.message).to.include('verification method intended for signing could not be determined'); } }); - it('throws an error if the resolved DID document is missing a public key', async () => { - // Stub the DID resolution method to return a DID document with no verification methods. - sinon.stub(DidJwk, 'resolve').returns(Promise.resolve({ - didDocument: { - id : 'did:jwk:...', - verificationMethod : [{ - id : 'did:jwk:...#0', - type : 'JsonWebKey2020', - controller : 'did:jwk:...' - }], - }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); + it('throws an error if a non-jwk method is used', async function () { + // Example DID Document with a non-jwk method + const didDocument: DidDocument = { + '@context' : 'https://www.w3.org/ns/did/v1', + id : 'did:example:123', + verificationMethod : [ + { + id : 'did:example:123#0', + type : 'JsonWebKey2020', + controller : 'did:example:123', + publicKeyJwk : {} as Jwk + } + ], + }; - const didUri = 'did:jwk:...'; try { - await DidJwk.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); + await DidJwk.getSigningMethod({ didDocument }); + expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('does not contain a public key'); - } finally { - sinon.restore(); + expect(error.message).to.equal('Method not supported: example'); } }); }); - describe('fromKeys()', () => { + describe('import()', () => { let portableDid: PortableDid; beforeEach(() => { // Define a DID to use for the test. portableDid = { - uri : 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjNFQmFfRUxvczJhbHZMb2pxSVZjcmJLcGlyVlhqNmNqVkQ1djJWaHdMejgifQ', - verificationMethods : [{ - publicKeyJwk: { - kty : 'OKP', + uri : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + document : { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1', + ], + id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + verificationMethod : [ + { + id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + type : 'JsonWebKey2020', + controller : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo', + kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ', + alg : 'EdDSA', + }, + }, + ], + authentication: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + assertionMethod: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + capabilityInvocation: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + capabilityDelegation: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + keyAgreement: [ + 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ#0', + ], + }, + metadata: { + }, + privateKeys: [ + { crv : 'Ed25519', - x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8' - }, - privateKeyJwk: { + d : '628WwXicdWc0BULN1JG_ybSrhwWWnz9NFwxbG09Ecr0', kty : 'OKP', - crv : 'Ed25519', - x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8', - d : 'hMqv-FAvhVWz2nxobesO7TzI0-GN0kvzkUGYdnZt_TA' + x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo', + kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ', + alg : 'EdDSA', }, - purposes: ['authentication'] - }] + ], }; }); - it('returns a DID JWK from the given set of verification method keys', async () => { - const did = await DidJwk.fromKeys(portableDid); + it('returns a BearerDid from the given DID JWK PortableDid', async () => { + const did = await DidJwk.import({ portableDid }); - expect(did).to.have.property('didDocument'); + expect(did).to.have.property('document'); expect(did).to.have.property('getSigner'); expect(did).to.have.property('keyManager'); expect(did).to.have.property('metadata'); expect(did).to.have.property('uri', portableDid.uri); + expect(did.document).to.deep.equal(portableDid.document); }); it('returns a DID with a getSigner function that can sign and verify data', async () => { - const did = await DidJwk.fromKeys(portableDid); + const did = await DidJwk.import({ portableDid }); const signer = await did.getSigner(); const data = new Uint8Array([1, 2, 3]); const signature = await signer.sign({ data }); @@ -372,168 +275,69 @@ describe('DidJwk', () => { expect(isValid).to.be.true; }); - it('returns a DID with a getSigner function that accepts a specific keyUri', async () => { - const did = await DidJwk.fromKeys(portableDid); - - // Retrieve the key URI of the verification method's public key. - const keyUri = await did.keyManager.getKeyUri({ key: portableDid.verificationMethods![0].publicKeyJwk! }); - - const signer = await did.getSigner({ keyUri }); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - }); - - it(`does not include the 'keyAgreement' relationship when JWK use is 'sig'`, async () => { - // Add the `sig` key use property. - portableDid.verificationMethods[0].privateKeyJwk!.use = 'sig'; - portableDid.verificationMethods[0].publicKeyJwk!.use = 'sig'; - - // Import the private key into a key manager. - const keyManager = new LocalKeyManager(); - await keyManager.importKey({ key: portableDid.verificationMethods![0].privateKeyJwk! }); - - // Create the DID using the key set. - let did = await DidJwk.fromKeys(portableDid); - - // Verify the DID document does not contain the `keyAgreement` relationship. - expect(did.didDocument).to.not.have.property('keyAgreement'); - }); - - it(`only specifies 'keyAgreement' relationship when JWK use is 'enc'`, async () => { - // Generate a random secp256k1 private key. - const keyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); - const publicKey = await keyManager.getPublicKey({ keyUri }); - const privateKey = await keyManager.exportKey({ keyUri }); - - // Add the `enc` key use property. - privateKey.use = 'enc'; - publicKey.use = 'enc'; - - // Swap the keys in the key set with the newly generated secp256k1 keys. - portableDid.verificationMethods[0].privateKeyJwk = privateKey; - portableDid.verificationMethods[0].publicKeyJwk = publicKey; - - // Create the DID using the key set. - let did = await DidJwk.fromKeys({ - keyManager, - verificationMethods: portableDid.verificationMethods! - }); - - // Verrify the DID document does not contain any verification relationships other than - // `keyAgreement`. - expect(did.didDocument).to.have.property('keyAgreement'); - expect(did.didDocument).to.not.have.property('assertionMethod'); - expect(did.didDocument).to.not.have.property('authentication'); - expect(did.didDocument).to.not.have.property('capabilityDelegation'); - expect(did.didDocument).to.not.have.property('capabilityInvocation'); - }); - - it('throws an error if no verification methods are given', async () => { - try { - // @ts-expect-error - Test case where verificationMethods is undefined. - await DidJwk.fromKeys({}); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('one verification method'); - } - }); - - it('throws an error if the given key set is empty', async () => { - try { - await DidJwk.fromKeys({ verificationMethods: [] }); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('one verification method'); - } - }); - - it('throws an error if the given key set is missing a public key', async () => { - delete portableDid.verificationMethods[0].publicKeyJwk; + it('throws an error if the DID method is not supported', async () => { + // Change the method to something other than 'jwk'. + portableDid.uri = 'did:unknown:abc123'; try { - await DidJwk.fromKeys(portableDid); + await DidJwk.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('does not contain a public and private key'); + expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('Method not supported'); } }); - it('throws an error if the given key set is missing a private key', async () => { - delete portableDid.verificationMethods[0].privateKeyJwk; + it('throws an error if the DID method cannot be determined', async () => { + // An unparsable DID URI. + portableDid.uri = 'did:abc123'; try { - await DidJwk.fromKeys(portableDid); + await DidJwk.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('does not contain a public and private key'); + expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('Method not supported'); } }); - it('throws an error if the key set contains two or more keys', async () => { - const verificationMethod: PortableDidVerificationMethod = { - publicKeyJwk: { - kty : 'OKP', - crv : 'Ed25519', - x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8' - }, - privateKeyJwk: { - kty : 'OKP', - crv : 'Ed25519', - x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8', - d : 'hMqv-FAvhVWz2nxobesO7TzI0-GN0kvzkUGYdnZt_TA' - }, - purposes: ['authentication'] - }; + it('throws an error if the DID document contains two or more verification methods', async () => { + // Add a second verification method to the DID document. + portableDid.document.verificationMethod?.push(portableDid.document.verificationMethod[0]); try { - await DidJwk.fromKeys({ - verificationMethods: [verificationMethod, verificationMethod] - }); + await DidJwk.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('one verification method'); + expect(error.code).to.equal(DidErrorCode.InvalidDidDocument); + expect(error.message).to.include('DID document must contain exactly one verification method'); } }); }); - describe('getSigningMethod()', () => { - it('handles didDocuments missing verification methods', async function () { - const result = await DidJwk.getSigningMethod({ - didDocument: { id: 'did:jwk:123' } - }); + describe('resolve()', () => { + it(`does not include the 'keyAgreement' relationship when JWK use is 'sig'`, async () => { + const didWithSigKeyUse = 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkMxeUttMzhGYWdLamZRblpjLVFuVEdFYm5wSXUwTE8tTGNIbXZUbE01b0UiLCJraWQiOiJ6d1RvZVFpb0NkbGROV20wZEtZNG95T1dlb1BSRzZ2UG40SW1Hb0M5ekZNIiwiYWxnIjoiRWREU0EiLCJ1c2UiOiJzaWcifQ'; - expect(result).to.be.undefined; + const resolutionResult = await DidJwk.resolve(didWithSigKeyUse); + + // Verify the DID document does not contain the `keyAgreement` relationship. + expect(resolutionResult.didDocument).to.not.have.property('keyAgreement'); }); - it('throws an error if a non-jwk method is used', async function () { - // Example DID Document with a non-jwk method - const didDocument: DidDocument = { - '@context' : 'https://www.w3.org/ns/did/v1', - id : 'did:example:123', - verificationMethod : [ - { - id : 'did:example:123#0', - type : 'JsonWebKey2020', - controller : 'did:example:123', - publicKeyJwk : {} as Jwk - } - ], - }; + it(`only specifies 'keyAgreement' relationship when JWK use is 'enc'`, async () => { + const didWithEncKeyUse = 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJCTVcwQ2lnMjBuTFozTTV5NzkxTEFuY2RyZnl6WS1qTE95UnNVU29tX1g4IiwieSI6IlVrajU4N0VJcVk4cl9jYU1zUmNOZkI4MWxjbGJPNjRmUG4yOXRHOEJWbUkiLCJraWQiOiI5Yi1oUTVlc0NiQlpKNkl5Z0hFZ0Z6T21rUkM1U2QzSlZ5R2FLS0ZGZUVFIiwiYWxnIjoiRVMyNTZLIiwidXNlIjoiZW5jIn0'; - try { - await DidJwk.getSigningMethod({ didDocument }); - expect.fail('Error should have been thrown'); - } catch (error: any) { - expect(error.message).to.equal('Method not supported: example'); - } + const resolutionResult = await DidJwk.resolve(didWithEncKeyUse); + + // Verrify the DID document does not contain any verification relationships other than `keyAgreement`. + expect(resolutionResult.didDocument).to.have.property('keyAgreement'); + expect(resolutionResult.didDocument).to.not.have.property('assertionMethod'); + expect(resolutionResult.didDocument).to.not.have.property('authentication'); + expect(resolutionResult.didDocument).to.not.have.property('capabilityDelegation'); + expect(resolutionResult.didDocument).to.not.have.property('capabilityInvocation'); }); - }); - describe('resolve()', () => { it('returns an error due to DID parsing failing', async function () { const invalidDidUri = 'did:invalidFormat'; const resolutionResult = await DidJwk.resolve(invalidDidUri); @@ -553,24 +357,6 @@ describe('DidJwk', () => { }); }); - describe('toKeys()', () => { - it('returns a single verification method for a DID', async () => { - // Create a DID to use for the test. - const did = await DidJwk.create(); - - const keySet = await DidJwk.toKeys({ did }); - - expect(keySet).to.have.property('verificationMethods'); - expect(keySet.verificationMethods).to.have.length(1); - expect(keySet.verificationMethods![0]).to.have.property('publicKeyJwk'); - expect(keySet.verificationMethods![0]).to.have.property('privateKeyJwk'); - expect(keySet.verificationMethods![0]).to.have.property('purposes'); - expect(keySet.verificationMethods![0]).to.have.property('type'); - expect(keySet.verificationMethods![0]).to.have.property('id'); - expect(keySet.verificationMethods![0]).to.have.property('controller'); - }); - }); - describe('Web5TestVectorsDidJwk', () => { it('resolve', async () => { type TestVector = { diff --git a/packages/dids/tests/methods/did-key.spec.ts b/packages/dids/tests/methods/did-key.spec.ts index cae7b5b25..4cec299e4 100644 --- a/packages/dids/tests/methods/did-key.spec.ts +++ b/packages/dids/tests/methods/did-key.spec.ts @@ -1,11 +1,10 @@ import type { Jwk } from '@web5/crypto'; -import sinon from 'sinon'; import { expect } from 'chai'; import { LocalKeyManager } from '@web5/crypto'; import type { DidDocument } from '../../src/types/did-core.js'; -import type { PortableDid, PortableDidVerificationMethod } from '../../src/methods/did-method.js'; +import type { PortableDid } from '../../src/types/portable-did.js'; import { DidErrorCode } from '../../src/did-error.js'; import { DidKey, DidKeyUtils } from '../../src/methods/did-key.js'; @@ -21,13 +20,13 @@ describe('DidKey', () => { it('creates a did:key DID', async () => { const did = await DidKey.create({ keyManager, options: { algorithm: 'Ed25519' } }); - expect(did).to.have.property('didDocument'); + expect(did).to.have.property('document'); expect(did).to.have.property('getSigner'); expect(did).to.have.property('keyManager'); expect(did).to.have.property('metadata'); expect(did).to.have.property('uri'); expect(did.uri.startsWith('did:key:')).to.be.true; - expect(did.didDocument.verificationMethod).to.have.length(1); + expect(did.document.verificationMethod).to.have.length(1); }); it('uses a default key manager and key generation algorithm if neither is given', async () => { @@ -48,7 +47,7 @@ describe('DidKey', () => { const did = await DidKey.create({ keyManager, options: { algorithm: 'secp256k1' } }); // Retrieve the public key from the key manager. - const keyUri = await keyManager.getKeyUri({ key: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); + const keyUri = await keyManager.getKeyUri({ key: did.document.verificationMethod![0]!.publicKeyJwk! }); const publicKey = await keyManager.getPublicKey({ keyUri }); // Verify the public key is an secp256k1 key. @@ -59,7 +58,7 @@ describe('DidKey', () => { const did = await DidKey.create({ keyManager, options: { verificationMethods: [{ algorithm: 'secp256k1' }] } }); // Retrieve the public key from the key manager. - const keyUri = await keyManager.getKeyUri({ key: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); + const keyUri = await keyManager.getKeyUri({ key: did.document.verificationMethod![0]!.publicKeyJwk! }); const publicKey = await keyManager.getPublicKey({ keyUri }); // Verify the public key is an secp256k1 key. @@ -70,7 +69,7 @@ describe('DidKey', () => { const did = await DidKey.create({ keyManager }); // Retrieve the public key from the key manager. - const keyUri = await keyManager.getKeyUri({ key: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); + const keyUri = await keyManager.getKeyUri({ key: did.document.verificationMethod![0]!.publicKeyJwk! }); const publicKey = await keyManager.getPublicKey({ keyUri }); // Verify the public key is an Ed25519 key. @@ -89,12 +88,12 @@ describe('DidKey', () => { it('supports multibase and JWK public key format', async () => { let did = await DidKey.create({ keyManager, options: { publicKeyFormat: 'JsonWebKey2020' } }); - expect(did.didDocument.verificationMethod![0]!.publicKeyJwk).to.exist; - expect(did.didDocument.verificationMethod![0]!.publicKeyMultibase).to.not.exist; + expect(did.document.verificationMethod![0]!.publicKeyJwk).to.exist; + expect(did.document.verificationMethod![0]!.publicKeyMultibase).to.not.exist; did = await DidKey.create({ keyManager, options: { publicKeyFormat: 'Ed25519VerificationKey2020' } }); - expect(did.didDocument.verificationMethod![0]!.publicKeyJwk).to.not.exist; - expect(did.didDocument.verificationMethod![0]!.publicKeyMultibase).to.exist; + expect(did.document.verificationMethod![0]!.publicKeyJwk).to.not.exist; + expect(did.document.verificationMethod![0]!.publicKeyMultibase).to.exist; }); it('accepts an alternate default context', async () => { @@ -105,33 +104,8 @@ describe('DidKey', () => { } }); - expect(did.didDocument['@context']).to.not.include('https://www.w3.org/ns/did/v1'); - expect(did.didDocument['@context']).to.include('https://www.w3.org/ns/did/v99'); - }); - - it('returns a getSigner() function that creates valid signatures that can be verified', async () => { - const did = await DidKey.create({ keyManager, options: { algorithm: 'Ed25519' } }); - - const signer = await did.getSigner(); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - }); - - it('returns a getSigner() function handles undefined params', async function () { - // Create a `did:key` DID. - const did = await DidKey.create({ keyManager, options: { algorithm: 'Ed25519' } }); - - // Simulate the creation of a signer with undefined params - const signer = await did.getSigner({ }); - - // Note: Since this test does not interact with an actual keyManager, it primarily ensures - // that the method doesn't break with undefined params. - expect(signer).to.have.property('sign'); - expect(signer).to.have.property('verify'); + expect(did.document['@context']).to.not.include('https://www.w3.org/ns/did/v1'); + expect(did.document['@context']).to.include('https://www.w3.org/ns/did/v99'); }); it('throws an error if both algorithm and verificationMethods are provided', async () => { @@ -148,192 +122,238 @@ describe('DidKey', () => { expect(error.message).to.include('options are mutually exclusive'); } }); - }); - describe('fromKeyManager()', () => { - let didUri: string; - let keyManager: LocalKeyManager; - let privateKey: Jwk; - - before(() => { - keyManager = new LocalKeyManager(); + it('throws an error if zero verificationMethods are given', async () => { + try { + // @ts-expect-error - Test case where verificationMethods is undefined. + await DidKey.create({ keyManager, options: { verificationMethods: [] } }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('must contain exactly one entry'); + } }); - beforeEach(() => { - didUri = 'did:key:z6MkqBvAA4RBFFATVs7TXxEf4FcL1QY3JntYvwAYJMptDt5D'; - - privateKey = { - kty : 'OKP', - crv : 'Ed25519', - x : 'n4JbpJYkl77eGav9miqxHJsf-hoZl7GrbcrTmLJ9NBA', - d : 'JZPFC1MVj65ZUnj1HWTUDqvdQU6W2yBdZXMrRxDSqVA' - }; + it('throws an error if two or more verificationMethods are given', async () => { + try { + await DidKey.create({ + keyManager, + // @ts-expect-error - Test case where verificationMethods has too many entries. + options: { verificationMethods: [{ algorithm: 'secp256k1' }, { algorithm: 'Ed25519' }] } + }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('must contain exactly one entry'); + } }); + }); - it('returns a DID Key from existing keys present in a key manager', async () => { - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: privateKey }); + describe('export()', () => { + it('returns a single verification method for a DID', async () => { + // Create a DID to use for the test. + const did = await DidKey.create(); - const did = await DidKey.fromKeyManager({ didUri, keyManager }); + const portableDid = await did.export(); - expect(did).to.have.property('didDocument'); - expect(did).to.have.property('getSigner'); - expect(did).to.have.property('keyManager'); - expect(did).to.have.property('metadata'); - expect(did).to.have.property('uri', didUri); + expect(portableDid.document).to.have.property('verificationMethod'); + expect(portableDid.document.verificationMethod).to.have.length(1); + expect(portableDid.document.verificationMethod![0]).to.have.property('publicKeyJwk'); + expect(portableDid.document.verificationMethod![0]).to.have.property('type'); + expect(portableDid.document.verificationMethod![0]).to.have.property('id'); + expect(portableDid.document.verificationMethod![0]).to.have.property('controller'); + expect(portableDid.privateKeys).to.have.length(1); + expect(portableDid.privateKeys![0]).to.have.property('crv'); + expect(portableDid.privateKeys![0]).to.have.property('x'); + expect(portableDid.privateKeys![0]).to.have.property('d'); }); + }); - it('returns a DID with a getSigner function that can sign and verify data', async () => { - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: privateKey }); + describe('getSigningMethod()', () => { + it('returns the signing method for a DID', async () => { + // Create a DID to use for the test. + const did = await DidKey.create(); - const did = await DidKey.fromKeyManager({ didUri, keyManager }); + const signingMethod = await DidKey.getSigningMethod({ didDocument: did.document }); - const signer = await did.getSigner(); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; + expect(signingMethod).to.have.property('type', 'JsonWebKey2020'); + expect(signingMethod).to.have.property('id'); + expect(signingMethod!.id).to.include(did.uri); + expect(signingMethod).to.have.property('controller', did.uri); }); - it('returns a DID with a getSigner function that accepts a specific keyUri', async () => { - // Import the test DID's keys into the key manager. - await keyManager.importKey({ key: privateKey }); - - const did = await DidKey.fromKeyManager({ didUri, keyManager }); - - // Retrieve the key URI of the verification method's public key. - const { d, ...publicKey } = privateKey; // Remove the private key component - const keyUri = await did.keyManager.getKeyUri({ key: publicKey }); - - const signer = await did.getSigner({ keyUri }); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); + it('returns the first assertionMethod verification method', async function () { + const verificationMethod = await DidKey.getSigningMethod({ + didDocument: { + id : 'did:key:123', + verificationMethod : [ + { + id : 'did:key:123#0', + type : 'JsonWebKey2020', + controller : 'did:key:123', + publicKeyJwk : {} as Jwk + } + ], + assertionMethod: ['did:key:123#0'] + } + }); - expect(signature).to.have.length(64); - expect(isValid).to.be.true; + expect(verificationMethod).to.exist; + expect(verificationMethod).to.have.property('id', 'did:key:123#0'); }); - it('throws an error if the given DID URI cannot be resolved', async () => { - const didUri = 'did:key:...'; + it('throws an error if the DID document is missing verification methods', async function () { try { - await DidKey.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); + await DidKey.getSigningMethod({ + didDocument: { id: 'did:key:123' } + }); + expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('missing verification methods'); + expect(error.message).to.include('verification method intended for signing could not be determined'); } }); - it('throws an error if an unsupported DID method is given', async () => { + it('throws an error if there is no assertionMethod verification method', async function () { try { - await DidKey.fromKeyManager({ didUri: 'did:example:z6Mk', keyManager }); - expect.fail('Expected an error to be thrown.'); + await DidKey.getSigningMethod({ + didDocument: { + id : 'did:key:123', + verificationMethod : [ + { + id : 'did:key:123#0', + type : 'JsonWebKey2020', + controller : 'did:key:123', + publicKeyJwk : {} as Jwk + } + ], + authentication: ['did:key:123#0'] + } + }); + expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('verification method intended for signing could not be determined'); } }); - it('throws an error if the resolved DID document lacks any verification methods', async () => { - // Stub the DID resolve method to return a DID document without a verificationMethod property. - sinon.stub(DidKey, 'resolve').returns(Promise.resolve({ - didDocument : { id: 'did:key:...' }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); - - const didUri = 'did:key:...'; - try { - await DidKey.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('missing verification methods'); - } finally { - sinon.restore(); - } - - // Stub the DID resolve method to return a DID document an empty verificationMethod property. - sinon.stub(DidKey, 'resolve').returns(Promise.resolve({ - didDocument : { id: 'did:key:...', verificationMethod: [] }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); - + it('throws an error if the only assertionMethod method is embedded', async function () { try { - await DidKey.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); + await DidKey.getSigningMethod({ + didDocument: { + id : 'did:key:123', + verificationMethod : [ + { + id : 'did:key:123#0', + type : 'JsonWebKey2020', + controller : 'did:key:123', + publicKeyJwk : {} as Jwk + } + ], + assertionMethod: [ + { + id : 'did:key:123#1', + type : 'JsonWebKey2020', + controller : 'did:key:123', + publicKeyJwk : {} as Jwk + } + ], + authentication: ['did:key:123#0'] + } + }); + expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('missing verification methods'); - } finally { - sinon.restore(); + expect(error.message).to.include('verification method intended for signing could not be determined'); } }); - it('throws an error if the resolved DID document is missing a public key', async () => { - // Stub the DID resolution method to return a DID document with no verification methods. - sinon.stub(DidKey, 'resolve').returns(Promise.resolve({ - didDocument: { - id : 'did:key:...', - verificationMethod : [{ - id : 'did:key:...#0', - type : 'JsonWebKey2020', - controller : 'did:key:...' - }], - }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - })); + it('throws an error if a non-key method is used', async function () { + // Example DID Document with a non-key method + const didDocument: DidDocument = { + '@context' : 'https://www.w3.org/ns/did/v1', + id : 'did:example:123', + verificationMethod : [ + { + id : 'did:example:123#0', + type : 'JsonWebKey2020', + controller : 'did:example:123', + publicKeyJwk : {} as Jwk + } + ], + }; - const didUri = 'did:key:...'; try { - await DidKey.fromKeyManager({ didUri, keyManager }); - expect.fail('Expected an error to be thrown.'); + await DidKey.getSigningMethod({ didDocument }); + expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('does not contain a public key'); - } finally { - sinon.restore(); + expect(error.message).to.equal('Method not supported: example'); } }); }); - describe('fromKeys()', () => { + describe('import()', () => { let portableDid: PortableDid; beforeEach(() => { // Define a DID to use for the test. portableDid = { - uri : 'did:key:z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN', - verificationMethods : [{ - publicKeyJwk: { - kty : 'OKP', + uri : 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + document : { + id : 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + verificationMethod : [ + { + id : 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU#z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + type : 'JsonWebKey2020', + controller : 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + publicKeyJwk : { + kty : 'OKP', + crv : 'Ed25519', + x : 'C4K4f9q7m-ObUYEZBZm4bD9maKUYnjcIzUI-JWkai9U', + kid : 'bSmUGl3783WDG3U8uGxKw6Vh1ikHJ-qoap2EEw4VhKA', + }, + }, + ], + authentication: [ + 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU#z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + ], + assertionMethod: [ + 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU#z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + ], + capabilityInvocation: [ + 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU#z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + ], + capabilityDelegation: [ + 'did:key:z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU#z6MkfEC95uQzsxT6E6oERYyY5UMqgYugQ5YdxCw5h9RPPSGU', + ], + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1', + ], + }, + metadata: { + }, + privateKeys: [ + { crv : 'Ed25519', - x : 'VnSOQ-n7kRcYd0XGW2MNCv7DDY5py5XhNcjM7-Y1HVM' - }, - privateKeyJwk: { + d : 'a-pqjsKCMFnbFZSyg8GKXfDgop1G2kvp910f3WRvuVs', kty : 'OKP', - crv : 'Ed25519', - x : 'VnSOQ-n7kRcYd0XGW2MNCv7DDY5py5XhNcjM7-Y1HVM', - d : 'iTD5DIOKZNkwgzsND-I8CLIXmgTxfQ1HUzl9fpMktAo' + x : 'C4K4f9q7m-ObUYEZBZm4bD9maKUYnjcIzUI-JWkai9U', + kid : 'bSmUGl3783WDG3U8uGxKw6Vh1ikHJ-qoap2EEw4VhKA', + alg : 'EdDSA', }, - purposes: ['authentication'] - }] + ], }; }); - it('returns a DID Key from the given set of verification method keys', async () => { - const did = await DidKey.fromKeys(portableDid); + it('returns a BearerDid from the given DID JWK PortableDid', async () => { + const did = await DidKey.import({ portableDid }); - expect(did).to.have.property('didDocument'); + expect(did).to.have.property('document'); expect(did).to.have.property('getSigner'); expect(did).to.have.property('keyManager'); expect(did).to.have.property('metadata'); expect(did).to.have.property('uri', portableDid.uri); + expect(did.document).to.deep.equal(portableDid.document); }); it('returns a DID with a getSigner function that can sign and verify data', async () => { - const did = await DidKey.fromKeys(portableDid); + const did = await DidKey.import({ portableDid }); const signer = await did.getSigner(); const data = new Uint8Array([1, 2, 3]); const signature = await signer.sign({ data }); @@ -343,184 +363,42 @@ describe('DidKey', () => { expect(isValid).to.be.true; }); - it('returns a DID with a getSigner function that accepts a specific keyUri', async () => { - const did = await DidKey.fromKeys(portableDid); - - // Retrieve the key URI of the verification method's public key. - const keyUri = await did.keyManager.getKeyUri({ key: portableDid.verificationMethods![0].publicKeyJwk! }); - - const signer = await did.getSigner({ keyUri }); - const data = new Uint8Array([1, 2, 3]); - const signature = await signer.sign({ data }); - const isValid = await signer.verify({ data, signature }); - - expect(signature).to.have.length(64); - expect(isValid).to.be.true; - }); - - it('throws an error if no verification methods are given', async () => { - try { - // @ts-expect-error - Test case where verificationMethods is undefined. - await DidKey.fromKeys({}); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('one verification method'); - } - }); + it('throws an error if the DID method is not supported', async () => { + // Change the method to something other than 'key'. + portableDid.uri = 'did:unknown:abc123'; - it('throws an error if the given key set is empty', async () => { try { - await DidKey.fromKeys({ verificationMethods: [] }); + await DidKey.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('one verification method'); - } - }); - - it('throws an error if the given key set is missing a public key', async () => { - delete portableDid.verificationMethods[0].publicKeyJwk; - - try { - await DidKey.fromKeys(portableDid); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('does not contain a public and private key'); + expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('Method not supported'); } }); - it('throws an error if the given key set is missing a private key', async () => { - delete portableDid.verificationMethods[0].privateKeyJwk; + it('throws an error if the DID method cannot be determined', async () => { + // An unparsable DID URI. + portableDid.uri = 'did:abc123'; try { - await DidKey.fromKeys(portableDid); + await DidKey.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('does not contain a public and private key'); + expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + expect(error.message).to.include('Method not supported'); } }); - it('throws an error if the key set contains two or more keys', async () => { - const verificationMethod: PortableDidVerificationMethod = { - publicKeyJwk: { - kty : 'OKP', - crv : 'Ed25519', - x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8' - }, - privateKeyJwk: { - kty : 'OKP', - crv : 'Ed25519', - x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8', - d : 'hMqv-FAvhVWz2nxobesO7TzI0-GN0kvzkUGYdnZt_TA' - }, - purposes: ['authentication'] - }; + it('throws an error if the DID document contains two or more verification methods', async () => { + // Add a second verification method to the DID document. + portableDid.document.verificationMethod?.push(portableDid.document.verificationMethod[0]); try { - await DidKey.fromKeys({ - verificationMethods: [verificationMethod, verificationMethod] - }); + await DidKey.import({ portableDid }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { - expect(error.message).to.include('one verification method'); - } - }); - }); - - describe('getSigningMethod()', () => { - it('returns the first authentication verification method', async function () { - const verificationMethod = await DidKey.getSigningMethod({ - didDocument: { - id : 'did:key:123', - verificationMethod : [ - { - id : 'did:key:123#0', - type : 'JsonWebKey2020', - controller : 'did:key:123', - publicKeyJwk : {} as Jwk - } - ], - authentication: ['did:key:123#0'] - } - }); - - expect(verificationMethod).to.exist; - expect(verificationMethod).to.have.property('id', 'did:key:123#0'); - }); - - it('returns undefined if there is no authentication verification method', async function () { - const verificationMethod = await DidKey.getSigningMethod({ - didDocument: { - id : 'did:key:123', - verificationMethod : [ - { - id : 'did:key:123#0', - type : 'JsonWebKey2020', - controller : 'did:key:123', - publicKeyJwk : {} as Jwk - } - ], - assertionMethod: ['did:key:123#0'] - } - }); - - expect(verificationMethod).to.not.exist; - }); - - it('returns undefined if the only authentication method is embedded', async function () { - const verificationMethod = await DidKey.getSigningMethod({ - didDocument: { - id : 'did:key:123', - verificationMethod : [ - { - id : 'did:key:123#0', - type : 'JsonWebKey2020', - controller : 'did:key:123', - publicKeyJwk : {} as Jwk - } - ], - authentication: [ - { - id : 'did:key:123#1', - type : 'JsonWebKey2020', - controller : 'did:key:123', - publicKeyJwk : {} as Jwk - } - ], - assertionMethod: ['did:key:123#0'] - } - }); - - expect(verificationMethod).to.not.exist; - }); - - it('handles didDocuments missing verification methods', async function () { - const result = await DidKey.getSigningMethod({ - didDocument: { id: 'did:key:123' } - }); - - expect(result).to.be.undefined; - }); - - it('throws an error if a non-key method is used', async function () { - // Example DID Document with a non-key method - const didDocument: DidDocument = { - '@context' : 'https://www.w3.org/ns/did/v1', - id : 'did:example:123', - verificationMethod : [ - { - id : 'did:example:123#0', - type : 'JsonWebKey2020', - controller : 'did:example:123', - publicKeyJwk : {} as Jwk - } - ], - }; - - try { - await DidKey.getSigningMethod({ didDocument }); - expect.fail('Error should have been thrown'); - } catch (error: any) { - expect(error.message).to.equal('Method not supported: example'); + expect(error.code).to.equal(DidErrorCode.InvalidDidDocument); + expect(error.message).to.include('DID document must contain exactly one verification method'); } }); }); @@ -646,52 +524,6 @@ describe('DidKey', () => { }); }); - describe('keyBytesToMultibaseId()', () => { - it('returns a multibase encoded string', () => { - const input = { - keyBytes : new Uint8Array(32), - multicodecName : 'ed25519-pub', - }; - const encoded = DidKeyUtils.keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); - expect(encoded).to.be.a.string; - expect(encoded.substring(0, 1)).to.equal('z'); - expect(encoded.substring(1, 4)).to.equal('6Mk'); - }); - - it('passes test vectors', () => { - let input: { keyBytes: Uint8Array, multicodecName: string }; - let output: string; - let encoded: string; - - // Test Vector 1. - input = { - keyBytes : (new Uint8Array(32)).fill(0), - multicodecName : 'ed25519-pub', - }; - output = 'z6MkeTG3bFFSLYVU7VqhgZxqr6YzpaGrQtFMh1uvqGy1vDnP'; - encoded = DidKeyUtils.keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); - expect(encoded).to.equal(output); - - // Test Vector 2. - input = { - keyBytes : (new Uint8Array(32)).fill(1), - multicodecName : 'ed25519-pub', - }; - output = 'z6MkeXBLjYiSvqnhFb6D7sHm8yKm4jV45wwBFRaatf1cfZ76'; - encoded = DidKeyUtils.keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); - expect(encoded).to.equal(output); - - // Test Vector 3. - input = { - keyBytes : (new Uint8Array(32)).fill(9), - multicodecName : 'ed25519-pub', - }; - output = 'z6Mkf4XhsxSXfEAWNK6GcFu7TyVs21AfUTRjiguqMhNQeDgk'; - encoded = DidKeyUtils.keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); - expect(encoded).to.equal(output); - }); - }); - describe('multicodecToJwk()', () => { it('converts ed25519 public key multicodec to JWK', async () => { const result = await DidKeyUtils.multicodecToJwk({ name: 'ed25519-pub' }); @@ -789,50 +621,6 @@ describe('DidKey', () => { }); }); - describe('multibaseIdToKeyBytes()', () => { - it('converts secp256k1-pub multibase identifiers', () => { - const multibaseKeyId = 'zQ3shMrXA3Ah8h5asMM69USP8qRDnPaCLRV3nPmitAXVfWhgp'; - - const { keyBytes, multicodecCode, multicodecName } = DidKeyUtils.multibaseIdToKeyBytes({ multibaseKeyId }); - - expect(keyBytes).to.exist; - expect(keyBytes).to.be.a('Uint8Array'); - expect(keyBytes).to.have.length(33); - expect(multicodecCode).to.exist; - expect(multicodecCode).to.equal(231); - expect(multicodecName).to.exist; - expect(multicodecName).to.equal('secp256k1-pub'); - }); - - it('converts ed25519-pub multibase identifiers', () => { - const multibaseKeyId = 'z6MkizSHspkM891CAnYZis1TJkB4fWwuyVjt4pV93rWPGYwW'; - - const { keyBytes, multicodecCode, multicodecName } = DidKeyUtils.multibaseIdToKeyBytes({ multibaseKeyId }); - - expect(keyBytes).to.exist; - expect(keyBytes).to.be.a('Uint8Array'); - expect(keyBytes).to.have.length(32); - expect(multicodecCode).to.exist; - expect(multicodecCode).to.equal(237); - expect(multicodecName).to.exist; - expect(multicodecName).to.equal('ed25519-pub'); - }); - - it('converts x25519-pub multibase identifiers', () => { - const multibaseKeyId = 'z6LSfsF6tQA7j56WSzNPT4yrzZprzGEK8137DMeAVLgGBJEz'; - - const { keyBytes, multicodecCode, multicodecName } = DidKeyUtils.multibaseIdToKeyBytes({ multibaseKeyId }); - - expect(keyBytes).to.exist; - expect(keyBytes).to.be.a('Uint8Array'); - expect(keyBytes).to.have.length(32); - expect(multicodecCode).to.exist; - expect(multicodecCode).to.equal(236); - expect(multicodecName).to.exist; - expect(multicodecName).to.equal('x25519-pub'); - }); - }); - describe('publicKeyToMultibaseId()', () => { it('supports Ed25519', async () => { const publicKey: Jwk = { diff --git a/packages/dids/tests/methods/did-method.spec.ts b/packages/dids/tests/methods/did-method.spec.ts index 7cb9daf44..7799eed35 100644 --- a/packages/dids/tests/methods/did-method.spec.ts +++ b/packages/dids/tests/methods/did-method.spec.ts @@ -1,307 +1,30 @@ -import type { CryptoApi, Jwk } from '@web5/crypto'; - -import sinon from 'sinon'; import { expect } from 'chai'; -import { LocalKeyManager } from '@web5/crypto'; - -import type { BearerDid } from '../../src/methods/did-method.js'; -import type { DidDocument, DidVerificationMethod } from '../../src/types/did-core.js'; import { DidMethod } from '../../src/methods/did-method.js'; -import { DidJwk } from '../../src/methods/did-jwk.js'; - -class DidTest extends DidMethod { - public static async getSigningMethod({ didDocument, methodId = '#0' }: { - didDocument: DidDocument; - methodId?: string; - }): Promise { - // Attempt to find the verification method in the DID Document. - return didDocument.verificationMethod?.find(vm => vm.id.endsWith(methodId)); - } -} describe('DidMethod', () => { - let keyManager: LocalKeyManager; - - before(() => { - keyManager = new LocalKeyManager(); - }); - - describe('fromKeyManager()', () => { - it('throws an error if the DID method implementation does not provide a resolve() function', async () => { - class DidTest extends DidMethod {} - - try { - await DidTest.fromKeyManager({ didUri: 'did:method:example', keyManager }); - expect.fail('Error should have been thrown'); - } catch (error: any) { - expect(error.message).to.include('must implement resolve()'); - } - }); - }); - - describe('getSigner()', () => { - let keyManagerMock: any; - let publicKey: Jwk; - let didDocument: DidDocument; - - beforeEach(() => { - // Mock for CryptoApi - keyManagerMock = { - digest : sinon.stub(), - generateKey : sinon.stub(), - getKeyUri : sinon.stub(), - getPublicKey : sinon.stub(), - sign : sinon.stub(), - verify : sinon.stub(), - }; - - // Example public key in JWK format - publicKey = { - kty : 'OKP', - use : 'sig', - crv : 'Ed25519', - kid : '...', - x : 'abc123', - alg : 'EdDSA' - }; - - // Example DID Document - didDocument = { - '@context' : 'https://www.w3.org/ns/did/v1', - id : 'did:jwk:example', - verificationMethod : [{ - id : 'did:jwk:example#0', - type : 'JsonWebKey2020', - controller : 'did:jwk:example', - publicKeyJwk : publicKey, - }], - }; - - keyManagerMock.getKeyUri.resolves('urn:jwk:example'); // Mock key URI retrieval - keyManagerMock.getPublicKey.resolves(publicKey); // Mock public key retrieval - keyManagerMock.sign.resolves(new Uint8Array(64).fill(0)); // Mock signature creation - keyManagerMock.verify.resolves(true); // Mock verification result - }); - - it('returns a signer with sign and verify functions', async () => { - const signer = await DidTest.getSigner({ didDocument, keyManager: keyManagerMock }); - - expect(signer).to.be.an('object'); - expect(signer).to.have.property('sign').that.is.a('function'); - expect(signer).to.have.property('verify').that.is.a('function'); - }); - - it('sign function should call keyManager.sign with correct parameters', async () => { - const signer = await DidTest.getSigner({ didDocument, keyManager: keyManagerMock }); - const dataToSign = new Uint8Array([0x00, 0x01]); - - await signer.sign({ data: dataToSign }); - - expect(keyManagerMock.sign.calledOnce).to.be.true; - expect(keyManagerMock.sign.calledWith(sinon.match({ data: dataToSign }))).to.be.true; - }); - - it('verify function should call keyManager.verify with correct parameters', async () => { - const signer = await DidTest.getSigner({ didDocument, keyManager: keyManagerMock }); - const dataToVerify = new Uint8Array([0x00, 0x01]); - const signature = new Uint8Array([0x01, 0x02]); - - await signer.verify({ data: dataToVerify, signature }); - - expect(keyManagerMock.verify.calledOnce).to.be.true; - expect(keyManagerMock.verify.calledWith(sinon.match({ data: dataToVerify, signature }))).to.be.true; - }); - - it('uses the provided keyUri to fetch the public key', async () => { - const keyUri = 'some-key-uri'; - keyManagerMock.getPublicKey.withArgs({ keyUri }).resolves(publicKey); - - const signer = await DidTest.getSigner({ didDocument, keyManager: keyManagerMock, keyUri }); - - expect(signer).to.be.an('object'); - expect(keyManagerMock.getPublicKey.calledWith({ keyUri })).to.be.true; - }); - + describe('getSigningMethod()', () => { it('throws an error if the DID method implementation does not provide a getSigningMethod() function', async () => { class DidTest extends DidMethod {} try { - await DidTest.getSigner({ didDocument, keyManager: keyManagerMock }); - expect.fail('Error should have been thrown'); - } catch (error: any) { - expect(error.message).to.include('must implement getSigningMethod'); - } - }); - - it('throws an error if the keyUri does not match any key in the DID Document', async () => { - const keyUri = 'nonexistent-key-uri'; - keyManagerMock.getPublicKey.withArgs({ keyUri }).resolves({ ...publicKey, x: 'def456' }); - - try { - await DidTest.getSigner({ didDocument, keyManager: keyManagerMock, keyUri }); - expect.fail('Error should have been thrown'); - } catch (error: any) { - expect(error.message).to.include(`is not present in the provided DID Document for '${didDocument.id}'`); - } - }); - - it('throws an error if no verification methods are found in the DID Document', async () => { - // Example DID Document with no verification methods - didDocument = { - '@context' : 'https://www.w3.org/ns/did/v1', - id : 'did:test:...', - verificationMethod : [], // Empty array indicates no verification methods - }; - - try { - await DidTest.getSigner({ didDocument, keyManager: keyManagerMock }); - expect.fail('Error should have been thrown'); - } catch (error: any) { - expect(error.message).to.include('No verification methods found'); - } - }); - - it('throws an error if the keys needed to create a signer are not determined', async function () { - keyManagerMock.getKeyUri.resolves(undefined); // Resolves to undefined to simulate missing publicKey - - try { - await DidTest.getSigner({ didDocument, keyManager: keyManagerMock }); + await DidTest.getSigningMethod({ didDocument: { id: 'did:method:example' } }); expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('Failed to determine the keys needed to create a signer'); + expect(error.message).to.include('must implement getSigningMethod()'); } }); }); - describe('toKeys()', () => { - let didJwk: BearerDid; - - beforeEach(async () => { - didJwk = { - didDocument: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1', - ], - id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ', - verificationMethod : [ - { - id : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ#0', - type : 'JsonWebKey2020', - controller : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ', - publicKeyJwk : { - crv : 'Ed25519', - kty : 'OKP', - x : 'm27WvTeQchsKs_ZfWcWPwQPpTcF2Mkc9RJs4ZpNoOYY', - kid : 'ontdoHRQTqCdJzG_ahsvrFXmLbGLXTka3SAR0xdd49A', - alg : 'EdDSA', - }, - }, - ], - authentication: [ - 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ#0', - ], - assertionMethod: [ - 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ#0', - ], - capabilityInvocation: [ - 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ#0', - ], - capabilityDelegation: [ - 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ#0', - ], - keyAgreement: [ - 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ#0', - ], - }, - getSigner : sinon.stub(), - keyManager, - metadata : {}, - uri : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im0yN1d2VGVRY2hzS3NfWmZXY1dQd1FQcFRjRjJNa2M5UkpzNFpwTm9PWVkiLCJraWQiOiJvbnRkb0hSUVRxQ2RKekdfYWhzdnJGWG1MYkdMWFRrYTNTQVIweGRkNDlBIiwiYWxnIjoiRWREU0EifQ' - }; - }); - - it('returns a set of verification method keys for a DID', async () => { - const did = await DidJwk.create(); - - const keySet = await DidMethod.toKeys({ did }); - - expect(keySet).to.have.property('verificationMethods'); - expect(keySet.verificationMethods).to.have.length(1); - expect(keySet.verificationMethods![0]).to.have.property('publicKeyJwk'); - expect(keySet.verificationMethods![0]).to.have.property('privateKeyJwk'); - expect(keySet.verificationMethods![0]).to.have.property('purposes'); - expect(keySet.verificationMethods![0]).to.have.property('type'); - expect(keySet.verificationMethods![0]).to.have.property('id'); - expect(keySet.verificationMethods![0]).to.have.property('controller'); - }); - - it('returns a key set with the expected key purposes', async () => { - // Create a DID to use for the test. - const did = await DidJwk.create(); - - // Delete all verification relationships except `keyAgreement`. - delete did.didDocument.assertionMethod; - delete did.didDocument.authentication; - delete did.didDocument.capabilityDelegation; - delete did.didDocument.capabilityInvocation; - - const keySet = await DidMethod.toKeys({ did }); - - expect(keySet.verificationMethods![0]).to.have.property('purposes'); - expect(keySet.verificationMethods![0].purposes).to.deep.equal(['keyAgreement']); - }); - - it('throws an error if the DID document lacks any verification methods', async () => { - // Delete the verification method property from the DID document. - delete didJwk.didDocument.verificationMethod; - - try { - await DidMethod.toKeys({ did: didJwk }); - expect.fail('Error should have been thrown'); - } catch (error: any) { - expect(error.message).to.include('missing verification methods'); - } - }); - - it('throws an error if the DID document does not contain a public key', async () => { - // Delete the public key from the DID document. - delete didJwk.didDocument.verificationMethod![0].publicKeyJwk; + describe('resolve()', () => { + it('throws an error if the DID method implementation does not provide a resolve() function', async () => { + class DidTest extends DidMethod {} try { - await DidMethod.toKeys({ did: didJwk }); + await DidTest.resolve('did:method:example'); expect.fail('Error should have been thrown'); } catch (error: any) { - expect(error.message).to.include('does not contain a public key'); - } - }); - - it('throws an error if the key manager does not support exporting keys', async () => { - // Create a key manager that does not support exporting keys. - const keyManagerWithoutExport: CryptoApi = { - digest : sinon.stub(), - generateKey : sinon.stub(), - getKeyUri : sinon.stub(), - getPublicKey : sinon.stub(), - sign : sinon.stub(), - verify : sinon.stub(), - }; - - // Create a DID to use for the test. - const did: BearerDid = { - didDocument : { id: 'did:jwk:123' }, - keyManager : keyManagerWithoutExport, - getSigner : sinon.stub(), - metadata : {}, - uri : 'did:jwk:123', - }; - - try { - await DidMethod.toKeys({ did }); - expect.fail('Expected an error to be thrown.'); - } catch (error: any) { - expect(error.message).to.include('does not support exporting keys'); + expect(error.message).to.include('must implement resolve()'); } }); }); diff --git a/packages/dids/tests/resolver/did-resolver.spec.ts b/packages/dids/tests/resolver/did-resolver.spec.ts index c31b8c1d1..f5e844c6a 100644 --- a/packages/dids/tests/resolver/did-resolver.spec.ts +++ b/packages/dids/tests/resolver/did-resolver.spec.ts @@ -79,7 +79,7 @@ describe('DidResolver', () => { it('returns a DID verification method resource as the value of contentStream if found', async () => { const did = await DidJwk.create(); - const result = await didResolver.dereference(did.didDocument!.verificationMethod![0].id); + const result = await didResolver.dereference(did.document!.verificationMethod![0].id); expect(result.contentStream).to.be.not.be.null; expect(result.dereferencingMetadata.error).to.not.exist; diff --git a/packages/dids/tests/utils.spec.ts b/packages/dids/tests/utils.spec.ts index c6791a63d..a975fa31c 100644 --- a/packages/dids/tests/utils.spec.ts +++ b/packages/dids/tests/utils.spec.ts @@ -1,15 +1,19 @@ import { expect } from 'chai'; -import type { DidDocument } from '../src/types/did-core.js'; +import { DidVerificationRelationship, type DidDocument } from '../src/types/did-core.js'; import { getServices, isDidService, isDwnDidService, - getVerificationMethodByKey, - isDidVerificationMethod, + extractDidFragment, + keyBytesToMultibaseId, + multibaseIdToKeyBytes, getVerificationMethods, + isDidVerificationMethod, + getVerificationMethodByKey, getVerificationMethodTypes, + getVerificationRelationshipsById, } from '../src/utils.js'; import DidUtilsgetVerificationMethodsTestVector from './fixtures/test-vectors/utils/get-verification-methods.json' assert { type: 'json' }; @@ -17,6 +21,51 @@ import DidUtilsGetVerificationMethodTypesTestVector from './fixtures/test-vector import DidUtilsGetVerificationMethodByKeyTestVector from './fixtures/test-vectors/utils/get-verification-method-by-key.json' assert { type: 'json' }; describe('DID Utils', () => { + describe('extractDidFragment()', () => { + it('returns the fragment when a DID string with a fragment is provided', () => { + const result = extractDidFragment('did:example:123#key-1'); + expect(result).to.equal('key-1'); + }); + + it('returns the input string when a string without a fragment is provided', () => { + let result = extractDidFragment('did:example:123'); + expect(result).to.equal('did:example:123'); + + result = extractDidFragment('0'); + expect(result).to.equal('0'); + }); + + it('returns undefined for non-string inputs', () => { + const result = extractDidFragment({ id: 'did:example:123#0', type: 'JsonWebKey' }); + expect(result).to.be.undefined; + }); + + it('returns undefined for array inputs', () => { + const result = extractDidFragment([{ id: 'did:example:123#0', type: 'JsonWebKey' }]); + expect(result).to.be.undefined; + }); + + it('returns undefined for undefined inputs', () => { + const result = extractDidFragment(undefined); + expect(result).to.be.undefined; + }); + + it('returns undefined for empty string input', () => { + const result = extractDidFragment(''); + expect(result).to.be.undefined; + }); + + it('returns "0" when input is "did:method:123#0"', () => { + const result = extractDidFragment('did:method:123#0'); + expect(result).to.equal('0'); + }); + + it('returns "0" when input is "#0"', () => { + const result = extractDidFragment('#0'); + expect(result).to.equal('0'); + }); + }); + describe('getServices()', () => { let didDocument: DidDocument = { id : 'did:example:123', @@ -227,6 +276,60 @@ describe('DID Utils', () => { }); }); + describe('getVerificationRelationshipsById', () => { + let didDocument: DidDocument; + + beforeEach(() => { + didDocument = { + id : 'did:example:123', + authentication : ['did:example:123#auth'], + assertionMethod : [ + { + id : 'did:example:123#assert', + type : 'JsonWebKey', + controller : 'did:example:123' + } + ], + capabilityDelegation: ['did:example:123#key-2'], + }; + }); + + it('should return an empty array if no relationships match the methodId', () => { + const result = getVerificationRelationshipsById({ didDocument, methodId: '0' }); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should return matching relationships by direct reference', () => { + const result = getVerificationRelationshipsById({ didDocument, methodId: 'auth' }); + expect(result).to.include(DidVerificationRelationship.authentication); + }); + + it('should return matching relationships by embedded method', () => { + const result = getVerificationRelationshipsById({ didDocument, methodId: 'assert' }); + expect(result).to.include(DidVerificationRelationship.assertionMethod); + }); + + it('handles method IDs with or without hash symbol prefix', () => { + let result = getVerificationRelationshipsById({ didDocument, methodId: 'key-2' }); + expect(result).to.include(DidVerificationRelationship.capabilityDelegation); + result = getVerificationRelationshipsById({ didDocument, methodId: '#key-2' }); + expect(result).to.include(DidVerificationRelationship.capabilityDelegation); + }); + + it('handles method IDs with a full DID URL', () => { + const result = getVerificationRelationshipsById({ didDocument, methodId: 'did:example:123#key-2' }); + expect(result).to.include(DidVerificationRelationship.capabilityDelegation); + }); + + it('ignores the DID if the method IDs is a full DID URL', () => { + // While not technically disallowed, it is not recommended for a verification method in a + // DID document to reference another DID. If a use case ever arises for this, we can revisit + // adding support to enable matching method IDs with the same identifier but different DIDs. + const result = getVerificationRelationshipsById({ didDocument, methodId: 'did:example:456#key-2' }); + expect(result).to.include(DidVerificationRelationship.capabilityDelegation); + }); + }); + describe('isDwnDidService', () => { it('returns true for a valid DwnDidService object', () => { const validDwnService = { @@ -429,4 +532,102 @@ describe('DID Utils', () => { expect(isDidVerificationMethod(extraProps)).to.be.true; }); }); + + describe('keyBytesToMultibaseId()', () => { + it('returns a multibase encoded string', () => { + const input = { + keyBytes : new Uint8Array(32), + multicodecName : 'ed25519-pub', + }; + const encoded = keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); + expect(encoded).to.be.a.string; + expect(encoded.substring(0, 1)).to.equal('z'); + expect(encoded.substring(1, 4)).to.equal('6Mk'); + }); + + it('passes test vectors', () => { + let input: { keyBytes: Uint8Array, multicodecName: string }; + let output: string; + let encoded: string; + + // Test Vector 1. + input = { + keyBytes : (new Uint8Array(32)).fill(0), + multicodecName : 'ed25519-pub', + }; + output = 'z6MkeTG3bFFSLYVU7VqhgZxqr6YzpaGrQtFMh1uvqGy1vDnP'; + encoded = keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); + expect(encoded).to.equal(output); + + // Test Vector 2. + input = { + keyBytes : (new Uint8Array(32)).fill(1), + multicodecName : 'ed25519-pub', + }; + output = 'z6MkeXBLjYiSvqnhFb6D7sHm8yKm4jV45wwBFRaatf1cfZ76'; + encoded = keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); + expect(encoded).to.equal(output); + + // Test Vector 3. + input = { + keyBytes : (new Uint8Array(32)).fill(9), + multicodecName : 'ed25519-pub', + }; + output = 'z6Mkf4XhsxSXfEAWNK6GcFu7TyVs21AfUTRjiguqMhNQeDgk'; + encoded = keyBytesToMultibaseId({ keyBytes: input.keyBytes, multicodecName: input.multicodecName }); + expect(encoded).to.equal(output); + }); + }); + + describe('multibaseIdToKeyBytes()', () => { + it('converts secp256k1-pub multibase identifiers', () => { + const multibaseKeyId = 'zQ3shMrXA3Ah8h5asMM69USP8qRDnPaCLRV3nPmitAXVfWhgp'; + + const { keyBytes, multicodecCode, multicodecName } = multibaseIdToKeyBytes({ multibaseKeyId }); + + expect(keyBytes).to.exist; + expect(keyBytes).to.be.a('Uint8Array'); + expect(keyBytes).to.have.length(33); + expect(multicodecCode).to.exist; + expect(multicodecCode).to.equal(231); + expect(multicodecName).to.exist; + expect(multicodecName).to.equal('secp256k1-pub'); + }); + + it('converts ed25519-pub multibase identifiers', () => { + const multibaseKeyId = 'z6MkizSHspkM891CAnYZis1TJkB4fWwuyVjt4pV93rWPGYwW'; + + const { keyBytes, multicodecCode, multicodecName } = multibaseIdToKeyBytes({ multibaseKeyId }); + + expect(keyBytes).to.exist; + expect(keyBytes).to.be.a('Uint8Array'); + expect(keyBytes).to.have.length(32); + expect(multicodecCode).to.exist; + expect(multicodecCode).to.equal(237); + expect(multicodecName).to.exist; + expect(multicodecName).to.equal('ed25519-pub'); + }); + + it('converts x25519-pub multibase identifiers', () => { + const multibaseKeyId = 'z6LSfsF6tQA7j56WSzNPT4yrzZprzGEK8137DMeAVLgGBJEz'; + + const { keyBytes, multicodecCode, multicodecName } = multibaseIdToKeyBytes({ multibaseKeyId }); + + expect(keyBytes).to.exist; + expect(keyBytes).to.be.a('Uint8Array'); + expect(keyBytes).to.have.length(32); + expect(multicodecCode).to.exist; + expect(multicodecCode).to.equal(236); + expect(multicodecName).to.exist; + expect(multicodecName).to.equal('x25519-pub'); + }); + + it('throws an error for an invalid multibase identifier', async () => { + try { + multibaseIdToKeyBytes({ multibaseKeyId: 'z6Mkiz' }); + } catch (error: any) { + expect(error.message).to.include('Invalid multibase identifier'); + } + }); + }); }); \ No newline at end of file