diff --git a/package-lock.json b/package-lock.json index 061e41181..87dd6ef81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -585,9 +585,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.465.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.465.0.tgz", - "integrity": "sha512-f+QNcWGswredzC1ExNAB/QzODlxwaTdXkNT5cvke2RLX8SFU5pYk6h4uCtWC0vWPELzOfMfloBrJefBzlarhsw==", + "version": "3.495.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.495.0.tgz", + "integrity": "sha512-MfaPXT0kLX2tQaR90saBT9fWQq2DHqSSJRzW+MZWsmF+y5LGCOhO22ac/2o6TKSQm7h0HRc2GaADqYYYor62yg==", "dependencies": { "tslib": "^2.5.0" }, @@ -688,6 +688,52 @@ "hash-wasm": "4.9.0" } }, + "node_modules/@decentralized-identity/ion-pow-sdk/node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/@decentralized-identity/ion-pow-sdk/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@decentralized-identity/ion-pow-sdk/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@decentralized-identity/ion-pow-sdk/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@decentralized-identity/ion-pow-sdk/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/@decentralized-identity/ion-sdk": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-sdk/-/ion-sdk-1.0.1.tgz", @@ -710,6 +756,18 @@ "npm": ">=7.0.0" } }, + "node_modules/@dnsquery/dns-packet": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@dnsquery/dns-packet/-/dns-packet-6.1.1.tgz", + "integrity": "sha512-WXTuFvL3G+74SchFAtz3FgIYVOe196ycvGsMgvSH/8Goptb1qpIQtIuM4SOK9G9lhMWYpHxnXyy544ZhluFOew==", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.4", + "utf8-codec": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.19.8", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", @@ -1361,9 +1419,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1632,9 +1690,9 @@ } }, "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "dependencies": { "b4a": "^1.6.4", @@ -1708,9 +1766,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz", - "integrity": "sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", "cpu": [ "arm" ], @@ -1721,9 +1779,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz", - "integrity": "sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", "cpu": [ "arm64" ], @@ -1734,9 +1792,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.4.tgz", - "integrity": "sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", "cpu": [ "arm64" ], @@ -1747,9 +1805,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.4.tgz", - "integrity": "sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", "cpu": [ "x64" ], @@ -1760,9 +1818,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.4.tgz", - "integrity": "sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", "cpu": [ "arm" ], @@ -1773,9 +1831,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.4.tgz", - "integrity": "sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", "cpu": [ "arm64" ], @@ -1786,9 +1844,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.4.tgz", - "integrity": "sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", "cpu": [ "arm64" ], @@ -1799,9 +1857,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.4.tgz", - "integrity": "sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", "cpu": [ "riscv64" ], @@ -1812,9 +1870,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.4.tgz", - "integrity": "sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", "cpu": [ "x64" ], @@ -1825,9 +1883,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.4.tgz", - "integrity": "sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", "cpu": [ "x64" ], @@ -1838,9 +1896,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.4.tgz", - "integrity": "sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", "cpu": [ "arm64" ], @@ -1851,9 +1909,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.4.tgz", - "integrity": "sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", "cpu": [ "ia32" ], @@ -1864,9 +1922,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.4.tgz", - "integrity": "sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", "cpu": [ "x64" ], @@ -1877,9 +1935,9 @@ ] }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" @@ -1921,11 +1979,11 @@ "dev": true }, "node_modules/@smithy/abort-controller": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.16.tgz", - "integrity": "sha512-4foO7738k8kM9flMHu3VLabqu7nPgvIj8TB909S0CnKx0YZz/dcDH3pZ/4JHdatfxlZdKF1JWOYCw9+v3HVVsw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.1.tgz", + "integrity": "sha512-1+qdrUqLhaALYL0iOcN43EP6yAXXQ2wWZ6taf4S2pNGowmOc5gx+iMQv+E42JizNJjB0+gEadOXeV1Bf7JWL1Q==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -1933,14 +1991,14 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.23.tgz", - "integrity": "sha512-XakUqgtP2YY8Mi+Nlif5BiqJgWdvfxJafSpOSQeCOMizu+PUhE4fBQSy6xFcR+eInrwVadaABNxoJyGUMn15ew==", - "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", - "@smithy/util-config-provider": "^2.1.0", - "@smithy/util-middleware": "^2.0.9", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.1.tgz", + "integrity": "sha512-lxfLDpZm+AWAHPFZps5JfDoO9Ux1764fOgvRUBpHIO8HWHcSN1dkgsago1qLRVgm1BZ8RCm8cgv99QvtaOWIhw==", + "dependencies": { + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -1948,17 +2006,17 @@ } }, "node_modules/@smithy/core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.2.2.tgz", - "integrity": "sha512-uLjrskLT+mWb0emTR5QaiAIxVEU7ndpptDaVDrTwwhD+RjvHhjIiGQ3YL5jKk1a5VSDQUA2RGkXvJ6XKRcz6Dg==", - "dependencies": { - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/protocol-http": "^3.0.12", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/util-middleware": "^2.0.9", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.1.tgz", + "integrity": "sha512-tf+NIu9FkOh312b6M9G4D68is4Xr7qptzaZGZUREELF8ysE1yLKphqt7nsomjKZVwW7WE5pDDex9idowNGRQ/Q==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -1966,14 +2024,14 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.1.5.tgz", - "integrity": "sha512-VfvE6Wg1MUWwpTZFBnUD7zxvPhLY8jlHCzu6bCjlIYoWgXCDzZAML76IlZUEf45nib3rjehnFgg0s1rgsuN/bg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.1.tgz", + "integrity": "sha512-7XHjZUxmZYnONheVQL7j5zvZXga+EWNgwEAP6OPZTi7l8J4JTeNh9aIOfE5fKHZ/ee2IeNOh54ZrSna+Vc6TFA==", "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/property-provider": "^2.0.17", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -1981,36 +2039,36 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.0.16.tgz", - "integrity": "sha512-umYh5pdCE9GHgiMAH49zu9wXWZKNHHdKPm/lK22WYISTjqu29SepmpWNmPiBLy/yUu4HFEGJHIFrDWhbDlApaw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.1.tgz", + "integrity": "sha512-E8KYBxBIuU4c+zrpR22VsVrOPoEDzk35bQR3E+xm4k6Pa6JqzkDOdMyf9Atac5GPNKHJBdVaQ4JtjdWX2rl/nw==", "dependencies": { "@aws-crypto/crc32": "3.0.0", - "@smithy/types": "^2.8.0", - "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/types": "^2.9.1", + "@smithy/util-hex-encoding": "^2.1.1", "tslib": "^2.5.0" } }, "node_modules/@smithy/fetch-http-handler": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.3.2.tgz", - "integrity": "sha512-O9R/OlnAOTsnysuSDjt0v2q6DcSvCz5cCFC/CFAWWcLyBwJDeFyGTCTszgpQTb19+Fi8uRwZE5/3ziAQBFeDMQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.1.tgz", + "integrity": "sha512-VYGLinPsFqH68lxfRhjQaSkjXM7JysUOJDTNjHBuN/ykyRb2f1gyavN9+VhhPTWCy32L4yZ2fdhpCs/nStEicg==", "dependencies": { - "@smithy/protocol-http": "^3.0.12", - "@smithy/querystring-builder": "^2.0.16", - "@smithy/types": "^2.8.0", - "@smithy/util-base64": "^2.0.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-base64": "^2.1.1", "tslib": "^2.5.0" } }, "node_modules/@smithy/hash-node": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.0.18.tgz", - "integrity": "sha512-gN2JFvAgnZCyDN9rJgcejfpK0uPPJrSortVVVVWsru9whS7eQey6+gj2eM5ln2i6rHNntIXzal1Fm9XOPuoaKA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.1.tgz", + "integrity": "sha512-Qhoq0N8f2OtCnvUpCf+g1vSyhYQrZjhSwvJ9qvR8BUGOtTXiyv2x1OD2e6jVGmlpC4E4ax1USHoyGfV9JFsACg==", "dependencies": { - "@smithy/types": "^2.8.0", - "@smithy/util-buffer-from": "^2.0.0", - "@smithy/util-utf8": "^2.0.2", + "@smithy/types": "^2.9.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2018,18 +2076,18 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.0.16.tgz", - "integrity": "sha512-apEHakT/kmpNo1VFHP4W/cjfeP9U0x5qvfsLJubgp7UM/gq4qYp0GbqdE7QhsjUaYvEnrftRqs7+YrtWreV0wA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.1.tgz", + "integrity": "sha512-7WTgnKw+VPg8fxu2v9AlNOQ5yaz6RA54zOVB4f6vQuR0xFKd+RzlCpt0WidYTsye7F+FYDIaS/RnJW4pxjNInw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" } }, "node_modules/@smithy/is-array-buffer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz", - "integrity": "sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz", + "integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==", "dependencies": { "tslib": "^2.5.0" }, @@ -2038,12 +2096,12 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.0.18.tgz", - "integrity": "sha512-ZJ9uKPTfxYheTKSKYB+GCvcj+izw9WGzRLhjn8n254q0jWLojUzn7Vw0l4R/Gq7Wdpf/qmk/ptD+6CCXHNVCaw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.1.tgz", + "integrity": "sha512-rSr9ezUl9qMgiJR0UVtVOGEZElMdGFyl8FzWEF5iEKTlcWxGr2wTqGfDwtH3LAB7h+FPkxqv4ZU4cpuCN9Kf/g==", "dependencies": { - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2051,16 +2109,16 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.3.0.tgz", - "integrity": "sha512-VsOAG2YQ8ykjSmKO+CIXdJBIWFo6AAvG6Iw95BakBTqk66/4BI7XyqLevoNSq/lZ6NgZv24sLmrcIN+fLDWBCg==", - "dependencies": { - "@smithy/middleware-serde": "^2.0.16", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/shared-ini-file-loader": "^2.2.8", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-middleware": "^2.0.9", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.1.tgz", + "integrity": "sha512-XPZTb1E2Oav60Ven3n2PFx+rX9EDsU/jSTA8VDamt7FXks67ekjPY/XrmmPDQaFJOTUHJNKjd8+kZxVO5Ael4Q==", + "dependencies": { + "@smithy/middleware-serde": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2068,17 +2126,17 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.0.26.tgz", - "integrity": "sha512-Qzpxo0U5jfNiq9iD38U3e2bheXwvTEX4eue9xruIvEgh+UKq6dKuGqcB66oBDV7TD/mfoJi9Q/VmaiqwWbEp7A==", - "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/protocol-http": "^3.0.12", - "@smithy/service-error-classification": "^2.0.9", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/util-middleware": "^2.0.9", - "@smithy/util-retry": "^2.0.9", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.1.tgz", + "integrity": "sha512-eMIHOBTXro6JZ+WWzZWd/8fS8ht5nS5KDQjzhNMHNRcG5FkNTqcKpYhw7TETMYzbLfhO5FYghHy1vqDWM4FLDA==", + "dependencies": { + "@smithy/node-config-provider": "^2.2.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/service-error-classification": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", "tslib": "^2.5.0", "uuid": "^8.3.2" }, @@ -2087,11 +2145,11 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.0.16.tgz", - "integrity": "sha512-5EAd4t30pcc4M8TSSGq7q/x5IKrxfXR5+SrU4bgxNy7RPHQo2PSWBUco9C+D9Tfqp/JZvprRpK42dnupZafk2g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.1.tgz", + "integrity": "sha512-D8Gq0aQBeE1pxf3cjWVkRr2W54t+cdM2zx78tNrVhqrDykRA7asq8yVJij1u5NDtKzKqzBSPYh7iW0svUKg76g==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2099,11 +2157,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.0.10.tgz", - "integrity": "sha512-I2rbxctNq9FAPPEcuA1ntZxkTKOPQFy7YBPOaD/MLg1zCvzv21CoNxR0py6J8ZVC35l4qE4nhxB0f7TF5/+Ldw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.1.tgz", + "integrity": "sha512-KPJhRlhsl8CjgGXK/DoDcrFGfAqoqvuwlbxy+uOO4g2Azn1dhH+GVfC3RAp+6PoL5PWPb+vt6Z23FP+Mr6qeCw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2111,13 +2169,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.1.9.tgz", - "integrity": "sha512-tUyW/9xrRy+s7RXkmQhgYkAPMpTIF8izK4orhHjNFEKR3QZiOCbWB546Y8iB/Fpbm3O9+q0Af9rpywLKJOwtaQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.1.tgz", + "integrity": "sha512-epzK3x1xNxA9oJgHQ5nz+2j6DsJKdHfieb+YgJ7ATWxzNcB7Hc+Uya2TUck5MicOPhDV8HZImND7ZOecVr+OWg==", "dependencies": { - "@smithy/property-provider": "^2.0.17", - "@smithy/shared-ini-file-loader": "^2.2.8", - "@smithy/types": "^2.8.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2125,14 +2183,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.2.2.tgz", - "integrity": "sha512-XO58TO/Eul/IBQKFKaaBtXJi0ItEQQCT+NI4IiKHCY/4KtqaUT6y/wC1EvDqlA9cP7Dyjdj7FdPs4DyynH3u7g==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.3.1.tgz", + "integrity": "sha512-gLA8qK2nL9J0Rk/WEZSvgin4AppvuCYRYg61dcUo/uKxvMZsMInL5I5ZdJTogOvdfVug3N2dgI5ffcUfS4S9PA==", "dependencies": { - "@smithy/abort-controller": "^2.0.16", - "@smithy/protocol-http": "^3.0.12", - "@smithy/querystring-builder": "^2.0.16", - "@smithy/types": "^2.8.0", + "@smithy/abort-controller": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2140,11 +2198,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.0.17.tgz", - "integrity": "sha512-+VkeZbVu7qtQ2DjI48Qwaf9fPOr3gZIwxQpuLJgRRSkWsdSvmaTCxI3gzRFKePB63Ts9r4yjn4HkxSCSkdWmcQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.1.tgz", + "integrity": "sha512-FX7JhhD/o5HwSwg6GLK9zxrMUrGnb3PzNBrcthqHKBc3dH0UfgEAU24xnJ8F0uow5mj17UeBEOI6o3CF2k7Mhw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2152,11 +2210,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.0.12.tgz", - "integrity": "sha512-Xz4iaqLiaBfbQpB9Hgi3VcZYbP7xRDXYhd8XWChh4v94uw7qwmvlxdU5yxzfm6ACJM66phHrTbS5TVvj5uQ72w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.1.1.tgz", + "integrity": "sha512-6ZRTSsaXuSL9++qEwH851hJjUA0OgXdQFCs+VDw4tGH256jQ3TjYY/i34N4vd24RV3nrjNsgd1yhb57uMoKbzQ==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2164,12 +2222,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.0.16.tgz", - "integrity": "sha512-Q/GsJT0C0mijXMRs7YhZLLCP5FcuC4797lYjKQkME5CZohnLC4bEhylAd2QcD3gbMKNjCw8+T2I27WKiV/wToA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.1.tgz", + "integrity": "sha512-C/ko/CeEa8jdYE4gt6nHO5XDrlSJ3vdCG0ZAc6nD5ZIE7LBp0jCx4qoqp7eoutBu7VrGMXERSRoPqwi1WjCPbg==", "dependencies": { - "@smithy/types": "^2.8.0", - "@smithy/util-uri-escape": "^2.0.0", + "@smithy/types": "^2.9.1", + "@smithy/util-uri-escape": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2177,11 +2235,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.16.tgz", - "integrity": "sha512-c4ueAuL6BDYKWpkubjrQthZKoC3L5kql5O++ovekNxiexRXTlLIVlCR4q3KziOktLIw66EU9SQljPXd/oN6Okg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.1.tgz", + "integrity": "sha512-H4+6jKGVhG1W4CIxfBaSsbm98lOO88tpDWmZLgkJpt8Zkk/+uG0FmmqMuCAc3HNM2ZDV+JbErxr0l5BcuIf/XQ==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2189,22 +2247,22 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.0.9.tgz", - "integrity": "sha512-0K+8GvtwI7VkGmmInPydM2XZyBfIqLIbfR7mDQ+oPiz8mIinuHbV6sxOLdvX1Jv/myk7XTK9orgt3tuEpBu/zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.1.tgz", + "integrity": "sha512-txEdZxPUgM1PwGvDvHzqhXisrc5LlRWYCf2yyHfvITWioAKat7srQvpjMAvgzf0t6t7j8yHrryXU9xt7RZqFpw==", "dependencies": { - "@smithy/types": "^2.8.0" + "@smithy/types": "^2.9.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.2.8.tgz", - "integrity": "sha512-E62byatbwSWrtq9RJ7xN40tqrRKDGrEL4EluyNpaIDvfvet06a/QC58oHw2FgVaEgkj0tXZPjZaKrhPfpoU0qw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.1.tgz", + "integrity": "sha512-2E2kh24igmIznHLB6H05Na4OgIEilRu0oQpYXo3LCNRrawHAcfDKq9004zJs+sAMt2X5AbY87CUCJ7IpqpSgdw==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2212,17 +2270,17 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.0.19.tgz", - "integrity": "sha512-nwc3JihdM+kcJjtORv/n7qRHN2Kfh7S2RJI2qr8pz9UcY5TD8rSCRGQ0g81HgyS3jZ5X9U/L4p014P3FonBPhg==", - "dependencies": { - "@smithy/eventstream-codec": "^2.0.16", - "@smithy/is-array-buffer": "^2.0.0", - "@smithy/types": "^2.8.0", - "@smithy/util-hex-encoding": "^2.0.0", - "@smithy/util-middleware": "^2.0.9", - "@smithy/util-uri-escape": "^2.0.0", - "@smithy/util-utf8": "^2.0.2", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.1.tgz", + "integrity": "sha512-Hb7xub0NHuvvQD3YwDSdanBmYukoEkhqBjqoxo+bSdC0ryV9cTfgmNjuAQhTPYB6yeU7hTR+sPRiFMlxqv6kmg==", + "dependencies": { + "@smithy/eventstream-codec": "^2.1.1", + "@smithy/is-array-buffer": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-uri-escape": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2230,15 +2288,15 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.2.1.tgz", - "integrity": "sha512-SpD7FLK92XV2fon2hMotaNDa2w5VAy5/uVjP9WFmjGSgWM8pTPVkHcDl1yFs5Z8LYbij0FSz+DbCBK6i+uXXUA==", - "dependencies": { - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", - "@smithy/util-stream": "^2.0.24", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.3.1.tgz", + "integrity": "sha512-YsTdU8xVD64r2pLEwmltrNvZV6XIAC50LN6ivDopdt+YiF/jGH6PY9zUOu0CXD/d8GMB8gbhnpPsdrjAXHS9QA==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-stream": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2246,9 +2304,9 @@ } }, "node_modules/@smithy/types": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.8.0.tgz", - "integrity": "sha512-h9sz24cFgt/W1Re22OlhQKmUZkNh244ApgRsUDYinqF8R+QgcsBIX344u2j61TPshsTz3CvL6HYU1DnQdsSrHA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.9.1.tgz", + "integrity": "sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw==", "dependencies": { "tslib": "^2.5.0" }, @@ -2257,21 +2315,21 @@ } }, "node_modules/@smithy/url-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.16.tgz", - "integrity": "sha512-Wfz5WqAoRT91TjRy1JeLR0fXtkIXHGsMbgzKFTx7E68SrZ55TB8xoG+vm11Ru4gheFTMXjAjwAxv1jQdC+pAQA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.1.tgz", + "integrity": "sha512-qC9Bv8f/vvFIEkHsiNrUKYNl8uKQnn4BdhXl7VzQRP774AwIjiSMMwkbT+L7Fk8W8rzYVifzJNYxv1HwvfBo3Q==", "dependencies": { - "@smithy/querystring-parser": "^2.0.16", - "@smithy/types": "^2.8.0", + "@smithy/querystring-parser": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" } }, "node_modules/@smithy/util-base64": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.1.tgz", - "integrity": "sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz", + "integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==", "dependencies": { - "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-buffer-from": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2279,17 +2337,17 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.1.tgz", - "integrity": "sha512-NXYp3ttgUlwkaug4bjBzJ5+yIbUbUx8VsSLuHZROQpoik+gRkIBeEG9MPVYfvPNpuXb/puqodeeUXcKFe7BLOQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz", + "integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==", "dependencies": { "tslib": "^2.5.0" } }, "node_modules/@smithy/util-body-length-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz", - "integrity": "sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz", + "integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==", "dependencies": { "tslib": "^2.5.0" }, @@ -2298,11 +2356,11 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz", - "integrity": "sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz", + "integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==", "dependencies": { - "@smithy/is-array-buffer": "^2.0.0", + "@smithy/is-array-buffer": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2310,9 +2368,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.1.0.tgz", - "integrity": "sha512-S6V0JvvhQgFSGLcJeT1CBsaTR03MM8qTuxMH9WPCCddlSo2W0V5jIHimHtIQALMLEDPGQ0ROSRr/dU0O+mxiQg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz", + "integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==", "dependencies": { "tslib": "^2.5.0" }, @@ -2321,13 +2379,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.24.tgz", - "integrity": "sha512-TsP5mBuLgO2C21+laNG2nHYZEyUdkbGURv2tHvSuQQxLz952MegX95uwdxOY2jR2H4GoKuVRfdJq7w4eIjGYeg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.1.tgz", + "integrity": "sha512-lqLz/9aWRO6mosnXkArtRuQqqZBhNpgI65YDpww4rVQBuUT7qzKbDLG5AmnQTCiU4rOquaZO/Kt0J7q9Uic7MA==", "dependencies": { - "@smithy/property-provider": "^2.0.17", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", "bowser": "^2.11.0", "tslib": "^2.5.0" }, @@ -2336,16 +2394,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.0.32", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.32.tgz", - "integrity": "sha512-d0S33dXA2cq1NyorVMroMrEtqKMr3MlyLITcfTBf9pXiigYiPMOtbSI7czHIfDbuVuM89Cg0urAgpt73QV9mPQ==", - "dependencies": { - "@smithy/config-resolver": "^2.0.23", - "@smithy/credential-provider-imds": "^2.1.5", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/property-provider": "^2.0.17", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", + "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==", + "dependencies": { + "@smithy/config-resolver": "^2.1.1", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2353,12 +2411,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.0.8.tgz", - "integrity": "sha512-l8zVuyZZ61IzZBYp5NWvsAhbaAjYkt0xg9R4xUASkg5SEeTT2meHOJwJHctKMFUXe4QZbn9fR2MaBYjP2119+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.1.tgz", + "integrity": "sha512-sI4d9rjoaekSGEtq3xSb2nMjHMx8QXcz2cexnVyRWsy4yQ9z3kbDpX+7fN0jnbdOp0b3KSTZJZ2Yb92JWSanLw==", "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2366,9 +2424,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz", - "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz", + "integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==", "dependencies": { "tslib": "^2.5.0" }, @@ -2377,11 +2435,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.0.9.tgz", - "integrity": "sha512-PnCnBJ07noMX1lMDTEefmxSlusWJUiLfrme++MfK5TD0xz8NYmakgoXy5zkF/16zKGmiwOeKAztWT/Vjk1KRIQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.1.tgz", + "integrity": "sha512-mKNrk8oz5zqkNcbcgAAepeJbmfUW6ogrT2Z2gDbIUzVzNAHKJQTYmH9jcy0jbWb+m7ubrvXKb6uMjkSgAqqsFA==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2389,12 +2447,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.0.9.tgz", - "integrity": "sha512-46BFWe9RqB6g7f4mxm3W3HlqknqQQmWHKlhoqSFZuGNuiDU5KqmpebMbvC3tjTlUkqn4xa2Z7s3Hwb0HNs5scw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.1.tgz", + "integrity": "sha512-Mg+xxWPTeSPrthpC5WAamJ6PW4Kbo01Fm7lWM1jmGRvmrRdsd3192Gz2fBXAMURyXpaNxyZf6Hr/nQ4q70oVEA==", "dependencies": { - "@smithy/service-error-classification": "^2.0.9", - "@smithy/types": "^2.8.0", + "@smithy/service-error-classification": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -2402,17 +2460,17 @@ } }, "node_modules/@smithy/util-stream": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.0.24.tgz", - "integrity": "sha512-hRpbcRrOxDriMVmbya+Mv77VZVupxRAsfxVDKS54XuiURhdiwCUXJP0X1iJhHinuUf6n8pBF0MkG9C8VooMnWw==", - "dependencies": { - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/types": "^2.8.0", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-buffer-from": "^2.0.0", - "@smithy/util-hex-encoding": "^2.0.0", - "@smithy/util-utf8": "^2.0.2", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.1.tgz", + "integrity": "sha512-J7SMIpUYvU4DQN55KmBtvaMc7NM3CZ2iWICdcgaovtLzseVhAqFRYqloT3mh0esrFw+3VEK6nQFteFsTqZSECQ==", + "dependencies": { + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-buffer-from": "^2.1.1", + "@smithy/util-hex-encoding": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2420,9 +2478,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz", - "integrity": "sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz", + "integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==", "dependencies": { "tslib": "^2.5.0" }, @@ -2431,11 +2489,11 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.2.tgz", - "integrity": "sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz", + "integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==", "dependencies": { - "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-buffer-from": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -2461,9 +2519,9 @@ } }, "node_modules/@sphereon/pex-models": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@sphereon/pex-models/-/pex-models-2.1.2.tgz", - "integrity": "sha512-Ec1qZl8tuPd+s6E+ZM7v+HkGkSOjGDMLNN1kqaxAfWpITBYtTLb+d5YvwjvBZ1P2upZ7zwNER97FfW5n/30y2w==" + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@sphereon/pex-models/-/pex-models-2.1.5.tgz", + "integrity": "sha512-7THexvdYUK/Dh8olBB46ErT9q/RnecnMdb5r2iwZ6be0Dt4vQLAUN7QU80H0HZBok4jRTb8ydt12x0raBSTHOg==" }, "node_modules/@sphereon/ssi-types": { "version": "0.13.0", @@ -2506,14 +2564,6 @@ "node": ">= 18" } }, - "node_modules/@tbd54566975/dwn-sdk-js/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, "node_modules/@tbd54566975/dwn-sdk-js/node_modules/lru-cache": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz", @@ -2522,44 +2572,6 @@ "node": "14 || >=16.14" } }, - "node_modules/@tbd54566975/dwn-sdk-js/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@tbd54566975/dwn-sdk-js/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/@tbd54566975/dwn-sdk-js/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/@tbd54566975/dwn-sdk-js/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/@tbd54566975/dwn-sql-store": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sql-store/-/dwn-sql-store-0.2.6.tgz", @@ -2577,9 +2589,9 @@ } }, "node_modules/@tbd54566975/dwn-sql-store/node_modules/@ipld/dag-cbor": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.0.8.tgz", - "integrity": "sha512-ETWJ7p7lmGw5X+BuI/7rf4/k56xyOvAOVNUVuQmnGYBdJjObLPgS+vyFxRk4odATlkyZqCq2MLNY52bhE6SlRA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.1.0.tgz", + "integrity": "sha512-7pMKjBaapEh+1Nk/1umPPhQGT6znb5E71lke2ekxlcuVZLLrPPdDSy0UAMwWgj3a28cjir/ZJ6CQH2DEs3DUOQ==", "dev": true, "dependencies": { "cborg": "^4.0.0", @@ -2597,9 +2609,9 @@ "dev": true }, "node_modules/@tbd54566975/dwn-sql-store/node_modules/cborg": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.0.7.tgz", - "integrity": "sha512-5h2n7973T4dkY2XLfHpwYR9IjeDSfolZibYtb8clW53BvvgTyq+X2EtwrjfhTAETrwaQOX4+lRua14/XjbZHaQ==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.0.8.tgz", + "integrity": "sha512-/6QDK0Hw//cV4YNWZZjdIUMFNw0DZmb56jdVGJPwXP7874gSN0AMYqM07mVKpAm+6Nn7U8lvYFzPgBGatC+5xw==", "dev": true, "bin": { "cborg": "lib/bin.js" @@ -2636,6 +2648,15 @@ "integrity": "sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==", "dev": true }, + "node_modules/@types/bencode": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/bencode/-/bencode-2.0.4.tgz", + "integrity": "sha512-sirDu3HUSG7jZMlhTDvCzSFiPR4lkUYBQA75CoMi6DEf2alFZWJWtHgfjBbb9PachPZhPMB1IlH09deyMNBipQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2699,9 +2720,9 @@ "dev": true }, "node_modules/@types/cookies": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.10.tgz", - "integrity": "sha512-hmUCjAk2fwZVPPkkPBcI7jGLIR5mg4OVoNMBwU6aVsMm/iNPY7z9/R+x2fSwLt/ZXoGua6C5Zy2k5xOo9jUyhQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", "dev": true, "dependencies": { "@types/connect": "*", @@ -2765,9 +2786,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.41", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", - "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "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==", "dev": true, "dependencies": { "@types/node": "*", @@ -2868,9 +2889,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", - "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", + "version": "20.11.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.13.tgz", + "integrity": "sha512-5G4zQwdiQBSWYTDAH1ctw2eidqdhMJaNsiIDKHFr55ihz5Trl2qqR8fdrT732yPBho5gkNxXm67OxWFBqX9aPg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3007,16 +3028,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.1.tgz", - "integrity": "sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==", + "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==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.18.1", - "@typescript-eslint/types": "6.18.1", - "@typescript-eslint/typescript-estree": "6.18.1", - "@typescript-eslint/visitor-keys": "6.18.1", + "@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", "debug": "^4.3.4" }, "engines": { @@ -3036,14 +3057,14 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz", - "integrity": "sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==", + "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==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.18.1", - "@typescript-eslint/visitor-keys": "6.18.1" + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3054,13 +3075,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz", - "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", + "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/types": "6.20.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -3169,9 +3190,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.1.tgz", - "integrity": "sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", + "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", "dev": true, "peer": true, "engines": { @@ -3183,14 +3204,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz", - "integrity": "sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==", + "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==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.18.1", - "@typescript-eslint/visitor-keys": "6.18.1", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3212,13 +3233,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz", - "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", + "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/types": "6.20.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -3353,15 +3374,15 @@ } }, "node_modules/@web/dev-server": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.1.tgz", - "integrity": "sha512-GHeyH8MBZQpODFiHiXAdX4hOVbeDyD/DUermUinh/nexWAZUcXyXa200RItuAL6b25MQ3D/5hKNDypujSvXxiw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.2.tgz", + "integrity": "sha512-5IS2Rev+DRqIPtIiecOumoj+GZ4volRS6BeX+3mvuMF0OA51pCGhOozqUMVFFpAVuhHScihqIGk1gnHhw9d9kQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.11", "@types/command-line-args": "^5.0.0", "@web/config-loader": "^0.3.0", - "@web/dev-server-core": "^0.7.0", + "@web/dev-server-core": "^0.7.1", "@web/dev-server-rollup": "^0.6.1", "camelcase": "^6.2.0", "command-line-args": "^5.1.1", @@ -3382,9 +3403,9 @@ } }, "node_modules/@web/dev-server-core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.0.tgz", - "integrity": "sha512-1FJe6cJ3r0x0ZmxY/FnXVduQD4lKX7QgYhyS6N+VmIpV+tBU4sGRbcrmeoYeY+nlnPa6p2oNuonk3X5ln/W95g==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.1.tgz", + "integrity": "sha512-alHd2j0f4e1ekqYDR8lWScrzR7D5gfsUZq3BP3De9bkFWM3AELINCmqqlVKmCtlkAdEc9VyQvNiEqrxraOdc2A==", "dev": true, "dependencies": { "@types/koa": "^2.11.6", @@ -3871,13 +3892,6 @@ "dev": true, "peer": true }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -4263,11 +4277,11 @@ } }, "node_modules/bencode": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bencode/-/bencode-3.1.1.tgz", - "integrity": "sha512-btsxX9201yoWh45TdqYg6+OZ5O1xTYKTYSGvJndICDFtznE/9zXgow8yjMvvhOqKKuzuL7h+iiCMpfkG8+QuBA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-4.0.0.tgz", + "integrity": "sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==", "dependencies": { - "uint8-util": "^2.1.6" + "uint8-util": "^2.2.2" }, "engines": { "node": ">=12.20.0" @@ -4340,17 +4354,6 @@ "node": ">=12.20.0" } }, - "node_modules/bittorrent-dht/node_modules/bencode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/bencode/-/bencode-4.0.0.tgz", - "integrity": "sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==", - "dependencies": { - "uint8-util": "^2.2.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -4651,9 +4654,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, "funding": [ { @@ -4671,8 +4674,8 @@ ], "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -4774,19 +4777,18 @@ } }, "node_modules/c8": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz", - "integrity": "sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.0.0.tgz", + "integrity": "sha512-nFJhU2Cz6Frh2awk3IW7wwk3wx27/U2v8ojQCHGc1GWTCHS6aMu4lal327/ZnnYj7oSThGF1X3qUP1yzAJBcOQ==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", - "foreground-child": "^2.0.0", + "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", - "rimraf": "^3.0.2", "test-exclude": "^6.0.0", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", @@ -4796,64 +4798,7 @@ "c8": "bin/c8.js" }, "engines": { - "node": ">=12" - } - }, - "node_modules/c8/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/c8/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/c8/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/c8/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=14.14.0" } }, "node_modules/cache-content-type": { @@ -4904,9 +4849,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001576", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", - "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", + "version": "1.0.30001581", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", + "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", "dev": true, "funding": [ { @@ -5252,9 +5197,9 @@ } }, "node_modules/classic-level": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz", - "integrity": "sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz", + "integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==", "hasInstallScript": true, "dependencies": { "abstract-level": "^1.0.2", @@ -5620,19 +5565,19 @@ "dev": true }, "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", "dependencies": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.12" } }, "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" }, "engines": { "node": "4.x || >=6.0.0" @@ -6067,9 +6012,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.628", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.628.tgz", - "integrity": "sha512-2k7t5PHvLsufpP6Zwk0nof62yLOsCf032wZx7/q0mv8gwlXjhcxI3lz6f0jBr0GrnWKcm3burXzI3t5IrcdUxw==", + "version": "1.4.651", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.651.tgz", + "integrity": "sha512-jjks7Xx+4I7dslwsbaFocSwqBbGHQmuXBJUK9QBZTIrzPq3pzn6Uf2szFSP728FtLYE3ldiccmlkOM/zhGKCpA==", "dev": true, "peer": true }, @@ -6911,9 +6856,9 @@ } }, "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", + "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -7137,16 +7082,19 @@ } }, "node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/formdata-polyfill": { @@ -7391,34 +7339,6 @@ "dev": true, "peer": true }, - "node_modules/glob/node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -7929,17 +7849,13 @@ } }, "node_modules/ipfs-unixfs": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-11.1.2.tgz", - "integrity": "sha512-HVjrACOhU8RgMskcrfydk+FDAE9pFKr8tneKLaVYQ2f81HUKXoiSdgsAJY/jt7Ieyj4tE12TZGduIeWtNpScOw==", + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-11.1.3.tgz", + "integrity": "sha512-sy6Koojwm/EcM8yvDlycRYA89C8wIcLcGTMMpqnCPUtqTCdl+JxsuPNCBgAu7tmO8Nipm7Tv7f0g/erxTGKKRA==", "dependencies": { "err-code": "^3.0.1", "protons-runtime": "^5.0.0", "uint8arraylist": "^2.4.3" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" } }, "node_modules/ipfs-unixfs-exporter": { @@ -9249,9 +9165,9 @@ } }, "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", "dev": true, "engines": { "node": ">= 0.6.0" @@ -9294,9 +9210,9 @@ } }, "node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -9911,9 +9827,9 @@ } }, "node_modules/mysql2": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.7.0.tgz", - "integrity": "sha512-c45jA3Jc1X8yJKzrWu1GpplBKGwv/wIV6ITZTlCSY7npF2YfJR+6nMP5e+NTQhUeJPSyOQAbGDCGEHbAl8HN9w==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz", + "integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==", "dev": true, "dependencies": { "denque": "^2.1.0", @@ -10042,9 +9958,9 @@ } }, "node_modules/nise": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.7.tgz", - "integrity": "sha512-wWtNUhkT7k58uvWTB/Gy26eA/EJKtPZFVAhEilN5UYVmmGRYOURbejRUyKm0Uu9XVEW7K5nBOZfR8VMB4QR2RQ==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0", @@ -10824,6 +10740,17 @@ "pkarr": "bin.js" } }, + "node_modules/pkarr/node_modules/bencode": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-3.1.1.tgz", + "integrity": "sha512-btsxX9201yoWh45TdqYg6+OZ5O1xTYKTYSGvJndICDFtznE/9zXgow8yjMvvhOqKKuzuL7h+iiCMpfkG8+QuBA==", + "dependencies": { + "uint8-util": "^2.1.6" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/pkarr/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -11073,10 +11000,11 @@ "dev": true }, "node_modules/protons-runtime": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-5.2.2.tgz", - "integrity": "sha512-o97rNPN9pE3cxOxjs/waZNRKlbY/DR11oc20rUvarWZgFzQLLLzJU0RFh5JPi6GJCN67VGVn9/FDIEtFblfB3A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/protons-runtime/-/protons-runtime-5.3.0.tgz", + "integrity": "sha512-RySXxx+jvz4mi/rr2VsnvgWvC6dFP2pVyWpVRFYb/jxmwih862ZjXJLUkjd+nOtXBx0Lwk2xeuY2f2fmd3FT9w==", "dependencies": { + "uint8-varint": "^2.0.2", "uint8arraylist": "^2.4.3", "uint8arrays": "^5.0.1" } @@ -11202,57 +11130,6 @@ } } }, - "node_modules/puppeteer-core/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/puppeteer-core/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/puppeteer-core/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/puppeteer-core/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/puppeteer-core/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/puppeteer-core/node_modules/ws": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", @@ -11677,6 +11554,12 @@ "node": ">=8" } }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -11767,9 +11650,9 @@ } }, "node_modules/rollup": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.4.tgz", - "integrity": "sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -11782,19 +11665,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.4", - "@rollup/rollup-android-arm64": "4.9.4", - "@rollup/rollup-darwin-arm64": "4.9.4", - "@rollup/rollup-darwin-x64": "4.9.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.4", - "@rollup/rollup-linux-arm64-gnu": "4.9.4", - "@rollup/rollup-linux-arm64-musl": "4.9.4", - "@rollup/rollup-linux-riscv64-gnu": "4.9.4", - "@rollup/rollup-linux-x64-gnu": "4.9.4", - "@rollup/rollup-linux-x64-musl": "4.9.4", - "@rollup/rollup-win32-arm64-msvc": "4.9.4", - "@rollup/rollup-win32-ia32-msvc": "4.9.4", - "@rollup/rollup-win32-x64-msvc": "4.9.4", + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", "fsevents": "~2.3.2" } }, @@ -11863,12 +11746,12 @@ ] }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -12061,14 +11944,15 @@ } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "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==", "dependencies": { "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "function-bind": "^1.1.2", + "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" @@ -12183,10 +12067,16 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/simple-concat": { "version": "1.0.1", @@ -12398,9 +12288,9 @@ } }, "node_modules/sodium-native": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.5.tgz", - "integrity": "sha512-YGimGhy7Ho6pTAAvuNdn3Tv9C2MD7HP89X1omReHat0Fd1mMnapGqwzb5YoHTAbIEh8tQmKP6+uLlwYCkf+EOA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.6.tgz", + "integrity": "sha512-uYsyycwcz9kYDwpXxJmL2YZosynsxcP6RPySbARVJdC9uNDa2CMjzJ7/WsMMvThKgvAYsBWdZc7L/WSVj9lTcA==", "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.6.0" @@ -12441,12 +12331,11 @@ } }, "node_modules/source-map-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", - "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.2.tgz", + "integrity": "sha512-oYwAqCuL0OZhBoSgmdrLa7mv9MjommVMiQIWgcztf+eS4+8BfcUee6nenFnDhKOhzAVnk5gpZdfnz1iiBv+5sg==", "dev": true, "dependencies": { - "abab": "^2.0.6", "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, @@ -12510,9 +12399,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -13075,9 +12964,9 @@ } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "peer": true, "dependencies": { @@ -13441,6 +13330,28 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uint8-varint": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/uint8-varint/-/uint8-varint-2.0.4.tgz", + "integrity": "sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==", + "dependencies": { + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/uint8-varint/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/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==", + "dependencies": { + "multiformats": "^13.0.0" + } + }, "node_modules/uint8arraylist": { "version": "2.4.8", "resolved": "https://registry.npmjs.org/uint8arraylist/-/uint8arraylist-2.4.8.tgz", @@ -13619,6 +13530,11 @@ "qs": "^6.11.2" } }, + "node_modules/utf8-codec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utf8-codec/-/utf8-codec-1.0.0.tgz", + "integrity": "sha512-S/QSLezp3qvG4ld5PUfXiH7mCFxLKjSVZRFkB3DOjgwHuJPFDkInAXc/anf7BAbHt/D38ozDzL+QMZ6/7gsI6w==" + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -13743,20 +13659,20 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.0.tgz", + "integrity": "sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w==", "dev": true, "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -13770,7 +13686,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -14207,11 +14123,11 @@ }, "packages/agent": { "name": "@web5/agent", - "version": "0.2.5", + "version": "0.2.6", "license": "Apache-2.0", "dependencies": { "@tbd54566975/dwn-sdk-js": "0.2.10", - "@web5/common": "0.2.2", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4", "level": "8.0.0", @@ -14223,6 +14139,7 @@ "@playwright/test": "1.40.1", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", + "@types/dns-packet": "5.6.4", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@types/readable-stream": "4.0.6", @@ -14231,7 +14148,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -14373,6 +14290,39 @@ "node": ">=18.0.0" } }, + "packages/agent/node_modules/@web5/dids": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-0.2.4.tgz", + "integrity": "sha512-e+m+xgpiM8ydTJgWcPdwmjILLMZYdl2kwahlO22mK0azSKVrg1klpGrUODzqkrWrQ5O0tnOyqEy39FcD5Sy11w==", + "dependencies": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", + "@decentralized-identity/ion-sdk": "1.0.1", + "@web5/common": "0.2.2", + "@web5/crypto": "0.2.2", + "did-resolver": "4.1.0", + "dns-packet": "5.6.1", + "level": "8.0.0", + "ms": "2.1.3", + "pkarr": "1.1.1", + "z32": "1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/agent/node_modules/@web5/dids/node_modules/@web5/common": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.2.tgz", + "integrity": "sha512-dRn6SmALExeTLMTK/W5ozGarfaddK+Lraf5OjuIGLAaLfcX1RWx3oDMoY5Hr9LjfxHJC8mGXB8DnKflbeYJRgA==", + "dependencies": { + "level": "8.0.0", + "multiformats": "11.0.2", + "readable-stream": "4.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -14571,11 +14521,11 @@ "license": "Apache-2.0", "dependencies": { "@tbd54566975/dwn-sdk-js": "0.2.10", - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4", - "@web5/user-agent": "0.2.5", + "@web5/user-agent": "0.2.6", "level": "8.0.0", "ms": "2.1.3", "readable-stream": "4.4.2", @@ -14594,7 +14544,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -14606,7 +14556,7 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" }, "engines": { @@ -14737,6 +14687,39 @@ "node": ">=18.0.0" } }, + "packages/api/node_modules/@web5/dids": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-0.2.4.tgz", + "integrity": "sha512-e+m+xgpiM8ydTJgWcPdwmjILLMZYdl2kwahlO22mK0azSKVrg1klpGrUODzqkrWrQ5O0tnOyqEy39FcD5Sy11w==", + "dependencies": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", + "@decentralized-identity/ion-sdk": "1.0.1", + "@web5/common": "0.2.2", + "@web5/crypto": "0.2.2", + "did-resolver": "4.1.0", + "dns-packet": "5.6.1", + "level": "8.0.0", + "ms": "2.1.3", + "pkarr": "1.1.1", + "z32": "1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/api/node_modules/@web5/dids/node_modules/@web5/common": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.2.tgz", + "integrity": "sha512-dRn6SmALExeTLMTK/W5ozGarfaddK+Lraf5OjuIGLAaLfcX1RWx3oDMoY5Hr9LjfxHJC8mGXB8DnKflbeYJRgA==", + "dependencies": { + "level": "8.0.0", + "multiformats": "11.0.2", + "readable-stream": "4.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/api/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -14931,7 +14914,7 @@ }, "packages/common": { "name": "@web5/common", - "version": "0.2.2", + "version": "0.2.3", "license": "Apache-2.0", "dependencies": { "level": "8.0.0", @@ -14949,7 +14932,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -15255,7 +15238,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "esbuild": "0.19.8", "eslint": "8.47.0", @@ -15369,6 +15352,19 @@ } } }, + "packages/credentials/node_modules/@web5/common": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.2.tgz", + "integrity": "sha512-dRn6SmALExeTLMTK/W5ozGarfaddK+Lraf5OjuIGLAaLfcX1RWx3oDMoY5Hr9LjfxHJC8mGXB8DnKflbeYJRgA==", + "dependencies": { + "level": "8.0.0", + "multiformats": "11.0.2", + "readable-stream": "4.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/credentials/node_modules/@web5/crypto": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.4.tgz", @@ -15383,6 +15379,82 @@ "node": ">=18.0.0" } }, + "packages/credentials/node_modules/@web5/dids": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-0.2.4.tgz", + "integrity": "sha512-e+m+xgpiM8ydTJgWcPdwmjILLMZYdl2kwahlO22mK0azSKVrg1klpGrUODzqkrWrQ5O0tnOyqEy39FcD5Sy11w==", + "dependencies": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", + "@decentralized-identity/ion-sdk": "1.0.1", + "@web5/common": "0.2.2", + "@web5/crypto": "0.2.2", + "did-resolver": "4.1.0", + "dns-packet": "5.6.1", + "level": "8.0.0", + "ms": "2.1.3", + "pkarr": "1.1.1", + "z32": "1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/credentials/node_modules/@web5/dids/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/credentials/node_modules/@web5/dids/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/credentials/node_modules/@web5/dids/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/credentials/node_modules/@web5/dids/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/credentials/node_modules/@web5/dids/node_modules/@web5/crypto/node_modules/@web5/common": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.1.tgz", + "integrity": "sha512-Tt5P17HgQCx+Epw0IHnhRKqp5UU3E4xtsE8PkdghOBnvntBB0op5P6efvR1WqmJft5+VunDHt3yZAZstuqQkNg==", + "dependencies": { + "level": "8.0.0", + "multiformats": "11.0.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/credentials/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -15577,13 +15649,13 @@ }, "packages/crypto": { "name": "@web5/crypto", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "0.4.1", "@noble/curves": "1.3.0", "@noble/hashes": "1.3.3", - "@web5/common": "0.2.2" + "@web5/common": "0.2.3" }, "devDependencies": { "@playwright/test": "1.40.1", @@ -15596,7 +15668,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -15607,7 +15679,7 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" }, "engines": { @@ -15616,11 +15688,11 @@ }, "packages/crypto-aws-kms": { "name": "@web5/crypto-aws-kms", - "version": "0.1.0", + "version": "0.2.0", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-kms": "3.478.0", - "@web5/crypto": "0.3.0" + "@web5/crypto": "0.4.0" }, "devDependencies": { "@playwright/test": "1.40.1", @@ -15633,8 +15705,8 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "@web5/common": "0.2.2", - "c8": "8.0.1", + "@web5/common": "0.2.3", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "eslint": "8.47.0", @@ -15644,7 +15716,7 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" }, "engines": { @@ -16173,25 +16245,22 @@ }, "packages/dids": { "name": "@web5/dids", - "version": "0.2.4", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { - "@decentralized-identity/ion-pow-sdk": "1.0.17", "@decentralized-identity/ion-sdk": "1.0.1", - "@web5/common": "0.2.2", - "@web5/crypto": "0.2.2", - "did-resolver": "4.1.0", - "dns-packet": "5.6.1", + "@dnsquery/dns-packet": "6.1.1", + "@web5/common": "0.2.3", + "@web5/crypto": "0.4.0", + "bencode": "4.0.0", "level": "8.0.0", - "ms": "2.1.3", - "pkarr": "1.1.1", - "z32": "1.0.1" + "ms": "2.1.3" }, "devDependencies": { "@playwright/test": "1.40.1", + "@types/bencode": "2.0.4", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", - "@types/dns-packet": "^5.6.1", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@types/sinon": "17.0.2", @@ -16199,7 +16268,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -16210,43 +16279,13 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" }, "engines": { "node": ">=18.0.0" } }, - "packages/dids/node_modules/@noble/ciphers": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", - "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "packages/dids/node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", - "dependencies": { - "@noble/hashes": "1.3.1" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "packages/dids/node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "packages/dids/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -16315,32 +16354,6 @@ } } }, - "packages/dids/node_modules/@web5/crypto": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", - "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", - "dependencies": { - "@noble/ciphers": "0.1.4", - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", - "@web5/common": "0.2.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "packages/dids/node_modules/@web5/crypto/node_modules/@web5/common": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.1.tgz", - "integrity": "sha512-Tt5P17HgQCx+Epw0IHnhRKqp5UU3E4xtsE8PkdghOBnvntBB0op5P6efvR1WqmJft5+VunDHt3yZAZstuqQkNg==", - "dependencies": { - "level": "8.0.0", - "multiformats": "11.0.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "packages/dids/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -16535,11 +16548,11 @@ }, "packages/identity-agent": { "name": "@web5/identity-agent", - "version": "0.2.5", + "version": "0.2.6", "license": "Apache-2.0", "dependencies": { - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4" }, @@ -16554,7 +16567,7 @@ "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", "@web5/api": "0.8.4", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -16695,6 +16708,39 @@ "node": ">=18.0.0" } }, + "packages/identity-agent/node_modules/@web5/dids": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-0.2.4.tgz", + "integrity": "sha512-e+m+xgpiM8ydTJgWcPdwmjILLMZYdl2kwahlO22mK0azSKVrg1klpGrUODzqkrWrQ5O0tnOyqEy39FcD5Sy11w==", + "dependencies": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", + "@decentralized-identity/ion-sdk": "1.0.1", + "@web5/common": "0.2.2", + "@web5/crypto": "0.2.2", + "did-resolver": "4.1.0", + "dns-packet": "5.6.1", + "level": "8.0.0", + "ms": "2.1.3", + "pkarr": "1.1.1", + "z32": "1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/identity-agent/node_modules/@web5/dids/node_modules/@web5/common": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.2.tgz", + "integrity": "sha512-dRn6SmALExeTLMTK/W5ozGarfaddK+Lraf5OjuIGLAaLfcX1RWx3oDMoY5Hr9LjfxHJC8mGXB8DnKflbeYJRgA==", + "dependencies": { + "level": "8.0.0", + "multiformats": "11.0.2", + "readable-stream": "4.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/identity-agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -16889,11 +16935,11 @@ }, "packages/proxy-agent": { "name": "@web5/proxy-agent", - "version": "0.2.5", + "version": "0.2.6", "license": "Apache-2.0", "dependencies": { - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4" }, @@ -16901,13 +16947,14 @@ "@playwright/test": "1.40.1", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", + "@types/dns-packet": "5.6.4", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@typescript-eslint/eslint-plugin": "6.4.0", "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -17048,6 +17095,39 @@ "node": ">=18.0.0" } }, + "packages/proxy-agent/node_modules/@web5/dids": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-0.2.4.tgz", + "integrity": "sha512-e+m+xgpiM8ydTJgWcPdwmjILLMZYdl2kwahlO22mK0azSKVrg1klpGrUODzqkrWrQ5O0tnOyqEy39FcD5Sy11w==", + "dependencies": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", + "@decentralized-identity/ion-sdk": "1.0.1", + "@web5/common": "0.2.2", + "@web5/crypto": "0.2.2", + "did-resolver": "4.1.0", + "dns-packet": "5.6.1", + "level": "8.0.0", + "ms": "2.1.3", + "pkarr": "1.1.1", + "z32": "1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/proxy-agent/node_modules/@web5/dids/node_modules/@web5/common": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.2.tgz", + "integrity": "sha512-dRn6SmALExeTLMTK/W5ozGarfaddK+Lraf5OjuIGLAaLfcX1RWx3oDMoY5Hr9LjfxHJC8mGXB8DnKflbeYJRgA==", + "dependencies": { + "level": "8.0.0", + "multiformats": "11.0.2", + "readable-stream": "4.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/proxy-agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -17242,11 +17322,11 @@ }, "packages/user-agent": { "name": "@web5/user-agent", - "version": "0.2.5", + "version": "0.2.6", "license": "Apache-2.0", "dependencies": { - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4" }, @@ -17254,13 +17334,14 @@ "@playwright/test": "1.40.1", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", + "@types/dns-packet": "5.6.4", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@typescript-eslint/eslint-plugin": "6.4.0", "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -17401,6 +17482,39 @@ "node": ">=18.0.0" } }, + "packages/user-agent/node_modules/@web5/dids": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-0.2.4.tgz", + "integrity": "sha512-e+m+xgpiM8ydTJgWcPdwmjILLMZYdl2kwahlO22mK0azSKVrg1klpGrUODzqkrWrQ5O0tnOyqEy39FcD5Sy11w==", + "dependencies": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", + "@decentralized-identity/ion-sdk": "1.0.1", + "@web5/common": "0.2.2", + "@web5/crypto": "0.2.2", + "did-resolver": "4.1.0", + "dns-packet": "5.6.1", + "level": "8.0.0", + "ms": "2.1.3", + "pkarr": "1.1.1", + "z32": "1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/user-agent/node_modules/@web5/dids/node_modules/@web5/common": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/common/-/common-0.2.2.tgz", + "integrity": "sha512-dRn6SmALExeTLMTK/W5ozGarfaddK+Lraf5OjuIGLAaLfcX1RWx3oDMoY5Hr9LjfxHJC8mGXB8DnKflbeYJRgA==", + "dependencies": { + "level": "8.0.0", + "multiformats": "11.0.2", + "readable-stream": "4.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/user-agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", diff --git a/packages/agent/.c8rc.json b/packages/agent/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/agent/.c8rc.json +++ b/packages/agent/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/agent/package.json b/packages/agent/package.json index 33d59e72d..699348b41 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/agent", - "version": "0.2.5", + "version": "0.2.6", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -69,7 +69,7 @@ }, "dependencies": { "@tbd54566975/dwn-sdk-js": "0.2.10", - "@web5/common": "0.2.2", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4", "level": "8.0.0", @@ -81,6 +81,7 @@ "@playwright/test": "1.40.1", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", + "@types/dns-packet": "5.6.4", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@types/readable-stream": "4.0.6", @@ -89,7 +90,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", diff --git a/packages/api/.c8rc.json b/packages/api/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/api/.c8rc.json +++ b/packages/api/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/api/package.json b/packages/api/package.json index 776b1bfbf..6d6051f46 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -77,11 +77,11 @@ }, "dependencies": { "@tbd54566975/dwn-sdk-js": "0.2.10", - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4", - "@web5/user-agent": "0.2.5", + "@web5/user-agent": "0.2.6", "level": "8.0.0", "ms": "2.1.3", "readable-stream": "4.4.2", @@ -100,7 +100,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -112,7 +112,7 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" } } diff --git a/packages/common/.c8rc.json b/packages/common/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/common/.c8rc.json +++ b/packages/common/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/common/package.json b/packages/common/package.json index 7449a8589..8779f29da 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@web5/common", - "version": "0.2.2", + "version": "0.2.3", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -83,7 +83,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", diff --git a/packages/common/src/convert.ts b/packages/common/src/convert.ts index 3ae75ef7b..036a8b7b3 100644 --- a/packages/common/src/convert.ts +++ b/packages/common/src/convert.ts @@ -1,5 +1,6 @@ import type { Multibase } from 'multiformats'; +import { base32z } from 'multiformats/bases/base32'; import { base58btc } from 'multiformats/bases/base58'; import { base64url } from 'multiformats/bases/base64'; @@ -28,6 +29,10 @@ export class Convert { return new Convert(data, 'AsyncIterable'); } + static base32Z(data: string): Convert { + return new Convert(data, 'Base32Z'); + } + static base58Btc(data: string): Convert { return new Convert(data, 'Base58Btc'); } @@ -131,6 +136,18 @@ export class Convert { } } + toBase32Z(): string { + switch (this.format) { + + case 'Uint8Array': { + return base32z.baseEncode(this.data); + } + + default: + throw new TypeError(`Conversion from ${this.format} to Base64Z is not supported.`); + } + } + toBase58Btc(): string { switch (this.format) { @@ -357,6 +374,10 @@ export class Convert { return new Uint8Array(this.data); } + case 'Base32Z': { + return base32z.baseDecode(this.data); + } + case 'Base58Btc': { return base58btc.baseDecode(this.data); } diff --git a/packages/common/src/type-utils.ts b/packages/common/src/type-utils.ts index dbf14d310..251208896 100644 --- a/packages/common/src/type-utils.ts +++ b/packages/common/src/type-utils.ts @@ -1,3 +1,67 @@ +/** + * Represents an array of a fixed length, preventing modifications to its size. + * + * The `FixedLengthArray` utility type transforms a standard array into a variant where + * methods that could alter the length are omitted. It leverages TypeScript's advanced types, + * such as conditional types and mapped types, to ensure that the array cannot be resized + * through methods like `push`, `pop`, `splice`, `shift`, and `unshift`. The utility type + * maintains all other characteristics of a standard array, including indexing, iteration, + * and type checking for its elements. + * + * Note: The type does not prevent direct assignment to indices, even if it would exceed + * the original length. However, such actions would lead to TypeScript type errors. + * + * @example + * ```ts + * // Declare a variable with a type of fixed-length array of three strings. + * let myFixedLengthArray: FixedLengthArray< [string, string, string]>; + * + * // Array declaration tests + * myFixedLengthArray = [ 'a', 'b', 'c' ]; // OK + * myFixedLengthArray = [ 'a', 'b', 123 ]; // TYPE ERROR + * myFixedLengthArray = [ 'a' ]; // LENGTH ERROR + * myFixedLengthArray = [ 'a', 'b' ]; // LENGTH ERROR + * + * // Index assignment tests + * myFixedLengthArray[1] = 'foo'; // OK + * myFixedLengthArray[1000] = 'foo'; // INVALID INDEX ERROR + * + * // Methods that mutate array length + * myFixedLengthArray.push('foo'); // MISSING METHOD ERROR + * myFixedLengthArray.pop(); // MISSING METHOD ERROR + * + * // Direct length manipulation + * myFixedLengthArray.length = 123; // READ-ONLY ERROR + * + * // Destructuring + * let [ a ] = myFixedLengthArray; // OK + * let [ a, b ] = myFixedLengthArray; // OK + * let [ a, b, c ] = myFixedLengthArray; // OK + * let [ a, b, c, d ] = myFixedLengthArray; // INVALID INDEX ERROR + * ``` + * + * @template T extends any[] - The array type to be transformed. + */ +export type FixedLengthArray = + Pick> + & { + /** + * Custom iterator for the `FixedLengthArray` type. + * + * This iterator allows the `FixedLengthArray` to be used in standard iteration + * contexts, such as `for...of` loops and spread syntax. It ensures that even though + * the array is of a fixed length with disabled mutation methods, it still retains + * iterable behavior similar to a regular array. + * + * @returns An IterableIterator for the array items. + */ + [Symbol.iterator]: () => IterableIterator> + }; + +/** Helper types for {@link FixedLengthArray} */ +type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number; +type ArrayItems> = T extends Array ? TItems : never; + /** * isArrayBufferSlice * @@ -72,6 +136,34 @@ export function isDefined(arg: T): arg is Exclude { return arg !== null && typeof arg !== 'undefined'; } +/** + * Utility type that transforms a type `T` to have only certain keys `K` as required, while the + * rest remain optional, except for keys specified in `O`, which are omitted entirely. + * + * This type is useful when you need a variation of a type where only specific properties are + * required, and others are either optional or not included at all. It allows for more flexible type + * definitions based on existing types without the need to redefine them. + * + * @template T - The original type to be transformed. + * @template K - The keys of `T` that should be required. + * @template O - The keys of `T` that should be omitted from the resulting type (optional). + * + * @example + * ```ts + * // Given an interface + * interface Example { + * requiredProp: string; + * optionalProp?: number; + * anotherOptionalProp?: boolean; + * } + * + * // Making 'optionalProp' required and omitting 'anotherOptionalProp' + * type ModifiedExample = RequireOnly; + * // Result: { requiredProp?: string; optionalProp: number; } + * ``` + */ +export type RequireOnly = Required> & Omit, O>; + /** * universalTypeOf * @@ -114,4 +206,27 @@ export function universalTypeOf(value: unknown) { const [_, type] = match as RegExpMatchArray; return type; -} \ No newline at end of file +} + +/** + * Utility type to extract the type resolved by a Promise. + * + * This type unwraps the type `T` from `Promise` if `T` is a Promise, otherwise returns `T` as + * is. It's useful in situations where you need to handle the type returned by a promise-based + * function in a synchronous context, such as defining types for test vectors or handling return + * types in non-async code blocks. + * + * @template T - The type to unwrap from the Promise. + * + * @example + * ```ts + * // For a Promise type, it extracts the resolved type. + * type AsyncNumber = Promise; + * type UnwrappedNumber = UnwrapPromise; // number + * + * // For a non-Promise type, it returns the type as is. + * type StringValue = string; + * type UnwrappedString = UnwrapPromise; // string + * ``` + */ +export type UnwrapPromise = T extends Promise ? U : T; \ No newline at end of file diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index cd24722c8..59e0800e6 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -40,9 +40,4 @@ export interface KeyValueStore { * @returns A promise that resolves when the value has been set. */ set(key: K, value: V): Promise; -} - -/** - * Represents an object type where a subset of keys are required and everything else is optional. - */ -export type RequireOnly = Required> & Omit, O>; \ No newline at end of file +} \ No newline at end of file diff --git a/packages/common/tests/convert.spec.ts b/packages/common/tests/convert.spec.ts index a4ae8740f..5df3eb665 100644 --- a/packages/common/tests/convert.spec.ts +++ b/packages/common/tests/convert.spec.ts @@ -236,6 +236,23 @@ describe('Convert', () =>{ }); }); + describe('from: Base64Z', () => { + it('to: Uint8Array', () => { + // Test Vector 1. + let input = '5umembtazeybqcd7grysfp711g1z56wzo8irzhae494hh58zguhy'; + let output = new Uint8Array([ + 220, 214, 133, 134, 56, 186, 0, 23, + 48, 125, 49, 1, 98, 183, 178, 145, + 165, 125, 250, 151, 129, 234, 75, 243, + 8, 215, 245, 206, 108, 247, 52, 248 + ]); + + let result = Convert.base32Z(input).toUint8Array(); + + expect(result).to.deep.equal(output); + }); + }); + describe('from: BufferSource', () => { it('to: ArrayBuffer', () => { // Test Vector 1 - BufferSource is Uint8Array. @@ -500,6 +517,21 @@ describe('Convert', () =>{ expect(result).to.deep.equal(output); }); + it('to: Base32Z', () => { + // Test Vector 1. + let input = new Uint8Array([ + 220, 214, 133, 134, 56, 186, 0, 23, + 48, 125, 49, 1, 98, 183, 178, 145, + 165, 125, 250, 151, 129, 234, 75, 243, + 8, 215, 245, 206, 108, 247, 52, 248 + ]); + let output = '5umembtazeybqcd7grysfp711g1z56wzo8irzhae494hh58zguhy'; + + let result = Convert.uint8Array(input).toBase32Z(); + + expect(result).to.deep.equal(output); + }); + it('to: Base58Btc', () => { // Test Vector 1. let input = new Uint8Array([51, 52, 53]); @@ -567,6 +599,10 @@ describe('Convert', () =>{ } }); + it('toBase32Z() throw an error', () => { + expect(() => unsupported.toBase32Z()).to.throw(TypeError, 'not supported'); + }); + it('toBase58Btc() throw an error', () => { expect(() => unsupported.toBase58Btc()).to.throw(TypeError, 'not supported'); }); diff --git a/packages/credentials/.c8rc.json b/packages/credentials/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/credentials/.c8rc.json +++ b/packages/credentials/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/credentials/package.json b/packages/credentials/package.json index a50c9498f..a62fe7585 100644 --- a/packages/credentials/package.json +++ b/packages/credentials/package.json @@ -89,7 +89,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "esbuild": "0.19.8", "eslint": "8.47.0", diff --git a/packages/crypto-aws-kms/.c8rc.json b/packages/crypto-aws-kms/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/crypto-aws-kms/.c8rc.json +++ b/packages/crypto-aws-kms/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/crypto-aws-kms/README.md b/packages/crypto-aws-kms/README.md index b6c45613e..603678439 100644 --- a/packages/crypto-aws-kms/README.md +++ b/packages/crypto-aws-kms/README.md @@ -67,13 +67,13 @@ npm install @web5/crypto-aws-kms Example ESM import: ```js -import { AwsKmsCrypto } from "@web5/crypto-aws-kms"; +import { AwsKeyManager } from "@web5/crypto-aws-kms"; ``` Example CJS require: ```js -const { AwsKmsCrypto } = require("@web5/crypto-aws-kms"); +const { AwsKeyManager } = require("@web5/crypto-aws-kms"); ``` ### Configure the AWS SDK @@ -157,9 +157,9 @@ private keys. Start by instantiating an AWS KMS implementation of the `CryptoApi` interface: ```ts -import { AwsKmsCrypto } from "@web5/crypto-aws-kms"; +import { AwsKeyManager } from "@web5/crypto-aws-kms"; -const crypto = new AwsKmsCrypto(); +const kms = new AwsKeyManager(); ``` If not provided, a default instance of @@ -172,7 +172,7 @@ client signs and encrypts all communication with the AWS KMS API. See Generate a random private key: ```ts -const privateKeyUri = await cypto.generateKey({ algorithm: "ES256K" }); +const privateKeyUri = await kms.generateKey({ algorithm: "ES256K" }); console.log(privateKeyUri); // Output: urn:jwk:U01_M3_A9vMLOWixG-rlfC-_f3LLdurttn7c7d3_upU ``` @@ -181,7 +181,7 @@ Create an ECDSA signature over arbitrary data using the private key: ```ts const data = new TextEncoder().encode("Message"); -const signature = await crypto.sign({ +const signature = await kms.sign({ keyUri: privateKeyUri, data, }); @@ -200,7 +200,7 @@ console.log(signature); Get the public key in JWK format: ```ts -const publicKey = await crypto.getPublicKey({ keyUri: privateKeyUri }); +const publicKey = await kms.getPublicKey({ keyUri: privateKeyUri }); console.log(publicKey); // Output: // { @@ -216,7 +216,7 @@ console.log(publicKey); Verify the signature using the public key: ```ts -const isValid = await crypto.verify({ +const isValid = await kms.verify({ key: publicKey, signature, data, @@ -229,7 +229,7 @@ Compute the hash digest of arbitrary data: ```ts const data = new TextEncoder().encode("Message"); -const hash = await crypto.digest({ algorithm: "SHA-256", data }); +const hash = await kms.digest({ algorithm: "SHA-256", data }); console.log(hash); // Output: // Uint8Array(32) [ @@ -244,20 +244,20 @@ console.log(hash); ### Configure the AWS SDK `KMSClient` -By default, `AwsKmsCrypto` creates an instance of +By default, `AwsKeyManager` creates an instance of [`KMSClient`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/kms/) which uses the credential and configuration information supplied in shared AWS `config` and `credentials` files or environment variables. To set the region, credentials, and other options used by `KMSClient` at -runtime, a custom instance can be passed to the `AwsKmsCrypto` constructor. +runtime, a custom instance can be passed to the `AwsKeyManager` constructor. For example, to set the AWS region to which the client will send requests: ```typescript import { KMSClient } from "@aws-sdk/client-kms"; -import { AwsKmsCrypto } from "@web5/crypto-aws-kms"; +import { AwsKeyManager } from "@web5/crypto-aws-kms"; const kmsClient = new KMSClient({ region: "us-east-1" }); -const crypto = new AwsKmsCrypto({ kmsClient }); +const kms = new AwsKeyManager({ kmsClient }); ``` Additional configuration fields of the `KMSClient` class constructor are described in the diff --git a/packages/crypto-aws-kms/package.json b/packages/crypto-aws-kms/package.json index 0459396db..0de42c962 100644 --- a/packages/crypto-aws-kms/package.json +++ b/packages/crypto-aws-kms/package.json @@ -1,6 +1,6 @@ { "name": "@web5/crypto-aws-kms", - "version": "0.1.0", + "version": "0.2.0", "description": "Web5 cryptographic library using AWS KMS", "type": "module", "main": "./dist/cjs/index.js", @@ -70,7 +70,7 @@ }, "dependencies": { "@aws-sdk/client-kms": "3.478.0", - "@web5/crypto": "0.3.0" + "@web5/crypto": "0.4.0" }, "devDependencies": { "@playwright/test": "1.40.1", @@ -83,8 +83,8 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "@web5/common": "0.2.2", - "c8": "8.0.1", + "@web5/common": "0.2.3", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "eslint": "8.47.0", @@ -94,7 +94,7 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" } } diff --git a/packages/crypto-aws-kms/src/ecdsa.ts b/packages/crypto-aws-kms/src/ecdsa.ts index 29ce68a24..055545911 100644 --- a/packages/crypto-aws-kms/src/ecdsa.ts +++ b/packages/crypto-aws-kms/src/ecdsa.ts @@ -52,13 +52,16 @@ export class EcdsaAlgorithm implements Signer { /** - * The `_crypto` private variable in the `EcdsaAlgorithm` class holds a reference to an instance - * of `CryptoApi`. This instance is used for performing various cryptographic operations, such as - * computing hash digests and retrieving public keys. By having this reference, `EcdsaAlgorithm` - * can leverage the comprehensive cryptographic functionalities provided by `CryptoApi`, enabling - * it to focus on ECDSA-specific logic while delegating other cryptographic tasks to `CryptoApi`. + * The `_keyManager` private variable in the `EcdsaAlgorithm` class holds a reference to an + * `AwsKeyManager` instance, which is an implementation of the `CryptoApi` interface. This + * instance is used for performing various cryptographic operations, such as computing hash + * digests and retrieving public keys. By having this reference, `EcdsaAlgorithm` focus on + * ECDSA-specific logic while delegating other cryptographic tasks to `AwsKeyManager`. + * + * @remarks + * The type is `CrytpoApi` instead of `AwsKeyManager` to avoid a circular dependency. */ - private _crypto: CryptoApi; + private _keyManager: CryptoApi; /** * A private instance of `KMSClient` from the AWS SDK. This client is used for all interactions @@ -71,14 +74,14 @@ export class EcdsaAlgorithm implements /** * * @param params - An object containing the parameters to use when instantiating the algorithm. - * @param params.crypto - An instance of `CryptoApi` from the `@web5/crypto` package. + * @param params.keyManager - An instance of `AwsKeyManager`. * @param params.kmsClient - An instance of `KMSClient` from the AWS SDK. */ - constructor({ crypto, kmsClient }: { - crypto: CryptoApi; + constructor({ keyManager, kmsClient }: { + keyManager: CryptoApi; kmsClient: KMSClient; }) { - this._crypto = crypto; + this._keyManager = keyManager; this._kmsClient = kmsClient; } @@ -88,7 +91,7 @@ export class EcdsaAlgorithm implements * * @example * ```ts - * const ecdsa = new EcdsaAlgorithm({ crypto, kmsClient }); + * const ecdsa = new EcdsaAlgorithm({ keyManager, kmsClient }); * const keyUri = await ecdsa.generateKey({ algorithm: 'ES256K' }); * console.log(keyUri); // Outputs the key URI * ``` @@ -128,10 +131,10 @@ export class EcdsaAlgorithm implements const awsKeyId = response.KeyMetadata.KeyId; // Retrieve the public key from AWS KMS. - const publicKey = await this._crypto.getPublicKey({ keyUri: awsKeyId }); + const publicKey = await this._keyManager.getPublicKey({ keyUri: awsKeyId }); // Compute the key URI. - const keyUri = await this._crypto.getKeyUri({ key: publicKey }); + const keyUri = await this._keyManager.getKeyUri({ key: publicKey }); // Set the key's alias in AWS KMS to the key URI. await createKeyAlias({ awsKeyId, alias: keyUri, kmsClient: this._kmsClient }); @@ -146,7 +149,7 @@ export class EcdsaAlgorithm implements * @remarks * This method uses the signature algorithm determined by the given `algorithm` to sign the * provided data. The `algorithm` is used to avoid another round trip to AWS KMS to determine the - * `KeySpec` since it was already retrieved in {@link AwsKmsCrypto.sign | `AwsKmsCrypto.sign()`}. + * `KeySpec` since it was already retrieved in {@link AwsKeyManager.sign | `AwsKeyManager.sign()`}. * * The signature can later be verified by parties with access to the corresponding * public key, ensuring that the data has not been tampered with and was indeed signed by the @@ -164,7 +167,7 @@ export class EcdsaAlgorithm implements * * @example * ```ts - * const ecdsa = new EcdsaAlgorithm({ crypto, kmsClient }); + * const ecdsa = new EcdsaAlgorithm({ keyManager, kmsClient }); * const data = new TextEncoder().encode('Message to sign'); * const signature = await ecdsa.sign({ * algorithm: 'ES256K', @@ -191,7 +194,7 @@ export class EcdsaAlgorithm implements case 'ES256K': { // Pre-hash the data to accommodate AWS KMS limitations for signature payloads.s - hashedData = await this._crypto.digest({ algorithm: 'SHA-256', data }); + hashedData = await this._keyManager.digest({ algorithm: 'SHA-256', data }); signingAlgorithm = SigningAlgorithmSpec.ECDSA_SHA_256; break; } @@ -238,7 +241,7 @@ export class EcdsaAlgorithm implements * * @example * ```ts - * const ecdsa = new EcdsaAlgorithm({ crypto, kmsClient }); + * const ecdsa = new EcdsaAlgorithm({ keyManager, kmsClient }); * const publicKey = { ... }; // Public key in JWK format corresponding to the private key that signed the data * const signature = new Uint8Array([...]); // Signature to verify * const isValid = await ecdsa.verify({ diff --git a/packages/crypto-aws-kms/src/index.ts b/packages/crypto-aws-kms/src/index.ts index e43ea7984..64e57da03 100644 --- a/packages/crypto-aws-kms/src/index.ts +++ b/packages/crypto-aws-kms/src/index.ts @@ -1,3 +1,3 @@ -export * from './api.js'; export * from './ecdsa.js'; +export * from './key-manager.js'; export * from './utils.js'; \ No newline at end of file diff --git a/packages/crypto-aws-kms/src/api.ts b/packages/crypto-aws-kms/src/key-manager.ts similarity index 87% rename from packages/crypto-aws-kms/src/api.ts rename to packages/crypto-aws-kms/src/key-manager.ts index b16555b83..95bf054d0 100644 --- a/packages/crypto-aws-kms/src/api.ts +++ b/packages/crypto-aws-kms/src/key-manager.ts @@ -26,7 +26,7 @@ import { convertSpkiToPublicKey, getKeySpec } from './utils.js'; * value is an object that provides the implementation class, the key specification (if applicable), * and an array of names associated with the algorithm. This structure allows for easy retrieval * and instantiation of algorithm implementations based on the algorithm name or key specification. - * It facilitates the support of multiple algorithms within the `AwsKmsCrypto` class. + * It facilitates the support of multiple algorithms within the `AwsKeyManager` class. */ const supportedAlgorithms = { 'ES256K': { @@ -54,17 +54,17 @@ type SupportedAlgorithm = keyof typeof supportedAlgorithms; type AlgorithmConstructor = typeof supportedAlgorithms[SupportedAlgorithm]['implementation']; /** - * The `AwsKmsCryptoParams` interface specifies the parameters for initializing an instance of - * `AwsKmsCrypto`, which is an implementation of the `CryptoApi` interface tailored for AWS KMS. + * The `AwsKeyManagerParams` interface specifies the parameters for initializing an instance of + * `AwsKeyManager`, which is an implementation of the `CryptoApi` interface tailored for AWS KMS. * * This interface allows the optional inclusion of a `KMSClient` instance, which is used for * interacting with AWS KMS. If not provided, a default `KMSClient` instance will be created and * used. */ -export type AwsKmsCryptoParams = { +export type AwsKeyManagerParams = { /** * An optional property to specify a custom `KMSClient` instance. If not provided, the - * `AwsKmsCrypto` class will instantiate a default `KMSClient`. This client is used for all + * `AwsKeyManager` class will instantiate a default `KMSClient`. This client is used for all * interactions with AWS Key Management Service (KMS), such as generating keys and signing data. * * @param kmsClient - A `KMSClient` instance from the AWS SDK. @@ -73,10 +73,10 @@ export type AwsKmsCryptoParams = { }; /** - * The `AwsKmsDigestParams` interface defines the algorithm-specific parameters that should be - * passed into the {@link AwsKmsCrypto.digest | `AwsKmsCrypto.digest()`} method. + * The `AwsKeyManagerDigestParams` interface defines the algorithm-specific parameters that should + * be passed into the {@link AwsKeyManager.digest | `AwsKeyManager.digest()`} method. */ -export interface AwsKmsDigestParams extends KmsDigestParams { +export interface AwsKeyManagerDigestParams extends KmsDigestParams { /** * A string defining the name of hash function to use. The value must be one of the following: * - `"SHA-256"`: Generates a 256-bit digest. @@ -85,11 +85,11 @@ export interface AwsKmsDigestParams extends KmsDigestParams { } /** - * The `AwsKmsGenerateKeyParams` interface defines the algorithm-specific parameters that should be - * passed into the {@link AwsKmsCrypto.generateKey | `AwsKmsCrypto.generateKey()`} method when - * generating a key in AWS KMS. + * The `AwsKeyManagerGenerateKeyParams` interface defines the algorithm-specific parameters that + * should be passed into the {@link AwsKeyManager.generateKey | `AwsKeyManager.generateKey()`} + * method when generating a key in AWS KMS. */ -export interface AwsKmsGenerateKeyParams extends KmsGenerateKeyParams { +export interface AwsKeyManagerGenerateKeyParams extends KmsGenerateKeyParams { /** * A string defining the type of key to generate. The value must be one of the following: * - `"ES256K"`: ECDSA using the secp256k1 curve and SHA-256. @@ -97,12 +97,12 @@ export interface AwsKmsGenerateKeyParams extends KmsGenerateKeyParams { algorithm: 'ES256K'; } -export class AwsKmsCrypto implements CryptoApi { +export class AwsKeyManager implements CryptoApi { /** - * A private map that stores instances of cryptographic algorithm implementations. Each key in this - * map is an `AlgorithmConstructor`, and its corresponding value is an instance of a class that - * implements a specific cryptographic algorithm. This map is used to cache and reuse instances for - * performance optimization, ensuring that each algorithm is instantiated only once. + * A private map that stores instances of cryptographic algorithm implementations. Each key in + * this map is an `AlgorithmConstructor`, and its corresponding value is an instance of a class + * that implements a specific cryptographic algorithm. This map is used to cache and reuse + * instances for performance optimization, ensuring that each algorithm is instantiated only once. */ private _algorithmInstances: Map = new Map(); @@ -114,7 +114,7 @@ export class AwsKmsCrypto implements CryptoApi { */ private _kmsClient: KMSClient; - constructor(params?: AwsKmsCryptoParams) { + constructor(params?: AwsKeyManagerParams) { this._kmsClient = params?.kmsClient ?? new KMSClient(); } @@ -132,9 +132,9 @@ export class AwsKmsCrypto implements CryptoApi { * * @example * ```ts - * const crypto = new AwsKmsCrypto(); + * const keyManager = new AwsKeyManager(); * const data = new Uint8Array([...]); - * const digest = await crypto.digest({ algorithm: 'SHA-256', data }); + * const digest = await keyManager.digest({ algorithm: 'SHA-256', data }); * ``` * * @param params - The parameters for the digest operation. @@ -144,7 +144,7 @@ export class AwsKmsCrypto implements CryptoApi { * @returns A Promise which will be fulfilled with the hash digest. */ public async digest({ algorithm, data }: - AwsKmsDigestParams + AwsKeyManagerDigestParams ): Promise { // Get the hash function implementation based on the specified `algorithm` parameter. const hasher = this.getAlgorithm({ algorithm }); @@ -170,8 +170,8 @@ export class AwsKmsCrypto implements CryptoApi { * * @example * ```ts - * const crypto = new AwsKmsCrypto(); - * const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + * const keyManager = new AwsKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'ES256K' }); * console.log(keyUri); // Outputs the key URI * ``` * @@ -181,7 +181,7 @@ export class AwsKmsCrypto implements CryptoApi { * @returns A Promise that resolves to the key URI, a unique identifier for the generated key. */ public async generateKey({ algorithm }: - AwsKmsGenerateKeyParams + AwsKeyManagerGenerateKeyParams ): Promise { // Get the key generator based on the specified `algorithm` parameter. const keyGenerator = this.getAlgorithm({ algorithm }); @@ -207,9 +207,9 @@ export class AwsKmsCrypto implements CryptoApi { * * @example * ```ts - * const crypto = new AwsKmsCrypto(); + * const keyManager = new AwsKeyManager(); * const publicKey = { ... }; // Public key in JWK format - * const keyUri = await crypto.getKeyUri({ key: publicKey }); + * const keyUri = await keyManager.getKeyUri({ key: publicKey }); * ``` * * @param params - The parameters for getting the key URI. @@ -235,9 +235,9 @@ export class AwsKmsCrypto implements CryptoApi { * * @example * ```ts - * const crypto = new AwsKmsCrypto(); - * const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); - * const publicKey = await crypto.getPublicKey({ keyUri }); + * const keyManager = new AwsKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'ES256K' }); + * const publicKey = await keyManager.getPublicKey({ keyUri }); * ``` * * @param params - The parameters for retrieving the public key. @@ -286,9 +286,9 @@ export class AwsKmsCrypto implements CryptoApi { * * @example * ```ts - * const crypto = new AwsKmsCrypto(); + * const keyManager = new AwsKeyManager(); * const data = new TextEncoder().encode('Message to sign'); - * const signature = await crypto.sign({ + * const signature = await keyManager.sign({ * keyUri: 'urn:jwk:...', * data * }); @@ -333,7 +333,7 @@ export class AwsKmsCrypto implements CryptoApi { * * @example * ```ts - * const crypto = new AwsKmsCrypto(); + * const keyManager = new AwsKeyManager(); * const publicKey = { ... }; // Public key in JWK format corresponding to the private key that signed the data * const data = new TextEncoder().encode('Message to sign'); // Data that was signed * const signature = new Uint8Array([...]); // Signature to verify @@ -399,8 +399,8 @@ export class AwsKmsCrypto implements CryptoApi { if (!this._algorithmInstances.has(AlgorithmImplementation)) { // If not, create a new instance and store it in the cache this._algorithmInstances.set(AlgorithmImplementation, new AlgorithmImplementation({ - crypto : this, - kmsClient : this._kmsClient + keyManager : this, + kmsClient : this._kmsClient })); } diff --git a/packages/crypto-aws-kms/tests/ecdsa.spec.ts b/packages/crypto-aws-kms/tests/ecdsa.spec.ts index 11da2495c..61bf377bb 100644 --- a/packages/crypto-aws-kms/tests/ecdsa.spec.ts +++ b/packages/crypto-aws-kms/tests/ecdsa.spec.ts @@ -5,19 +5,19 @@ import { expect } from 'chai'; import { Convert } from '@web5/common'; import { CreateKeyCommand, DescribeKeyCommand, KMSClient, SignCommand } from '@aws-sdk/client-kms'; -import { AwsKmsCrypto } from '../src/api.js'; +import { AwsKeyManager } from '../src/key-manager.js'; import { EcdsaAlgorithm } from '../src/ecdsa.js'; import { mockEcdsaSecp256k1, mockSignCommandOutput } from './fixtures/mock-ecdsa-secp256k1.js'; describe('EcdsaAlgorithm', () => { - let crypto: AwsKmsCrypto; + let keyManager: AwsKeyManager; let ecdsa: EcdsaAlgorithm; let kmsClientStub: sinon.SinonStubbedInstance; beforeEach(() => { kmsClientStub = sinon.createStubInstance(KMSClient); - crypto = new AwsKmsCrypto({ kmsClient: kmsClientStub as unknown as KMSClient }); - ecdsa = new EcdsaAlgorithm({ crypto, kmsClient: kmsClientStub as unknown as KMSClient }); + keyManager = new AwsKeyManager({ kmsClient: kmsClientStub as unknown as KMSClient }); + ecdsa = new EcdsaAlgorithm({ keyManager, kmsClient: kmsClientStub as unknown as KMSClient }); }); afterEach(() => { @@ -73,10 +73,10 @@ describe('EcdsaAlgorithm', () => { const key = mockEcdsaSecp256k1.verify.input.key as Jwk; // Public key generated with AWS KMS // Test the method. - const signature = await crypto.sign(mockEcdsaSecp256k1.sign.input); + const signature = await keyManager.sign(mockEcdsaSecp256k1.sign.input); - // Validate the signature with crypto.verify() which uses Secp256k1.verify(). - const isValid = await crypto.verify({ + // Validate the signature with keyManager.verify() which uses Secp256k1.verify(). + const isValid = await keyManager.verify({ key, signature, data: mockEcdsaSecp256k1.sign.input.data @@ -183,7 +183,6 @@ describe('EcdsaAlgorithm', () => { // Setup. const unsupportedEcPublicKey: Jwk = { kty : 'EC', - // @ts-expect-error because unsupported curve is being tested. crv : 'unsupported-curve', x : 'oJDigSHQ3lb1Zg82KB6huToMeGPKDcSG1Z8i7u958M8', y : 'xvkCbFcmo9tbyfphIxOa96dfqt9yJgab77J3qOcMYcE' diff --git a/packages/crypto-aws-kms/tests/api.spec.ts b/packages/crypto-aws-kms/tests/key-manager.spec.ts similarity index 83% rename from packages/crypto-aws-kms/tests/api.spec.ts rename to packages/crypto-aws-kms/tests/key-manager.spec.ts index c986c9e68..33db28a31 100644 --- a/packages/crypto-aws-kms/tests/api.spec.ts +++ b/packages/crypto-aws-kms/tests/key-manager.spec.ts @@ -5,18 +5,18 @@ import { expect } from 'chai'; import { Convert } from '@web5/common'; import { CreateAliasCommand, CreateKeyCommand, DescribeKeyCommand, GetPublicKeyCommand, KMSClient, SignCommand } from '@aws-sdk/client-kms'; -import type { AwsKmsGenerateKeyParams } from '../src/api.js'; +import type { AwsKeyManagerGenerateKeyParams } from '../src/key-manager.js'; -import { AwsKmsCrypto } from '../src/api.js'; +import { AwsKeyManager } from '../src/key-manager.js'; import { mockEcdsaSecp256k1 } from './fixtures/mock-ecdsa-secp256k1.js'; describe('AWS KMS Crypto API', () => { - let crypto: AwsKmsCrypto; + let keyManager: AwsKeyManager; let kmsClientStub: sinon.SinonStubbedInstance; beforeEach(() => { kmsClientStub = sinon.createStubInstance(KMSClient); - crypto = new AwsKmsCrypto({ kmsClient: kmsClientStub as unknown as KMSClient }); + keyManager = new AwsKeyManager({ kmsClient: kmsClientStub as unknown as KMSClient }); }); afterEach(() => { @@ -26,11 +26,11 @@ describe('AWS KMS Crypto API', () => { describe('constructor', () => { it('instantiates a KMSClient if one is not given', () => { // Execute the test. - const crypto = new AwsKmsCrypto(); + const keyManager = new AwsKeyManager(); // Validate the result. - expect(crypto).to.exist; - expect(crypto).to.be.an.instanceOf(AwsKmsCrypto); + expect(keyManager).to.exist; + expect(keyManager).to.be.an.instanceOf(AwsKeyManager); }); it('accepts the KMSClient that is given', () => { @@ -38,11 +38,11 @@ describe('AWS KMS Crypto API', () => { const kmsClient = new KMSClient({}); // Execute the test. - const crypto = new AwsKmsCrypto({ kmsClient }); + const keyManager = new AwsKeyManager({ kmsClient }); // Validate the result. - expect(crypto).to.exist; - expect(crypto).to.be.an.instanceOf(AwsKmsCrypto); + expect(keyManager).to.exist; + expect(keyManager).to.be.an.instanceOf(AwsKeyManager); }); }); @@ -50,7 +50,7 @@ describe('AWS KMS Crypto API', () => { it('computes and returns a digest as a Uint8Array', async () => { const data = new Uint8Array([0, 1, 2, 3, 4]); - const digest = await crypto.digest({ algorithm: 'SHA-256', data }); + const digest = await keyManager.digest({ algorithm: 'SHA-256', data }); expect(digest).to.exist; expect(digest).to.be.an.instanceOf(Uint8Array); @@ -62,7 +62,7 @@ describe('AWS KMS Crypto API', () => { const expectedOutput = Convert.hex('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad').toUint8Array(); // Test the method. - const digest = await crypto.digest({ algorithm: 'SHA-256', data }); + const digest = await keyManager.digest({ algorithm: 'SHA-256', data }); // Validate the result. expect(digest).to.exist; @@ -80,7 +80,7 @@ describe('AWS KMS Crypto API', () => { kmsClientStub.send.withArgs(sinon.match.instanceOf(CreateAliasCommand)).resolves(mockEcdsaSecp256k1.createKeyAlias.output); // Test the method. - const keyUri = await crypto.generateKey(mockEcdsaSecp256k1.generateKey.input as AwsKmsGenerateKeyParams); + const keyUri = await keyManager.generateKey(mockEcdsaSecp256k1.generateKey.input as AwsKeyManagerGenerateKeyParams); // Validate the result. expect(keyUri).to.exist; @@ -96,7 +96,7 @@ describe('AWS KMS Crypto API', () => { kmsClientStub.send.withArgs(sinon.match.instanceOf(CreateAliasCommand)).resolves(mockEcdsaSecp256k1.createKeyAlias.output); // Test the method. - const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + const keyUri = await keyManager.generateKey({ algorithm: 'ES256K' }); // Validate the result. expect(keyUri).to.exist; @@ -110,7 +110,7 @@ describe('AWS KMS Crypto API', () => { // Test the method. try { // @ts-expect-error because an unsupported algorithm is being tested. - await crypto.generateKey({ algorithm }); + await keyManager.generateKey({ algorithm }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { @@ -128,7 +128,7 @@ describe('AWS KMS Crypto API', () => { const key = mockEcdsaSecp256k1.verify.input.key as Jwk; // Test the method. - const keyUri = await crypto.getKeyUri({ key }); + const keyUri = await keyManager.getKeyUri({ key }); // Validate the result. expect(keyUri).to.exist; @@ -143,7 +143,7 @@ describe('AWS KMS Crypto API', () => { const expectedKeyUri = 'urn:jwk:' + expectedThumbprint; // Test the method. - const keyUri = await crypto.getKeyUri({ key }); + const keyUri = await keyManager.getKeyUri({ key }); expect(keyUri).to.equal(expectedKeyUri); }); @@ -155,7 +155,7 @@ describe('AWS KMS Crypto API', () => { kmsClientStub.send.withArgs(sinon.match.instanceOf(GetPublicKeyCommand)).resolves(mockEcdsaSecp256k1.getPublicKey.output); // Test the method. - const result = await crypto.getPublicKey(mockEcdsaSecp256k1.getPublicKey.input); + const result = await keyManager.getPublicKey(mockEcdsaSecp256k1.getPublicKey.input); // Validate the result. expect(result).to.be.an('object'); @@ -169,7 +169,7 @@ describe('AWS KMS Crypto API', () => { kmsClientStub.send.withArgs(sinon.match.instanceOf(GetPublicKeyCommand)).resolves(mockEcdsaSecp256k1.getPublicKey.output); // Test the method. - const publicKey = await crypto.getPublicKey({ keyUri: 'arn:aws:kms:us-east-1:364764707041:key/bb48abe3-5948-48e0-80d8-605c04d68171' }); + const publicKey = await keyManager.getPublicKey({ keyUri: 'arn:aws:kms:us-east-1:364764707041:key/bb48abe3-5948-48e0-80d8-605c04d68171' }); // Validate the result. expect(publicKey).to.exist; @@ -183,7 +183,7 @@ describe('AWS KMS Crypto API', () => { kmsClientStub.send.withArgs(sinon.match.instanceOf(GetPublicKeyCommand)).resolves(mockEcdsaSecp256k1.getPublicKey.output); // Test the method. - const publicKey = await crypto.getPublicKey(mockEcdsaSecp256k1.getPublicKey.input); + const publicKey = await keyManager.getPublicKey(mockEcdsaSecp256k1.getPublicKey.input); // Validate the result. expect(publicKey).to.exist; @@ -203,7 +203,7 @@ describe('AWS KMS Crypto API', () => { // Test the method. try { - await crypto.getPublicKey(mockEcdsaSecp256k1.getPublicKey.input); + await keyManager.getPublicKey(mockEcdsaSecp256k1.getPublicKey.input); expect.fail('Expected an error to be thrown.'); } catch (error: any) { @@ -222,7 +222,7 @@ describe('AWS KMS Crypto API', () => { kmsClientStub.send.withArgs(sinon.match.instanceOf(DescribeKeyCommand)).resolves(mockEcdsaSecp256k1.getKeySpec.output); // Test the method. - const signature = await crypto.sign(mockEcdsaSecp256k1.sign.input); + const signature = await keyManager.sign(mockEcdsaSecp256k1.sign.input); // Validate the result. expect(signature).to.be.a('Uint8Array'); @@ -235,7 +235,7 @@ describe('AWS KMS Crypto API', () => { kmsClientStub.send.withArgs(sinon.match.instanceOf(DescribeKeyCommand)).resolves(mockEcdsaSecp256k1.getKeySpec.output); // Test the method. - const signature = await crypto.sign(mockEcdsaSecp256k1.sign.input); + const signature = await keyManager.sign(mockEcdsaSecp256k1.sign.input); // Validate the result. expect(signature).to.have.length(64); @@ -250,7 +250,7 @@ describe('AWS KMS Crypto API', () => { const data = mockEcdsaSecp256k1.verify.input.data; // Test the method. - const isValid = await crypto.verify({ key, signature, data }); + const isValid = await keyManager.verify({ key, signature, data }); // Validate the result. expect(isValid).to.be.true; @@ -263,7 +263,7 @@ describe('AWS KMS Crypto API', () => { const data = mockEcdsaSecp256k1.verify.input.data; // Test the method. - const isValid = await crypto.verify({ key, signature, data }); + const isValid = await keyManager.verify({ key, signature, data }); // Validate the result. expect(isValid).to.be.false; @@ -276,7 +276,7 @@ describe('AWS KMS Crypto API', () => { const data = mockEcdsaSecp256k1.verify.input.data; // Test the method. - const isValid = await crypto.verify({ key, signature, data }); + const isValid = await keyManager.verify({ key, signature, data }); // Validate the result. expect(isValid).to.be.false; @@ -284,14 +284,13 @@ describe('AWS KMS Crypto API', () => { it('throws an error when public key algorithm and curve are unsupported', async () => { // Setup. - // @ts-expect-error because an unsupported algorithm and currve is being tested. const key: Jwk = { kty: 'EC', alg: 'unsupported-algorithm', crv: 'unsupported-curve', x: 'x', y: 'y' }; const signature = new Uint8Array(64); const data = new Uint8Array(0); // Test the method. try { - await crypto.verify({ key, signature, data }); + await keyManager.verify({ key, signature, data }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { diff --git a/packages/crypto/README.md b/packages/crypto/README.md index a7e76d740..fa5048881 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -184,9 +184,9 @@ cloud-based services (e.g., AWS KMS). Extensions that support external KMS servi Start by instantiating a local KMS implementation of the `CryptoApi` interface: ```ts -import { LocalKmsCrypto } from "@web5/crypto"; +import { LocalKeyManager } from "@web5/crypto"; -const crypto = new LocalKmsCrypto(); +const kms = new LocalKeyManager(); ``` An ephemeral, in-memory key store is used by default but any persistent store that implements the @@ -197,7 +197,7 @@ for an example. Generate a random private key: ```ts -const privateKeyUri = await cypto.generateKey({ algorithm: "Ed25519" }); +const privateKeyUri = await kms.generateKey({ algorithm: "Ed25519" }); console.log(privateKeyUri); // Output: urn:jwk:8DaTzHZcvQXUVvl8ezQKgGQHza1hiOZlPkdrB55Vt6Q ``` @@ -206,7 +206,7 @@ Create an EdDSA signature over arbitrary data using the private key: ```ts const data = new TextEncoder().encode("Message"); -const signature = await crypto.sign({ +const signature = await kms.sign({ keyUri: privateKeyUri, data, }); @@ -225,7 +225,7 @@ console.log(signature); Get the public key in JWK format: ```ts -const publicKey = await crypto.getPublicKey({ keyUri: privateKeyUri }); +const publicKey = await kms.getPublicKey({ keyUri: privateKeyUri }); console.log(publicKey); // Output: // { @@ -240,7 +240,7 @@ console.log(publicKey); Verify the signature using the public key: ```ts -const isValid = await crypto.verify({ +const isValid = await kms.verify({ key: publicKey, signature, data, @@ -252,7 +252,7 @@ console.log(isValid); Export the private key: ```ts -const privateKey = await crypto.exportKey({ keyUri }); +const privateKey = await kms.exportKey({ keyUri }); console.log(privateKey); // Output: // { @@ -277,14 +277,14 @@ const privateKey: Jwk = { d: "0xLuQyXFaWjrqp2o0orhwvwhtYhp2Z7KeRcioIs78CY", }; -const keyUri = await crypto.importKey({ key: privateKey }); +const keyUri = await kms.importKey({ key: privateKey }); ``` Compute the hash digest of arbitrary data: ```ts const data = new TextEncoder().encode("Message"); -const hash = await crypto.digest({ algorithm: "SHA-256", data }); +const hash = await kms.digest({ algorithm: "SHA-256", data }); console.log(hash); // Output: // Uint8Array(32) [ @@ -396,7 +396,7 @@ const uuid = randomUuid(); ### Persistent Local KMS Key Store -By default, `LocalKmsCrypto` uses an in-memory key store to simplify prototyping and testing. +By default, `LocalKeyManager` uses an in-memory key store to simplify prototyping and testing. To persist keys that are generated or imported, an implementation of the [`KeyValueStore`](https://github.com/TBD54566975/web5-js/blob/5f364bc0d859e28f1388524ebe8ef152a71727c4/packages/common/src/types.ts#L4-L43) interface can be passed. @@ -412,14 +412,14 @@ const db = new Level("db_location", { valueEncoding: "json", }); const keyStore = new LevelStore({ db }); -const crypto = new LocalKmsCrypto({ keyStore }); +const kms = new LocalKeyManager({ keyStore }); ``` ## Cryptographic Primitives This library encourages using its capabilities through concrete implementations of the `CryptoApi` -interface (e.g., [`LocalKmsCrypto`](#using-a-local-kms) or -[`AwsKmsCrypto`][crypto-aws-kms-repo-link]). These implementations provides high-level, +interface (e.g., [`LocalKeyManager`](#using-a-local-kms) or +[`AwsKeyManager`][crypto-aws-kms-repo-link]). These implementations provides high-level, user-friendly access to a range of cryptographic functions. However, for developers requiring lower-level control or specific customizations, the library also exposes its cryptographic primitives directly. These primitives include a variety of cipher, hash, signature, key derivation, diff --git a/packages/crypto/package.json b/packages/crypto/package.json index efb5d241b..30e989c59 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@web5/crypto", - "version": "0.3.0", + "version": "0.4.0", "description": "Web5 cryptographic library", "type": "module", "main": "./dist/cjs/index.js", @@ -77,7 +77,7 @@ "@noble/ciphers": "0.4.1", "@noble/curves": "1.3.0", "@noble/hashes": "1.3.3", - "@web5/common": "0.2.2" + "@web5/common": "0.2.3" }, "devDependencies": { "@playwright/test": "1.40.1", @@ -90,7 +90,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -101,7 +101,7 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" } } diff --git a/packages/crypto/src/algorithms/ecdsa.ts b/packages/crypto/src/algorithms/ecdsa.ts index bac78d01a..cb3fc448c 100644 --- a/packages/crypto/src/algorithms/ecdsa.ts +++ b/packages/crypto/src/algorithms/ecdsa.ts @@ -4,8 +4,9 @@ import type { AsymmetricKeyGenerator } from '../types/key-generator.js'; import type { ComputePublicKeyParams, GenerateKeyParams, GetPublicKeyParams, SignParams, VerifyParams } from '../types/params-direct.js'; import { Secp256k1 } from '../primitives/secp256k1.js'; -import { isEcPrivateJwk, isEcPublicJwk } from '../jose/jwk.js'; +import { Secp256r1 } from '../primitives/secp256r1.js'; import { CryptoAlgorithm } from './crypto-algorithm.js'; +import { isEcPrivateJwk, isEcPublicJwk } from '../jose/jwk.js'; /** * The `EcdsaGenerateKeyParams` interface defines the algorithm-specific parameters that should be @@ -14,9 +15,12 @@ import { CryptoAlgorithm } from './crypto-algorithm.js'; export interface EcdsaGenerateKeyParams extends GenerateKeyParams { /** * A string defining the type of key to generate. The value must be one of the following: + * - `"ES256"`: ECDSA using the secp256r1 (P-256) curve and SHA-256. * - `"ES256K"`: ECDSA using the secp256k1 curve and SHA-256. + * - `"secp256k1"`: ECDSA using the secp256k1 curve and SHA-256. + * - `"secp256r1"`: ECDSA using the secp256r1 (P-256) curve and SHA-256. */ - algorithm: 'ES256K'; + algorithm: 'ES256' | 'ES256K' | 'secp256k1' | 'secp256r1'; } /** @@ -66,6 +70,12 @@ export class EcdsaAlgorithm extends CryptoAlgorithm return publicKey; } + case 'P-256': { + const publicKey = await Secp256r1.computePublicKey({ key }); + publicKey.alg = 'ES256'; + return publicKey; + } + default: { throw new Error(`Unsupported curve: ${key.crv}`); } @@ -91,9 +101,17 @@ export class EcdsaAlgorithm extends CryptoAlgorithm ): Promise { switch (algorithm) { - case 'ES256K': { + case 'ES256K': + case 'secp256k1': { const privateKey = await Secp256k1.generateKey(); - privateKey.alg = algorithm; + privateKey.alg = 'ES256K'; + return privateKey; + } + + case 'ES256': + case 'secp256r1': { + const privateKey = await Secp256r1.generateKey(); + privateKey.alg = 'ES256'; return privateKey; } } @@ -138,6 +156,12 @@ export class EcdsaAlgorithm extends CryptoAlgorithm return publicKey; } + case 'P-256': { + const publicKey = await Secp256r1.getPublicKey({ key }); + publicKey.alg = 'ES256'; + return publicKey; + } + default: { throw new Error(`Unsupported curve: ${key.crv}`); } @@ -183,6 +207,10 @@ export class EcdsaAlgorithm extends CryptoAlgorithm return await Secp256k1.sign({ key, data }); } + case 'P-256': { + return await Secp256r1.sign({ key, data }); + } + default: { throw new Error(`Unsupported curve: ${key.crv}`); } @@ -229,6 +257,10 @@ export class EcdsaAlgorithm extends CryptoAlgorithm return await Secp256k1.verify({ key, signature, data }); } + case 'P-256': { + return await Secp256r1.verify({ key, signature, data }); + } + default: { throw new Error(`Unsupported curve: ${key.crv}`); } diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index d2952615e..755d74f87 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -1,5 +1,4 @@ -export * from './jose.js'; -export * from './local-kms-crypto.js'; +export * from './local-key-manager.js'; export * as utils from './utils.js'; export * from './algorithms/aes-ctr.js'; @@ -19,6 +18,7 @@ export * from './primitives/aes-ctr.js'; export * from './primitives/aes-gcm.js'; export * from './primitives/concat-kdf.js'; export * from './primitives/ed25519.js'; +export * from './primitives/secp256r1.js'; export * from './primitives/pbkdf2.js'; export * from './primitives/secp256k1.js'; export * from './primitives/sha256.js'; @@ -30,6 +30,7 @@ export type * from './types/cipher.js'; export type * from './types/crypto-api.js'; export type * from './types/hasher.js'; export type * from './types/identifier.js'; +export type * from './types/key-compressor.js'; export type * from './types/key-converter.js'; export type * from './types/key-deriver.js'; export type * from './types/key-generator.js'; diff --git a/packages/crypto/src/jose.ts b/packages/crypto/src/jose.ts deleted file mode 100644 index f53c3cf58..000000000 --- a/packages/crypto/src/jose.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { Multicodec, MulticodecCode, MulticodecDefinition } from '@web5/common'; - -import type { Jwk } from './jose/jwk.js'; - -import { keyToMultibaseId } from './utils.js'; -import { X25519 } from './primitives/x25519.js'; -import { Ed25519 } from './primitives/ed25519.js'; -import { Secp256k1 } from './primitives/secp256k1.js'; - -/** - * A mapping from multicodec names to their corresponding JOSE (JSON Object Signing and Encryption) - * representations. This mapping facilitates the conversion of multicodec key formats to - * JWK (JSON Web Key) formats. - * - * @example - * ```ts - * const joseKey = multicodecToJoseMapping['ed25519-pub']; - * // Returns a partial JWK for an Ed25519 public key - * ``` - * - * @remarks - * The keys of this object are multicodec names, such as 'ed25519-pub', 'ed25519-priv', etc. - * The values are objects representing the corresponding JWK properties for that key type. - */ -const multicodecToJoseMapping: { [key: string]: Jwk } = { - 'ed25519-pub' : { crv: 'Ed25519', kty: 'OKP', x: '' }, - 'ed25519-priv' : { crv: 'Ed25519', kty: 'OKP', x: '', d: '' }, - 'secp256k1-pub' : { crv: 'secp256k1', kty: 'EC', x: '', y: ''}, - 'secp256k1-priv' : { crv: 'secp256k1', kty: 'EC', x: '', y: '', d: '' }, - 'x25519-pub' : { crv: 'X25519', kty: 'OKP', x: '' }, - 'x25519-priv' : { crv: 'X25519', kty: 'OKP', x: '', d: '' }, -}; - -/** - * A mapping from JOSE property descriptors to multicodec names. - * This mapping is used to convert keys in JWK (JSON Web Key) format to multicodec format. - * - * @example - * ```ts - * const multicodecName = joseToMulticodecMapping['Ed25519:public']; - * // Returns 'ed25519-pub', the multicodec name for an Ed25519 public key - * ``` - * - * @remarks - * The keys of this object are strings that describe the JOSE key type and usage, - * such as 'Ed25519:public', 'Ed25519:private', etc. - * The values are the corresponding multicodec names used to represent these key types. - */ -const joseToMulticodecMapping: { [key: string]: string } = { - 'Ed25519:public' : 'ed25519-pub', - 'Ed25519:private' : 'ed25519-priv', - 'secp256k1:public' : 'secp256k1-pub', - 'secp256k1:private' : 'secp256k1-priv', - 'X25519:public' : 'x25519-pub', - 'X25519:private' : 'x25519-priv', -}; - -/** - * The `Jose` class provides utility functions for converting between JOSE (JSON Object Signing and - * Encryption) formats and multicodec representations. - */ -export class Jose { - /** - * Converts a JWK (JSON Web Key) to a Multicodec code and name. - * - * @example - * ```ts - * const jwk: Jwk = { crv: 'Ed25519', kty: 'OKP', x: '...' }; - * const { code, name } = await Jose.jwkToMulticodec({ jwk }); - * ``` - * - * @param params - The parameters for the conversion. - * @param params.jwk - The JSON Web Key to be converted. - * @returns A promise that resolves to a Multicodec definition. - */ - public static async jwkToMulticodec({ jwk }: { - jwk: Jwk - }): Promise> { - const params: string[] = []; - - if ('crv' in jwk) { - params.push(jwk.crv); - if ('d' in jwk) { - params.push('private'); - } else { - params.push('public'); - } - } - - const lookupKey = params.join(':'); - const name = joseToMulticodecMapping[lookupKey]; - - if (name === undefined) { - throw new Error(`Unsupported JOSE to Multicodec conversion: '${lookupKey}'`); - } - - const code = Multicodec.getCodeFromName({ name }); - - return { code, name }; - } - - /** - * Converts a public key in JWK (JSON Web Key) format to a multibase identifier. - * - * @remarks - * Note: All secp public keys are converted to compressed point encoding - * before the multibase identifier is computed. - * - * Per {@link https://github.com/multiformats/multicodec/blob/master/table.csv | Multicodec table}: - * Public keys for Elliptic Curve cryptography algorithms (e.g., secp256k1, - * secp256k1r1, secp384r1, etc.) are always represented with compressed point - * encoding (e.g., secp256k1-pub, p256-pub, p384-pub, etc.). - * - * Per {@link https://datatracker.ietf.org/doc/html/rfc8812#name-jose-and-cose-secp256k1-cur | RFC 8812}: - * "As a compressed point encoding representation is not defined for JWK - * elliptic curve points, the uncompressed point encoding defined there - * MUST be used. The x and y values represented MUST both be exactly - * 256 bits, with any leading zeros preserved." - * - * @example - * ```ts - * const publicKey = { crv: 'Ed25519', kty: 'OKP', x: '...' }; - * const multibaseId = await Jose.publicKeyToMultibaseId({ publicKey }); - * ``` - * - * @param params - The parameters for the conversion. - * @param params.publicKey - The public key in JWK format. - * @returns A promise that resolves to the multibase identifier. - */ - public static async publicKeyToMultibaseId({ publicKey }: { - publicKey: Jwk - }): Promise { - if (!('crv' in publicKey)) { - throw new Error(`Jose: Unsupported public key type: ${publicKey.kty}`); - } - - let publicKeyBytes: Uint8Array; - - switch (publicKey.crv) { - case 'Ed25519': { - publicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey }); - break; - } - - case 'secp256k1': { - publicKeyBytes = await Secp256k1.publicKeyToBytes({ publicKey }); - // Convert secp256k1 public keys to compressed format. - publicKeyBytes = await Secp256k1.compressPublicKey({ publicKeyBytes }); - break; - } - - case 'X25519': { - publicKeyBytes = await X25519.publicKeyToBytes({ publicKey }); - break; - } - - default: { - throw new Error(`Jose: Unsupported public key curve: ${publicKey.crv}`); - } - } - - // Convert the JSON Web Key (JWK) parameters to a Multicodec name. - const { name: multicodecName } = await Jose.jwkToMulticodec({ jwk: publicKey }); - - // Compute the multibase identifier based on the provided key. - const multibaseId = keyToMultibaseId({ key: publicKeyBytes, multicodecName }); - - return multibaseId; - } - - /** - * Converts a Multicodec code or name to parial JWK (JSON Web Key). - * - * @example - * ```ts - * const partialJwk = await Jose.multicodecToJose({ name: 'ed25519-pub' }); - * ``` - * - * @param params - The parameters for the conversion. - * @param params.code - Optional Multicodec code to convert. - * @param params.name - Optional Multicodec name to convert. - * @returns A promise that resolves to a JOSE format key. - */ - public static async multicodecToJose({ code, name }: { - code?: MulticodecCode, - name?: string - }): Promise { - // Either code or name must be specified, but not both. - if (!(name ? !code : code)) { - throw new Error(`Either 'name' or 'code' must be defined, but not both.`); - } - - // If name is undefined, lookup by code. - name = (name === undefined ) ? Multicodec.getNameFromCode({ code: code! }) : name; - - const lookupKey = name; - const jose = multicodecToJoseMapping[lookupKey]; - - if (jose === undefined) { - throw new Error(`Unsupported Multicodec to JOSE conversion`); - } - - return { ...jose }; - } -} \ No newline at end of file diff --git a/packages/crypto/src/jose/jwe.ts b/packages/crypto/src/jose/jwe.ts index 0ead6605b..0fbceff11 100644 --- a/packages/crypto/src/jose/jwe.ts +++ b/packages/crypto/src/jose/jwe.ts @@ -1,5 +1,13 @@ import type { JoseHeaderParams } from './jws.js'; +/** + * JSON Web Encryption (JWE) Header Parameters + * + * The Header Parameter names for use in JWEs are registered in the IANA "JSON Web Signature and + * Encryption Header Parameters" registry. + * + * @see {@link https://datatracker.ietf.org/doc/html/rfc7516#section-4.1 | RFC 7516, Section 4.1} + */ export interface JweHeaderParams extends JoseHeaderParams { /** * Algorithm Header Parameter @@ -50,8 +58,28 @@ export interface JweHeaderParams extends JoseHeaderParams { // an unregistered, case-sensitive, collision-resistant string | string; + /** + * Agreement PartyUInfo Header Parameter + * + * The "apu" (agreement PartyUInfo) value is a base64url-encoded octet sequence containing + * information about the producer of the JWE. This information is used by the recipient to + * determine the key agreement algorithm and key encryption algorithm to use to decrypt the JWE. + * + * Note: This parameter is intended only for use when the recipient is a key agreement algorithm + * that uses public key cryptography. + */ apu?: Uint8Array; + /** + * Agreement PartyVInfo Header Parameter + * + * The "apv" (agreement PartyVInfo) value is a base64url-encoded octet sequence containing + * information about the recipient of the JWE. This information is used by the recipient to + * determine the key agreement algorithm and key encryption algorithm to use to decrypt the JWE. + * + * Note: This parameter is intended only for use when the recipient is a key agreement algorithm + * that uses public key cryptography. + */ apv?: Uint8Array; /** @@ -98,14 +126,69 @@ export interface JweHeaderParams extends JoseHeaderParams { // an unregistered, case-sensitive, collision-resistant string | string; + /** + * Ephemeral Public Key Header Parameter + * + * The "epk" (ephemeral public key) value created by the originator for the use in key agreement + * algorithms. It is the ephemeral public key that corresponds to the key used to encrypt the + * JWE. This value is represented as a JSON Web Key (JWK). + * + * Note: This parameter is intended only for use when the recipient is a key agreement algorithm + * that uses public key cryptography. + */ epk?: Uint8Array; + /** + * Initialization Vector Header Parameter + * + * The "iv" (initialization vector) value is a base64url-encoded octet sequence used by the + * specified "enc" algorithm. The length of this Initialization Vector value MUST be exactly + * equal to the value that would be produced by the "enc" algorithm. + * + * Note: With symmetric encryption algorithms such as AES GCM, this Header Parameter MUST + * be present and MUST be understood and processed by implementations. + */ iv?: Uint8Array; + /** + * PBES2 Count Header Parameter + * + * The "p2c" (PBES2 count) value is an integer indicating the number of iterations of the PBKDF2 + * algorithm performed during key derivation. + * + * Note: The iteration count adds computational expense, ideally compounded by the possible range + * of keys introduced by the salt. A minimum iteration count of 1000 is RECOMMENDED. + */ p2c?: number; + /** + * PBES2 Salt Input Header Parameter + * + * The "p2s" (PBES2 salt) value is a base64url-encoded octet sequence used as the salt value + * input to the PBKDF2 algorithm during key derivation. + * + * The salt value used is (UTF8(Alg) || 0x00 || Salt Input), where Alg is the "alg" (algorithm) + * Header Parameter value. + * + * Note: The salt value is used to ensure that each key derived from the master key is + * independent of every other key. A suitable source of salt value is a sequence of + * cryptographically random bytes containing 8 or more octets. + */ p2s?: string; + /** + * Authentication Tag Header Parameter + * + * The "tag" value is a base64url-encoded octet sequence containing the value of the + * Authentication Tag output by the specified "enc" algorithm. The length of this + * Authentication Tag value MUST be exactly equal to the value that would be produced by the + * "enc" algorithm. + * + * Note: With authenticated encryption algorithms such as AES GCM, this Header Parameter MUST + * be present and MUST be understood and processed by implementations. + */ + tag?: Uint8Array; + /** * Additional Public or Private Header Parameter names. */ diff --git a/packages/crypto/src/jose/jwk.ts b/packages/crypto/src/jose/jwk.ts index 67650a0c5..6878d9f1f 100644 --- a/packages/crypto/src/jose/jwk.ts +++ b/packages/crypto/src/jose/jwk.ts @@ -321,8 +321,11 @@ export type JwkParamsRsaPrivate = JwkParamsRsaPublic & { qi?: string; /** Other primes information (optional in RFC 7518) */ oth?: { + /** Other primes' factor */ r: string; + /** Other primes' CRT exponent */ d: string; + /** Other primes' CRT coefficient */ t: string; }[]; }; @@ -333,17 +336,99 @@ export type PublicKeyJwk = JwkParamsEcPublic | JwkParamsOkpPublic | JwkParamsRsa /** Parameters used with private keys in JWK format. */ export type PrivateKeyJwk = JwkParamsEcPrivate | JwkParamsOkpPrivate | JwkParamsOctPrivate | JwkParamsRsaPrivate; -/** Object representing an asymmetric key pair in JWK format. */ -export type JwkKeyPair = { - publicKeyJwk: PublicKeyJwk; - privateKeyJwk: PrivateKeyJwk; -} - /** * JSON Web Key ({@link https://datatracker.ietf.org/doc/html/rfc7517 | JWK}). * "RSA", "EC", "OKP", and "oct" key types are supported. */ -export type Jwk = PrivateKeyJwk | PublicKeyJwk; +export interface Jwk { + // Common properties that apply to all key types. + + /** JWK Algorithm Parameter. The algorithm intended for use with the key. */ + alg?: string; + /** JWK Extractable Parameter */ + ext?: 'true' | 'false'; + /** JWK Key Operations Parameter */ + key_ops?: JwkOperation[]; + /** JWK Key ID Parameter */ + kid?: string; + /** JWK Key Type Parameter */ + kty: JwkType; + /** JWK Public Key Use Parameter */ + use?: JwkUse; + /** JWK X.509 Certificate Chain Parameter */ + x5c?: string; + /** JWK X.509 Certificate SHA-1 Thumbprint Parameter */ + x5t?: string; + /** JWK X.509 Certificate SHA-256 Thumbprint Parameter */ + 'x5t#S256'?: string; + /** JWK X.509 URL Parameter */ + x5u?: string; + + // Elliptic Curve (EC or OKP) public key properties. + + /** The cryptographic curve used with the key. */ + crv?: string; + /** The x-coordinate for the Elliptic Curve point. */ + x?: string; + /** The y-coordinate for the Elliptic Curve point. */ + y?: string; + + // Symmetric key properties. + + /** The "k" (key value) parameter contains the value of the symmetric (or other single-valued) key. */ + k?: string; + + // RSA public key properties. + + /** Public exponent for RSA */ + e?: string; + /** Modulus for RSA */ + n?: string; + /** First prime factor for RSA */ + p?: string; + /** Second prime factor for RSA */ + q?: string; + /** First factor's CRT exponent for RSA */ + dp?: string; + /** Second factor's CRT exponent for RSA */ + dq?: string; + /** First CRT coefficient for RSA */ + qi?: string; + /** Other primes information (optional in RFC 7518) */ + oth?: { + /** Other primes' factor */ + r: string; + /** Other primes' CRT exponent */ + d: string; + /** Other primes' CRT coefficient */ + t: string; + }[]; + + // Elliptic Curve and RSA private key properties. + + /** Private key component for EC, OKP, or RSA keys. */ + d?: string; + + // Additional public or private properties. + [key: string]: unknown; +} + +/** + * JSON Web Key Set ({@link https://datatracker.ietf.org/doc/html/rfc7517 | JWK Set}) + * + * @remarks + * A JWK Set is a JSON object that represents a set of JWKs. The JSON object MUST have a "keys" + * member, with its value being an array of JWKs. + * + * Additional members can be present in the JWK Set but member names MUST be unique. If not + * understood by implementations encountering them, they MUST be ignored. Parameters for + * representing additional properties of JWK Sets should either be registered in the IANA + * "JSON Web Key Set Parameters" registry or be a value that contains a Collision-Resistant Name. + */ +export interface JwkSet { + /** Array of JWKs */ + keys: Jwk[] +} /** * Computes the thumbprint of a JSON Web Key (JWK) using the method diff --git a/packages/crypto/src/jose/jws.ts b/packages/crypto/src/jose/jws.ts index 5f12092e9..dc35b7ab7 100644 --- a/packages/crypto/src/jose/jws.ts +++ b/packages/crypto/src/jose/jws.ts @@ -1,5 +1,17 @@ import type { Jwk } from './jwk.js'; +/** + * JSON Object Signing and Encryption (JOSE) Header Parameters + * + * The Header Parameter names for use in both JWSs and JWEs are registered in the IANA "JSON Web + * Signature and Encryption Header Parameters" registry. + * + * As indicated by the common registry, JWSs and JWEs share a common Header Parameter space; when a + * parameter is used by both specifications, its usage must be compatible between the + * specifications. + * + * @see {@link https://datatracker.ietf.org/doc/html/rfc7515#section-4.1 | RFC 7515, Section 4.1} + */ export interface JoseHeaderParams { /** Content Type Header Parameter */ cty?: string; @@ -26,6 +38,14 @@ export interface JoseHeaderParams { x5u?: string; } +/** + * JSON Web Signature (JWS) Header Parameters + * + * The Header Parameter names for use in JWSs are registered in the IANA "JSON Web Signature and + * Encryption Header Parameters" registry. + * + * @see {@link https://datatracker.ietf.org/doc/html/rfc7515#section-4.1 | RFC 7515, Section 4.1} + */ export interface JwsHeaderParams extends JoseHeaderParams { /** * Algorithm Header Parameter diff --git a/packages/crypto/src/local-kms-crypto.ts b/packages/crypto/src/local-key-manager.ts similarity index 82% rename from packages/crypto/src/local-kms-crypto.ts rename to packages/crypto/src/local-key-manager.ts index 40c9f9436..8013fea31 100644 --- a/packages/crypto/src/local-kms-crypto.ts +++ b/packages/crypto/src/local-key-manager.ts @@ -31,17 +31,21 @@ import { computeJwkThumbprint, isPrivateJwk, KEY_URI_PREFIX_JWK } from './jose/j * implementation class and any relevant names or identifiers for the algorithm. This structure * allows for easy retrieval and instantiation of algorithm implementations based on the algorithm * name or key specification. It facilitates the support of multiple algorithms within the - * `LocalKmsCrypto` class. + * `LocalKeyManager` class. */ const supportedAlgorithms = { 'Ed25519': { implementation : EdDsaAlgorithm, names : ['Ed25519'], }, - 'ES256K': { + 'secp256k1': { implementation : EcdsaAlgorithm, names : ['ES256K', 'secp256k1'], }, + 'secp256r1': { + implementation : EcdsaAlgorithm, + names : ['ES256', 'secp256r1'], + }, 'SHA-256': { implementation : Sha2Algorithm, names : ['SHA-256'] @@ -60,27 +64,27 @@ type SupportedAlgorithm = keyof typeof supportedAlgorithms; type AlgorithmConstructor = typeof supportedAlgorithms[SupportedAlgorithm]['implementation']; /** - * The `LocalKmsCryptoParams` interface specifies the parameters for initializing an instance of - * `LocalKmsCrypto`. It allows the optional inclusion of a `KeyValueStore` instance for key + * The `LocalKeyManagerParams` interface specifies the parameters for initializing an instance of + * `LocalKeyManager`. It allows the optional inclusion of a `KeyValueStore` instance for key * management. If not provided, a default `MemoryStore` instance will be used for storing keys in * memory. Note that the `MemoryStore` is not persistent and will be cleared when the application * exits. */ -export type LocalKmsCryptoParams = { +export type LocalKeyManagerParams = { /** * An optional property to specify a custom `KeyValueStore` instance for key management. If not - * provided, {@link LocalKmsCrypto | `LocalKmsCrypto`} uses a default `MemoryStore` instance. This - * store is responsible for managing cryptographic keys, allowing them to be retrieved, stored, - * and managed during cryptographic operations. + * provided, {@link LocalKeyManager | `LocalKeyManager`} uses a default `MemoryStore` instance. + * This store is responsible for managing cryptographic keys, allowing them to be retrieved, + * stored, and managed during cryptographic operations. */ keyStore?: KeyValueStore; }; /** - * The `LocalKmsDigestParams` interface defines the algorithm-specific parameters that should be - * passed into the {@link LocalKmsCrypto.digest | `LocalKmsCrypto.digest()`} method. + * The `LocalKeyManagerDigestParams` interface defines the algorithm-specific parameters that should + * be passed into the {@link LocalKeyManager.digest | `LocalKeyManager.digest()`} method. */ -export interface LocalKmsDigestParams extends KmsDigestParams { +export interface LocalKeyManagerDigestParams extends KmsDigestParams { /** * A string defining the name of hash function to use. The value must be one of the following: * - `"SHA-256"`: Generates a 256-bit digest. @@ -89,34 +93,34 @@ export interface LocalKmsDigestParams extends KmsDigestParams { } /** - * The `LocalKmsGenerateKeyParams` interface defines the algorithm-specific parameters that should - * be passed into the {@link LocalKmsCrypto.generateKey | `LocalKmsCrypto.generateKey()`} method - * when generating a key in the local KMS. + * The `LocalKeyManagerGenerateKeyParams` interface defines the algorithm-specific parameters that + * should be passed into the {@link LocalKeyManager.generateKey | `LocalKeyManager.generateKey()`} + * method when generating a key in the local KMS. */ -export interface LocalKmsGenerateKeyParams extends KmsGenerateKeyParams { +export interface LocalKeyManagerGenerateKeyParams extends KmsGenerateKeyParams { /** * A string defining the type of key to generate. The value must be one of the following: - * - `"Ed25519"`: EdDSA using the Ed25519 curve. - * - `"ES256K"`: ECDSA using the secp256k1 curve and SHA-256. + * - `"Ed25519"` + * - `"secp256k1"` */ - algorithm: 'Ed25519' | 'ES256K'; + algorithm: 'Ed25519' | 'secp256k1' | 'secp256r1'; } -export class LocalKmsCrypto implements +export class LocalKeyManager implements CryptoApi, KeyImporterExporter { /** - * A private map that stores instances of cryptographic algorithm implementations. Each key in this - * map is an `AlgorithmConstructor`, and its corresponding value is an instance of a class that - * implements a specific cryptographic algorithm. This map is used to cache and reuse instances for - * performance optimization, ensuring that each algorithm is instantiated only once. + * A private map that stores instances of cryptographic algorithm implementations. Each key in + * this map is an `AlgorithmConstructor`, and its corresponding value is an instance of a class + * that implements a specific cryptographic algorithm. This map is used to cache and reuse + * instances for performance optimization, ensuring that each algorithm is instantiated only once. */ private _algorithmInstances: Map> = new Map(); /** - * The `_keyStore` private variable in `LocalKmsCrypto` is a `KeyValueStore` instance used for - * storing and managing cryptographic keys. It allows the `LocalKmsCrypto` class to save, + * The `_keyStore` private variable in `LocalKeyManager` is a `KeyValueStore` instance used for + * storing and managing cryptographic keys. It allows the `LocalKeyManager` class to save, * retrieve, and handle keys efficiently within the local Key Management System (KMS) context. * This variable can be configured to use different storage backends, like in-memory storage or * persistent storage, providing flexibility in key management according to the application's @@ -124,7 +128,7 @@ export class LocalKmsCrypto implements */ private _keyStore: KeyValueStore; - constructor(params?: LocalKmsCryptoParams) { + constructor(params?: LocalKeyManagerParams) { this._keyStore = params?.keyStore ?? new MemoryStore(); } @@ -142,9 +146,9 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const crypto = new LocalKmsCrypto(); + * const keyManager = new LocalKeyManager(); * const data = new Uint8Array([...]); - * const digest = await crypto.digest({ algorithm: 'SHA-256', data }); + * const digest = await keyManager.digest({ algorithm: 'SHA-256', data }); * ``` * * @param params - The parameters for the digest operation. @@ -154,7 +158,7 @@ export class LocalKmsCrypto implements * @returns A Promise which will be fulfilled with the hash digest. */ public async digest({ algorithm, data }: - LocalKmsDigestParams + LocalKeyManagerDigestParams ): Promise { // Get the hash function implementation based on the specified `algorithm` parameter. const hasher = this.getAlgorithm({ algorithm }) as Hasher; @@ -174,9 +178,9 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const crypto = new LocalKmsCrypto(); - * const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); - * const privateKey = await crypto.exportKey({ keyUri }); + * const keyManager = new LocalKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); + * const privateKey = await keyManager.exportKey({ keyUri }); * ``` * * @param params - Parameters for exporting the key. @@ -199,8 +203,8 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const cryptoApi = new LocalKmsCrypto(); - * const keyUri = await cryptoApi.generateKey({ algorithm: 'ES256K' }); + * const keyManager = new LocalKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); * console.log(keyUri); // Outputs the key URI * ``` * @@ -210,10 +214,10 @@ export class LocalKmsCrypto implements * @returns A Promise that resolves to the key URI, a unique identifier for the generated key. */ public async generateKey({ algorithm }: - LocalKmsGenerateKeyParams + LocalKeyManagerGenerateKeyParams ): Promise { // Get the key generator implementation based on the specified `algorithm` parameter. - const keyGenerator = this.getAlgorithm({ algorithm }) as KeyGenerator; + const keyGenerator = this.getAlgorithm({ algorithm }) as KeyGenerator; // Generate the key. const key = await keyGenerator.generateKey({ algorithm }); @@ -246,10 +250,10 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const crypto = new LocalKmsCrypto(); - * const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); - * const publicKey = await crypto.getPublicKey({ keyUri }); - * const keyUriFromPublicKey = await crypto.getKeyUri({ key: publicKey }); + * const keyManager = new LocalKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); + * const publicKey = await keyManager.getPublicKey({ keyUri }); + * const keyUriFromPublicKey = await keyManager.getKeyUri({ key: publicKey }); * console.log(keyUri === keyUriFromPublicKey); // Outputs `true` * ``` * @@ -276,9 +280,9 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const crypto = new LocalKmsCrypto(); - * const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); - * const publicKey = await crypto.getPublicKey({ keyUri }); + * const keyManager = new LocalKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); + * const publicKey = await keyManager.getPublicKey({ keyUri }); * ``` * * @param params - The parameters for retrieving the public key. @@ -296,7 +300,7 @@ export class LocalKmsCrypto implements const algorithm = this.getAlgorithmName({ key: privateKey }); // Get the key generator based on the algorithm name. - const keyGenerator = this.getAlgorithm({ algorithm }) as AsymmetricKeyGenerator; + const keyGenerator = this.getAlgorithm({ algorithm }) as AsymmetricKeyGenerator; // Get the public key properties from the private JWK. const publicKey = await keyGenerator.getPublicKey({ key: privateKey }); @@ -318,9 +322,9 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const crypto = new LocalKmsCrypto(); + * const keyManager = new LocalKeyManager(); * const privateKey = { ... } // A private key in JWK format - * const keyUri = await crypto.importKey({ key: privateKey }); + * const keyUri = await keyManager.importKey({ key: privateKey }); * ``` * * @param params - Parameters for importing the key. @@ -359,10 +363,10 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const crypto = new LocalKmsCrypto(); - * const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + * const keyManager = new LocalKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); * const data = new TextEncoder().encode('Message to sign'); - * const signature = await crypto.sign({ keyUri, data }); + * const signature = await keyManager.sign({ keyUri, data }); * ``` * * @param params - The parameters for the signing operation. @@ -378,7 +382,7 @@ export class LocalKmsCrypto implements const privateKey = await this.getPrivateKey({ keyUri }); // Determine the algorithm name based on the JWK's `alg` and `crv` properties. - let algorithm = this.getAlgorithmName({ key: privateKey }); + const algorithm = this.getAlgorithmName({ key: privateKey }); // Get the signature algorithm based on the algorithm name. const signer = this.getAlgorithm({ algorithm }) as Signer; @@ -400,11 +404,11 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const crypto = new LocalKmsCrypto(); - * const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + * const keyManager = new LocalKeyManager(); + * const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); * const data = new TextEncoder().encode('Message to sign'); - * const signature = await crypto.sign({ keyUri, data }); - * const isSignatureValid = await crypto.verify({ keyUri, data, signature }); + * const signature = await keyManager.sign({ keyUri, data }); + * const isSignatureValid = await keyManager.verify({ keyUri, data, signature }); * ``` * * @param params - The parameters for the verification operation. @@ -418,7 +422,7 @@ export class LocalKmsCrypto implements KmsVerifyParams ): Promise { // Determine the algorithm name based on the JWK's `alg` and `crv` properties. - let algorithm = this.getAlgorithmName({ key }); + const algorithm = this.getAlgorithmName({ key }); // Get the signature algorithm based on the algorithm name. const signer = this.getAlgorithm({ algorithm }) as Signer; @@ -439,7 +443,7 @@ export class LocalKmsCrypto implements * * @example * ```ts - * const signer = this.getAlgorithm({ algorithm: 'ES256K' }); + * const signer = this.getAlgorithm({ algorithm: 'Ed25519' }); * ``` * * @param params - The parameters for retrieving the algorithm implementation. diff --git a/packages/crypto/src/primitives/concat-kdf.ts b/packages/crypto/src/primitives/concat-kdf.ts index 568e8a0e8..683b9b7a6 100644 --- a/packages/crypto/src/primitives/concat-kdf.ts +++ b/packages/crypto/src/primitives/concat-kdf.ts @@ -2,7 +2,16 @@ import { sha256 } from '@noble/hashes/sha256'; import { Convert, universalTypeOf } from '@web5/common'; import { TypedArray, concatBytes } from '@noble/hashes/utils'; -export type ConcatKdfOtherInfo = { +/** + * ConcatKDF FixedInfo Parameters. + * + * This implementation follows the recommended format for `FixedInfo` specified in section 5.8.2 + * of the NIST.800-56A publication. + * + * @see {@link https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar3.pdf | NIST.800-56A} + * @see {@link https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 | RFC 7518, Section 4.6.2} + */ +export type ConcatKdfFixedInfo = { /** * The algorithm the derived secret keying material will be used with. */ @@ -46,12 +55,12 @@ export type ConcatKdfOtherInfo = { * specifically uses SHA-256 as the pseudorandom function (PRF). * * Note: This implementation allows for only a single round / repetition using the function - * `K(1) = H(counter || Z || OtherInfo)`, where: + * `K(1) = H(counter || Z || FixedInfo)`, where: * - `K(1)` is the derived key material after one round * - `H` is the SHA-256 hashing function * - `counter` is a 32-bit, big-endian bit string counter set to 0x00000001 * - `Z` is the shared secret value obtained from a key agreement protocol - * - `OtherInfo` is a bit string used to ensure that the derived keying material is adequately + * - `FixedInfo` is a bit string used to ensure that the derived keying material is adequately * "bound" to the key-agreement transaction. * * @example @@ -60,7 +69,7 @@ export type ConcatKdfOtherInfo = { * const derivedKeyingMaterial = await ConcatKdf.deriveKey({ * sharedSecret: utils.randomBytes(32), * keyDataLen: 128, - * otherInfo: { + * fixedInfo: { * algorithmId: "A128GCM", * partyUInfo: "Alice", * partyVInfo: "Bob", @@ -94,7 +103,7 @@ export class ConcatKdf { * const derivedKeyingMaterial = await ConcatKdf.deriveKey({ * sharedSecret: utils.randomBytes(32), * keyDataLen: 128, - * otherInfo: { + * fixedInfo: { * algorithmId: "A128GCM", * partyUInfo: "Alice", * partyVInfo: "Bob", @@ -106,14 +115,14 @@ export class ConcatKdf { * @param params - Input parameters for key derivation. * @param params.keyDataLen - The desired length of the derived key in bits. * @param params.sharedSecret - The shared secret key to derive from. - * @param params.otherInfo - Additional public information to use in key derivation. + * @param params.fixedInfo - Additional public information to use in key derivation. * @returns The derived key as a Uint8Array. * * @throws {Error} If the `keyDataLen` would require multiple rounds. */ - public static async deriveKey({ keyDataLen, otherInfo, sharedSecret }: { + public static async deriveKey({ keyDataLen, fixedInfo, sharedSecret }: { keyDataLen: number; - otherInfo: ConcatKdfOtherInfo; + fixedInfo: ConcatKdfFixedInfo; sharedSecret: Uint8Array; }): Promise { // RFC 7518 Section 4.6.2 specifies using SHA-256 for ECDH key agreement: @@ -132,36 +141,36 @@ export class ConcatKdf { const counter = new Uint8Array(4); new DataView(counter.buffer).setUint32(0, roundCount); - // Compute the OtherInfo bit-string. - const otherInfoBytes = ConcatKdf.computeOtherInfo(otherInfo); + // Compute the FixedInfo bit-string. + const fixedInfoBytes = ConcatKdf.computeFixedInfo(fixedInfo); - // Compute K(i) = H(counter || Z || OtherInfo) - // return concatBytes(counter, sharedSecretZ, otherInfo); - const derivedKeyingMaterial = sha256(concatBytes(counter, sharedSecret, otherInfoBytes)); + // Compute K(i) = H(counter || Z || FixedInfo) + // return concatBytes(counter, sharedSecretZ, fixedInfo); + const derivedKeyingMaterial = sha256(concatBytes(counter, sharedSecret, fixedInfoBytes)); // Return the bit string of derived keying material of length keyDataLen bits. return derivedKeyingMaterial.slice(0, keyDataLen / 8); } /** - * Computes the `OtherInfo` parameter for Concat KDF, which binds the derived key material to the + * Computes the `FixedInfo` parameter for Concat KDF, which binds the derived key material to the * context of the key agreement transaction. * * @remarks - * This implementation follows the recommended format for `OtherInfo` specified in section + * This implementation follows the recommended format for `FixedInfo` specified in section * 5.8.1.2.1 of the NIST.800-56A publication. * - * `OtherInfo` is a bit string equal to the following concatenation: + * `FixedInfo` is a bit string equal to the following concatenation: * `AlgorithmID || PartyUInfo || PartyVInfo {|| SuppPubInfo }{|| SuppPrivInfo }`. * * `SuppPubInfo` is the key length in bits, big endian encoded as a 32-bit number. For example, * 128 would be [0, 0, 0, 128] and 256 would be [0, 0, 1, 0]. * - * @param params - Input data to construct OtherInfo. - * @returns OtherInfo as a Uint8Array. + * @param params - Input data to construct FixedInfo. + * @returns FixedInfo as a Uint8Array. */ - private static computeOtherInfo(params: - ConcatKdfOtherInfo + private static computeFixedInfo(params: + ConcatKdfFixedInfo ): Uint8Array { // Required sub-fields. const algorithmId = ConcatKdf.toDataLenData({ data: params.algorithmId }); @@ -172,9 +181,9 @@ export class ConcatKdf { const suppPrivInfo = ConcatKdf.toDataLenData({ data: params.suppPrivInfo }); // Concatenate AlgorithmID || PartyUInfo || PartyVInfo || SuppPubInfo || SuppPrivInfo. - const otherInfo = concatBytes(algorithmId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); + const fixedInfo = concatBytes(algorithmId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); - return otherInfo; + return fixedInfo; } /** diff --git a/packages/crypto/src/primitives/ed25519.ts b/packages/crypto/src/primitives/ed25519.ts index ee1f804cf..22b0aa97f 100644 --- a/packages/crypto/src/primitives/ed25519.ts +++ b/packages/crypto/src/primitives/ed25519.ts @@ -52,7 +52,7 @@ import { computeJwkThumbprint, isOkpPrivateJwk, isOkpPublicJwk } from '../jose/j * const publicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey }); * * // Key Validation - * const isPublicKeyValid = Ed25519.validatePublicKey({ publicKeyBytes }); + * const isPublicKeyValid = await Ed25519.validatePublicKey({ publicKeyBytes }); * ``` */ export class Ed25519 { @@ -262,7 +262,7 @@ export class Ed25519 { const ed25519PublicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey }); // Verify Edwards public key is valid. - const isValid = Ed25519.validatePublicKey({ publicKey: ed25519PublicKeyBytes }); + const isValid = await Ed25519.validatePublicKey({ publicKeyBytes: ed25519PublicKeyBytes }); if (!isValid) { throw new Error('Ed25519: Invalid public key.'); } @@ -481,23 +481,23 @@ export class Ed25519 { * * @example * ```ts - * const publicKey = new Uint8Array([...]); // A public key in byte format - * const isValid = Ed25519.validatePublicKey({ publicKey }); + * const publicKeyBytes = new Uint8Array([...]); // A public key in byte format + * const isValid = await Ed25519.validatePublicKey({ publicKeyBytes }); * console.log(isValid); // true if the key is valid on the Edwards curve, false otherwise * ``` * * @param params - The parameters for the public key validation. - * @param params.publicKey - The public key to validate, represented as a Uint8Array. + * @param params.publicKeyBytes - The public key to validate, represented as a Uint8Array. * * @returns A Promise that resolves to a boolean indicating whether the key * corresponds to a valid point on the Edwards curve. */ - public static validatePublicKey({ publicKey }: { - publicKey: Uint8Array; - }): boolean { + public static async validatePublicKey({ publicKeyBytes }: { + publicKeyBytes: Uint8Array; + }): Promise { try { // Decode Edwards points from key bytes. - const point = ed25519.ExtendedPoint.fromHex(publicKey); + const point = ed25519.ExtendedPoint.fromHex(publicKeyBytes); // Check if points are on the Twisted Edwards curve. point.assertValidity(); diff --git a/packages/crypto/src/primitives/secp256k1.ts b/packages/crypto/src/primitives/secp256k1.ts index add7da40f..bc5a52dc3 100644 --- a/packages/crypto/src/primitives/secp256k1.ts +++ b/packages/crypto/src/primitives/secp256k1.ts @@ -1,3 +1,5 @@ +import type { AffinePoint } from '@noble/curves/abstract/weierstrass'; + import { Convert } from '@web5/common'; import { sha256 } from '@noble/hashes/sha256'; import { secp256k1 } from '@noble/curves/secp256k1'; @@ -144,7 +146,7 @@ export class Secp256k1 { * @remarks * This method takes a private key represented as a byte array (Uint8Array) and * converts it into a JWK object. The conversion involves extracting the - * elliptic curve points (x and y coordinates) from the private key and encoding + * elliptic curve point (x and y coordinates) from the private key and encoding * them into base64url format, alongside other JWK parameters. * * The resulting JWK object includes the following properties: @@ -172,16 +174,16 @@ export class Secp256k1 { public static async bytesToPrivateKey({ privateKeyBytes }: { privateKeyBytes: Uint8Array; }): Promise { - // Get the elliptic curve points (x and y coordinates) for the provided private key. - const points = await Secp256k1.getCurvePoints({ keyBytes: privateKeyBytes }); + // Get the elliptic curve point (x and y coordinates) for the provided private key. + const point = await Secp256k1.getCurvePoint({ keyBytes: privateKeyBytes }); // Construct the private key in JWK format. const privateKey: Jwk = { kty : 'EC', crv : 'secp256k1', d : Convert.uint8Array(privateKeyBytes).toBase64Url(), - x : Convert.uint8Array(points.x).toBase64Url(), - y : Convert.uint8Array(points.y).toBase64Url() + x : Convert.uint8Array(point.x).toBase64Url(), + y : Convert.uint8Array(point.y).toBase64Url() }; // Compute the JWK thumbprint and set as the key ID. @@ -195,7 +197,7 @@ export class Secp256k1 { * * @remarks * This method accepts a public key in a byte array (Uint8Array) format and - * transforms it to a JWK object. It involves decoding the elliptic curve points + * transforms it to a JWK object. It involves decoding the elliptic curve point * (x and y coordinates) from the raw public key bytes and encoding them into * base64url format, along with setting appropriate JWK parameters. * @@ -223,15 +225,15 @@ export class Secp256k1 { public static async bytesToPublicKey({ publicKeyBytes }: { publicKeyBytes: Uint8Array; }): Promise { - // Get the elliptic curve points (x and y coordinates) for the provided public key. - const points = await Secp256k1.getCurvePoints({ keyBytes: publicKeyBytes }); + // Get the elliptic curve point (x and y coordinates) for the provided public key. + const point = await Secp256k1.getCurvePoint({ keyBytes: publicKeyBytes }); // Construct the public key in JWK format. const publicKey: Jwk = { kty : 'EC', crv : 'secp256k1', - x : Convert.uint8Array(points.x).toBase64Url(), - y : Convert.uint8Array(points.y).toBase64Url() + x : Convert.uint8Array(point.x).toBase64Url(), + y : Convert.uint8Array(point.y).toBase64Url() }; // Compute the JWK thumbprint and set as the key ID. @@ -278,7 +280,7 @@ export class Secp256k1 { * @remarks * This method takes a private key in JWK format and derives its corresponding public key, * also in JWK format. The derivation process involves converting the private key to a raw - * byte array, then computing the elliptic curve points (x and y coordinates) from this private + * byte array, then computing the elliptic curve point (x and y coordinates) from this private * key. These coordinates are then encoded into base64url format to construct the public key in * JWK format. * @@ -304,15 +306,15 @@ export class Secp256k1 { // Convert the provided private key to a byte array. const privateKeyBytes = await Secp256k1.privateKeyToBytes({ privateKey: key }); - // Get the elliptic curve points (x and y coordinates) for the provided private key. - const points = await Secp256k1.getCurvePoints({ keyBytes: privateKeyBytes }); + // Get the elliptic curve point (x and y coordinates) for the provided private key. + const point = await Secp256k1.getCurvePoint({ keyBytes: privateKeyBytes }); // Construct the public key in JWK format. const publicKey: Jwk = { kty : 'EC', crv : 'secp256k1', - x : Convert.uint8Array(points.x).toBase64Url(), - y : Convert.uint8Array(points.y).toBase64Url() + x : Convert.uint8Array(point.x).toBase64Url(), + y : Convert.uint8Array(point.y).toBase64Url() }; // Compute the JWK thumbprint and set as the key ID. @@ -794,10 +796,10 @@ export class Secp256k1 { } /** - * Returns the elliptic curve points (x and y coordinates) for a given secp256k1 key. + * Returns the elliptic curve point (x and y coordinates) for a given secp256k1 key. * * @remarks - * This method extracts the elliptic curve points from a given secp256k1 key, whether + * This method extracts the elliptic curve point from a given secp256k1 key, whether * it's a private or a public key. For a private key, the method first computes the * corresponding public key and then extracts the x and y coordinates. For a public key, * it directly returns these coordinates. The coordinates are represented as Uint8Array. @@ -810,15 +812,15 @@ export class Secp256k1 { * ```ts * // For a private key * const privateKey = new Uint8Array([...]); // A 32-byte private key - * const { x: xFromPrivateKey, y: yFromPrivateKey } = await Secp256k1.getCurvePoints({ keyBytes: privateKey }); + * const { x: xFromPrivateKey, y: yFromPrivateKey } = await Secp256k1.getCurvePoint({ keyBytes: privateKey }); * * // For a public key * const publicKey = new Uint8Array([...]); // A 33-byte or 65-byte public key - * const { x: xFromPublicKey, y: yFromPublicKey } = await Secp256k1.getCurvePoints({ keyBytes: publicKey }); + * const { x: xFromPublicKey, y: yFromPublicKey } = await Secp256k1.getCurvePoint({ keyBytes: publicKey }); * ``` * * @param params - The parameters for the curve point decoding operation. - * @param params.keyBytes - The key for which to get the elliptic curve points. + * @param params.keyBytes - The key for which to get the elliptic curve point. * Can be either a private key or a public key. * The key should be passed as a `Uint8Array`. * @@ -826,15 +828,15 @@ export class Secp256k1 { * each being a Uint8Array representing the x and y coordinates of the key point on the * elliptic curve. */ - private static async getCurvePoints({ keyBytes }: { + private static async getCurvePoint({ keyBytes }: { keyBytes: Uint8Array; - }): Promise<{ x: Uint8Array, y: Uint8Array }> { + }): Promise> { // If key is a private key, first compute the public key. if (keyBytes.byteLength === 32) { keyBytes = secp256k1.getPublicKey(keyBytes); } - // Decode Weierstrass points from key bytes. + // Decode Weierstrass affine point from key bytes. const point = secp256k1.ProjectivePoint.fromHex(keyBytes); // Get x- and y-coordinate values and convert to Uint8Array. diff --git a/packages/crypto/src/primitives/secp256r1.ts b/packages/crypto/src/primitives/secp256r1.ts new file mode 100644 index 000000000..ae9914e23 --- /dev/null +++ b/packages/crypto/src/primitives/secp256r1.ts @@ -0,0 +1,850 @@ +import type { AffinePoint } from '@noble/curves/abstract/weierstrass'; + +import { Convert } from '@web5/common'; +import { sha256 } from '@noble/hashes/sha256'; +import { secp256r1 } from '@noble/curves/p256'; +import { numberToBytesBE } from '@noble/curves/abstract/utils'; + +import type { Jwk } from '../jose/jwk.js'; +import type { ComputePublicKeyParams, GetPublicKeyParams, SignParams, VerifyParams } from '../types/params-direct.js'; + +import { computeJwkThumbprint, isEcPrivateJwk, isEcPublicJwk } from '../jose/jwk.js'; + +/** + * The `Secp256r1` class provides a comprehensive suite of utilities for working with + * the secp256r1 (aka P-256) elliptic curve, commonly used in blockchain and cryptographic + * applications. This class includes methods for key generation, conversion, signing, verification, + * and Elliptic Curve Diffie-Hellman (ECDH) key agreement. + * + * The class supports conversions between raw byte formats and JSON Web Key (JWK) formats. It + * adheres to RFC6979 for ECDSA signing and verification and RFC6090 for ECDH. + * + * Key Features: + * - Key Generation: Generate secp256r1 private keys in JWK format. + * - Key Conversion: Transform keys between raw byte arrays and JWK formats. + * - Public Key Derivation: Derive public keys from private keys. + * - ECDH Shared Secret Computation: Securely derive shared secrets using private and public keys. + * - ECDSA Signing and Verification: Sign data and verify signatures with secp256r1 keys. + * - Key Validation: Validate the mathematical correctness of secp256r1 keys. + * + * The methods in this class are asynchronous, returning Promises to accommodate various + * JavaScript environments, and use `Uint8Array` for binary data handling. + * + * @example + * ```ts + * // Key Generation + * const privateKey = await Secp256r1.generateKey(); + * + * // Public Key Derivation + * const publicKey = await Secp256r1.computePublicKey({ key: privateKey }); + * console.log(publicKey === await Secp256r1.getPublicKey({ key: privateKey })); // Output: true + * + * // ECDH Shared Secret Computation + * const sharedSecret = await Secp256r1.sharedSecret({ + * privateKeyA: privateKey, + * publicKeyB: anotherPublicKey + * }); + * + * // ECDSA Signing + * const signature = await Secp256r1.sign({ + * key: privateKey, + * data: new TextEncoder().encode('Message') + * }); + * + * // ECDSA Signature Verification + * const isValid = await Secp256r1.verify({ + * key: publicKey, + * signature: signature, + * data: new TextEncoder().encode('Message') + * }); + * + * // Key Conversion + * const publicKeyBytes = await Secp256r1.publicKeyToBytes({ publicKey }); + * const privateKeyBytes = await Secp256r1.privateKeyToBytes({ privateKey }); + * const compressedPublicKey = await Secp256r1.compressPublicKey({ publicKeyBytes }); + * const uncompressedPublicKey = await Secp256r1.decompressPublicKey({ publicKeyBytes }); + * + * // Key Validation + * const isPrivateKeyValid = await Secp256r1.validatePrivateKey({ privateKeyBytes }); + * const isPublicKeyValid = await Secp256r1.validatePublicKey({ publicKeyBytes }); + * ``` + */ +export class Secp256r1 { +/** + * Adjusts an ECDSA signature to a normalized, low-S form. + * + * @remarks + * All ECDSA signatures, regardless of the curve, consist of two components, `r` and `s`, both of + * which are integers. The curve's order (the total number of points on the curve) is denoted by + * `n`. In a valid ECDSA signature, both `r` and `s` must be in the range [1, n-1]. However, due + * to the mathematical properties of ECDSA, if `(r, s)` is a valid signature, then `(r, n - s)` is + * also a valid signature for the same message and public key. In other words, for every + * signature, there's a "mirror" signature that's equally valid. For these elliptic curves: + * + * - Low S Signature: A signature where the `s` component is in the lower half of the range, + * specifically less than or equal to `n/2`. + * + * - High S Signature: This is where the `s` component is in the upper half of the range, greater + * than `n/2`. + * + * The practical implication is that a third-party can forge a second valid signature for the same + * message by negating the `s` component of the original signature, without any knowledge of the + * private key. This is known as a "signature malleability" attack. + * + * This type of forgery is not a problem in all systems, but it can be an issue in systems that + * rely on digital signature uniqueness to ensure transaction integrity. For example, in Bitcoin, + * transaction malleability is an issue because it allows for the modification of transaction + * identifiers (and potentially, transactions themselves) after they're signed but before they're + * confirmed in a block. By enforcing low `s` values, the Bitcoin network reduces the likelihood of + * this occurring, making the system more secure and predictable. + * + * For this reason, it's common practice to normalize ECDSA signatures to a low-S form. This + * form is considered standard and preferable in some systems and is known as the "normalized" + * form of the signature. + * + * This method takes a signature, and if it's high-S, returns the normalized low-S form. If the + * signature is already low-S, it's returned unmodified. It's important to note that this + * method does not change the validity of the signature but makes it compliant with systems that + * enforce low-S signatures. + * + * @example + * ```ts + * const signature = new Uint8Array([...]); // Your ECDSA signature + * const adjustedSignature = await Secp256r1.adjustSignatureToLowS({ signature }); + * // Now 'adjustedSignature' is in the low-S form. + * ``` + * + * @param params - The parameters for the signature adjustment. + * @param params.signature - The ECDSA signature as a `Uint8Array`. + * + * @returns A Promise that resolves to the adjusted signature in low-S form as a `Uint8Array`. + */ + public static async adjustSignatureToLowS({ signature }: { + signature: Uint8Array; + }): Promise { + // Convert the signature to a `Secp256r1.Signature` object. + const signatureObject = secp256r1.Signature.fromCompact(signature); + + if (signatureObject.hasHighS()) { + // Adjust the signature to low-S format if it's high-S. + const adjustedSignatureObject = signatureObject.normalizeS(); + + // Convert the adjusted signature object back to a byte array. + const adjustedSignature = adjustedSignatureObject.toCompactRawBytes(); + + return adjustedSignature; + + } else { + // Return the unmodified signature if it is already in low-S format. + return signature; + } + } + + /** + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * @remarks + * This method takes a private key represented as a byte array (Uint8Array) and + * converts it into a JWK object. The conversion involves extracting the + * elliptic curve point (x and y coordinates) from the private key and encoding + * them into base64url format, alongside other JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'EC' for Elliptic Curve. + * - `crv`: Curve Name, set to 'P-256'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The x-coordinate of the public key point, base64url-encoded. + * - `y`: The y-coordinate of the public key point, base64url-encoded. + * + * This method is useful for converting raw public keys into a standardized + * JSON format, facilitating their use in cryptographic operations and making + * them easy to share and store. + * + * @example + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual private key bytes + * const privateKey = await Secp256r1.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param params - The parameters for the private key conversion. + * @param params.privateKeyBytes - The raw private key as a Uint8Array. + * + * @returns A Promise that resolves to the private key in JWK format. + */ + public static async bytesToPrivateKey({ privateKeyBytes }: { + privateKeyBytes: Uint8Array; + }): Promise { + // Get the elliptic curve points (x and y coordinates) for the provided private key. + const point = await Secp256r1.getCurvePoint({ keyBytes: privateKeyBytes }); + + // Construct the private key in JWK format. + const privateKey: Jwk = { + kty : 'EC', + crv : 'P-256', + d : Convert.uint8Array(privateKeyBytes).toBase64Url(), + x : Convert.uint8Array(point.x).toBase64Url(), + y : Convert.uint8Array(point.y).toBase64Url() + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await computeJwkThumbprint({ jwk: privateKey }); + + return privateKey; + } + + /** + * Converts a raw public key in bytes to its corresponding JSON Web Key (JWK) format. + * + * @remarks + * This method accepts a public key in a byte array (Uint8Array) format and + * transforms it to a JWK object. It involves decoding the elliptic curve point + * (x and y coordinates) from the raw public key bytes and encoding them into + * base64url format, along with setting appropriate JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'EC' for Elliptic Curve. + * - `crv`: Curve Name, set to 'P-256'. + * - `x`: The x-coordinate of the public key point, base64url-encoded. + * - `y`: The y-coordinate of the public key point, base64url-encoded. + * + * This method is useful for converting raw public keys into a standardized + * JSON format, facilitating their use in cryptographic operations and making + * them easy to share and store. + * + * @example + * ```ts + * const publicKeyBytes = new Uint8Array([...]); // Replace with actual public key bytes + * const publicKey = await Secp256r1.bytesToPublicKey({ publicKeyBytes }); + * ``` + * + * @param params - The parameters for the public key conversion. + * @param params.publicKeyBytes - The raw public key as a Uint8Array. + * + * @returns A Promise that resolves to the public key in JWK format. + */ + public static async bytesToPublicKey({ publicKeyBytes }: { + publicKeyBytes: Uint8Array; + }): Promise { + // Get the elliptic curve point (x and y coordinates) for the provided public key. + const point = await Secp256r1.getCurvePoint({ keyBytes: publicKeyBytes }); + + // Construct the public key in JWK format. + const publicKey: Jwk = { + kty : 'EC', + crv : 'P-256', + x : Convert.uint8Array(point.x).toBase64Url(), + y : Convert.uint8Array(point.y).toBase64Url() + }; + + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await computeJwkThumbprint({ jwk: publicKey }); + + return publicKey; + } + + /** + * Converts a public key to its compressed form. + * + * @remarks + * This method takes a public key represented as a byte array and compresses it. Public key + * compression is a process that reduces the size of the public key by removing the y-coordinate, + * making it more efficient for storage and transmission. The compressed key retains the same + * level of security as the uncompressed key. + * + * @example + * ```ts + * const uncompressedPublicKeyBytes = new Uint8Array([...]); // Replace with actual uncompressed public key bytes + * const compressedPublicKey = await Secp256r1.compressPublicKey({ + * publicKeyBytes: uncompressedPublicKeyBytes + * }); + * ``` + * + * @param params - The parameters for the public key compression. + * @param params.publicKeyBytes - The public key as a Uint8Array. + * + * @returns A Promise that resolves to the compressed public key as a Uint8Array. + */ + public static async compressPublicKey({ publicKeyBytes }: { + publicKeyBytes: Uint8Array; + }): Promise { + // Decode Weierstrass points from the public key byte array. + const point = secp256r1.ProjectivePoint.fromHex(publicKeyBytes); + + // Return the compressed form of the public key. + return point.toRawBytes(true); + } + + /** + * Derives the public key in JWK format from a given private key. + * + * @remarks + * This method takes a private key in JWK format and derives its corresponding public key, + * also in JWK format. The derivation process involves converting the private key to a raw + * byte array, then computing the elliptic curve point (x and y coordinates) from this private + * key. These coordinates are then encoded into base64url format to construct the public key in + * JWK format. + * + * The process ensures that the derived public key correctly corresponds to the given private key, + * adhering to the secp256r1 elliptic curve standards. This method is useful in cryptographic + * operations where a public key is needed for operations like signature verification, but only + * the private key is available. + * + * @example + * ```ts + * const privateKey = { ... }; // A Jwk object representing a secp256r1 private key + * const publicKey = await Secp256r1.computePublicKey({ key: privateKey }); + * ``` + * + * @param params - The parameters for the public key derivation. + * @param params.key - The private key in JWK format from which to derive the public key. + * + * @returns A Promise that resolves to the derived public key in JWK format. + */ + public static async computePublicKey({ key }: + ComputePublicKeyParams + ): Promise { + // Convert the provided private key to a byte array. + const privateKeyBytes = await Secp256r1.privateKeyToBytes({ privateKey: key }); + + // Get the elliptic curve point (x and y coordinates) for the provided private key. + const point = await Secp256r1.getCurvePoint({ keyBytes: privateKeyBytes }); + + // Construct the public key in JWK format. + const publicKey: Jwk = { + kty : 'EC', + crv : 'P-256', + x : Convert.uint8Array(point.x).toBase64Url(), + y : Convert.uint8Array(point.y).toBase64Url() + }; + + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await computeJwkThumbprint({ jwk: publicKey }); + + return publicKey; + } + + /** + * Converts an ASN.1 DER encoded ECDSA signature to a compact R+S format. + * + * @remarks + * This method is used for converting an ECDSA signature from the ASN.1 DER encoding to the more + * compact R+S format. This conversion is often required when dealing with ECDSA signatures in + * certain cryptographic standards such as JWS (JSON Web Signature). + * + * The method decodes the DER-encoded signature, extracts the R and S values, and concatenates + * them into a single byte array. This process involves handling the ASN.1 structure to correctly + * parse the R and S values, considering padding and integer encoding specifics of DER. + * + * @example + * ```ts + * const derSignature = new Uint8Array([...]); // Replace with your DER-encoded signature + * const signature = await Secp256r1.convertDerToCompactSignature({ derSignature }); + * ``` + * + * @param params - The parameters for the signature conversion. + * @param params.derSignature - The signature in ASN.1 DER format as a `Uint8Array`. + * + * @returns A Promise that resolves to the signature in compact R+S format as a `Uint8Array`. + */ + public static async convertDerToCompactSignature({ derSignature }: { + derSignature: Uint8Array; + }): Promise { + // Convert the DER-encoded signature into a `Secp256r1.Signature` object. + // This involves parsing the ASN.1 DER structure to extract the R and S components. + const signatureObject = secp256r1.Signature.fromDER(derSignature); + + // Convert the signature object into compact R+S format, which concatenates the R and S values + // into a single byte array. + const compactSignature = signatureObject.toCompactRawBytes(); + + return compactSignature; + } + + /** + * Converts a public key to its uncompressed form. + * + * @remarks + * This method takes a compressed public key represented as a byte array and decompresses it. + * Public key decompression involves reconstructing the y-coordinate from the x-coordinate, + * resulting in the full public key. This method is used when the uncompressed key format is + * required for certain cryptographic operations or interoperability. + * + * @example + * ```ts + * const compressedPublicKeyBytes = new Uint8Array([...]); // Replace with actual compressed public key bytes + * const decompressedPublicKey = await Secp256r1.decompressPublicKey({ + * publicKeyBytes: compressedPublicKeyBytes + * }); + * ``` + * + * @param params - The parameters for the public key decompression. + * @param params.publicKeyBytes - The public key as a Uint8Array. + * + * @returns A Promise that resolves to the uncompressed public key as a Uint8Array. + */ + public static async decompressPublicKey({ publicKeyBytes }: { + publicKeyBytes: Uint8Array; + }): Promise { + // Decode Weierstrass points from the public key byte array. + const point = secp256r1.ProjectivePoint.fromHex(publicKeyBytes); + + // Return the uncompressed form of the public key. + return point.toRawBytes(false); + } + + /** + * Generates a secp256r1 private key in JSON Web Key (JWK) format. + * + * @remarks + * This method creates a new private key suitable for use with the secp256r1 + * elliptic curve. The key is generated using cryptographically secure random + * number generation to ensure its uniqueness and security. The resulting + * private key adheres to the JWK format, specifically tailored for secp256r1, + * making it compatible with common cryptographic standards and easy to use in + * various cryptographic processes. + * + * The private key generated by this method includes the following components: + * - `kty`: Key Type, set to 'EC' for Elliptic Curve. + * - `crv`: Curve Name, set to 'P-256'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The x-coordinate of the public key point, derived from the private key, base64url-encoded. + * - `y`: The y-coordinate of the public key point, derived from the private key, base64url-encoded. + * + * The key is returned in a format suitable for direct use in signin and key agreement operations. + * + * @example + * ```ts + * const privateKey = await Secp256r1.generateKey(); + * ``` + * + * @returns A Promise that resolves to the generated private key in JWK format. + */ + public static async generateKey(): Promise { + // Generate a random private key. + const privateKeyBytes = secp256r1.utils.randomPrivateKey(); + + // Convert private key from bytes to JWK format. + const privateKey = await Secp256r1.bytesToPrivateKey({ privateKeyBytes }); + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await computeJwkThumbprint({ jwk: privateKey }); + + return privateKey; + } + + /** + * Retrieves the public key properties from a given private key in JWK format. + * + * @remarks + * This method extracts the public key portion from a secp256r1 private key in JWK format. It does + * so by removing the private key property 'd' and making a shallow copy, effectively yielding the + * public key. The method sets the 'kid' (key ID) property using the JWK thumbprint if it is not + * already defined. This approach is used under the assumption that a private key in JWK format + * always contains the corresponding public key properties. + * + * Note: This method offers a significant performance advantage, being about 200 times faster + * than `computePublicKey()`. However, it does not mathematically validate the private key, nor + * does it derive the public key from the private key. It simply extracts existing public key + * properties from the private key object. This makes it suitable for scenarios where speed is + * critical and the private key's integrity is already assured. + * + * @example + * ```ts + * const privateKey = { ... }; // A Jwk object representing a secp256r1 private key + * const publicKey = await Secp256r1.getPublicKey({ key: privateKey }); + * ``` + * + * @param params - The parameters for retrieving the public key properties. + * @param params.key - The private key in JWK format. + * + * @returns A Promise that resolves to the public key in JWK format. + */ + public static async getPublicKey({ key }: + GetPublicKeyParams + ): Promise { + // Verify the provided JWK represents an elliptic curve (EC) secp256r1 private key. + if (!(isEcPrivateJwk(key) && key.crv === 'P-256')) { + throw new Error(`Secp256r1: The provided key is not a 'P-256' private JWK.`); + } + + // Remove the private key property ('d') and make a shallow copy of the provided key. + let { d, ...publicKey } = key; + + // If the key ID is undefined, set it to the JWK thumbprint. + publicKey.kid ??= await computeJwkThumbprint({ jwk: publicKey }); + + return publicKey; + } + + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * @remarks + * This method takes a private key in JWK format and extracts its raw byte representation. + * It specifically focuses on the 'd' parameter of the JWK, which represents the private + * key component in base64url encoding. The method decodes this value into a byte array. + * + * This conversion is essential for operations that require the private key in its raw + * binary form, such as certain low-level cryptographic operations or when interfacing + * with systems and libraries that expect keys in a byte array format. + * + * @example + * ```ts + * const privateKey = { ... }; // An X25519 private key in JWK format + * const privateKeyBytes = await Secp256r1.privateKeyToBytes({ privateKey }); + * ``` + * + * @param params - The parameters for the private key conversion. + * @param params.privateKey - The private key in JWK format. + * + * @returns A Promise that resolves to the private key as a Uint8Array. + */ + public static async privateKeyToBytes({ privateKey }: { + privateKey: Jwk; + }): Promise { + // Verify the provided JWK represents a valid EC P-256 private key. + if (!isEcPrivateJwk(privateKey)) { + throw new Error(`Secp256r1: The provided key is not a valid EC private key.`); + } + + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + + return privateKeyBytes; + } + + /** + * Converts a public key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * @remarks + * This method accepts a public key in JWK format and converts it into its raw binary + * form. The conversion process involves decoding the 'x' and 'y' parameters of the JWK + * (which represent the x and y coordinates of the elliptic curve point, respectively) + * from base64url format into a byte array. The method then concatenates these values, + * along with a prefix indicating the key format, to form the full public key. + * + * This function is particularly useful for use cases where the public key is needed + * in its raw byte format, such as for certain cryptographic operations or when + * interfacing with systems that require raw key formats. + * + * @example + * ```ts + * const publicKey = { ... }; // A Jwk public key object + * const publicKeyBytes = await Secp256r1.publicKeyToBytes({ publicKey }); + * ``` + * + * @param params - The parameters for the public key conversion. + * @param params.publicKey - The public key in JWK format. + * + * @returns A Promise that resolves to the public key as a Uint8Array. + */ + public static async publicKeyToBytes({ publicKey }: { + publicKey: Jwk; + }): Promise { + // Verify the provided JWK represents a valid EC P-256 public key, which must have a 'y' value. + if (!(isEcPublicJwk(publicKey) && publicKey.y)) { + throw new Error(`Secp256r1: The provided key is not a valid EC public key.`); + } + + // Decode the provided public key to bytes. + const prefix = new Uint8Array([0x04]); // Designates an uncompressed key. + const x = Convert.base64Url(publicKey.x).toUint8Array(); + const y = Convert.base64Url(publicKey.y).toUint8Array(); + + // Concatenate the prefix, x-coordinate, and y-coordinate as a single byte array. + const publicKeyBytes = new Uint8Array([...prefix, ...x, ...y]); + + return publicKeyBytes; + } + + /** + * Computes an RFC6090-compliant Elliptic Curve Diffie-Hellman (ECDH) shared secret + * using secp256r1 private and public keys in JSON Web Key (JWK) format. + * + * @remarks + * This method facilitates the ECDH key agreement protocol, which is a method of securely + * deriving a shared secret between two parties based on their private and public keys. + * It takes the private key of one party (privateKeyA) and the public key of another + * party (publicKeyB) to compute a shared secret. The shared secret is derived from the + * x-coordinate of the elliptic curve point resulting from the multiplication of the + * public key with the private key. + * + * Note: When performing Elliptic Curve Diffie-Hellman (ECDH) key agreement, + * the resulting shared secret is a point on the elliptic curve, which + * consists of an x-coordinate and a y-coordinate. With a 256-bit curve like + * secp256r1, each of these coordinates is 32 bytes (256 bits) long. However, + * in the ECDH process, it's standard practice to use only the x-coordinate + * of the shared secret point as the resulting shared key. This is because + * the y-coordinate does not add to the entropy of the key, and both parties + * can independently compute the x-coordinate. Consquently, this implementation + * omits the y-coordinate for simplicity and standard compliance. + * + * @example + * ```ts + * const privateKeyA = { ... }; // A Jwk private key object for party A + * const publicKeyB = { ... }; // A Jwk public key object for party B + * const sharedSecret = await Secp256r1.sharedSecret({ + * privateKeyA, + * publicKeyB + * }); + * ``` + * + * @param params - The parameters for the shared secret computation. + * @param params.privateKeyA - The private key in JWK format of one party. + * @param params.publicKeyB - The public key in JWK format of the other party. + * + * @returns A Promise that resolves to the computed shared secret as a Uint8Array. + */ + public static async sharedSecret({ privateKeyA, publicKeyB }: { + privateKeyA: Jwk; + publicKeyB: Jwk; + }): Promise { + // Ensure that keys from the same key pair are not specified. + if ('x' in privateKeyA && 'x' in publicKeyB && privateKeyA.x === publicKeyB.x) { + throw new Error(`Secp256r1: ECDH shared secret cannot be computed from a single key pair's public and private keys.`); + } + + // Convert the provided private and public keys to bytes. + const privateKeyABytes = await Secp256r1.privateKeyToBytes({ privateKey: privateKeyA }); + const publicKeyBBytes = await Secp256r1.publicKeyToBytes({ publicKey: publicKeyB }); + + // Compute the compact representation shared secret between the public and private keys. + const sharedSecret = secp256r1.getSharedSecret(privateKeyABytes, publicKeyBBytes, true); + + // Remove the leading byte that indicates the sign of the y-coordinate + // of the point on the elliptic curve. See note above. + return sharedSecret.slice(1); + } + + /** + * Generates an RFC6979-compliant ECDSA signature of given data using a secp256r1 private key. + * + * @remarks + * This method signs the provided data with a specified private key using the ECDSA + * (Elliptic Curve Digital Signature Algorithm) signature algorithm, as defined in RFC6979. + * The data to be signed is first hashed using the SHA-256 algorithm, and this hash is then + * signed using the private key. The output is a digital signature in the form of a + * Uint8Array, which uniquely corresponds to both the data and the private key used for signing. + * + * This method is commonly used in cryptographic applications to ensure data integrity and + * authenticity. The signature can later be verified by parties with access to the corresponding + * public key, ensuring that the data has not been tampered with and was indeed signed by the + * holder of the private key. + * + * @example + * ```ts + * const data = new TextEncoder().encode('Messsage'); // Data to be signed + * const privateKey = { ... }; // A Jwk object representing a secp256r1 private key + * const signature = await Secp256r1.sign({ + * key: privateKey, + * data + * }); + * ``` + * + * @param params - The parameters for the signing operation. + * @param params.key - The private key to use for signing, represented in JWK format. + * @param params.data - The data to sign, represented as a Uint8Array. + * + * @returns A Promise that resolves to the signature as a Uint8Array. + */ + public static async sign({ data, key }: + SignParams + ): Promise { + // Convert the private key from JWK format to bytes. + const privateKeyBytes = await Secp256r1.privateKeyToBytes({ privateKey: key }); + + // Generate a digest of the data using the SHA-256 hash function. + const digest = sha256(data); + + // Sign the provided data using the ECDSA algorithm. + // The `Secp256r1.sign` operation returns a signature object with { r, s, recovery } properties. + const signatureObject = secp256r1.sign(digest, privateKeyBytes); + + // Convert the signature object to Uint8Array. + const signature = signatureObject.toCompactRawBytes(); + + return signature; + } + + /** + * Validates a given private key to ensure its compliance with the secp256r1 curve standards. + * + * @remarks + * This method checks whether a provided private key is a valid 32-byte number and falls within + * the range defined by the secp256r1 curve's order. It is essential for ensuring the private + * key's mathematical correctness in the context of secp256r1-based cryptographic operations. + * + * Note that this validation strictly pertains to the key's format and numerical validity; it does + * not assess whether the key corresponds to a known entity or its security status (e.g., whether + * it has been compromised). + * + * @example + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // A 32-byte private key + * const isValid = await Secp256r1.validatePrivateKey({ privateKeyBytes }); + * console.log(isValid); // true or false based on the key's validity + * ``` + * + * @param params - The parameters for the key validation. + * @param params.privateKeyBytes - The private key to validate, represented as a Uint8Array. + * + * @returns A Promise that resolves to a boolean indicating whether the private key is valid. + */ + public static async validatePrivateKey({ privateKeyBytes }: { + privateKeyBytes: Uint8Array; + }): Promise { + return secp256r1.utils.isValidPrivateKey(privateKeyBytes); + } + + /** + * Validates a given public key to confirm its mathematical correctness on the secp256r1 curve. + * + * @remarks + * This method checks if the provided public key represents a valid point on the secp256r1 curve. + * It decodes the key's Weierstrass points (x and y coordinates) and verifies their validity + * against the curve's parameters. A valid point must lie on the curve and meet specific + * mathematical criteria defined by the curve's equation. + * + * It's important to note that this method does not verify the key's ownership or whether it has + * been compromised; it solely focuses on the key's adherence to the curve's mathematical + * principles. + * + * @example + * ```ts + * const publicKeyBytes = new Uint8Array([...]); // A public key in byte format + * const isValid = await Secp256r1.validatePublicKey({ publicKeyBytes }); + * console.log(isValid); // true if the key is valid on the secp256r1 curve, false otherwise + * ``` + * + * @param params - The parameters for the key validation. + * @param params.publicKeyBytes - The public key to validate, represented as a Uint8Array. + * + * @returns A Promise that resolves to a boolean indicating the public key's validity on + * the secp256r1 curve. + */ + public static async validatePublicKey({ publicKeyBytes }: { + publicKeyBytes: Uint8Array; + }): Promise { + try { + // Decode Weierstrass points from key bytes. + const point = secp256r1.ProjectivePoint.fromHex(publicKeyBytes); + + // Check if points are on the Short Weierstrass curve. + point.assertValidity(); + + } catch(error: any) { + return false; + } + + return true; + } + + /** + * Verifies an RFC6979-compliant ECDSA signature against given data and a secp256r1 public key. + * + * @remarks + * This method validates a digital signature to ensure that it was generated by the holder of the + * corresponding private key and that the signed data has not been altered. The signature + * verification is performed using the ECDSA (Elliptic Curve Digital Signature Algorithm) as + * specified in RFC6979. The data to be verified is first hashed using the SHA-256 algorithm, and + * this hash is then used along with the public key to verify the signature. + * + * The method returns a boolean value indicating whether the signature is valid. A valid signature + * proves that the signed data was indeed signed by the owner of the private key corresponding to + * the provided public key and that the data has not been tampered with since it was signed. + * + * Note: The verification process does not consider the malleability of low-s signatures, which + * may be relevant in certain contexts, such as Bitcoin transactions. + * + * @example + * ```ts + * const data = new TextEncoder().encode('Messsage'); // Data that was signed + * const publicKey = { ... }; // Public key in JWK format corresponding to the private key that signed the data + * const signature = new Uint8Array([...]); // Signature to verify + * const isSignatureValid = await Secp256r1.verify({ + * key: publicKey, + * signature, + * data + * }); + * console.log(isSignatureValid); // true if the signature is valid, false otherwise + * ``` + * + * @param params - The parameters for the signature verification. + * @param params.key - The public key used for verification, represented in JWK format. + * @param params.signature - The signature to verify, represented as a Uint8Array. + * @param params.data - The data that was signed, represented as a Uint8Array. + * + * @returns A Promise that resolves to a boolean indicating whether the signature is valid. + */ + public static async verify({ key, signature, data }: + VerifyParams + ): Promise { + // Convert the public key from JWK format to bytes. + const publicKeyBytes = await Secp256r1.publicKeyToBytes({ publicKey: key }); + + // Generate a digest of the data using the SHA-256 hash function. + const digest = sha256(data); + + /** Perform the verification of the signature. + * This verify operation has the malleability check disabled. Guaranteed support + * for low-s signatures across languages is unlikely especially in the context + * of SSI. Notable Cloud KMS providers do not natively support it either. It is + * also worth noting that low-s signatures are a requirement for Bitcoin. */ + const isValid = secp256r1.verify(signature, digest, publicKeyBytes, { lowS: false }); + + return isValid; + } + + /** + * Returns the elliptic curve point (x and y coordinates) for a given secp256r1 key. + * + * @remarks + * This method extracts the elliptic curve point from a given secp256r1 key, whether + * it's a private or a public key. For a private key, the method first computes the + * corresponding public key and then extracts the x and y coordinates. For a public key, + * it directly returns these coordinates. The coordinates are represented as Uint8Array. + * + * The x and y coordinates represent the key's position on the elliptic curve and can be + * used in various cryptographic operations, such as digital signatures or key agreement + * protocols. + * + * @example + * ```ts + * // For a private key + * const privateKey = new Uint8Array([...]); // A 32-byte private key + * const { x: xFromPrivateKey, y: yFromPrivateKey } = await Secp256r1.getCurvePoint({ keyBytes: privateKey }); + * + * // For a public key + * const publicKey = new Uint8Array([...]); // A 33-byte or 65-byte public key + * const { x: xFromPublicKey, y: yFromPublicKey } = await Secp256r1.getCurvePoint({ keyBytes: publicKey }); + * ``` + * + * @param params - The parameters for the curve point decoding operation. + * @param params.keyBytes - The key for which to get the elliptic curve point. + * Can be either a private key or a public key. + * The key should be passed as a `Uint8Array`. + * + * @returns A Promise that resolves to an object with properties 'x' and 'y', + * each being a Uint8Array representing the x and y coordinates of the key point on the + * elliptic curve. + */ + private static async getCurvePoint({ keyBytes }: { + keyBytes: Uint8Array; + }): Promise> { + // If key is a private key, first compute the public key. + if (keyBytes.byteLength === 32) { + keyBytes = secp256r1.getPublicKey(keyBytes); + } + + // Decode Weierstrass affine point from key bytes. + const point = secp256r1.ProjectivePoint.fromHex(keyBytes); + + // Get x- and y-coordinate values and convert to Uint8Array. + const x = numberToBytesBE(point.x, 32); + const y = numberToBytesBE(point.y, 32); + + return { x, y }; + } +} + +export { Secp256r1 as P256 }; \ No newline at end of file diff --git a/packages/crypto/src/primitives/xchacha20-poly1305.ts b/packages/crypto/src/primitives/xchacha20-poly1305.ts index 510758ee7..4eea6d177 100644 --- a/packages/crypto/src/primitives/xchacha20-poly1305.ts +++ b/packages/crypto/src/primitives/xchacha20-poly1305.ts @@ -15,7 +15,7 @@ import { computeJwkThumbprint, isOctPrivateJwk } from '../jose/jwk.js'; * a strong level of security for message authentication, verifying the integrity and * authenticity of the data during decryption. */ -const POLY1305_TAG_LENGTH = 16; +export const POLY1305_TAG_LENGTH = 16; /** * The `XChaCha20Poly1305` class provides a suite of utilities for cryptographic operations @@ -119,40 +119,36 @@ export class XChaCha20Poly1305 { * ```ts * const encryptedData = new Uint8Array([...]); // Encrypted data * const nonce = new Uint8Array(24); // 24-byte nonce - * const tag = new Uint8Array([...]); // Authentication tag * const additionalData = new Uint8Array([...]); // Optional AAD * const key = { ... }; // A Jwk object representing the XChaCha20-Poly1305 key * const decryptedData = await XChaCha20Poly1305.decrypt({ * data: encryptedData, * nonce, - * tag, * additionalData, * key * }); * ``` * * @param params - The parameters for the decryption operation. - * @param params.data - The encrypted data to decrypt, represented as a Uint8Array. + * @param params.data - The encrypted data to decrypt including the authentication tag, + * represented as a Uint8Array. * @param params.key - The key to use for decryption, represented in JWK format. * @param params.nonce - The nonce used during the encryption process. - * @param params.tag - The authentication tag generated during encryption. * @param params.additionalData - Optional additional authenticated data. * * @returns A Promise that resolves to the decrypted data as a Uint8Array. */ - public static async decrypt({ data, key, nonce, tag, additionalData }: { + public static async decrypt({ data, key, nonce, additionalData }: { additionalData?: Uint8Array; data: Uint8Array; key: Jwk; nonce: Uint8Array; - tag: Uint8Array; }): Promise { // Convert the private key from JWK format to bytes. const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey: key }); const xc20p = xchacha20poly1305(privateKeyBytes, nonce, additionalData); - const ciphertext = new Uint8Array([...data, ...tag]); - const plaintext = xc20p.decrypt(ciphertext); + const plaintext = xc20p.decrypt(data); return plaintext; } @@ -173,7 +169,7 @@ export class XChaCha20Poly1305 { * const nonce = utils.randomBytes(24); // 24-byte nonce * const additionalData = new TextEncoder().encode('Associated data'); // Optional AAD * const key = { ... }; // A Jwk object representing an XChaCha20-Poly1305 key - * const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + * const encryptedData = await XChaCha20Poly1305.encrypt({ * data, * nonce, * additionalData, @@ -187,25 +183,22 @@ export class XChaCha20Poly1305 { * @param params.nonce - A 24-byte nonce for the encryption process. * @param params.additionalData - Optional additional authenticated data. * - * @returns A Promise that resolves to an object containing the encrypted data (`ciphertext`) and - * the authentication tag (`tag`). + * @returns A Promise that resolves to a byte array containing the encrypted data and the + * authentication tag. */ public static async encrypt({ data, key, nonce, additionalData}: { additionalData?: Uint8Array; data: Uint8Array; key: Jwk; nonce: Uint8Array; - }): Promise<{ ciphertext: Uint8Array, tag: Uint8Array }> { + }): Promise { // Convert the private key from JWK format to bytes. const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey: key }); const xc20p = xchacha20poly1305(privateKeyBytes, nonce, additionalData); - const cipherOutput = xc20p.encrypt(data); - - const ciphertext = cipherOutput.subarray(0, -POLY1305_TAG_LENGTH); - const tag = cipherOutput.subarray(-POLY1305_TAG_LENGTH); + const ciphertext = xc20p.encrypt(data); - return { ciphertext, tag }; + return ciphertext; } /** diff --git a/packages/crypto/src/types/key-compressor.ts b/packages/crypto/src/types/key-compressor.ts new file mode 100644 index 000000000..3369c3c7c --- /dev/null +++ b/packages/crypto/src/types/key-compressor.ts @@ -0,0 +1,25 @@ +/** + * `KeyCompressor` interface for converting public keys between compressed and uncompressed form. + */ +export interface KeyCompressor { + + /** + * Converts a public key to its compressed form. + * + * @param params - The parameters for the public key compression. + * @param params.publicKeyBytes - The public key as a Uint8Array. + * + * @returns A Promise that resolves to the compressed public key as a Uint8Array. + */ + compressPublicKey(params: { publicKeyBytes: Uint8Array }): Promise; + + /** + * Converts a public key to its uncompressed form. + * + * @param params - The parameters for the public key decompression. + * @param params.publicKeyBytes - The public key as a Uint8Array. + * + * @returns A Promise that resolves to the uncompressed public key as a Uint8Array. + */ + decompressPublicKey(params: { publicKeyBytes: Uint8Array }): Promise; +} \ No newline at end of file diff --git a/packages/crypto/src/types/key-generator.ts b/packages/crypto/src/types/key-generator.ts index 672529bc1..37c92b9ae 100644 --- a/packages/crypto/src/types/key-generator.ts +++ b/packages/crypto/src/types/key-generator.ts @@ -65,4 +65,55 @@ export interface AsymmetricKeyGenerator< * @returns A Promise resolving to the public key in JWK format. */ getPublicKey(params: GetPublicKeyInput): Promise; -} \ No newline at end of file +} + +/** + * Infers the supported algorithm type from the `generateKey` method of a key generator. + * + * @remarks + * The `InferKeyGeneratorAlgorithm` utility type extracts the algorithm type from the input + * parameters of the `generateKey` method implemented in a key generator. This type is useful when + * working with various cryptographic key generators, as it enables TypeScript to infer the + * supported algorithms based on the key generator's implementation. This inference ensures type + * safety and improves developer experience by providing relevant suggestions and checks for the + * supported algorithms during development. + * + * This utility type can be particularly advantageous in contexts where the specific key generator + * may vary, but the code needs to adapt dynamically based on the supported algorithms of the + * provided key generator instance. + * + * @example + * ```ts + * export interface MyKmsGenerateKeyParams extends KmsGenerateKeyParams { + * algorithm: 'Ed25519' | 'secp256k1'; + * } + * + * class MyKms implements KeyGenerator { + * generateKey(params: MyKmsGenerateKeyParams): Promise { + * // Implementation for generating a key... + * } + * } + * + * type SupportedAlgorithms = InferKeyGeneratorAlgorithm; + * // `SupportedAlgorithms` will be inferred as 'Ed25519' | 'secp256k1' + * ``` + * + * @template T - The type of the key generator from which to infer the algorithm type. + */ +export type InferKeyGeneratorAlgorithm = T extends { + /** + * The `generateKey` method signature from which the algorithm type is inferred. + * This is an internal implementation detail and not part of the public API. + */ + generateKey(params: infer P): any; + } + ? P extends { + /** + * The `algorithm` property within the parameters of `generateKey`. + * This internal element is used to infer the algorithm type. + */ + algorithm: infer A + } + ? A + : never + : never; \ No newline at end of file diff --git a/packages/crypto/src/types/params-kms.ts b/packages/crypto/src/types/params-kms.ts index 705a32f98..a44acc1ce 100644 --- a/packages/crypto/src/types/params-kms.ts +++ b/packages/crypto/src/types/params-kms.ts @@ -75,7 +75,12 @@ export interface KmsGenerateKeyParams { algorithm: AlgorithmIdentifier; } +/** + * Parameters for computing the Key URI of a public key. Intended for use with a Key Management + * System. + */ export interface KmsGetKeyUriParams { + /** A {@link Jwk} containing the public key for which the Key URI will be computed. */ key: Jwk; } @@ -136,7 +141,9 @@ export interface KmsWrapKeyParams { wrapAlgorithm: AlgorithmIdentifier; } - +/** + * Parameters for unwrapping a key using a KMS. Intended for use with a Key Management System. + */ export interface KmsUnwrapKeyParams { /** The wrapped key in a byte array. */ wrappedKey: Uint8Array; diff --git a/packages/crypto/src/utils.ts b/packages/crypto/src/utils.ts index fc3fcdeee..3dce127de 100644 --- a/packages/crypto/src/utils.ts +++ b/packages/crypto/src/utils.ts @@ -1,5 +1,4 @@ import { crypto } from '@noble/hashes/crypto'; -import { Convert, Multicodec } from '@web5/common'; import { randomBytes as nobleRandomBytes } from '@noble/hashes/utils'; /** @@ -64,39 +63,6 @@ export function checkValidProperty(params: { } } -/** - * 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 = keyToMultibaseId({ key, multicodecName: 'ed25519-pub' }); - * ``` - * - * @param params - The parameters for the conversion. - * @param params.key - The cryptographic key as a Uint8Array. - * @param params.multicodecCode - Optional multicodec code to prefix the key with. - * @param params.multicodecName - Optional multicodec name corresponding to the code. - * @returns The multibase identifier as a string. - */ -export function keyToMultibaseId({ key, multicodecCode, multicodecName }: { - key: Uint8Array, - multicodecCode?: number, - multicodecName?: string -}): string { - const prefixedKey = Multicodec.addPrefix({ code: multicodecCode, data: key, name: multicodecName }); - const prefixedKeyB58 = Convert.uint8Array(prefixedKey).toBase58Btc(); - const multibaseKeyId = Convert.base58Btc(prefixedKeyB58).toMultibase(); - - return multibaseKeyId; -} - /** * Checks if the Web Crypto API is supported in the current runtime environment. * @@ -132,35 +98,6 @@ export function isWebCryptoSupported(): boolean { } } -/** - * 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. - */ -export function multibaseIdToKey({ multibaseKeyId }: { - multibaseKeyId: string -}): { key: Uint8Array, multicodecCode: number, multicodecName: string } { - const prefixedKeyB58 = Convert.multibase(multibaseKeyId).toBase58Btc(); - const prefixedKey = Convert.base58Btc(prefixedKeyB58).toUint8Array(); - const { code, data, name } = Multicodec.removePrefix({ prefixedData: prefixedKey }); - - return { key: data, multicodecCode: code, multicodecName: name }; -} - /** * Generates secure pseudorandom values of the specified length using * `crypto.getRandomValues`, which defers to the operating system. diff --git a/packages/crypto/tests/algorithms/aes-ctr.spec.ts b/packages/crypto/tests/algorithms/aes-ctr.spec.ts index 28c151bb0..7111940e9 100644 --- a/packages/crypto/tests/algorithms/aes-ctr.spec.ts +++ b/packages/crypto/tests/algorithms/aes-ctr.spec.ts @@ -79,7 +79,7 @@ describe('AesCtrAlgorithm', () => { const algorithms = ['A128CTR', 'A192CTR', 'A256CTR'] as const; for (const algorithm of algorithms) { const privateKey = await aesCtr.generateKey({ algorithm }); - if (!('k' in privateKey)) throw new Error('Expected privateKey to have a `k` property'); // TypeScript type guard. + if (!privateKey.k) throw new Error('Expected privateKey to have a `k` property'); // TypeScript type guard. const privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); expect(privateKeyBytes.byteLength * 8).to.equal(parseInt(algorithm.slice(1, 4))); } diff --git a/packages/crypto/tests/algorithms/aes-gcm.spec.ts b/packages/crypto/tests/algorithms/aes-gcm.spec.ts index cca1cb966..132431a5f 100644 --- a/packages/crypto/tests/algorithms/aes-gcm.spec.ts +++ b/packages/crypto/tests/algorithms/aes-gcm.spec.ts @@ -80,7 +80,7 @@ describe('AesGcmAlgorithm', () => { const algorithms = ['A128GCM', 'A192GCM', 'A256GCM'] as const; for (const algorithm of algorithms) { const privateKey = await aesGcm.generateKey({ algorithm }); - if (!('k' in privateKey)) throw new Error('Expected privateKey to have a `k` property'); // TypeScript type guard. + if (!privateKey.k) throw new Error('Expected privateKey to have a `k` property'); // TypeScript type guard. const privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); expect(privateKeyBytes.byteLength * 8).to.equal(parseInt(algorithm.slice(1, 4))); } diff --git a/packages/crypto/tests/algorithms/ecdsa.spec.ts b/packages/crypto/tests/algorithms/ecdsa.spec.ts index 0a3b86009..e2c0602b2 100644 --- a/packages/crypto/tests/algorithms/ecdsa.spec.ts +++ b/packages/crypto/tests/algorithms/ecdsa.spec.ts @@ -55,6 +55,45 @@ describe('EcdsaAlgorithm', () => { expect(publicKey).to.have.property('crv', 'secp256k1'); }); + it('accepts secp256k1 as an alias for the ES256K algorithm identifier', async () => { + // Setup. + const privateKey = await ecdsa.generateKey({ algorithm: 'secp256k1' }); + + // Test the method. + const publicKey = await ecdsa.computePublicKey({ key: privateKey }); + + // Validate the result. + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('alg', 'ES256K'); + expect(publicKey).to.have.property('crv', 'secp256k1'); + }); + + it('supports ECDSA using secp256r1 curve and SHA-256', async () => { + // Setup. + const privateKey = await ecdsa.generateKey({ algorithm: 'ES256' }); + + // Test the method. + const publicKey = await ecdsa.computePublicKey({ key: privateKey }); + + // Validate the result. + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('alg', 'ES256'); + expect(publicKey).to.have.property('crv', 'P-256'); + }); + + it('accepts secp256r1 as an alias for the ES256 algorithm identifier', async () => { + // Setup. + const privateKey = await ecdsa.generateKey({ algorithm: 'secp256r1' }); + + // Test the method. + const publicKey = await ecdsa.computePublicKey({ key: privateKey }); + + // Validate the result. + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('alg', 'ES256'); + expect(publicKey).to.have.property('crv', 'P-256'); + }); + it('throws an error if the key provided is not an EC private key', async () => { // Setup. const privateKey: Jwk = { @@ -80,7 +119,6 @@ describe('EcdsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const privateKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', d : 'd', kty : 'EC', @@ -119,6 +157,30 @@ describe('EcdsaAlgorithm', () => { expect(privateKey).to.have.property('alg', 'ES256K'); expect(privateKey).to.have.property('crv', 'secp256k1'); }); + + it('accepts secp256k1 as an alias for the ES256K algorithm identifier', async () => { + // Test the method. + const privateKey = await ecdsa.generateKey({ algorithm: 'secp256k1' }); + + expect(privateKey).to.have.property('alg', 'ES256K'); + expect(privateKey).to.have.property('crv', 'secp256k1'); + }); + + it('supports ECDSA using secp256r1 curve and SHA-256', async () => { + // Test the method. + const privateKey = await ecdsa.generateKey({ algorithm: 'ES256' }); + + expect(privateKey).to.have.property('alg', 'ES256'); + expect(privateKey).to.have.property('crv', 'P-256'); + }); + + it('accepts secp256r1 as an alias for the ES256 algorithm identifier', async () => { + // Test the method. + const privateKey = await ecdsa.generateKey({ algorithm: 'secp256r1' }); + + expect(privateKey).to.have.property('alg', 'ES256'); + expect(privateKey).to.have.property('crv', 'P-256'); + }); }); describe('getPublicKey()', () => { @@ -158,6 +220,19 @@ describe('EcdsaAlgorithm', () => { expect(publicKey).to.have.property('crv', 'secp256k1'); }); + it('supports ECDSA using secp256r1 curve and SHA-256', async () => { + // Setup. + const privateKey = await ecdsa.generateKey({ algorithm: 'ES256' }); + + // Test the method. + const publicKey = await ecdsa.getPublicKey({ key: privateKey }); + + // Validate the result. + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('alg', 'ES256'); + expect(publicKey).to.have.property('crv', 'P-256'); + }); + it('throws an error if the key provided is not an EC private key', async () => { // Setup. const privateKey: Jwk = { @@ -183,7 +258,6 @@ describe('EcdsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const privateKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', d : 'd', kty : 'EC', @@ -225,6 +299,28 @@ describe('EcdsaAlgorithm', () => { expect(signature).to.have.length(64); }); + it('supports ECDSA using secp256k1 curve and SHA-256', async () => { + // Setup. + const privateKey = await ecdsa.generateKey({ algorithm: 'ES256K'}); + + // Test the method. + const signature = await ecdsa.sign({ key: privateKey, data }); + + // Validate the result. + expect(signature).to.have.length(64); + }); + + it('supports ECDSA using secp256r1 curve and SHA-256', async () => { + // Setup. + const privateKey = await ecdsa.generateKey({ algorithm: 'ES256'}); + + // Test the method. + const signature = await ecdsa.sign({ key: privateKey, data }); + + // Validate the result. + expect(signature).to.have.length(64); + }); + it('throws an error if the key provided is not an EC private key', async () => { // Setup. const privateKey: Jwk = { @@ -250,7 +346,6 @@ describe('EcdsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const privateKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', d : 'd', kty : 'EC', @@ -310,6 +405,32 @@ describe('EcdsaAlgorithm', () => { expect(isValid).to.be.false; }); + it('supports ECDSA using secp256k1 curve and SHA-256', async () => { + // Setup. + privateKey = await ecdsa.generateKey({ algorithm: 'ES256K' }); + publicKey = await ecdsa.getPublicKey({ key: privateKey }); + signature = await ecdsa.sign({ key: privateKey, data }); + + // Test the method. + const isValid = await ecdsa.verify({ key: publicKey, signature, data }); + + // Validate the result. + expect(isValid).to.be.true; + }); + + it('supports ECDSA using secp256r1 curve and SHA-256', async () => { + // Setup. + privateKey = await ecdsa.generateKey({ algorithm: 'ES256' }); + publicKey = await ecdsa.getPublicKey({ key: privateKey }); + signature = await ecdsa.sign({ key: privateKey, data }); + + // Test the method. + const isValid = await ecdsa.verify({ key: publicKey, signature, data }); + + // Validate the result. + expect(isValid).to.be.true; + }); + it('throws an error if the key provided is not an EC public key', async () => { // Setup. const publicKey: Jwk = { @@ -334,7 +455,6 @@ describe('EcdsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const publicKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', kty : 'EC', x : 'x', diff --git a/packages/crypto/tests/algorithms/eddsa.spec.ts b/packages/crypto/tests/algorithms/eddsa.spec.ts index 3175c04bd..df7bccc60 100644 --- a/packages/crypto/tests/algorithms/eddsa.spec.ts +++ b/packages/crypto/tests/algorithms/eddsa.spec.ts @@ -81,7 +81,6 @@ describe('EdDsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const privateKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', d : 'd', kty : 'OKP', @@ -183,7 +182,6 @@ describe('EdDsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const privateKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', d : 'd', kty : 'OKP', @@ -250,7 +248,6 @@ describe('EdDsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const privateKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', d : 'd', kty : 'OKP', @@ -334,7 +331,6 @@ describe('EdDsaAlgorithm', () => { it('throws an error for an unsupported curve', async () => { // Setup. const publicKey: Jwk = { - // @ts-expect-error because an unsupported curve is intentionally provided. crv : 'unsupported-curve', kty : 'OKP', x : 'x' diff --git a/packages/crypto/tests/fixtures/test-vectors/jose.ts b/packages/crypto/tests/fixtures/test-vectors/jose.ts deleted file mode 100644 index c5b0de666..000000000 --- a/packages/crypto/tests/fixtures/test-vectors/jose.ts +++ /dev/null @@ -1,135 +0,0 @@ -export const joseToMulticodecTestVectors = [ - { - output : { code: 237, name: 'ed25519-pub' }, - input : { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', - }, - }, - { - output : { code: 4864, name: 'ed25519-priv' }, - input : { - d : '', - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'c5UR1q2r1lOT_ygDhSkU3paf5Bmukg-jX-1t4kIKJvA', - }, - }, - { - output : { code: 231, name: 'secp256k1-pub' }, - input : { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', - }, - }, - { - output : { code: 4865, name: 'secp256k1-priv' }, - input : { - d : '', - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', - }, - }, - { - output : { code: 236, name: 'x25519-pub' }, - input : { - crv : 'X25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', - }, - }, - { - output : { code: 4866, name: 'x25519-priv' }, - input : { - d : '', - crv : 'X25519', - kty : 'OKP', - x : 'MBZd77wAy5932AEP7MHXOevv_MLzzD9OP_fZAOlnIWM', - }, - }, -]; - -export const jwkToThumbprintTestVectors = [ - { - output : 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs', - input : { - kty : 'RSA', - n : '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', - e : 'AQAB', - alg : 'RS256', - kid : '2011-04-29', - }, - }, - { - output : 'legaImFEtXYAJYZ8_ZGbZnx-bhc_9nN53pxGpOum3Io', - input : { - alg : 'A128CBC', - kty : 'oct', - k : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', - }, - }, - { - output : 'dwzDb6KNsqS3QMTqH0jfBHcoHJzYZBc5scB5n5VLe1E', - input : { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', - }, - }, - { - output : 'KCfBQ0EA2cWr1Kbt-mnlj8LQ9C2AJfcuEm8mtgOe7wQ', - input : { - crv : 'X25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', - }, - }, - { - output : 'TQdUBtR3MvnNE-7p5sotzCGgZNyQC7EgsiKQz1Erzc4', - input : { - d : '', - crv : 'X25519', - kty : 'OKP', - x : 'MBZd77wAy5932AEP7MHXOevv_MLzzD9OP_fZAOlnIWM', - }, - }, -]; - -export const jwkToMultibaseIdTestVectors = [ - { - input: { - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', - }, - output: 'zQ3sheTFzDvGpXAc9AXtwGF3MW1CusKovnwM4pSsUamqKCyLB', - }, - { - input: { - crv : 'X25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', - }, - output: 'z6LSjQhGhqqYgrFsNFoZL9wzuKpS1xQ7YNE6fnLgSyW2hUt2', - }, - { - input: { - crv : 'Ed25519', - kty : 'OKP', - x : 'wwk7wOlocpOHDopgc0cZVCnl_7zFrp-JpvZe9vr5500' - }, - output: 'z6MksabiHWJ5wQqJGDzxw1EiV5zi6BE6QRENTnHBcKHSqLaQ', - }, -]; diff --git a/packages/crypto/tests/fixtures/test-vectors/jwk.ts b/packages/crypto/tests/fixtures/test-vectors/jwk.ts new file mode 100644 index 000000000..2f32a0937 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/jwk.ts @@ -0,0 +1,47 @@ +export const jwkToThumbprintTestVectors = [ + { + input: { + kty : 'RSA', + n : '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', + e : 'AQAB', + alg : 'RS256', + kid : '2011-04-29', + }, + output: 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs', + }, + { + input: { + alg : 'A128CBC', + kty : 'oct', + k : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', + }, + output: 'legaImFEtXYAJYZ8_ZGbZnx-bhc_9nN53pxGpOum3Io', + }, + { + input: { + alg : 'ES256K', + crv : 'secp256k1', + kty : 'EC', + x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', + y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', + }, + output: 'dwzDb6KNsqS3QMTqH0jfBHcoHJzYZBc5scB5n5VLe1E', + }, + { + input: { + crv : 'X25519', + kty : 'OKP', + x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', + }, + output: 'KCfBQ0EA2cWr1Kbt-mnlj8LQ9C2AJfcuEm8mtgOe7wQ', + }, + { + input: { + d : 'MJf4AAqcwfBC68Wkb8nRbmnIdHb07zYM7vU_TAOgmtM', + crv : 'X25519', + kty : 'OKP', + x : 'Uszsfy4vkz9MKeflgUpQot7sJhDyco2aYWCRXKTrcQg', + }, + output: 'lQN1EkHZz4VkAcVGD4gsc0JBcLwvkUprOxkiO4kpbbs', + }, +]; \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json index 472ea3d3d..d930183e6 100644 --- a/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json @@ -16,7 +16,7 @@ "output": false }, { - "description" : "returns false if an compressed public key is given", + "description" : "returns false if a compressed public key is given", "input" : { "privateKeyBytes": "026bcdccc644b309921d3b0c266183a20786650c1634d34e8dfa1ed74cd66ce214" }, diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256r1/bytes-to-private-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256r1/bytes-to-private-key.json new file mode 100644 index 000000000..b4f402b08 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256r1/bytes-to-private-key.json @@ -0,0 +1,19 @@ +{ + "description" : "Secp256k1 bytesToPrivateKey test vectors", + "vectors" : [ + { + "description" : "converts RFC6979 vector 1 to the expected private key", + "input" : { + "privateKeyBytes": "c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721" + }, + "output": { + "crv" : "P-256", + "d" : "ya-p2EW6dRZrXCFXZ7HWk05Qw9s26JsSe4piKxIPZyE", + "kid" : "DOvxvJiAdIqVWIkFt5hDtCunXLF0BV4-JGv4f-ALSm0", + "kty" : "EC", + "x" : "YP7UuiVanTHJYet0xjVtaMBJuJI7Yfps5mliLmDyn7Y", + "y": "eQP-EAi4vJmkGunpVii8ZPLxsgwtfp9Rd6PClNRGIpk" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256r1/bytes-to-public-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256r1/bytes-to-public-key.json new file mode 100644 index 000000000..9bab2691f --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256r1/bytes-to-public-key.json @@ -0,0 +1,44 @@ +{ + "description" : "Secp256r1 bytesToPublicKey test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected public key", + "input" : { + "publicKeyBytes": "042927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513e" + }, + "output": { + "crv" : "P-256", + "kid" : "UB0bE6ogZhikgZQC5i4LIZIpUDDiJ6AnzpDOzOEwJiA", + "kty" : "EC", + "x" : "KSexBRK64-3c_kZ4KBKLrSkDJpkZ9whgacjE32xzKDg", + "y": "x3h5ZOqsAOWSH7FJimD0YGdms9loUAFVjRqXTnNBUT4" + } + }, + { + "description" : "converts wycheproof vector 2 to the expected public key", + "input" : { + "publicKeyBytes": "040ad99500288d466940031d72a9f5445a4d43784640855bf0a69874d2de5fe103c5011e6ef2c42dcd50d5d3d29f99ae6eba2c80c9244f4c5422f0979ff0c3ba5e" + }, + "output": { + "crv" : "P-256", + "kid" : "rC40rZh4ODQ5Y3RKw0dLQBXkVQbkEpCHEoNyFzeSIMU", + "kty" : "EC", + "x" : "CtmVACiNRmlAAx1yqfVEWk1DeEZAhVvwpph00t5f4QM", + "y": "xQEebvLELc1Q1dPSn5mubrosgMkkT0xUIvCXn_DDul4" + } + }, + { + "description" : "converts wycheproof vector 3 to the expected public key", + "input" : { + "publicKeyBytes": "04ab05fd9d0de26b9ce6f4819652d9fc69193d0aa398f0fba8013e09c58220455419235271228c786759095d12b75af0692dd4103f19f6a8c32f49435a1e9b8d45" + }, + "output": { + "crv" : "P-256", + "kid" : "UnGLajygjRCFie0aEyUb9y6Ec_ZohzbqY7sggZGL6Tk", + "kty" : "EC", + "x" : "qwX9nQ3ia5zm9IGWUtn8aRk9CqOY8PuoAT4JxYIgRVQ", + "y": "GSNScSKMeGdZCV0St1rwaS3UED8Z9qjDL0lDWh6bjUU" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256r1/get-curve-points.json b/packages/crypto/tests/fixtures/test-vectors/secp256r1/get-curve-points.json new file mode 100644 index 000000000..9bdb44b37 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256r1/get-curve-points.json @@ -0,0 +1,25 @@ +{ + "description" : "Secp256k1 getCurvePoints test vectors", + "vectors" : [ + { + "description" : "returns public key x and y coordinates given a public key", + "input" : { + "keyBytes": "048b542fa180e78bc981e6671374a64413e0323b439d06870dc49cb56e97775d96a0e469310d10a8ff2cb253a08d46fd845ae330e3ac4e41d0d0a85fbeb8e15795" + }, + "output": { + "x": "8b542fa180e78bc981e6671374a64413e0323b439d06870dc49cb56e97775d96", + "y": "a0e469310d10a8ff2cb253a08d46fd845ae330e3ac4e41d0d0a85fbeb8e15795" + } + }, + { + "description" : "returns public key x and y coordinates given a private key", + "input" : { + "keyBytes": "08169cf81812f2e288a1131de246ebdf29b020c7625a98d098296a30a876d35a" + }, + "output": { + "x": "25f61964e7797e36d9c369b752f53e33033c473e6db4697d74950095a1bfbe49", + "y": "9c2077c6252c520501e365868a22e3a8a8106bf7be95096394d9095c55239366" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256r1/private-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/secp256r1/private-key-to-bytes.json new file mode 100644 index 000000000..f0687b0b7 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256r1/private-key-to-bytes.json @@ -0,0 +1,19 @@ +{ + "description" : "Secp256k1 bytesToPrivateKey test vectors", + "vectors" : [ + { + "description" : "converts RFC6979 vector 1 to the expected private key", + "input" : { + "privateKey": { + "crv" : "P-256", + "d" : "ya-p2EW6dRZrXCFXZ7HWk05Qw9s26JsSe4piKxIPZyE", + "kid" : "DOvxvJiAdIqVWIkFt5hDtCunXLF0BV4-JGv4f-ALSm0", + "kty" : "EC", + "x" : "YP7UuiVanTHJYet0xjVtaMBJuJI7Yfps5mliLmDyn7Y", + "y": "eQP-EAi4vJmkGunpVii8ZPLxsgwtfp9Rd6PClNRGIpk" + } + }, + "output": "c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256r1/public-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/secp256r1/public-key-to-bytes.json new file mode 100644 index 000000000..9ba3fd6c1 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256r1/public-key-to-bytes.json @@ -0,0 +1,44 @@ +{ + "description" : "Secp256r1 bytesToPublicKey test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected public key", + "input" : { + "publicKey": { + "crv" : "P-256", + "kid" : "UB0bE6ogZhikgZQC5i4LIZIpUDDiJ6AnzpDOzOEwJiA", + "kty" : "EC", + "x" : "KSexBRK64-3c_kZ4KBKLrSkDJpkZ9whgacjE32xzKDg", + "y": "x3h5ZOqsAOWSH7FJimD0YGdms9loUAFVjRqXTnNBUT4" + } + }, + "output": "042927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513e" + }, + { + "description" : "converts wycheproof vector 2 to the expected public key", + "input" : { + "publicKey": { + "crv" : "P-256", + "kid" : "rC40rZh4ODQ5Y3RKw0dLQBXkVQbkEpCHEoNyFzeSIMU", + "kty" : "EC", + "x" : "CtmVACiNRmlAAx1yqfVEWk1DeEZAhVvwpph00t5f4QM", + "y": "xQEebvLELc1Q1dPSn5mubrosgMkkT0xUIvCXn_DDul4" + } + }, + "output": "040ad99500288d466940031d72a9f5445a4d43784640855bf0a69874d2de5fe103c5011e6ef2c42dcd50d5d3d29f99ae6eba2c80c9244f4c5422f0979ff0c3ba5e" + }, + { + "description" : "converts wycheproof vector 3 to the expected public key", + "input" : { + "publicKey": { + "crv" : "P-256", + "kid" : "UnGLajygjRCFie0aEyUb9y6Ec_ZohzbqY7sggZGL6Tk", + "kty" : "EC", + "x" : "qwX9nQ3ia5zm9IGWUtn8aRk9CqOY8PuoAT4JxYIgRVQ", + "y": "GSNScSKMeGdZCV0St1rwaS3UED8Z9qjDL0lDWh6bjUU" + } + }, + "output": "04ab05fd9d0de26b9ce6f4819652d9fc69193d0aa398f0fba8013e09c58220455419235271228c786759095d12b75af0692dd4103f19f6a8c32f49435a1e9b8d45" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256r1/validate-private-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256r1/validate-private-key.json new file mode 100644 index 000000000..b79b1b8e2 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256r1/validate-private-key.json @@ -0,0 +1,33 @@ +{ + "description" : "Secp256r1 validatePrivateKey test vectors", + "vectors" : [ + { + "description" : "returns true for valid private keys", + "input" : { + "privateKeyBytes": "08169cf81812f2e288a1131de246ebdf29b020c7625a98d098296a30a876d35a" + }, + "output": true + }, + { + "description" : "returns false for invalid private keys", + "input" : { + "privateKeyBytes": "02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" + }, + "output": false + }, + { + "description" : "returns false if a compressed public key is given", + "input" : { + "privateKeyBytes": "02ca156301f628b64ef0ccff5aba2f78f29bc865fc1da35f1b4e8f3726f1f2d987" + }, + "output": false + }, + { + "description" : "returns false if an uncompressed public key is given", + "input" : { + "privateKeyBytes": "048b542fa180e78bc981e6671374a64413e0323b439d06870dc49cb56e97775d96a0e469310d10a8ff2cb253a08d46fd845ae330e3ac4e41d0d0a85fbeb8e15795" + }, + "output": false + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256r1/validate-public-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256r1/validate-public-key.json new file mode 100644 index 000000000..f4ce580fd --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256r1/validate-public-key.json @@ -0,0 +1,33 @@ +{ + "description" : "Secp256r1 validatePublicKey test vectors", + "vectors" : [ + { + "description" : "returns true for valid compressed public keys", + "input" : { + "publicKeyBytes": "02ca156301f628b64ef0ccff5aba2f78f29bc865fc1da35f1b4e8f3726f1f2d987" + }, + "output": true + }, + { + "description" : "returns true for valid uncompressed public keys", + "input" : { + "publicKeyBytes": "048b542fa180e78bc981e6671374a64413e0323b439d06870dc49cb56e97775d96a0e469310d10a8ff2cb253a08d46fd845ae330e3ac4e41d0d0a85fbeb8e15795" + }, + "output": true + }, + { + "description" : "returns false for invalid public keys", + "input" : { + "publicKeyBytes": "02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" + }, + "output": false + }, + { + "description" : "returns false if a private key is given", + "input" : { + "publicKeyBytes": "08169cf81812f2e288a1131de246ebdf29b020c7625a98d098296a30a876d35a" + }, + "output": false + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/jose.spec.ts b/packages/crypto/tests/jose.spec.ts deleted file mode 100644 index 3bb2cc327..000000000 --- a/packages/crypto/tests/jose.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -import chai, { expect } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import { MulticodecCode, MulticodecDefinition } from '@web5/common'; - -import type { Jwk, PublicKeyJwk } from '../src/jose/jwk.js'; - -import { Jose } from '../src/jose.js'; -import { - joseToMulticodecTestVectors, - jwkToMultibaseIdTestVectors, -} from './fixtures/test-vectors/jose.js'; - -chai.use(chaiAsPromised); - -describe('Jose', () => { - describe('joseToMulticodec()', () => { - it('converts JOSE to Multicodec', async () => { - let multicoded: MulticodecDefinition; - for (const vector of joseToMulticodecTestVectors) { - multicoded = await Jose.jwkToMulticodec({ - jwk: vector.input as Jwk, - }); - expect(multicoded).to.deep.equal(vector.output); - } - }); - - it('throws an error if unsupported JOSE has been passed', async () => { - await expect( - // @ts-expect-error because parameters are intentionally omitted to trigger an error. - Jose.jwkToMulticodec({ jwk: { crv: '123' } }) - ).to.eventually.be.rejectedWith(Error, `Unsupported JOSE to Multicodec conversion: '123:public'`); - }); - }); - - describe('publicKeyToMultibaseId()', () => { - it('passes all test vectors', async () => { - let multibaseId: string; - - for (const vector of jwkToMultibaseIdTestVectors) { - multibaseId = await Jose.publicKeyToMultibaseId({ publicKey: vector.input as PublicKeyJwk}); - expect(multibaseId).to.equal(vector.output); - } - }); - - it('throws an error for an unsupported public key type', async () => { - await expect( - Jose.publicKeyToMultibaseId({ - publicKey: { - kty : 'RSA', - n : 'r0YDzIV4GPJ1wFb1Gftdd3C3VE6YeknVq1C7jGypq5WTTmX0yRDBqzL6mBR3_c-mKRuE5Z5VMGniA1lFnFmv8m0A2engKfALXHPJqoL6WzqN1SyjSM2aI6v8JVTj4H0RdYV9R4jxIB-zK5X-ZyL6CwHx-3dKZkCvZSEp8b-5I8c2Fz8E8Hl7qKkD_qEz6ZOmKVhJLGiEag1qUQYJv2TcRdiyZfwwVsV3nI3IcVfMCTjDZTw2jI0YHJgLi7-MkP4DO7OJ4D4AFtL-7CkZ7V2xG0piBz4b02_-ZGnBZ5zHJxGoUZnTY6HX4V9bPQI_ME8qCjFXf-TcwCfDFcwMm70L2Q', - e : 'AQAB', - alg : 'RS256' - } - }) - ).to.eventually.be.rejectedWith(Error, `Unsupported public key type`); - }); - - it('throws an error for an unsupported public key curve', async () => { - await expect( - Jose.publicKeyToMultibaseId({ - publicKey: { - kty : 'EC', - crv : 'P-256', - x : 'SVqB4JcUD6lsfvqMr-OKUNUphdNn64Eay60978ZlL74', - y : 'lf0u0pMj4lGAzZix5u4Cm5CMQIgMNpkwy163wtKYVKI' - } - }) - ).to.eventually.be.rejectedWith(Error, `Unsupported public key curve`); - }); - }); - - describe('multicodecToJose()', () => { - it('converts ed25519 public key multicodec to JWK', async () => { - const result = await Jose.multicodecToJose({ name: 'ed25519-pub' }); - expect(result).to.deep.equal({ - crv : 'Ed25519', - kty : 'OKP', - x : '' // x value would be populated with actual key material in real use - }); - }); - - it('converts ed25519 private key multicodec to JWK', async () => { - const result = await Jose.multicodecToJose({ name: 'ed25519-priv' }); - expect(result).to.deep.equal({ - crv : 'Ed25519', - kty : 'OKP', - x : '', // x value would be populated with actual key material in real use - d : '' // d value would be populated with actual key material in real use - }); - }); - - it('converts secp256k1 public key multicodec to JWK', async () => { - const result = await Jose.multicodecToJose({ name: 'secp256k1-pub' }); - expect(result).to.deep.equal({ - crv : 'secp256k1', - kty : 'EC', - x : '', // x value would be populated with actual key material in real use - y : '' // y value would be populated with actual key material in real use - }); - }); - - it('converts secp256k1 private key multicodec to JWK', async () => { - const result = await Jose.multicodecToJose({ name: 'secp256k1-priv' }); - expect(result).to.deep.equal({ - crv : 'secp256k1', - kty : 'EC', - x : '', // x value would be populated with actual key material in real use - y : '', // y value would be populated with actual key material in real use - d : '' // d value would be populated with actual key material in real use - }); - }); - - it('converts x25519 public key multicodec to JWK', async () => { - const result = await Jose.multicodecToJose({ name: 'x25519-pub' }); - expect(result).to.deep.equal({ - crv : 'X25519', - kty : 'OKP', - x : '' // x value would be populated with actual key material in real use - }); - }); - - it('converts x25519 private key multicodec to JWK', async () => { - const result = await Jose.multicodecToJose({ name: 'x25519-priv' }); - expect(result).to.deep.equal({ - crv : 'X25519', - kty : 'OKP', - x : '', // x value would be populated with actual key material in real use - d : '' // d value would be populated with actual key material in real use - }); - }); - - it('throws an error when name is undefined and code is not provided', async () => { - try { - await Jose.multicodecToJose({}); - expect.fail('Should have thrown an error for undefined name and code'); - } catch (e: any) { - expect(e.message).to.equal('Either \'name\' or \'code\' must be defined, but not both.'); - } - }); - - it('throws an error when both name and code are provided', async () => { - try { - await Jose.multicodecToJose({ name: 'ed25519-pub', code: 0xed }); - expect.fail('Should have thrown an error for both name and code being defined'); - } catch (e: any) { - expect(e.message).to.equal('Either \'name\' or \'code\' must be defined, but not both.'); - } - }); - - it('throws an error for unsupported multicodec name', async () => { - try { - await Jose.multicodecToJose({ name: 'unsupported-key-type' }); - expect.fail('Should have thrown an error for unsupported multicodec name'); - } catch (e: any) { - expect(e.message).to.include('Unsupported Multicodec to JOSE conversion'); - } - }); - - it('throws an error for unsupported multicodec code', async () => { - try { - await Jose.multicodecToJose({ code: 0x9999 }); - expect.fail('Should have thrown an error for unsupported multicodec code'); - } catch (e: any) { - expect(e.message).to.include('Unsupported multicodec'); - } - }); - }); -}); \ No newline at end of file diff --git a/packages/crypto/tests/jose/jwk.spec.ts b/packages/crypto/tests/jose/jwk.spec.ts index 83c147b43..d2f65edd8 100644 --- a/packages/crypto/tests/jose/jwk.spec.ts +++ b/packages/crypto/tests/jose/jwk.spec.ts @@ -13,7 +13,7 @@ import { isOkpPrivateJwk, computeJwkThumbprint, } from '../../src/jose/jwk.js'; -import { jwkToThumbprintTestVectors } from '../fixtures/test-vectors/jose.js'; +import { jwkToThumbprintTestVectors } from '../fixtures/test-vectors/jwk.js'; chai.use(chaiAsPromised); diff --git a/packages/crypto/tests/local-kms-crypto.spec.ts b/packages/crypto/tests/local-key-manager.spec.ts similarity index 76% rename from packages/crypto/tests/local-kms-crypto.spec.ts rename to packages/crypto/tests/local-key-manager.spec.ts index ef687ac2d..c3f434802 100644 --- a/packages/crypto/tests/local-kms-crypto.spec.ts +++ b/packages/crypto/tests/local-key-manager.spec.ts @@ -6,28 +6,28 @@ import type { Jwk } from '../src/jose/jwk.js'; import type { KeyIdentifier } from '../src/types/identifier.js'; import { EcdsaAlgorithm } from '../src/algorithms/ecdsa.js'; -import { LocalKmsCrypto } from '../src/local-kms-crypto.js'; +import { LocalKeyManager } from '../src/local-key-manager.js'; -describe('LocalKmsCrypto', () => { - let crypto: LocalKmsCrypto; +describe('LocalKeyManager', () => { + let keyManager: LocalKeyManager; beforeEach(() => { - crypto = new LocalKmsCrypto(); + keyManager = new LocalKeyManager(); }); describe('constructor', () => { it('initializes with default parameters', () => { - const crypto = new LocalKmsCrypto(); - expect(crypto).to.exist; - expect(crypto).to.be.an.instanceOf(LocalKmsCrypto); + const keyManager = new LocalKeyManager(); + expect(keyManager).to.exist; + expect(keyManager).to.be.an.instanceOf(LocalKeyManager); }); it('initializes with a custom in-memory key store', () => { const keyStore = new MemoryStore(); - const crypto = new LocalKmsCrypto({ keyStore }); + const keyManager = new LocalKeyManager({ keyStore }); - expect(crypto).to.exist; - expect(crypto).to.be.an.instanceOf(LocalKmsCrypto); + expect(keyManager).to.exist; + expect(keyManager).to.be.an.instanceOf(LocalKeyManager); }); }); @@ -37,7 +37,7 @@ describe('LocalKmsCrypto', () => { const data = new Uint8Array([0, 1, 2, 3, 4]); // Test the method. - const digest = await crypto.digest({ algorithm: 'SHA-256', data }); + const digest = await keyManager.digest({ algorithm: 'SHA-256', data }); // Validate the result. expect(digest).to.exist; @@ -50,7 +50,7 @@ describe('LocalKmsCrypto', () => { const expectedOutput = Convert.hex('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad').toUint8Array(); // Test the method. - const digest = await crypto.digest({ algorithm: 'SHA-256', data }); + const digest = await keyManager.digest({ algorithm: 'SHA-256', data }); // Validate the result. expect(digest).to.exist; @@ -62,9 +62,9 @@ describe('LocalKmsCrypto', () => { describe('exportKey()', () => { it('exports a private key as a JWK', async () => { - const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + const keyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); - const jwk = await crypto.exportKey({ keyUri }); + const jwk = await keyManager.exportKey({ keyUri }); expect(jwk).to.exist; expect(jwk).to.be.an('object'); @@ -76,7 +76,7 @@ describe('LocalKmsCrypto', () => { const keyUri = 'urn:jwk:does-not-exist'; try { - await crypto.exportKey({ keyUri }); + await keyManager.exportKey({ keyUri }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { @@ -89,15 +89,15 @@ describe('LocalKmsCrypto', () => { describe('generateKey()', () => { it('generates a key and returns a key URI', async () => { - const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + const keyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); expect(keyUri).to.exist; expect(keyUri).to.be.a.string; expect(keyUri.indexOf('urn:jwk:')).to.equal(0); }); - it(`supports generating 'ES256K' keys`, async () => { - const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + it(`supports generating 'secp256k1' keys`, async () => { + const keyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); expect(keyUri).to.exist; expect(keyUri).to.be.a.string; @@ -105,7 +105,7 @@ describe('LocalKmsCrypto', () => { }); it(`supports generating 'Ed25519' keys`, async () => { - const keyUri = await crypto.generateKey({ algorithm: 'Ed25519' }); + const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); expect(keyUri).to.exist; expect(keyUri).to.be.a.string; @@ -119,7 +119,7 @@ describe('LocalKmsCrypto', () => { // Test the method. try { // @ts-expect-error because an unsupported algorithm is being tested. - await crypto.generateKey({ algorithm }); + await keyManager.generateKey({ algorithm }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { @@ -134,11 +134,11 @@ describe('LocalKmsCrypto', () => { // Setup. const mockKeyGenerator = { generateKey: sinon.stub() }; // @ts-expect-error because we're accessing a private property. - crypto._algorithmInstances.set(EcdsaAlgorithm, mockKeyGenerator); // Replace the algorithm instance with the mock. + keyManager._algorithmInstances.set(EcdsaAlgorithm, mockKeyGenerator); // Replace the algorithm instance with the mock. // Test the method. try { - await crypto.generateKey({ algorithm: 'ES256K' }); + await keyManager.generateKey({ algorithm: 'secp256k1' }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { @@ -164,7 +164,7 @@ describe('LocalKmsCrypto', () => { }; // Test the method. - const keyUri = await crypto.getKeyUri({ key }); + const keyUri = await keyManager.getKeyUri({ key }); // Validate the result. expect(keyUri).to.exist; @@ -184,7 +184,7 @@ describe('LocalKmsCrypto', () => { const expectedKeyUri = 'urn:jwk:' + expectedThumbprint; // Test the method. - const keyUri = await crypto.getKeyUri({ key }); + const keyUri = await keyManager.getKeyUri({ key }); expect(keyUri).to.equal(expectedKeyUri); }); @@ -192,9 +192,9 @@ describe('LocalKmsCrypto', () => { describe('getPublicKey()', () => { it('computes the public key and returns a JWK', async () => { - const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + const keyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); - const publicKey = await crypto.getPublicKey({ keyUri }); + const publicKey = await keyManager.getPublicKey({ keyUri }); expect(publicKey).to.exist; expect(publicKey).to.be.an('object'); @@ -202,9 +202,9 @@ describe('LocalKmsCrypto', () => { }); it('supports ECDSA using secp256k1 curve and SHA-256', async () => { - const keyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + const keyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); - const publicKey = await crypto.getPublicKey({ keyUri }); + const publicKey = await keyManager.getPublicKey({ keyUri }); expect(publicKey).to.exist; expect(publicKey).to.be.an('object'); @@ -218,10 +218,10 @@ describe('LocalKmsCrypto', () => { it('supports EdDSA using Ed25519 curve', async () => { // Setup. - const keyUri = await crypto.generateKey({ algorithm: 'Ed25519' }); + const keyUri = await keyManager.generateKey({ algorithm: 'Ed25519' }); // Test the method. - const publicKey = await crypto.getPublicKey({ keyUri }); + const publicKey = await keyManager.getPublicKey({ keyUri }); expect(publicKey).to.exist; expect(publicKey).to.be.an('object'); @@ -238,7 +238,7 @@ describe('LocalKmsCrypto', () => { it('imports a private key and return a key URI', async () => { // Setup. const memoryStore = new MemoryStore(); - const crypto = new LocalKmsCrypto({ keyStore: memoryStore }); + const keyManager = new LocalKeyManager({ keyStore: memoryStore }); const privateKey: Jwk = { kty : 'EC', crv : 'secp256k1', @@ -251,7 +251,7 @@ describe('LocalKmsCrypto', () => { const expectedKeyUri = 'urn:jwk:' + expectedThumbprint; // Test the method. - const keyUri = await crypto.importKey({ key: privateKey }); + const keyUri = await keyManager.importKey({ key: privateKey }); // Validate the result. expect(keyUri).to.equal(expectedKeyUri); @@ -262,7 +262,7 @@ describe('LocalKmsCrypto', () => { it('does not modify the kid property, if provided', async () => { // Setup. const memoryStore = new MemoryStore(); - const crypto = new LocalKmsCrypto({ keyStore: memoryStore }); + const keyManager = new LocalKeyManager({ keyStore: memoryStore }); const privateKey: Jwk = { kty : 'EC', crv : 'secp256k1', @@ -273,7 +273,7 @@ describe('LocalKmsCrypto', () => { }; // Test the method. - const keyUri = await crypto.importKey({ key: privateKey }); + const keyUri = await keyManager.importKey({ key: privateKey }); // Validate the result. const storedKey = await memoryStore.get(keyUri); @@ -283,7 +283,7 @@ describe('LocalKmsCrypto', () => { it('adds the kid property, if missing', async () => { // Setup. const memoryStore = new MemoryStore(); - const crypto = new LocalKmsCrypto({ keyStore: memoryStore }); + const keyManager = new LocalKeyManager({ keyStore: memoryStore }); const privateKey: Jwk = { kty : 'EC', crv : 'secp256k1', @@ -293,7 +293,7 @@ describe('LocalKmsCrypto', () => { }; // Test the method. - const keyUri = await crypto.importKey({ key: privateKey }); + const keyUri = await keyManager.importKey({ key: privateKey }); // Validate the result. const storedKey = await memoryStore.get(keyUri); @@ -312,7 +312,7 @@ describe('LocalKmsCrypto', () => { const privateKeyCopy = structuredClone(privateKey); // Test the method. - await crypto.importKey({ key: privateKey }); + await keyManager.importKey({ key: privateKey }); // Validate the result. expect(privateKey).to.deep.equal(privateKeyCopy); @@ -325,7 +325,7 @@ describe('LocalKmsCrypto', () => { // Test the method. try { - await crypto.importKey({ key: invalidJwk }); + await keyManager.importKey({ key: invalidJwk }); expect.fail('Should have thrown an error'); } catch (error: any) { @@ -345,7 +345,7 @@ describe('LocalKmsCrypto', () => { // Test the method. try { - await crypto.importKey({ key: publicKey }); + await keyManager.importKey({ key: publicKey }); expect.fail('Should have thrown an error'); } catch (error: any) { @@ -358,11 +358,11 @@ describe('LocalKmsCrypto', () => { describe('sign()', () => { it('generates signatures as Uint8Array', async () => { // Setup. - const privateKeyUri = await crypto.generateKey({ algorithm: 'ES256K' }); + const privateKeyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); const data = new Uint8Array([0, 1, 2, 3, 4]); // Test the method. - const signature = await crypto.sign({ keyUri: privateKeyUri, data }); + const signature = await keyManager.sign({ keyUri: privateKeyUri, data }); // Validate the result. expect(signature).to.be.a('Uint8Array'); @@ -372,13 +372,13 @@ describe('LocalKmsCrypto', () => { describe('verify()', () => { it('returns true for a valid signature', async () => { // Setup. - const privateKeyUri = await crypto.generateKey({ algorithm: 'ES256K' }); - const publicKey = await crypto.getPublicKey({ keyUri: privateKeyUri }); + const privateKeyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); + const publicKey = await keyManager.getPublicKey({ keyUri: privateKeyUri }); const data = new Uint8Array([0, 1, 2, 3, 4]); - const signature = await crypto.sign({ keyUri: privateKeyUri, data }); + const signature = await keyManager.sign({ keyUri: privateKeyUri, data }); // Test the method. - const isValid = await crypto.verify({ key: publicKey, signature, data }); + const isValid = await keyManager.verify({ key: publicKey, signature, data }); // Validate the result. expect(isValid).to.be.true; @@ -386,13 +386,13 @@ describe('LocalKmsCrypto', () => { it('returns false for an invalid signature', async () => { // Setup. - const privateKeyUri = await crypto.generateKey({ algorithm: 'ES256K' }); - const publicKey = await crypto.getPublicKey({ keyUri: privateKeyUri }); + const privateKeyUri = await keyManager.generateKey({ algorithm: 'secp256k1' }); + const publicKey = await keyManager.getPublicKey({ keyUri: privateKeyUri }); const data = new Uint8Array([0, 1, 2, 3, 4]); const signature = new Uint8Array(64); // Test the method. - const isValid = await crypto.verify({ key: publicKey, signature, data }); + const isValid = await keyManager.verify({ key: publicKey, signature, data }); // Validate the result. expect(isValid).to.be.false; @@ -401,14 +401,13 @@ describe('LocalKmsCrypto', () => { it('throws an error when public key algorithm and curve are unsupported', async () => { // Setup. - // @ts-expect-error because an unsupported algorithm and currve is being tested. const key: Jwk = { kty: 'EC', alg: 'unsupported-algorithm', crv: 'unsupported-curve', x: 'x', y: 'y' }; const signature = new Uint8Array(64); const data = new Uint8Array(0); // Test the method. try { - await crypto.verify({ key, signature, data }); + await keyManager.verify({ key, signature, data }); expect.fail('Expected an error to be thrown.'); } catch (error: any) { diff --git a/packages/crypto/tests/primitives/aes-ctr.spec.ts b/packages/crypto/tests/primitives/aes-ctr.spec.ts index 8899ce383..440a793f3 100644 --- a/packages/crypto/tests/primitives/aes-ctr.spec.ts +++ b/packages/crypto/tests/primitives/aes-ctr.spec.ts @@ -182,17 +182,17 @@ describe('AesCtr', () => { // 128 bits privateKey = await AesCtr.generateKey({ length: 128 }) as JwkParamsOctPrivate; - privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + privateKeyBytes = Convert.base64Url(privateKey.k!).toUint8Array(); expect(privateKeyBytes.byteLength).to.equal(16); // 192 bits privateKey = await AesCtr.generateKey({ length: 192 }) as JwkParamsOctPrivate; - privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + privateKeyBytes = Convert.base64Url(privateKey.k!).toUint8Array(); expect(privateKeyBytes.byteLength).to.equal(24); // 256 bits privateKey = await AesCtr.generateKey({ length: 256 }) as JwkParamsOctPrivate; - privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + privateKeyBytes = Convert.base64Url(privateKey.k!).toUint8Array(); expect(privateKeyBytes.byteLength).to.equal(32); }); diff --git a/packages/crypto/tests/primitives/concat-kdf.spec.ts b/packages/crypto/tests/primitives/concat-kdf.spec.ts index 3b96692be..da6c7732b 100644 --- a/packages/crypto/tests/primitives/concat-kdf.spec.ts +++ b/packages/crypto/tests/primitives/concat-kdf.spec.ts @@ -14,7 +14,7 @@ describe('ConcatKdf', () => { const input = { sharedSecret : Convert.base64Url(inputSharedSecret).toUint8Array(), keyDataLen : 128, - otherInfo : { + fixedInfo : { algorithmId : 'A128GCM', partyUInfo : 'Alice', partyVInfo : 'Bob', @@ -34,11 +34,11 @@ describe('ConcatKdf', () => { const inputBase = { sharedSecret : new Uint8Array([1, 2, 3]), keyDataLen : 256, - otherInfo : {} + fixedInfo : {} }; // String input. - const inputString = { ...inputBase, otherInfo: { + const inputString = { ...inputBase, fixedInfo: { algorithmId : 'A128GCM', partyUInfo : 'Alice', partyVInfo : 'Bob', @@ -49,7 +49,7 @@ describe('ConcatKdf', () => { expect(derivedKeyingMaterial.byteLength).to.equal(32); // TypedArray input. - const inputTypedArray = { ...inputBase, otherInfo: { + const inputTypedArray = { ...inputBase, fixedInfo: { algorithmId : 'A128GCM', partyUInfo : Convert.string('Alice').toUint8Array(), partyVInfo : Convert.string('Bob').toUint8Array(), @@ -72,7 +72,7 @@ describe('ConcatKdf', () => { ConcatKdf.deriveKey({ sharedSecret : new Uint8Array([1, 2, 3]), keyDataLen : 128, - otherInfo : { + fixedInfo : { algorithmId : 'A128GCM', partyUInfo : 'Alice', partyVInfo : 'Bob', @@ -84,7 +84,7 @@ describe('ConcatKdf', () => { }); }); - describe('computeOtherInfo()', () => { + describe('computeFixedInfo()', () => { it('returns concatenated and formatted Uint8Array', () => { const input = { algorithmId : 'A128GCM', @@ -95,11 +95,11 @@ describe('ConcatKdf', () => { }; const output = 'AAAAB0ExMjhHQ00AAAAFQWxpY2UAAAADQm9iAAAAgAAAACtnSTBHQUlMQmR1N1Q1M2FrckZtTXlHY3NGM241ZE83TW13TkJIS1c1U1Yw'; - // @ts-expect-error because computeOtherInfo() is a private method. - const otherInfo = ConcatKdf.computeOtherInfo(input); + // @ts-expect-error because computeFixedInfo() is a private method. + const fixedInfo = ConcatKdf.computeFixedInfo(input); const expectedResult = Convert.base64Url(output).toUint8Array(); - expect(otherInfo).to.deep.equal(expectedResult); + expect(fixedInfo).to.deep.equal(expectedResult); }); it('matches RFC 7518 ECDH-ES key agreement computation example', async () => { @@ -112,11 +112,11 @@ describe('ConcatKdf', () => { }; const output = 'AAAAB0ExMjhHQ00AAAAFQWxpY2UAAAADQm9iAAAAgA'; - // @ts-expect-error because computeOtherInfo() is a private method. - const otherInfo = ConcatKdf.computeOtherInfo(input); + // @ts-expect-error because computeFixedInfo() is a private method. + const fixedInfo = ConcatKdf.computeFixedInfo(input); const expectedResult = Convert.base64Url(output).toUint8Array(); - expect(otherInfo).to.deep.equal(expectedResult); + expect(fixedInfo).to.deep.equal(expectedResult); }); }); }); \ No newline at end of file diff --git a/packages/crypto/tests/primitives/ed25519.spec.ts b/packages/crypto/tests/primitives/ed25519.spec.ts index 8cf2efccf..efab724c8 100644 --- a/packages/crypto/tests/primitives/ed25519.spec.ts +++ b/packages/crypto/tests/primitives/ed25519.spec.ts @@ -337,22 +337,20 @@ describe('Ed25519', () => { describe('validatePublicKey()', () => { it('returns true for valid public keys', async () => { - const publicKey = Convert.hex('a12c2beb77265f2aac953b5009349d94155a03ada416aad451319480e983ca4c').toUint8Array(); - const isValid = await Ed25519.validatePublicKey({ publicKey }); + const publicKeyBytes = Convert.hex('a12c2beb77265f2aac953b5009349d94155a03ada416aad451319480e983ca4c').toUint8Array(); + const isValid = await Ed25519.validatePublicKey({ publicKeyBytes }); expect(isValid).to.be.true; }); it('returns false for invalid public keys', async () => { - const key = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); - // @ts-expect-error because validatePublicKey() is a private method. - const isValid = await Ed25519.validatePublicKey({ key }); + const publicKeyBytes = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); + const isValid = await Ed25519.validatePublicKey({ publicKeyBytes }); expect(isValid).to.be.false; }); it('returns false if a private key is given', async () => { - const key = Convert.hex('0a23a20072891237aa0864b5765139514908787878cd77135a0059881d313f00').toUint8Array(); - // @ts-expect-error because validatePublicKey() is a private method. - const isValid = await Ed25519.validatePublicKey({ key }); + const publicKeyBytes = Convert.hex('0a23a20072891237aa0864b5765139514908787878cd77135a0059881d313f00').toUint8Array(); + const isValid = await Ed25519.validatePublicKey({ publicKeyBytes }); expect(isValid).to.be.false; }); }); diff --git a/packages/crypto/tests/primitives/secp256k1.spec.ts b/packages/crypto/tests/primitives/secp256k1.spec.ts index d9ab948d5..ebc3da158 100644 --- a/packages/crypto/tests/primitives/secp256k1.spec.ts +++ b/packages/crypto/tests/primitives/secp256k1.spec.ts @@ -326,12 +326,12 @@ describe('Secp256k1', () => { }); }); - describe('getCurvePoints()', () => { + describe('getCurvePoint()', () => { for (const vector of secp256k1GetCurvePoints.vectors) { it(vector.description, async () => { const keyBytes = Convert.hex(vector.input.keyBytes).toUint8Array(); - // @ts-expect-error because getCurvePoints() is a private method. - const points = await Secp256k1.getCurvePoints({ keyBytes }); + // @ts-expect-error because getCurvePoint() is a private method. + const points = await Secp256k1.getCurvePoint({ keyBytes }); expect(points.x).to.deep.equal(Convert.hex(vector.output.x).toUint8Array()); expect(points.y).to.deep.equal(Convert.hex(vector.output.y).toUint8Array()); }); @@ -339,8 +339,8 @@ describe('Secp256k1', () => { it('throws error with invalid input key length', async () => { await expect( - // @ts-expect-error because getCurvePoints() is a private method. - Secp256k1.getCurvePoints({ keyBytes: new Uint8Array(16) }) + // @ts-expect-error because getCurvePoint() is a private method. + Secp256k1.getCurvePoint({ keyBytes: new Uint8Array(16) }) ).to.eventually.be.rejectedWith(Error, 'Point of length 16 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes'); }); }); diff --git a/packages/crypto/tests/primitives/secp256r1.spec.ts b/packages/crypto/tests/primitives/secp256r1.spec.ts new file mode 100644 index 000000000..a22afb2d0 --- /dev/null +++ b/packages/crypto/tests/primitives/secp256r1.spec.ts @@ -0,0 +1,578 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { Jwk, JwkParamsEcPrivate } from '../../src/jose/jwk.js'; + +import secp256r1GetCurvePoints from '../fixtures/test-vectors/secp256r1/get-curve-points.json' assert { type: 'json' }; +import secp256r1BytesToPublicKey from '../fixtures/test-vectors/secp256r1/bytes-to-public-key.json' assert { type: 'json' }; +import secp256r1PublicKeyToBytes from '../fixtures/test-vectors/secp256r1/public-key-to-bytes.json' assert { type: 'json' }; +import secp256r1ValidatePublicKey from '../fixtures/test-vectors/secp256r1/validate-public-key.json' assert { type: 'json' }; +import secp256r1BytesToPrivateKey from '../fixtures/test-vectors/secp256r1/bytes-to-private-key.json' assert { type: 'json' }; +import secp256r1PrivateKeyToBytes from '../fixtures/test-vectors/secp256r1/private-key-to-bytes.json' assert { type: 'json' }; +import secp256r1ValidatePrivateKey from '../fixtures/test-vectors/secp256r1/validate-private-key.json' assert { type: 'json' }; + +import { Secp256r1 } from '../../src/primitives/secp256r1.js'; + +chai.use(chaiAsPromised); + +describe('Secp256r1', () => { + let privateKey: Jwk; + let publicKey: Jwk; + + before(async () => { + privateKey = await Secp256r1.generateKey(); + publicKey = await Secp256r1.computePublicKey({ key: privateKey }); + }); + + describe('adjustSignatureToLowS()', () => { + it('returns a 64-byte signature of type Uint8Array', async () => { + const data = new Uint8Array([51, 52, 53]); + const signature = await Secp256r1.sign({ key: privateKey, data }); + + const adjustedSignature = await Secp256r1.adjustSignatureToLowS({ signature }); + + expect(adjustedSignature).to.be.instanceOf(Uint8Array); + expect(adjustedSignature.byteLength).to.equal(64); + }); + + it('returns the low-S form given a high-S signature', async () => { + // Source: https://github.com/paulmillr/noble-curves/blob/37eab5a28a43c35b87e9e95a12ae6086393ac38b/test/wycheproof/ecdsa_secp256r1_sha256_test.json#L209-L218 + const signatureHighS = Convert.hex('2ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e18b329f479a2bbd0a5c384ee1493b1f5186a87139cac5df4087c134b49156847db').toUint8Array(); + + const adjustedSignature = await Secp256r1.adjustSignatureToLowS({ signature: signatureHighS }); + + expect(adjustedSignature).to.not.deep.equal(signatureHighS); + }); + + it('returns the signature unmodified if already in low-S form', async () => { + // Source: https://github.com/paulmillr/noble-curves/blob/37eab5a28a43c35b87e9e95a12ae6086393ac38b/test/wycheproof/ecdsa_secp256r1_sha256_test.json#L189-L198 + const signatureLowS = Convert.hex('2ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e184cd60b855d442f5b3c7b11eb6c4e0ae7525fe710fab9aa7c77a67f79e6fadd76').toUint8Array(); + + const adjustedSignature = await Secp256r1.adjustSignatureToLowS({ signature: signatureLowS }); + + expect(adjustedSignature).to.deep.equal(signatureLowS); + }); + + it('returns signatures that can be verified regardless of low- or high-S form', async () => { + // Source: https://github.com/paulmillr/noble-curves/blob/37eab5a28a43c35b87e9e95a12ae6086393ac38b/test/wycheproof/ecdsa_secp256r1_sha256_test.json#L176-L198 + const publicKeyBytes = Convert.hex('042927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513e').toUint8Array(); + const publicKey = await Secp256r1.bytesToPublicKey({ publicKeyBytes }); + const data = Convert.hex('313233343030').toUint8Array(); + const signatureLowS = Convert.hex('2ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e184cd60b855d442f5b3c7b11eb6c4e0ae7525fe710fab9aa7c77a67f79e6fadd76').toUint8Array(); + const signatureHighS = Convert.hex('2ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e18b329f479a2bbd0a5c384ee1493b1f5186a87139cac5df4087c134b49156847db').toUint8Array(); + + // Verify that the returned signature is valid when input in low-S form. + let adjustedSignature = await Secp256r1.adjustSignatureToLowS({ signature: signatureLowS }); + let isValid = await Secp256r1.verify({ key: publicKey, signature: adjustedSignature, data }); + expect(isValid).to.be.true; + + // Verify that the returned signature is valid when input in high-S form. + adjustedSignature = await Secp256r1.adjustSignatureToLowS({ signature: signatureHighS }); + isValid = await Secp256r1.verify({ key: publicKey, signature: adjustedSignature, data }); + expect(isValid).to.be.true; + }); + }); + + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('08169cf81812f2e288a1131de246ebdf29b020c7625a98d098296a30a876d35a').toUint8Array(); + const privateKey = await Secp256r1.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('crv', 'P-256'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'EC'); + expect(privateKey).to.have.property('x'); + expect(privateKey).to.have.property('y'); + }); + + for (const vector of secp256r1BytesToPrivateKey.vectors) { + it(vector.description, async () => { + const privateKey = await Secp256r1.bytesToPrivateKey({ + privateKeyBytes: Convert.hex(vector.input.privateKeyBytes).toUint8Array() + }); + + expect(privateKey).to.deep.equal(vector.output); + }); + } + }); + + describe('bytesToPublicKey()', () => { + it('returns a public key in JWK format', async () => { + const publicKeyBytes = Convert.hex('048b542fa180e78bc981e6671374a64413e0323b439d06870dc49cb56e97775d96a0e469310d10a8ff2cb253a08d46fd845ae330e3ac4e41d0d0a85fbeb8e15795').toUint8Array(); + const publicKey = await Secp256r1.bytesToPublicKey({ publicKeyBytes }); + + expect(publicKey).to.have.property('crv', 'P-256'); + expect(publicKey).to.have.property('kid'); + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.have.property('y'); + expect(publicKey).to.not.have.property('d'); + }); + + for (const vector of secp256r1BytesToPublicKey.vectors) { + it(vector.description, async () => { + const publicKey = await Secp256r1.bytesToPublicKey({ + publicKeyBytes: Convert.hex(vector.input.publicKeyBytes).toUint8Array() + }); + expect(publicKey).to.deep.equal(vector.output); + }); + } + }); + + describe('compressPublicKey()', () => { + it('converts an uncompressed public key to compressed format', async () => { + const compressedPublicKeyBytes = Convert.hex('02d7251f4572325f4b1a9642600427adfe11ea3bd4dfe1cd7f4932612129e18784').toUint8Array(); + const uncompressedPublicKeyBytes = Convert.hex('04d7251f4572325f4b1a9642600427adfe11ea3bd4dfe1cd7f4932612129e187844247b3c6302e7ecd611dbb666380e1117b198f37a9d183de422947f6b6183098').toUint8Array(); + + const output = await Secp256r1.compressPublicKey({ + publicKeyBytes: uncompressedPublicKeyBytes + }); + + // Confirm the length of the resulting public key is 33 bytes + expect(output.byteLength).to.equal(33); + + // Confirm the output matches the expected compressed public key. + expect(output).to.deep.equal(compressedPublicKeyBytes); + }); + + it('throws an error for an invalid uncompressed public key', async () => { + // Invalid uncompressed public key. + const invalidPublicKey = Convert.hex('dfebc16793a5737ac51f606a43524df8373c063e41d5a99b2f1530afd987284bd1c7cde1658a9a756e71f44a97b4783ea9dee5ccb7f1447eb4836d8de9bd4f81fd').toUint8Array(); + + try { + await Secp256r1.compressPublicKey({ + publicKeyBytes: invalidPublicKey, + }); + expect.fail('Expected method to throw an error.'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Point of length 65 was invalid'); + } + }); + }); + + describe('computePublicKey()', () => { + it('returns a public key in JWK format', async () => { + publicKey = await Secp256r1.computePublicKey({ key: privateKey }); + + expect(publicKey).to.have.property('crv', 'P-256'); + expect(publicKey).to.not.have.property('d'); + expect(publicKey).to.have.property('kid'); + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.have.property('y'); + }); + + it('computes and adds a kid property, if missing', async () => { + const { kid, ...privateKeyWithoutKid } = privateKey; + const publicKey = await Secp256r1.computePublicKey({ key: privateKeyWithoutKid }); + + expect(publicKey).to.have.property('kid', kid); + }); + }); + + describe('convertDerToCompactSignature()', () => { + it('returns compact R+S format signature as a Uint8Array', async () => { + const derSignature = Convert.hex('3045022100b292a619339f6e567a305c951c0dcbcc42d16e47f219f9e98e76e09d8770b34a02200177e60492c5a8242f76f07bfe3661bde59ec2a17ce5bd2dab2abebdf89a62e2').toUint8Array(); + + const compactSignature = await Secp256r1.convertDerToCompactSignature({ derSignature }); + + expect(compactSignature).to.be.instanceOf(Uint8Array); + expect(compactSignature.byteLength).to.equal(64); + }); + + it('converted ASN.1 DER encoded ECDSA signature matches the expected compact R+S signature', async () => { + const derSignature = Convert.hex('3045022100b292a619339f6e567a305c951c0dcbcc42d16e47f219f9e98e76e09d8770b34a02200177e60492c5a8242f76f07bfe3661bde59ec2a17ce5bd2dab2abebdf89a62e2').toUint8Array(); + const expectedCompactSignature = Convert.hex('b292a619339f6e567a305c951c0dcbcc42d16e47f219f9e98e76e09d8770b34a0177e60492c5a8242f76f07bfe3661bde59ec2a17ce5bd2dab2abebdf89a62e2').toUint8Array(); + + const compactSignature = await Secp256r1.convertDerToCompactSignature({ derSignature }); + + expect(compactSignature).to.deep.equal(expectedCompactSignature); + }); + + it('passes Wycheproof test vector', async () => { + // Source: https://github.com/paulmillr/noble-curves/blob/37eab5a28a43c35b87e9e95a12ae6086393ac38b/test/wycheproof/ecdsa_secp256r1_sha256_test.json#L189-L198 + const publicKeyBytes = Convert.hex('042927b10512bae3eddcfe467828128bad2903269919f7086069c8c4df6c732838c7787964eaac00e5921fb1498a60f4606766b3d9685001558d1a974e7341513e').toUint8Array(); + const publicKey = await Secp256r1.bytesToPublicKey({ publicKeyBytes }); + const message = Convert.hex('313233343030').toUint8Array(); + const derSignature = Convert.hex('304402202ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e1802204cd60b855d442f5b3c7b11eb6c4e0ae7525fe710fab9aa7c77a67f79e6fadd76').toUint8Array(); + + const compactSignature = await Secp256r1.convertDerToCompactSignature({ derSignature }); + + const isValid = await Secp256r1.verify({ + key : publicKey, + signature : compactSignature, + data : message + }); + + expect(isValid).to.be.true; + }); + + it('throws an error for an invalid ASN.1 DER encoded ECDSA signature due to incorrect length', async () => { + // Invalid ASN.1 DER encoded ECDSA signature. + // Source: https://github.com/paulmillr/noble-curves/blob/37eab5a28a43c35b87e9e95a12ae6086393ac38b/test/wycheproof/ecdsa_secp256r1_sha256_test.json#L239-L248 + const invalidDerSignature = Convert.hex('304602202ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e18022100b329f479a2bbd0a5c384ee1493b1f5186a87139cac5df4087c134b49156847db').toUint8Array(); + + try { + await Secp256r1.convertDerToCompactSignature({ derSignature: invalidDerSignature }); + expect.fail('Expected method to throw an error.'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Invalid signature: incorrect length'); + } + }); + + it('throws an error for an invalid ASN.1 DER encoded ECDSA signature due to appending zeros to sequence', async () => { + // Invalid ASN.1 DER encoded ECDSA signature. + // Source: https://github.com/paulmillr/noble-curves/blob/37eab5a28a43c35b87e9e95a12ae6086393ac38b/test/wycheproof/ecdsa_secp256r1_sha256_test.json#L369-L378 + const invalidDerSignature = Convert.hex('304702202ba3a8be6b94d5ec80a6d9d1190a436effe50d85a1eee859b8cc6af9bd5c2e18022100b329f479a2bbd0a5c384ee1493b1f5186a87139cac5df4087c134b49156847db0000').toUint8Array(); + + try { + await Secp256r1.convertDerToCompactSignature({ derSignature: invalidDerSignature }); + expect.fail('Expected method to throw an error.'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Invalid signature: left bytes after parsing'); + } + }); + }); + + describe('decompressPublicKey()', () => { + it('converts a compressed public key to an uncompressed format', async () => { + const compressedPublicKeyBytes = Convert.hex('02d7251f4572325f4b1a9642600427adfe11ea3bd4dfe1cd7f4932612129e18784').toUint8Array(); + const uncompressedPublicKeyBytes = Convert.hex('04d7251f4572325f4b1a9642600427adfe11ea3bd4dfe1cd7f4932612129e187844247b3c6302e7ecd611dbb666380e1117b198f37a9d183de422947f6b6183098').toUint8Array(); + + const output = await Secp256r1.decompressPublicKey({ + publicKeyBytes: compressedPublicKeyBytes + }); + + // Confirm the length of the resulting public key is 65 bytes + expect(output.byteLength).to.equal(65); + + // Confirm the output matches the expected uncompressed public key. + expect(output).to.deep.equal(uncompressedPublicKeyBytes); + }); + + it('throws an error for an invalid compressed public key', async () => { + // Invalid compressed public key. + const invalidPublicKey = Convert.hex('fef0b998921eafb58f49efdeb0adc47123aa28a4042924236f08274d50c72fe7b0').toUint8Array(); + + try { + await Secp256r1.decompressPublicKey({ + publicKeyBytes: invalidPublicKey, + }); + expect.fail('Expected method to throw an error.'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Point of length 33 was invalid'); + } + }); + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await Secp256r1.generateKey(); + + expect(privateKey).to.have.property('crv', 'P-256'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'EC'); + expect(privateKey).to.have.property('x'); + expect(privateKey).to.have.property('y'); + }); + + it('returns a 32-byte private key', async () => { + const privateKey = await Secp256r1.generateKey() as JwkParamsEcPrivate; + + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(32); + }); + }); + + describe('getCurvePoint()', () => { + for (const vector of secp256r1GetCurvePoints.vectors) { + it(vector.description, async () => { + const keyBytes = Convert.hex(vector.input.keyBytes).toUint8Array(); + // @ts-expect-error because getCurvePoint() is a private method. + const points = await Secp256r1.getCurvePoint({ keyBytes }); + expect(points.x).to.deep.equal(Convert.hex(vector.output.x).toUint8Array()); + expect(points.y).to.deep.equal(Convert.hex(vector.output.y).toUint8Array()); + }); + } + + it('throws error with invalid input key length', async () => { + await expect( + // @ts-expect-error because getCurvePoint() is a private method. + Secp256r1.getCurvePoint({ keyBytes: new Uint8Array(16) }) + ).to.eventually.be.rejectedWith(Error, 'Point of length 16 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes'); + }); + }); + + describe('getPublicKey()', () => { + it('returns a public key in JWK format', async () => { + const publicKey = await Secp256r1.getPublicKey({ key: privateKey }); + + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('crv', 'P-256'); + expect(publicKey).to.have.property('kid'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.have.property('y'); + expect(publicKey).to.not.have.property('d'); + }); + + it('computes and adds a kid property, if missing', async () => { + const { kid, ...privateKeyWithoutKid } = privateKey; + const publicKey = await Secp256r1.getPublicKey({ key: privateKeyWithoutKid }); + + expect(publicKey).to.have.property('kid', kid); + }); + + it('returns the same output as computePublicKey()', async () => { + const publicKey = await Secp256r1.getPublicKey({ key: privateKey }); + expect(publicKey).to.deep.equal(await Secp256r1.computePublicKey({ key: privateKey })); + }); + + it('throws an error when provided a secp256r1 public key', async () => { + const secp256r1PublicKey: Jwk = { + kty : 'EC', + crv : 'P-256', + x : 'FRHEAeCMFUIWsDR4POZZ_MEaSePdq5UhcvKTHXOmAHQ', + y : 'XWaWp9dkMUqQ5ourD1421YJLHQmu4bhbr2QSMnTR35o' + }; + + await expect( + Secp256r1.getPublicKey({ key: secp256r1PublicKey }) + ).to.eventually.be.rejectedWith(Error, `key is not a 'P-256' private JWK`); + }); + + it('throws an error when provided an Ed25519 private key', async () => { + const ed25519PrivateKey: Jwk = { + crv : 'Ed25519', + d : 'TM0Imyj_ltqdtsNG7BFOD1uKMZ81q6Yk2oz27U-4pvs', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + kid : 'FtIu-VbGrfe_KB6CH7GNwODB72MNxj_ml11dEvO-7kk' + }; + + await expect( + Secp256r1.getPublicKey({ key: ed25519PrivateKey }) + ).to.eventually.be.rejectedWith(Error, `key is not a 'P-256' private JWK`); + }); + + it('throws an error when provided a secp256k1 private key', async () => { + const secp256k1PrivateKey: Jwk = { + kty : 'EC', + crv : 'secp256k1', + x : 'oCdX60O5sBTvHjSHqp5Z2Ik-iKROm2TSNi9Z7SFNpRQ', + y : '31b9rwHHVyynXw632oTVW7f2xcczjxf6BRAF7UuzYsE', + d : 'ycNY9W7EY-0VmVMWPAgUMiXO0O7_OhzPjZKXzVi0xKY' + }; + await expect( + Secp256r1.getPublicKey({ key: secp256k1PrivateKey }) + ).to.eventually.be.rejectedWith(Error, `key is not a 'P-256' private JWK`); + }); + + it('throws an error when provided an Ed25519 private key', async () => { + const ed25519PrivateKey: Jwk = { + crv : 'Ed25519', + d : 'TM0Imyj_ltqdtsNG7BFOD1uKMZ81q6Yk2oz27U-4pvs', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + kid : 'FtIu-VbGrfe_KB6CH7GNwODB72MNxj_ml11dEvO-7kk' + }; + + await expect( + Secp256r1.getPublicKey({ key: ed25519PrivateKey }) + ).to.eventually.be.rejectedWith(Error, `key is not a 'P-256' private JWK`); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey: Jwk = { + kty : 'EC', + crv : 'P-256', + d : 'xqQrTkJTX2GGbCW9V7Sp8ILlqzNlnbVF2BM4OkDqY3o', + x : 'uageVRxl4FPxSGXr5dXS4MfwiP56Ue-0qZmpM-VybJM', + y : 'cVm_UIPl7deVqHL-jXG5Ar1ZpHEVqwOyk-ugOg2W6ns' + }; + const privateKeyBytes = await Secp256r1.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('c6a42b4e42535f61866c25bd57b4a9f082e5ab33659db545d813383a40ea637a').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided a secp256r1 public key', async () => { + const publicKey: Jwk = { + kty : 'EC', + crv : 'P-256', + x : 'yRtzkBQdSfYwZ7EH6d9UMN-PV-r4ZZzXF3hGy8D9yy4', + y : 'bQUbIZeqUIUKV-N5265jD7_l2-xybjpFsr3kN4GdA_k' + }; + + await expect( + Secp256r1.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid EC private key'); + }); + + for (const vector of secp256r1PrivateKeyToBytes.vectors) { + it(vector.description, async () => { + const privateKeyBytes = await Secp256r1.privateKeyToBytes({ + privateKey: vector.input.privateKey as Jwk + }); + expect(privateKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('publicKeyToBytes()', () => { + it('returns a public key in JWK format', async () => { + const publicKey: Jwk = { + kty : 'EC', + crv : 'P-256', + x : 'yRtzkBQdSfYwZ7EH6d9UMN-PV-r4ZZzXF3hGy8D9yy4', + y : 'bQUbIZeqUIUKV-N5265jD7_l2-xybjpFsr3kN4GdA_k' + }; + + const publicKeyBytes = await Secp256r1.publicKeyToBytes({ publicKey }); + + expect(publicKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('04c91b7390141d49f63067b107e9df5430df8f57eaf8659cd7177846cbc0fdcb2e6d051b2197aa50850a57e379dbae630fbfe5dbec726e3a45b2bde437819d03f9').toUint8Array(); + expect(publicKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an Ed25519 private key', async () => { + const privateKey: Jwk = { + kty : 'EC', + crv : 'secp256k1', + d : 'dA7GmBDemtG48pjx0sDmpS3R6VjcKvyFdkvsFpwiLog', + x : 'N1KVEnQCMpbIp0sP_kL4L_S01LukMmR3QicD92H1klg', + y : 'wmp0ZbmnesDD8c7bE5xCiwsfu1UWhntSdjbzKG9wVVM', + kid : 'iwwOeCqgvREo5xGeBS-obWW9ZGjv0o1M65gUYN6SYh4' + }; + + await expect( + Secp256r1.publicKeyToBytes({ publicKey: privateKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid EC public key'); + }); + + for (const vector of secp256r1PublicKeyToBytes.vectors) { + it(vector.description, async () => { + const publicKeyBytes = await Secp256r1.publicKeyToBytes({ + publicKey: vector.input.publicKey as Jwk + }); + expect(publicKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('sharedSecret()', () => { + let ownPrivateKey: Jwk; + let ownPublicKey: Jwk; + let otherPartyPrivateKey: Jwk; + let otherPartyPublicKey: Jwk; + + beforeEach(async () => { + ownPrivateKey = privateKey; + ownPublicKey = publicKey; + + otherPartyPrivateKey = await Secp256r1.generateKey(); + otherPartyPublicKey = await Secp256r1.computePublicKey({ key: otherPartyPrivateKey }); + }); + + it('generates a 32-byte shared secret', async () => { + const sharedSecret = await Secp256r1.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : otherPartyPublicKey + }); + expect(sharedSecret).to.be.instanceOf(Uint8Array); + expect(sharedSecret.byteLength).to.equal(32); + }); + + it('is commutative', async () => { + const sharedSecretOwnOther = await Secp256r1.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : otherPartyPublicKey + }); + + const sharedSecretOtherOwn = await Secp256r1.sharedSecret({ + privateKeyA : otherPartyPrivateKey, + publicKeyB : ownPublicKey + }); + + expect(sharedSecretOwnOther).to.deep.equal(sharedSecretOtherOwn); + }); + + it('throws an error if the public/private keys from the same key pair are specified', async () => { + await expect( + Secp256r1.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : ownPublicKey + }) + ).to.eventually.be.rejectedWith(Error, 'shared secret cannot be computed from a single key pair'); + }); + }); + + describe('sign()', () => { + it('returns a 64-byte signature of type Uint8Array', async () => { + const data = new Uint8Array([51, 52, 53]); + const signature = await Secp256r1.sign({ key: privateKey, data }); + expect(signature).to.be.instanceOf(Uint8Array); + expect(signature.byteLength).to.equal(64); + }); + + it('accepts input data as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const key = privateKey; + let signature: Uint8Array; + + signature = await Secp256r1.sign({ key, data }); + expect(signature).to.be.instanceOf(Uint8Array); + }); + }); + + describe('validatePrivateKey()', () => { + for (const vector of secp256r1ValidatePrivateKey.vectors) { + it(vector.description, async () => { + const privateKeyBytes = Convert.hex(vector.input.privateKeyBytes).toUint8Array(); + const isValid = await Secp256r1.validatePrivateKey({ privateKeyBytes }); + expect(isValid).to.equal(vector.output); + }); + } + }); + + describe('validatePublicKey()', () => { + for (const vector of secp256r1ValidatePublicKey.vectors) { + it(vector.description, async () => { + const publicKeyBytes = Convert.hex(vector.input.publicKeyBytes).toUint8Array(); + const isValid = await Secp256r1.validatePublicKey({ publicKeyBytes }); + expect(isValid).to.equal(vector.output); + }); + } + }); + + describe('verify()', () => { + it('returns a boolean result', async () => { + const data = new Uint8Array([51, 52, 53]); + const signature = await Secp256r1.sign({ key: privateKey, data }); + + const isValid = await Secp256r1.verify({ key: publicKey, signature, data }); + expect(isValid).to.exist; + expect(isValid).to.be.true; + }); + + it('accepts input data as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + let isValid: boolean; + let signature: Uint8Array; + + // TypedArray - Uint8Array + signature = await Secp256r1.sign({ key: privateKey, data }); + isValid = await Secp256r1.verify({ key: publicKey, signature, data }); + expect(isValid).to.be.true; + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/primitives/xchacha20-poly1305.spec.ts b/packages/crypto/tests/primitives/xchacha20-poly1305.spec.ts index f40556e56..ea401f7a0 100644 --- a/packages/crypto/tests/primitives/xchacha20-poly1305.spec.ts +++ b/packages/crypto/tests/primitives/xchacha20-poly1305.spec.ts @@ -4,7 +4,7 @@ import chaiAsPromised from 'chai-as-promised'; import type { Jwk } from '../../src/jose/jwk.js'; -import { XChaCha20Poly1305 } from '../../src/primitives/xchacha20-poly1305.js'; +import { POLY1305_TAG_LENGTH, XChaCha20Poly1305 } from '../../src/primitives/xchacha20-poly1305.js'; chai.use(chaiAsPromised); @@ -35,11 +35,12 @@ describe('XChaCha20Poly1305', () => { describe('decrypt()', () => { it('returns Uint8Array plaintext with length matching input', async () => { + const ciphertext = Convert.hex('789e9689e5208d7fd9e1').toUint8Array(); + const tag = Convert.hex('09701fb9f36ab77a0f136ca539229a34').toUint8Array(); const plaintext = await XChaCha20Poly1305.decrypt({ - data : Convert.hex('789e9689e5208d7fd9e1').toUint8Array(), + data : new Uint8Array([...ciphertext, ...tag]), key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), nonce : new Uint8Array(24), - tag : Convert.hex('09701fb9f36ab77a0f136ca539229a34').toUint8Array() }); expect(plaintext).to.be.an('Uint8Array'); expect(plaintext.byteLength).to.equal(10); @@ -49,11 +50,14 @@ describe('XChaCha20Poly1305', () => { const privateKeyBytes = Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(); const privateKey = await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes }); + const ciphertext = Convert.hex('80246ca517c0fb5860c19090a7e7a2b030dde4882520102cbc64fad937916596ca9d').toUint8Array(); + const tag = Convert.hex('9e10a121d990e6a290f6b534516aa32f').toUint8Array(); + const nonce = Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array(); + const input = { - data : Convert.hex('80246ca517c0fb5860c19090a7e7a2b030dde4882520102cbc64fad937916596ca9d').toUint8Array(), - key : privateKey, - nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array(), - tag : Convert.hex('9e10a121d990e6a290f6b534516aa32f').toUint8Array() + data : new Uint8Array([...ciphertext, ...tag]), + key : privateKey, + nonce }; const output = Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(); @@ -61,7 +65,6 @@ describe('XChaCha20Poly1305', () => { data : input.data, key : input.key, nonce : input.nonce, - tag : input.tag }); expect(plaintext).to.deep.equal(output); @@ -70,10 +73,9 @@ describe('XChaCha20Poly1305', () => { it('throws an error if an invalid tag is given', async () => { await expect( XChaCha20Poly1305.decrypt({ - data : new Uint8Array(10), + data : new Uint8Array([...new Uint8Array(10), ...new Uint8Array(16)]), key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), - nonce : new Uint8Array(24), - tag : new Uint8Array(16) + nonce : new Uint8Array(24) }) ).to.eventually.be.rejectedWith(Error, 'invalid tag'); }); @@ -81,35 +83,38 @@ describe('XChaCha20Poly1305', () => { describe('encrypt()', () => { it('returns Uint8Array ciphertext and tag', async () => { - const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + const ciphertext = await XChaCha20Poly1305.encrypt({ data : new Uint8Array(10), key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), nonce : new Uint8Array(24) }); expect(ciphertext).to.be.an('Uint8Array'); - expect(ciphertext.byteLength).to.equal(10); - expect(tag).to.be.an('Uint8Array'); - expect(tag.byteLength).to.equal(16); + expect(ciphertext.byteLength).to.equal(10 + POLY1305_TAG_LENGTH); }); it('accepts additional authenticated data', async () => { - const { ciphertext: ciphertextAad, tag: tagAad } = await XChaCha20Poly1305.encrypt({ + const ciphertextAad = await XChaCha20Poly1305.encrypt({ additionalData : new Uint8Array(64), data : new Uint8Array(10), key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), nonce : new Uint8Array(24) }); - const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + const ciphertext = await XChaCha20Poly1305.encrypt({ data : new Uint8Array(10), key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), nonce : new Uint8Array(24) }); - expect(ciphertextAad.byteLength).to.equal(10); - expect(ciphertext.byteLength).to.equal(10); - expect(ciphertextAad).to.deep.equal(ciphertext); - expect(tagAad).to.not.deep.equal(tag); + const ciphertextWithAad = ciphertextAad.slice(0, -POLY1305_TAG_LENGTH); + const tagWithAad = ciphertextAad.slice(-POLY1305_TAG_LENGTH); + const ciphertextWithoutAad = ciphertext.slice(0, -POLY1305_TAG_LENGTH); + const tagWithoutAad = ciphertext.slice(-POLY1305_TAG_LENGTH); + + expect(ciphertextWithAad.byteLength).to.equal(10); + expect(ciphertextWithoutAad.byteLength).to.equal(10); + expect(ciphertextWithAad).to.deep.equal(ciphertextWithoutAad); + expect(tagWithAad).to.not.deep.equal(tagWithoutAad); }); it('passes test vectors', async () => { @@ -126,13 +131,16 @@ describe('XChaCha20Poly1305', () => { tag : Convert.hex('9e10a121d990e6a290f6b534516aa32f').toUint8Array() }; - const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + const ciphertext = await XChaCha20Poly1305.encrypt({ data : input.data, key : input.key, nonce : input.nonce }); - expect(ciphertext).to.deep.equal(output.ciphertext); + const ciphertextOnly = ciphertext.slice(0, -POLY1305_TAG_LENGTH); + const tag = ciphertext.slice(-POLY1305_TAG_LENGTH); + + expect(ciphertextOnly).to.deep.equal(output.ciphertext); expect(tag).to.deep.equal(output.tag); }); }); diff --git a/packages/crypto/tests/tsconfig.json b/packages/crypto/tests/tsconfig.json index 0cdb42f92..1d39d9695 100644 --- a/packages/crypto/tests/tsconfig.json +++ b/packages/crypto/tests/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "outDir": "compiled", "declarationDir": "compiled/types", - "resolveJsonModule": true, "sourceMap": true }, "include": [ diff --git a/packages/crypto/tests/utils.spec.ts b/packages/crypto/tests/utils.spec.ts index cdc967c3c..2bdc3e555 100644 --- a/packages/crypto/tests/utils.spec.ts +++ b/packages/crypto/tests/utils.spec.ts @@ -4,8 +4,6 @@ import * as sinon from 'sinon'; import { randomUuid, randomBytes, - keyToMultibaseId, - multibaseIdToKey, checkValidProperty, isWebCryptoSupported, checkRequiredProperty, @@ -85,96 +83,6 @@ describe('Crypto Utils', () => { }); }); - describe('keyToMultibaseId()', () => { - it('returns a multibase encoded string', () => { - const input = { - key : new Uint8Array(32), - multicodecName : 'ed25519-pub', - }; - const encoded = keyToMultibaseId({ key: input.key, 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: { key: Uint8Array, multicodecName: string }; - let output: string; - let encoded: string; - - // Test Vector 1. - input = { - key : (new Uint8Array(32)).fill(0), - multicodecName : 'ed25519-pub', - }; - output = 'z6MkeTG3bFFSLYVU7VqhgZxqr6YzpaGrQtFMh1uvqGy1vDnP'; - encoded = keyToMultibaseId({ key: input.key, multicodecName: input.multicodecName }); - expect(encoded).to.equal(output); - - // Test Vector 2. - input = { - key : (new Uint8Array(32)).fill(1), - multicodecName : 'ed25519-pub', - }; - output = 'z6MkeXBLjYiSvqnhFb6D7sHm8yKm4jV45wwBFRaatf1cfZ76'; - encoded = keyToMultibaseId({ key: input.key, multicodecName: input.multicodecName }); - expect(encoded).to.equal(output); - - // Test Vector 3. - input = { - key : (new Uint8Array(32)).fill(9), - multicodecName : 'ed25519-pub', - }; - output = 'z6Mkf4XhsxSXfEAWNK6GcFu7TyVs21AfUTRjiguqMhNQeDgk'; - encoded = keyToMultibaseId({ key: input.key, multicodecName: input.multicodecName }); - expect(encoded).to.equal(output); - }); - }); - - describe('multibaseIdToKey()', () => { - it('converts secp256k1-pub multibase identifiers', () => { - const multibaseKeyId = 'zQ3shMrXA3Ah8h5asMM69USP8qRDnPaCLRV3nPmitAXVfWhgp'; - - const { key, multicodecCode, multicodecName } = multibaseIdToKey({ multibaseKeyId }); - - expect(key).to.exist; - expect(key).to.be.a('Uint8Array'); - expect(key).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 { key, multicodecCode, multicodecName } = multibaseIdToKey({ multibaseKeyId }); - - expect(key).to.exist; - expect(key).to.be.a('Uint8Array'); - expect(key).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 { key, multicodecCode, multicodecName } = multibaseIdToKey({ multibaseKeyId }); - - expect(key).to.exist; - expect(key).to.be.a('Uint8Array'); - expect(key).to.have.length(32); - expect(multicodecCode).to.exist; - expect(multicodecCode).to.equal(236); - expect(multicodecName).to.exist; - expect(multicodecName).to.equal('x25519-pub'); - }); - }); - describe('randomBytes()', () => { it('returns a Uint8Array of the specified length', () => { const length = 16; diff --git a/packages/dids/.c8rc.json b/packages/dids/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/dids/.c8rc.json +++ b/packages/dids/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/dids/package.json b/packages/dids/package.json index c588b889b..41226316d 100644 --- a/packages/dids/package.json +++ b/packages/dids/package.json @@ -1,6 +1,6 @@ { "name": "@web5/dids", - "version": "0.2.4", + "version": "0.3.0", "description": "TBD DIDs library", "type": "module", "main": "./dist/cjs/index.js", @@ -75,22 +75,19 @@ "node": ">=18.0.0" }, "dependencies": { - "@decentralized-identity/ion-pow-sdk": "1.0.17", "@decentralized-identity/ion-sdk": "1.0.1", - "@web5/common": "0.2.2", - "@web5/crypto": "0.2.2", - "did-resolver": "4.1.0", - "dns-packet": "5.6.1", + "@dnsquery/dns-packet": "6.1.1", + "@web5/common": "0.2.3", + "@web5/crypto": "0.4.0", + "bencode": "4.0.0", "level": "8.0.0", - "ms": "2.1.3", - "pkarr": "1.1.1", - "z32": "1.0.1" + "ms": "2.1.3" }, "devDependencies": { "@playwright/test": "1.40.1", + "@types/bencode": "2.0.4", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", - "@types/dns-packet": "^5.6.1", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@types/sinon": "17.0.2", @@ -98,7 +95,7 @@ "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", @@ -109,7 +106,7 @@ "playwright": "1.40.1", "rimraf": "4.4.0", "sinon": "16.1.3", - "source-map-loader": "4.0.1", + "source-map-loader": "4.0.2", "typescript": "5.1.6" } } diff --git a/packages/dids/src/dht.ts b/packages/dids/src/dht.ts deleted file mode 100644 index 39185a8ad..000000000 --- a/packages/dids/src/dht.ts +++ /dev/null @@ -1,356 +0,0 @@ -import type { Packet, TxtAnswer } from 'dns-packet'; -import type { PublicKeyJwk, Web5Crypto} from '@web5/crypto'; - -import { Jose } from '@web5/crypto'; -import { Convert } from '@web5/common'; -import { Pkarr, SignedPacket, z32 } from 'pkarr'; -import dns, { AUTHORITATIVE_ANSWER } from 'dns-packet'; - -import type { DidDocument } from './types.js'; - -const PKARR_RELAY = 'https://diddht.tbddev.org'; -const TTL = 7200; - -/** - * A class to handle operations related to DHT-based Decentralized Identifiers (DIDs). - * It provides methods to: - * - Parse a DNS packet into a DID Document. - * - Retrieve a DID Document from the DHT. - * - Publish a DID Document to the DHT. - * - Convert a DID Document to a DNS packet. - * - * The class assumes that DIDs and DID Documents are compliant with the did:dht specification. - */ -export class DidDht { - - /** - * Parses a DNS packet into a DID Document. - * @param did The DID of the document. - * @param packet A DNS packet to parse into a DID Document. - * @returns A Promise that resolves to the parsed DidDocument. - */ - public static async fromDnsPacket({ did, packet }: { - did: string, - packet: Packet - }): Promise { - const document: Partial = { - id: did, - }; - - const keyLookup = new Map(); - - for (const answer of packet.answers) { - if (answer.type !== 'TXT') continue; - - const dataStr = answer.data?.toString(); - // Extracts 'k' or 's' from "_k0._did" or "_s0._did" - const recordType = answer.name?.split('.')[0].substring(1, 2); - - /*eslint-disable no-case-declarations*/ - switch (recordType) { - case 'k': { - const { id, t, k } = DidDht.parseTxtData({ data: dataStr }); - const keyConfigurations: { [keyType: string]: Partial } = { - '0': { - crv : 'Ed25519', - kty : 'OKP', - alg : 'EdDSA' - }, - '1': { - crv : 'secp256k1', - kty : 'EC', - alg : 'ES256K' - } - }; - const keyConfig = keyConfigurations[t]; - if (!keyConfig) { - throw new Error('Unsupported key type'); - } - - const publicKeyJwk = await Jose.keyToJwk({ - ...keyConfig, - kid : id, - keyMaterial : Convert.base64Url(k).toUint8Array(), - keyType : 'public' - }) as PublicKeyJwk; - - if (!document.verificationMethod) { - document.verificationMethod = []; - } - document.verificationMethod.push({ - id : `${did}#${id}`, - type : 'JsonWebKey2020', - controller : did, - publicKeyJwk : publicKeyJwk, - }); - keyLookup.set(answer.name, id); - - break; - } - - case 's': { - const {id: sId, t: sType, uri} = DidDht.parseTxtData({ data: dataStr }); - - if (!document.service) { - document.service = []; - } - document.service.push({ - id : `${did}#${sId}`, - type : sType, - serviceEndpoint : uri - }); - - break; - } - } - } - - // Extract relationships from root record - const didSuffix = did.split('did:dht:')[1]; - const potentialRootNames = ['_did', `_did.${didSuffix}`]; - - let actualRootName = null; - const root = packet.answers - .filter(answer => { - if (potentialRootNames.includes(answer.name)) { - actualRootName = answer.name; - return true; - } - return false; - }) as dns.TxtAnswer[]; - - if (root.length === 0) { - throw new Error('No root record found'); - } - - if (root.length > 1) { - throw new Error('Multiple root records found'); - } - const singleRoot = root[0] as dns.TxtAnswer; - const rootRecord = singleRoot.data?.toString().split(';'); - rootRecord?.forEach(record => { - const [type, ids] = record.split('='); - let idList = ids?.split(',').map(id => `#${keyLookup.get(`_${id}.${actualRootName}`)}`); - switch (type) { - case 'auth': - document.authentication = idList; - break; - case 'asm': - document.assertionMethod = idList; - break; - case 'agm': - document.keyAgreement = idList; - break; - case 'inv': - document.capabilityInvocation = idList; - break; - case 'del': - document.capabilityDelegation = idList; - break; - } - }); - - return document as DidDocument; - } - - /** - * Retrieves a DID Document from the DHT. - * - * @param did The DID of the document to retrieve. - * @param relay The relay to use to retrieve the document; defaults to `PKARR_RELAY`. - * @returns A Promise that resolves to the retrieved DidDocument. - */ - public static async getDidDocument({ did, relay = PKARR_RELAY }: { - did: string, - relay?: string - }): Promise { - const didFragment = did.replace('did:dht:', ''); - const publicKeyBytes = new Uint8Array(z32.decode(didFragment)); - const resolved = await Pkarr.relayGet(relay, publicKeyBytes); - if (resolved) { - return await DidDht.fromDnsPacket({ did, packet: resolved.packet() }); - } - throw new Error('No packet found'); - } - - /** - * Publishes a DID Document to the DHT. - * - * @param keyPair The key pair to sign the document with. - * @param didDocument The DID Document to publish. - * @param relay The relay to use to retrieve the document; defaults to `PKARR_RELAY`. - * @returns A boolean indicating the success of the publishing operation. - */ - public static async publishDidDocument({ keyPair, didDocument, relay = PKARR_RELAY }: { - didDocument: DidDocument, - keyPair: Web5Crypto.CryptoKeyPair, - relay?: string - }): Promise { - const packet = await DidDht.toDnsPacket({ didDocument }); - const pkarrKeypair = { - publicKey : keyPair.publicKey.material, - secretKey : new Uint8Array([...keyPair.privateKey.material, ...keyPair.publicKey.material]) - }; - const signedPacket = SignedPacket.fromPacket(pkarrKeypair, packet); - const results = await Pkarr.relayPut(relay, signedPacket); - - return results.ok; - } - - /** - * Converts a DID Document to a DNS packet according to the did:dht spec. - * - * @param didDocument The DID Document to convert. - * @returns A DNS packet converted from the DID Document. - */ - public static async toDnsPacket({ didDocument }: { didDocument: DidDocument }): Promise { - const packet: Partial = { - id : 0, - type : 'response', - flags : AUTHORITATIVE_ANSWER, - answers : [] - }; - - const vmIds: string[] = []; - const svcIds: string[] = []; - const rootRecord: string[] = []; - const keyLookup = new Map(); - - // Add key records for each verification method - for (const vm of didDocument.verificationMethod) { - const index = didDocument.verificationMethod.indexOf(vm); - const recordIdentifier = `k${index}`; - let vmId = DidDht.identifierFragment({ identifier: vm.id }); - keyLookup.set(vmId, recordIdentifier); - - let keyType: number; - switch (vm.publicKeyJwk.alg) { - case 'EdDSA': - keyType = 0; - break; - case 'ES256K': - keyType = 1; - break; - default: - keyType = 0; // Default value or throw an error if needed - } - - const cryptoKey = await Jose.jwkToCryptoKey({ key: vm.publicKeyJwk }); - const keyBase64Url = Convert.uint8Array(cryptoKey.material).toBase64Url(); - - const keyRecord: TxtAnswer = { - type : 'TXT', - name : `_${recordIdentifier}._did`, - ttl : TTL, - data : `id=${vmId},t=${keyType},k=${keyBase64Url}` - }; - - packet.answers.push(keyRecord); - vmIds.push(recordIdentifier); - } - - // Add service records - didDocument.service?.forEach((service, index) => { - const recordIdentifier = `s${index}`; - let sId = DidDht.identifierFragment({ identifier: service.id }); - const serviceRecord: TxtAnswer = { - type : 'TXT', - name : `_${recordIdentifier}._did`, - ttl : TTL, - data : `id=${sId},t=${service.type},uri=${service.serviceEndpoint}` - }; - - packet.answers.push(serviceRecord); - svcIds.push(recordIdentifier); - }); - - // add root record for vms and svcs - if (vmIds.length) { - rootRecord.push(`vm=${vmIds.join(',')}`); - } - if (svcIds.length) { - rootRecord.push(`svc=${svcIds.join(',')}`); - } - - // add verification relationships - if (didDocument.authentication) { - const authIds: string[] = didDocument.authentication - .map(id => DidDht.identifierFragment({ identifier: id })) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`auth=${authIds.join(',')}`); - } - } - if (didDocument.assertionMethod) { - const authIds: string[] = didDocument.assertionMethod - .map(id => DidDht.identifierFragment({ identifier: id })) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`asm=${authIds.join(',')}`); - } - } - if (didDocument.keyAgreement) { - const authIds: string[] = didDocument.keyAgreement - .map(id => DidDht.identifierFragment({ identifier: id })) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`agm=${authIds.join(',')}`); - } - } - if (didDocument.capabilityInvocation) { - const authIds: string[] = didDocument.capabilityInvocation - .map(id => DidDht.identifierFragment({ identifier: id })) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`inv=${authIds.join(',')}`); - } - } - if (didDocument.capabilityDelegation) { - const authIds: string[] = didDocument.capabilityDelegation - .map(id => DidDht.identifierFragment({ identifier: id })) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`del=${authIds.join(',')}`); - } - } - - // Add root record - packet.answers.push({ - type : 'TXT', - name : '_did', - ttl : TTL, - data : rootRecord.join(';') - }); - - return packet as Packet; - } - - /** - * Extracts the fragment from a DID. - * - * @param identifier The DID to extract the fragment from. - * @returns The fragment from the DID or the complete DID if no fragment exists. - */ - private static identifierFragment({ identifier }: { identifier: string }): string { - return identifier.includes('#') ? identifier.substring(identifier.indexOf('#') + 1) : identifier; - } - - /** - * Parses TXT data from a DNS answer to extract key or service information. - * - * @param data The TXT record string data containing key-value pairs separated by commas. - * @returns An object containing parsed attributes such as 'id', 't', 'k', and 'uri'. - */ - private static parseTxtData({ data }: { data: string }): { [key: string]: string } { - return data.split(',').reduce((acc, pair) => { - const [key, value] = pair.split('='); - acc[key] = value; - return acc; - }, {} as { [key: string]: string }); - } -} \ No newline at end of file diff --git a/packages/dids/src/did-dht.ts b/packages/dids/src/did-dht.ts deleted file mode 100644 index b0973c926..000000000 --- a/packages/dids/src/did-dht.ts +++ /dev/null @@ -1,314 +0,0 @@ -import type { JwkKeyPair, PublicKeyJwk, Web5Crypto } from '@web5/crypto'; - -import z32 from 'z32'; -import { EcdsaAlgorithm, EdDsaAlgorithm, Jose } from '@web5/crypto'; - -import type { - DidMethod, - DidService, - DidDocument, - PortableDid, - DidResolutionResult, - DidResolutionOptions, - VerificationRelationship, - DidKeySetVerificationMethodKey, -} from './types.js'; - -import { DidDht } from './dht.js'; -import { parseDid } from './utils.js'; - -const SupportedCryptoKeyTypes = [ - 'Ed25519', - 'secp256k1' -] as const; - -export type DidDhtCreateOptions = { - publish?: boolean; - keySet?: DidDhtKeySet; - services?: DidService[]; -} - -export type DidDhtKeySet = { - verificationMethodKeys?: DidKeySetVerificationMethodKey[]; -} - -export class DidDhtMethod implements DidMethod { - - public static methodName = 'dht'; - - /** - * Creates a new DID Document according to the did:dht spec. - * @param options The options to use when creating the DID Document, including whether to publish it. - * @returns A promise that resolves to a PortableDid object. - */ - public static async create(options?: DidDhtCreateOptions): Promise { - const { publish = false, keySet: initialKeySet, services } = options ?? {}; - - // Generate missing keys, if not provided in the options. - const keySet = await this.generateKeySet({ keySet: initialKeySet }); - - // Get the identifier and set it. - const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); - const id = await this.getDidIdentifier({ key: identityKey.publicKeyJwk }); - - // Add all other keys to the verificationMethod and relationship arrays. - const relationshipsMap: Partial> = {}; - const verificationMethods = keySet.verificationMethodKeys.map(key => { - for (const relationship of key.relationships) { - if (relationshipsMap[relationship]) { - relationshipsMap[relationship].push(`#${key.publicKeyJwk.kid}`); - } else { - relationshipsMap[relationship] = [`#${key.publicKeyJwk.kid}`]; - } - } - - return { - id : `${id}#${key.publicKeyJwk.kid}`, - type : 'JsonWebKey2020', - controller : id, - publicKeyJwk : key.publicKeyJwk - }; - }); - - // Add DID identifier to the service IDs. - services?.map(service => { - service.id = `${id}#${service.id}`; - }); - - // Assemble the DID Document. - const document: DidDocument = { - id, - verificationMethod: [...verificationMethods], - ...relationshipsMap, - ...services && { service: services } - }; - - // If the publish flag is set, publish the DID Document to the DHT. - if (publish) { - await this.publish({ identityKey, didDocument: document }); - } - - return { - did : document.id, - document : document, - keySet : keySet - }; - } - - - /** - * Generates a JWK key pair. - * @param options The key algorithm and key ID to use. - * @returns A promise that resolves to a JwkKeyPair object. - */ - public static async generateJwkKeyPair(options: { - keyAlgorithm: typeof SupportedCryptoKeyTypes[number], - keyId?: string - }): Promise { - const {keyAlgorithm, keyId} = options; - - let cryptoKeyPair: Web5Crypto.CryptoKeyPair; - - switch (keyAlgorithm) { - case 'Ed25519': { - cryptoKeyPair = await new EdDsaAlgorithm().generateKey({ - algorithm : {name: 'EdDSA', namedCurve: 'Ed25519'}, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - break; - } - - case 'secp256k1': { - cryptoKeyPair = await new EcdsaAlgorithm().generateKey({ - algorithm : {name: 'ECDSA', namedCurve: 'secp256k1'}, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - break; - } - - default: { - throw new Error(`Unsupported crypto algorithm: '${keyAlgorithm}'`); - } - } - - // Convert the CryptoKeyPair to JwkKeyPair. - const jwkKeyPair = await Jose.cryptoKeyToJwkPair({keyPair: cryptoKeyPair}); - - // Set kid values. - if (keyId) { - jwkKeyPair.privateKeyJwk.kid = keyId; - jwkKeyPair.publicKeyJwk.kid = keyId; - } else { - // If a key ID is not specified, generate RFC 7638 JWK thumbprint. - const jwkThumbprint = await Jose.jwkThumbprint({key: jwkKeyPair.publicKeyJwk}); - jwkKeyPair.privateKeyJwk.kid = jwkThumbprint; - jwkKeyPair.publicKeyJwk.kid = jwkThumbprint; - } - - return jwkKeyPair; - } - - /** - * Generates a key set for a DID Document. - * @param options The key set to use when generating the key set. - * @returns A promise that resolves to a DidDhtKeySet object. - */ - public static async generateKeySet(options?: { - keySet?: DidDhtKeySet - }): Promise { - let { keySet = {} } = options ?? {}; - - // If the key set is missing a `verificationMethodKeys` array, create one. - if (!keySet.verificationMethodKeys) keySet.verificationMethodKeys = []; - - // If the key set lacks an identity key (`kid: 0`), generate one. - if (!keySet.verificationMethodKeys.some(key => key.publicKeyJwk.kid === '0')) { - const identityKey = await this.generateJwkKeyPair({ - keyAlgorithm : 'Ed25519', - keyId : '0' - }); - keySet.verificationMethodKeys.push({ - ...identityKey, - relationships: ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] - }); - } - - // Generate RFC 7638 JWK thumbprints if `kid` is missing from any key. - for (const key of keySet.verificationMethodKeys) { - if (key.publicKeyJwk) key.publicKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.publicKeyJwk}); - if (key.privateKeyJwk) key.privateKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.privateKeyJwk}); - } - - return keySet; - } - - /** - * Gets the identifier fragment from a DID. - * @param options The key to get the identifier fragment from. - * @returns A promise that resolves to a string containing the identifier. - */ - public static async getDidIdentifier(options: { - key: PublicKeyJwk - }): Promise { - const { key } = options; - - const cryptoKey = await Jose.jwkToCryptoKey({ key }); - const identifier = z32.encode(cryptoKey.material); - return 'did:dht:' + identifier; - } - - /** - * Gets the identifier fragment from a DID. - * @param options The key to get the identifier fragment from. - * @returns A promise that resolves to a string containing the identifier fragment. - */ - public static async getDidIdentifierFragment(options: { - key: PublicKeyJwk - }): Promise { - const { key } = options; - const cryptoKey = await Jose.jwkToCryptoKey({ key }); - return z32.encode(cryptoKey.material); - } - - /** - * Publishes a DID Document to the DHT. - * @param keySet The key set to use to sign the DHT payload. - * @param didDocument The DID Document to publish. - * @returns A boolean indicating the success of the publishing operation. - */ - public static async publish({ didDocument, identityKey }: { - didDocument: DidDocument, - identityKey: DidKeySetVerificationMethodKey - }): Promise { - const publicCryptoKey = await Jose.jwkToCryptoKey({key: identityKey.publicKeyJwk}); - const privateCryptoKey = await Jose.jwkToCryptoKey({key: identityKey.privateKeyJwk}); - - const isPublished = await DidDht.publishDidDocument({ - keyPair: { - publicKey : publicCryptoKey, - privateKey : privateCryptoKey - }, - didDocument - }); - - return isPublished; - } - - /** - * Resolves a DID Document based on the specified options. - * - * @param options - Configuration for resolving a DID Document. - * @param options.didUrl - The DID URL to resolve. - * @param options.resolutionOptions - Optional settings for the DID resolution process as defined in the DID Core specification. - * @returns A Promise that resolves to a `DidResolutionResult`, containing the resolved DID Document and associated metadata. - */ - public static async resolve(options: { - didUrl: string, - resolutionOptions?: DidResolutionOptions - }): Promise { - const { didUrl, resolutionOptions: _ } = options; - // TODO: Implement resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution - - const parsedDid = parseDid({ didUrl }); - if (!parsedDid) { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : null, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'invalidDid', - errorMessage : `Cannot parse DID: ${didUrl}` - } - }; - } - - if (parsedDid.method !== 'dht') { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : null, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'methodNotSupported', - errorMessage : `Method not supported: ${parsedDid.method}` - } - }; - } - - let didDocument: DidDocument; - - /** - * TODO: This is a temporary workaround for the following issue: https://github.com/TBD54566975/web5-js/issues/331 - * As of 5 Dec 2023, the `pkarr` library throws an error if the DID is not found. Until a - * better solution is found, catch the error and return a DID Resolution Result with an - * error message. - */ - try { - didDocument = await DidDht.getDidDocument({ did: parsedDid.did }); - } catch (error: any) { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : null, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'internalError', - errorMessage : `An unexpected error occurred while resolving DID: ${parsedDid.did}` - } - }; - } - - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument, - didDocumentMetadata : {}, - didResolutionMetadata : { - did: { - didString : parsedDid.did, - methodSpecificId : parsedDid.id, - method : parsedDid.method - } - } - }; - } -} \ No newline at end of file diff --git a/packages/dids/src/did-error.ts b/packages/dids/src/did-error.ts new file mode 100644 index 000000000..efc0b93d5 --- /dev/null +++ b/packages/dids/src/did-error.ts @@ -0,0 +1,72 @@ +/** + * A custom error class for DID-related errors. + */ +export class DidError extends Error { + /** + * Constructs an instance of DidError, a custom error class for handling DID-related errors. + * + * @param code - A {@link DidErrorCode} representing the specific type of error encountered. + * @param message - A human-readable description of the error. + */ + constructor(public code: DidErrorCode, message: string) { + super(message); + this.name = 'DidError'; + + // Ensures that instanceof works properly, the correct prototype chain when using inheritance, + // and that V8 stack traces (like Chrome, Edge, and Node.js) are more readable and relevant. + Object.setPrototypeOf(this, new.target.prototype); + + // Captures the stack trace in V8 engines (like Chrome, Edge, and Node.js). + // In non-V8 environments, the stack trace will still be captured. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, DidError); + } + } +} + +/** + * An enumeration of possible DID error codes. + */ +export enum DidErrorCode { + /** The DID supplied does not conform to valid syntax. */ + InvalidDid = 'invalidDid', + + /** The supplied method name is not supported by the DID method and/or DID resolver implementation. */ + MethodNotSupported = 'methodNotSupported', + + /** An unexpected error occurred during the requested DID operation. */ + InternalError = 'internalError', + + /** The DID document supplied does not conform to valid syntax. */ + InvalidDidDocument = 'invalidDidDocument', + + /** The byte length of a DID document does not match the expected value. */ + InvalidDidDocumentLength = 'invalidDidDocumentLength', + + /** The DID URL supplied to the dereferencing function does not conform to valid syntax. */ + InvalidDidUrl = 'invalidDidUrl', + + /** An invalid public key is detected during a DID operation. */ + InvalidPublicKey = 'invalidPublicKey', + + /** The byte length of a public key does not match the expected value. */ + InvalidPublicKeyLength = 'invalidPublicKeyLength', + + /** An invalid public key type was detected during a DID operation. */ + InvalidPublicKeyType = 'invalidPublicKeyType', + + /** Verification of a signature failed during a DID operation. */ + InvalidSignature = 'invalidSignature', + + /** The DID resolver was unable to find the DID document resulting from the resolution request. */ + NotFound = 'notFound', + + /** + * The representation requested via the `accept` input metadata property is not supported by the + * DID method and/or DID resolver implementation. + */ + RepresentationNotSupported = 'representationNotSupported', + + /** The type of a public key is not supported by the DID method and/or DID resolver implementation. */ + UnsupportedPublicKeyType = 'unsupportedPublicKeyType', +} \ No newline at end of file diff --git a/packages/dids/src/did-ion.ts b/packages/dids/src/did-ion.ts deleted file mode 100644 index 7c832aa9f..000000000 --- a/packages/dids/src/did-ion.ts +++ /dev/null @@ -1,611 +0,0 @@ -import type { JwkKeyPair, PrivateKeyJwk, PublicKeyJwk, Web5Crypto } from '@web5/crypto'; -import type { IonDocumentModel, IonPublicKeyModel, JwkEd25519, JwkEs256k } from '@decentralized-identity/ion-sdk'; - -import { Convert, universalTypeOf } from '@web5/common'; -import IonProofOfWork from '@decentralized-identity/ion-pow-sdk'; -// import { IonProofOfWork } from '@decentralized-identity/ion-pow-sdk'; -import { EcdsaAlgorithm, EdDsaAlgorithm, Jose } from '@web5/crypto'; -import { IonDid, IonPublicKeyPurpose, IonRequest } from '@decentralized-identity/ion-sdk'; - -import type { DidDocument, DidKeySetVerificationMethodKey, DidMethod, DidResolutionOptions, DidResolutionResult, DidService, DwnServiceEndpoint, PortableDid } from './types.js'; - -import { getServices, isDwnServiceEndpoint, parseDid } from './utils.js'; - -export type DidIonAnchorOptions = { - challengeEnabled?: boolean; - challengeEndpoint?: string; - operationsEndpoint?: string; - keySet: DidIonKeySet; - services: DidService[]; -} - -export type DidIonCreateOptions = { - anchor?: boolean; - keyAlgorithm?: typeof SupportedCryptoAlgorithms[number]; - keySet?: DidIonKeySet; - services?: DidService[]; -} - -export type DidIonKeySet = { - recoveryKey?: JwkKeyPair; - updateKey?: JwkKeyPair; - verificationMethodKeys?: DidKeySetVerificationMethodKey[]; -} - -enum OperationType { - Create = 'create', - Update = 'update', - Deactivate = 'deactivate', - Recover = 'recover' -} - -/** - * Data model representing a public key in the DID Document. - */ -export interface IonCreateRequestModel { - type: OperationType; - suffixData: { - deltaHash: string; - recoveryCommitment: string; - }; - delta: { - updateCommitment: string; - patches: { - action: string; - document: IonDocumentModel; - }[]; - } -} - -const SupportedCryptoAlgorithms = [ - 'Ed25519', - 'secp256k1' -] as const; - -const VerificationRelationshipToIonPublicKeyPurpose = { - assertionMethod : IonPublicKeyPurpose.AssertionMethod, - authentication : IonPublicKeyPurpose.Authentication, - capabilityDelegation : IonPublicKeyPurpose.CapabilityDelegation, - capabilityInvocation : IonPublicKeyPurpose.CapabilityInvocation, - keyAgreement : IonPublicKeyPurpose.KeyAgreement -}; - -export class DidIonMethod implements DidMethod { - /** - * Name of the DID method - */ - public static methodName = 'ion'; - - public static async anchor(options: { - services: DidService[], - keySet: DidIonKeySet, - challengeEnabled?: boolean, - challengeEndpoint?: string, - operationsEndpoint?: string - }): Promise { - const { - challengeEnabled = false, - challengeEndpoint = 'https://beta.ion.msidentity.com/api/v1.0/proof-of-work-challenge', - keySet, - services, - operationsEndpoint = 'https://ion.tbd.engineering/operations' - } = options; - - // Create ION Document. - const ionDocument = await DidIonMethod.createIonDocument({ - keySet: keySet, - services - }); - - const createRequest = await DidIonMethod.getIonCreateRequest({ - ionDocument, - recoveryPublicKeyJwk : keySet.recoveryKey.publicKeyJwk, - updatePublicKeyJwk : keySet.updateKey.publicKeyJwk - }); - - let resolutionResult: DidResolutionResult; - - if (challengeEnabled) { - const response = await IonProofOfWork.submitIonRequest( - challengeEndpoint, - operationsEndpoint, - JSON.stringify(createRequest) - ); - - if (response !== undefined && universalTypeOf(response) === 'String') { - resolutionResult = JSON.parse(response); - } - - } else { - const response = await fetch(operationsEndpoint, { - method : 'POST', - mode : 'cors', - body : JSON.stringify(createRequest), - headers : { - 'Content-Type': 'application/json' - } - }); - - if (response.ok) { - resolutionResult = await response.json(); - } - } - - return resolutionResult; - } - - public static async create(options?: DidIonCreateOptions): Promise { - let { anchor, keyAlgorithm, keySet, services } = options ?? { }; - - // Begin constructing a PortableDid. - const did: Partial = {}; - - // If any member of the key set is missing, generate the keys. - did.keySet = await DidIonMethod.generateKeySet({ keyAlgorithm, keySet }); - - // Generate Long Form DID URI. - did.did = await DidIonMethod.getLongFormDid({ - keySet: did.keySet, - services - }); - - // Get short form DID. - did.canonicalId = await DidIonMethod.getShortFormDid({ didUrl: did.did }); - - let didResolutionResult: DidResolutionResult | undefined; - if (anchor) { - // Attempt to anchor the DID. - didResolutionResult = await DidIonMethod.anchor({ - keySet: did.keySet, - services - }); - - } else { - // If anchoring was not requested, then resolve the long form DID. - didResolutionResult = await DidIonMethod.resolve({ didUrl: did.did }); - } - - // Store the DID Document. - did.document = didResolutionResult.didDocument; - - return did as PortableDid; - } - - public static async decodeLongFormDid(options: { - didUrl: string - }): Promise { - const { didUrl } = options; - - const parsedDid = parseDid({ didUrl }); - - if (!parsedDid) { - throw new Error(`DidIonMethod: Unable to parse DID: ${didUrl}`); - } - - const decodedLongFormDid = Convert.base64Url( - parsedDid.id.split(':').pop() - ).toObject() as Pick; - - const createRequest: IonCreateRequestModel = { - ...decodedLongFormDid, - type: OperationType.Create - }; - - return createRequest; - } - - /** - * Generates two key pairs used for authorization and encryption purposes - * when interfacing with DWNs. The IDs of these keys are referenced in the - * service object that includes the dwnUrls provided. - */ - public static async generateDwnOptions(options: { - encryptionKeyId?: string, - serviceEndpointNodes: string[], - serviceId?: string, - signingKeyAlgorithm?: typeof SupportedCryptoAlgorithms[number] - signingKeyId?: string, - }): Promise { - const { - signingKeyAlgorithm = 'Ed25519', // Generate Ed25519 key pairs, by default. - serviceId = '#dwn', // Use default ID value, unless overridden. - signingKeyId = '#dwn-sig', // Use default key ID value, unless overridden. - encryptionKeyId = '#dwn-enc', // Use default key ID value, unless overridden. - serviceEndpointNodes } = options; - - const signingKeyPair = await DidIonMethod.generateJwkKeyPair({ - keyAlgorithm : signingKeyAlgorithm, - keyId : signingKeyId - }); - - /** Currently, `dwn-sdk-js` has only implemented support for record - * encryption using the `ECIES-ES256K` crypto algorithm. Until the - * DWN SDK supports ECIES with EdDSA, the encryption key pair must - * use secp256k1. */ - const encryptionKeyPair = await DidIonMethod.generateJwkKeyPair({ - keyAlgorithm : 'secp256k1', - keyId : encryptionKeyId - }); - - const keySet: DidIonKeySet = { - verificationMethodKeys: [ - { ...signingKeyPair, relationships: ['authentication'] }, - { ...encryptionKeyPair, relationships: ['keyAgreement'] } - ] - }; - - const serviceEndpoint: DwnServiceEndpoint = { - encryptionKeys : [encryptionKeyId], - nodes : serviceEndpointNodes, - signingKeys : [signingKeyId] - }; - - const services: DidService[] = [{ - id : serviceId, - serviceEndpoint, - type : 'DecentralizedWebNode', - }]; - - return { keySet, services }; - } - - public static async generateJwkKeyPair(options: { - keyAlgorithm: typeof SupportedCryptoAlgorithms[number], - keyId?: string - }): Promise { - const { keyAlgorithm, keyId } = options; - - let cryptoKeyPair: Web5Crypto.CryptoKeyPair; - - switch (keyAlgorithm) { - case 'Ed25519': { - cryptoKeyPair = await new EdDsaAlgorithm().generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - break; - } - - case 'secp256k1': { - cryptoKeyPair = await new EcdsaAlgorithm().generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - break; - } - - default: { - throw new Error(`Unsupported crypto algorithm: '${keyAlgorithm}'`); - } - } - - // Convert the CryptoKeyPair to JwkKeyPair. - const jwkKeyPair = await Jose.cryptoKeyToJwkPair({ keyPair: cryptoKeyPair }); - - // Set kid values. - if (keyId) { - jwkKeyPair.privateKeyJwk.kid = keyId; - jwkKeyPair.publicKeyJwk.kid = keyId; - } else { - // If a key ID is not specified, generate RFC 7638 JWK thumbprint. - const jwkThumbprint = await Jose.jwkThumbprint({ key: jwkKeyPair.publicKeyJwk }); - jwkKeyPair.privateKeyJwk.kid = jwkThumbprint; - jwkKeyPair.publicKeyJwk.kid = jwkThumbprint; - } - - return jwkKeyPair; - } - - public static async generateKeySet(options?: { - keyAlgorithm?: typeof SupportedCryptoAlgorithms[number], - keySet?: DidIonKeySet - }): Promise { - // Generate Ed25519 authentication key pair, by default. - let { keyAlgorithm = 'Ed25519', keySet = {} } = options ?? {}; - - // If keySet lacks verification method keys, generate one. - if (keySet.verificationMethodKeys === undefined) { - const authenticationkeyPair = await DidIonMethod.generateJwkKeyPair({ - keyAlgorithm, - keyId: 'dwn-sig' - }); - keySet.verificationMethodKeys = [{ - ...authenticationkeyPair, - relationships: ['authentication', 'assertionMethod'] - }]; - } - - // If keySet lacks recovery key, generate one. - if (keySet.recoveryKey === undefined) { - // Note: ION/Sidetree only supports secp256k1 recovery keys. - keySet.recoveryKey = await DidIonMethod.generateJwkKeyPair({ - keyAlgorithm : 'secp256k1', - keyId : 'ion-recovery-1' - }); - } - - // If keySet lacks update key, generate one. - if (keySet.updateKey === undefined) { - // Note: ION/Sidetree only supports secp256k1 update keys. - keySet.updateKey = await DidIonMethod.generateJwkKeyPair({ - keyAlgorithm : 'secp256k1', - keyId : 'ion-update-1' - }); - } - - // Generate RFC 7638 JWK thumbprints if `kid` is missing from any key. - for (const key of [...keySet.verificationMethodKeys, keySet.recoveryKey, keySet.updateKey]) { - if ('publicKeyJwk' in key) key.publicKeyJwk.kid ??= await Jose.jwkThumbprint({ key: key.publicKeyJwk }); - if ('privateKeyJwk' in key) key.privateKeyJwk.kid ??= await Jose.jwkThumbprint({ key: key.privateKeyJwk }); - } - - return keySet; - } - - /** - * Given the W3C DID Document of a `did:ion` DID, return the identifier of - * the verification method key that will be used for signing messages and - * credentials, by default. - * - * @param document = DID Document to get the default signing key from. - * @returns Verification method identifier for the default signing key. - */ - public static async getDefaultSigningKey(options: { - didDocument: DidDocument - }): Promise { - const { didDocument } = options; - - if (!didDocument.id) { - throw new Error(`DidIonMethod: DID document is missing 'id' property`); - } - - /** If the DID document contains a DWN service endpoint in the expected - * format, return the first entry in the `signingKeys` array. */ - const [dwnService] = getServices({ didDocument, type: 'DecentralizedWebNode' }); - if (isDwnServiceEndpoint(dwnService?.serviceEndpoint)) { - const [verificationMethodId] = dwnService.serviceEndpoint.signingKeys; - const did = didDocument.id; - const signingKeyId = `${did}${verificationMethodId}`; - return signingKeyId; - } - - /** Otherwise, fallback to a naive approach of returning the first key ID - * in the `authentication` verification relationships array. */ - if (didDocument.authentication - && Array.isArray(didDocument.authentication) - && didDocument.authentication.length > 0 - && typeof didDocument.authentication[0] === 'string') { - const [verificationMethodId] = didDocument.authentication; - const did = didDocument.id; - const signingKeyId = `${did}${verificationMethodId}`; - return signingKeyId; - } - } - - public static async getLongFormDid(options: { - services: DidService[], - keySet: DidIonKeySet - }): Promise { - const { services = [], keySet } = options; - - // Create ION Document. - const ionDocument = await DidIonMethod.createIonDocument({ - keySet: keySet, - services - }); - - // Filter JWK to only those properties expected by ION/Sidetree. - const recoveryKey = DidIonMethod.jwkToIonJwk({ key: keySet.recoveryKey.publicKeyJwk }) as JwkEs256k; - const updateKey = DidIonMethod.jwkToIonJwk({ key: keySet.updateKey.publicKeyJwk }) as JwkEs256k; - - // Create an ION DID create request operation. - const did = await IonDid.createLongFormDid({ - document: ionDocument, - recoveryKey, - updateKey - }); - - return did; - } - - public static async getShortFormDid(options: { - didUrl: string - }): Promise { - const { didUrl } = options; - - const parsedDid = parseDid({ didUrl }); - - if (!parsedDid) { - throw new Error(`DidIonMethod: Unable to parse DID: ${didUrl}`); - } - - const shortFormDid = parsedDid.did.split(':', 3).join(':'); - - return shortFormDid; - } - - public static async resolve(options: { - didUrl: string, - resolutionOptions?: DidResolutionOptions - }): Promise { - // TODO: Implement resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution - const { didUrl, resolutionOptions = {} } = options; - - const parsedDid = parseDid({ didUrl }); - if (!parsedDid) { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'invalidDid', - errorMessage : `Cannot parse DID: ${didUrl}` - } - }; - } - - if (parsedDid.method !== 'ion') { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'methodNotSupported', - errorMessage : `Method not supported: ${parsedDid.method}` - } - }; - } - - const { resolutionEndpoint = 'https://ion.tbd.engineering/identifiers/' } = resolutionOptions; - const normalizeUrl = (url: string): string => url.endsWith('/') ? url : url + '/'; - const resolutionUrl = `${normalizeUrl(resolutionEndpoint)}${parsedDid.did}`; - - const response = await fetch(resolutionUrl); - - let resolutionResult: DidResolutionResult | object; - try { - resolutionResult = await response.json(); - } catch (error) { - resolutionResult = {}; - } - - if (response.ok) { - return resolutionResult as DidResolutionResult; - } - - // Response was not "OK" (HTTP 4xx-5xx status code) - - // Return result if it contains DID resolution metadata. - if ('didResolutionMetadata' in resolutionResult) { - return resolutionResult; - } - - // Set default error code and message. - let error = 'internalError'; - let errorMessage = `DID resolver responded with HTTP status code: ${response.status}`; - - /** The Microsoft resolution endpoint does not return a valid DidResolutionResult - * when an ION DID is "not found" so normalization is needed. */ - if ('error' in resolutionResult && - typeof resolutionResult.error === 'object' && - 'code' in resolutionResult.error && - typeof resolutionResult.error.code === 'string' && - 'message' in resolutionResult.error && - typeof resolutionResult.error.message === 'string') { - error = resolutionResult.error.code.includes('not_found') ? 'notFound' : error; - errorMessage = resolutionResult.error.message ?? errorMessage; - } - - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, - didDocumentMetadata : {}, - didResolutionMetadata : { - error, - errorMessage - } - }; - } - - private static async createIonDocument(options: { - keySet: DidIonKeySet, - services?: DidService[] - }): Promise { - const { services = [], keySet } = options; - - /** - * STEP 1: Convert key set verification method keys to ION SDK format. - */ - - const ionPublicKeys: IonPublicKeyModel[] = []; - - for (const key of keySet.verificationMethodKeys) { - // Map W3C DID verification relationship names to ION public key purposes. - const ionPurposes: IonPublicKeyPurpose[] = []; - for (const relationship of key.relationships) { - ionPurposes.push( - VerificationRelationshipToIonPublicKeyPurpose[relationship] - ); - } - - /** During certain ION operations, JWK validation will throw an error - * if key IDs provided as input are prefixed with `#`. ION operation - * outputs and DID document resolution always include the `#` prefix - * for key IDs resulting in a confusing mismatch between inputs and - * outputs. To improve the developer experience, this inconsistency - * is addressed by normalizing input key IDs before being passed - * to ION SDK methods. */ - const publicKeyId = (key.publicKeyJwk.kid.startsWith('#')) - ? key.publicKeyJwk.kid.substring(1) - : key.publicKeyJwk.kid; - - // Convert public key JWK to ION format. - const publicKey: IonPublicKeyModel = { - id : publicKeyId, - publicKeyJwk : DidIonMethod.jwkToIonJwk({ key: key.publicKeyJwk }), - purposes : ionPurposes, - type : 'JsonWebKey2020' - }; - - ionPublicKeys.push(publicKey); - } - - /** - * STEP 2: Convert service entries, if any, to ION SDK format. - */ - const ionServices = services.map(service => ({ - ...service, - id: service.id.startsWith('#') ? service.id.substring(1) : service.id - })); - - /** - * STEP 3: Format as ION document. - */ - - const ionDocumentModel: IonDocumentModel = { - publicKeys : ionPublicKeys, - services : ionServices - }; - - return ionDocumentModel; - } - - private static async getIonCreateRequest(options: { - ionDocument: IonDocumentModel, - recoveryPublicKeyJwk: PublicKeyJwk, - updatePublicKeyJwk: PublicKeyJwk - }): Promise { - const { ionDocument, recoveryPublicKeyJwk, updatePublicKeyJwk } = options; - - // Create an ION DID create request operation. - const createRequest = await IonRequest.createCreateRequest({ - document : ionDocument, - recoveryKey : DidIonMethod.jwkToIonJwk({ key: recoveryPublicKeyJwk }) as JwkEs256k, - updateKey : DidIonMethod.jwkToIonJwk({ key: updatePublicKeyJwk }) as JwkEs256k - }); - - return createRequest; - } - - private static jwkToIonJwk({ key }: { key: PrivateKeyJwk | PublicKeyJwk }): JwkEd25519 | JwkEs256k { - let ionJwk: Partial = { }; - - if ('crv' in key) { - ionJwk.crv = key.crv; - ionJwk.kty = key.kty; - ionJwk.x = key.x; - if ('d' in key) ionJwk.d = key.d; - - if ('y' in key && key.y) { - // secp256k1 JWK. - return { ...ionJwk, y: key.y} as JwkEs256k; - } - // Ed25519 JWK. - return { ...ionJwk } as JwkEd25519; - } - - throw new Error(`jwkToIonJwk: Unsupported key algorithm.`); - } -} \ No newline at end of file diff --git a/packages/dids/src/did-key.ts b/packages/dids/src/did-key.ts deleted file mode 100644 index aafba5df0..000000000 --- a/packages/dids/src/did-key.ts +++ /dev/null @@ -1,787 +0,0 @@ -import type { PrivateKeyJwk, PublicKeyJwk, Web5Crypto } from '@web5/crypto'; - -import { universalTypeOf } from '@web5/common'; -import { - Jose, - Ed25519, - Secp256k1, - EcdsaAlgorithm, - EdDsaAlgorithm, - utils as cryptoUtils, -} from '@web5/crypto'; - -import type { - DidMethod, - DidDocument, - PortableDid, - VerificationMethod, - DidResolutionResult, - DidResolutionOptions, - DidKeySetVerificationMethodKey, -} from './types.js'; - -import { getVerificationMethodTypes, parseDid } from './utils.js'; - -const SupportedCryptoAlgorithms = [ - 'Ed25519', - 'secp256k1' -] as const; - -const SupportedPublicKeyFormats = [ - 'Ed25519VerificationKey2020', - 'JsonWebKey2020', - 'X25519KeyAgreementKey2020' -]; - -const VERIFICATION_METHOD_TYPES: Record = { - 'Ed25519VerificationKey2020' : 'https://w3id.org/security/suites/ed25519-2020/v1', - 'JsonWebKey2020' : 'https://w3id.org/security/suites/jws-2020/v1', - 'X25519KeyAgreementKey2020' : 'https://w3id.org/security/suites/x25519-2020/v1', -} as const; - -export type DidVerificationMethodType = keyof typeof VERIFICATION_METHOD_TYPES; - -const MULTICODEC_PUBLIC_KEY_LENGTH: Record = { - // secp256k1-pub - Secp256k1 public key (compressed) - 33 bytes - 0xe7: 33, - - // x25519-pub - Curve25519 public key - 32 bytes - 0xec: 32, - - // ed25519-pub - Ed25519 public key - 32 bytes - 0xed: 32 -}; - -export type DidKeyCreateOptions = { - enableEncryptionKeyDerivation?: boolean; - keyAlgorithm?: typeof SupportedCryptoAlgorithms[number]; - keySet?: DidKeyKeySet; - publicKeyFormat?: DidVerificationMethodType; -} - -export type DidKeyCreateDocumentOptions = { - defaultContext?: string; - did: string; - enableEncryptionKeyDerivation?: boolean; - enableExperimentalPublicKeyTypes?: boolean; - publicKeyFormat?: DidVerificationMethodType; -} - -export type DidKeyDeriveEncryptionKeyResult = { - key: Uint8Array; - multicodecCode: number; -} - -export type DidKeyIdentifier = { - fragment: string; - method: string; - multibaseValue: string; - scheme: string; - version: string; -} - -export type DidKeyKeySet = { - verificationMethodKeys?: DidKeySetVerificationMethodKey[]; -} - -export class DidKeyMethod implements DidMethod { - /** - * Name of the DID method - */ - public static methodName = 'key'; - - public static async create(options?: DidKeyCreateOptions): Promise { - let { - enableEncryptionKeyDerivation = false, - keyAlgorithm, - keySet, - publicKeyFormat = 'JsonWebKey2020' - } = options ?? { }; - - // If keySet not given, generate a default key set. - if (keySet === undefined) { - keySet = await DidKeyMethod.generateKeySet({ keyAlgorithm }); - } - - const portableDid: Partial = {}; - let multibaseId = ''; - - if (keySet.verificationMethodKeys?.[0]?.publicKeyJwk) { - // Compute the multibase identifier based on the JSON Web Key. - const publicKeyJwk = keySet.verificationMethodKeys[0].publicKeyJwk; - multibaseId = await Jose.jwkToMultibaseId({ key: publicKeyJwk }); - } - - if (!multibaseId) { - throw new Error('DidKeyMethod: Failed to create DID with given input.'); - } - - // Concatenate the DID identifier. - portableDid.did = `did:key:${multibaseId}`; - - // Expand the DID identifier to a DID document. - portableDid.document = await DidKeyMethod.createDocument({ - did: portableDid.did, - publicKeyFormat, - enableEncryptionKeyDerivation - }); - - // Return the given or generated key set. - portableDid.keySet = keySet; - - return portableDid as PortableDid; - } - - /** - * Expands a did:key identifier to a DID Document. - * - * Reference: https://w3c-ccg.github.io/did-method-key/#document-creation-algorithm - * - * @param options - * @returns - A DID dodcument. - */ - public static async createDocument(options: DidKeyCreateDocumentOptions): Promise { - const { - defaultContext = 'https://www.w3.org/ns/did/v1', - did, - enableEncryptionKeyDerivation = false, - enableExperimentalPublicKeyTypes = false, - publicKeyFormat = 'JsonWebKey2020' - } = options; - - /** - * 1. Initialize document to an empty object. - */ - const document: Partial = {}; - - /** - * 2. Using a colon (:) as the delimiter, split the identifier into its - * components: a scheme, a method, a version, and a multibaseValue. - * If there are only three components set the version to the string - * value 1 and use the last value as the multibaseValue. - * - * Note: The W3C DID specification makes no mention of a version value - * being part of the DID syntax. Additionally, there does not - * appear to be any real-world usage of the version number. - * Consequently, this implementation will ignore the version - * related guidance in the did:key specification. - */ - let multibaseValue: string; - try { - ({ id: multibaseValue } = parseDid({ didUrl: did })); - } catch (error: any) { - throw new Error(`invalidDid: Unknown format: ${did}`); - } - - /** - * 3. Check the validity of the input identifier. - * The scheme MUST be the value did. The method MUST be the value key. - * The version MUST be convertible to a positive integer value. The - * multibaseValue MUST be a string and begin with the letter z. If any - * of these requirements fail, an invalidDid error MUST be raised. - */ - if (!DidKeyMethod.validateIdentifier({ did })) { - throw new Error(`invalidDid: Invalid identifier format: ${did}`); - } - - /** - * 4. Initialize the signatureVerificationMethod to the result of passing - * identifier, multibaseValue, and options to a - * {@link https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm | Signature Method Creation Algorithm}. - */ - const signatureVerificationMethod = await DidKeyMethod.createSignatureMethod({ - did, - enableExperimentalPublicKeyTypes, - multibaseValue, - publicKeyFormat - }); - - /** - * 5. Set document.id to identifier. If document.id is not a valid DID, - * an invalidDid error MUST be raised. - * - * Note: Identifier was already confirmed to be valid in Step 3, so - * skipping the redundant validation. - */ - document.id = did; - - /** - * 6. Initialize the verificationMethod property in document to an array - * where the first value is the signatureVerificationMethod. - */ - document.verificationMethod = [signatureVerificationMethod]; - - /** - * 7. Initialize the authentication, assertionMethod, capabilityInvocation, - * and the capabilityDelegation properties in document to an array where - * the first item is the value of the id property in - * signatureVerificationMethod. - */ - document.authentication = [signatureVerificationMethod.id]; - document.assertionMethod = [signatureVerificationMethod.id]; - document.capabilityInvocation = [signatureVerificationMethod.id]; - document.capabilityDelegation = [signatureVerificationMethod.id]; - - /** - * 8. If options.enableEncryptionKeyDerivation is set to true: - * Add the encryptionVerificationMethod value to the verificationMethod - * array. Initialize the keyAgreement property in document to an array - * where the first item is the value of the id property in - * encryptionVerificationMethod. - */ - if (enableEncryptionKeyDerivation === true) { - /** - * Although not covered by the did:key method specification, a sensible - * default will be taken to use the 'X25519KeyAgreementKey2020' - * verification method type if the given publicKeyFormat is - * 'Ed25519VerificationKey2020' and 'JsonWebKey2020' otherwise. - */ - const encryptionPublicKeyFormat = - (publicKeyFormat === 'Ed25519VerificationKey2020') - ? 'X25519KeyAgreementKey2020' - : 'JsonWebKey2020'; - - /** - * 8.1 Initialize the encryptionVerificationMethod to the result of - * passing identifier, multibaseValue, and options to an - * {@link https://w3c-ccg.github.io/did-method-key/#encryption-method-creation-algorithm | Encryption Method Creation Algorithm}. - */ - const encryptionVerificationMethod = await this.createEncryptionMethod({ - did, - enableExperimentalPublicKeyTypes, - multibaseValue, - publicKeyFormat: encryptionPublicKeyFormat - }); - - /** - * 8.2 Add the encryptionVerificationMethod value to the - * verificationMethod array. - */ - document.verificationMethod.push(encryptionVerificationMethod); - - /** - * 8.3. Initialize the keyAgreement property in document to an array - * where the first item is the value of the id property in - * encryptionVerificationMethod. - */ - document.keyAgreement = [encryptionVerificationMethod.id]; - } - - /** - * 9. Initialize the @context property in document to the result of passing - * document and options to the Context Creation algorithm. - */ - // Set contextArray to an array that is initialized to - // options.defaultContext. - const contextArray = [defaultContext]; - - // For every object in every verification relationship listed in document, - // add a string value to the contextArray based on the object type value, - // if it doesn't already exist, according to the following table: - // {@link https://w3c-ccg.github.io/did-method-key/#context-creation-algorithm | Context Type URL} - const verificationMethodTypes = getVerificationMethodTypes({ didDocument: document }); - verificationMethodTypes.forEach((typeName: string) => { - const typeUrl = VERIFICATION_METHOD_TYPES[typeName]; - contextArray.push(typeUrl); - }); - document['@context'] = contextArray; - - /** - * 10. Return document. - */ - return document as DidDocument; - } - - /** - * Decoding a multibase-encoded multicodec value into a verification method - * that is suitable for verifying that encrypted information will be - * received by the intended recipient. - */ - public static async createEncryptionMethod(options: { - did: string, - enableExperimentalPublicKeyTypes: boolean, - multibaseValue: string, - publicKeyFormat: DidVerificationMethodType - }): Promise { - const { did, enableExperimentalPublicKeyTypes, multibaseValue, publicKeyFormat } = options; - - /** - * 1. Initialize verificationMethod to an empty object. - */ - const verificationMethod: Partial = {}; - - /** - * 2. Set multicodecValue and rawPublicKeyBytes to the result of passing - * multibaseValue and options to a Derive Encryption Key algorithm. - */ - const { - key: rawPublicKeyBytes, - multicodecCode: multicodecValue, - } = await DidKeyMethod.deriveEncryptionKey({ multibaseValue }); - - /** - * 3. Ensure the proper key length of rawPublicKeyBytes based on the - * multicodecValue table provided below: - * - * Multicodec hexadecimal value: 0xec - * - * If the byte length of rawPublicKeyBytes - * does not match the expected public key length for the associated - * multicodecValue, an invalidPublicKeyLength error MUST be raised. - */ - const actualLength = rawPublicKeyBytes.byteLength; - const expectedLength = MULTICODEC_PUBLIC_KEY_LENGTH[multicodecValue]; - if (actualLength !== expectedLength) { - throw new Error(`invalidPublicKeyLength: Expected ${actualLength} bytes. Actual ${expectedLength} bytes.`); - } - - /** - * 4. Create the multibaseValue by concatenating the letter 'z' and the - * base58-btc encoding of the concatenation of the multicodecValue and - * the rawPublicKeyBytes. - */ - const kemMultibaseValue = cryptoUtils.keyToMultibaseId({ - key : rawPublicKeyBytes, - multicodecCode : multicodecValue - }); - - /** - * 5. Set the verificationMethod.id value by concatenating identifier, - * a hash character (#), and the multibaseValue. If verificationMethod.id - * is not a valid DID URL, an invalidDidUrl error MUST be raised. - */ - verificationMethod.id = `${did}#${kemMultibaseValue}`; - try { - new URL(verificationMethod.id); - } catch (error: any) { - throw new Error('invalidDidUrl: Verification Method ID is not a valid DID URL.'); - } - - /** - * 6. Set the publicKeyFormat value to the options.publicKeyFormat value. - * 7. If publicKeyFormat is not known to the implementation, an - * unsupportedPublicKeyType error MUST be raised. - */ - if (!(SupportedPublicKeyFormats.includes(publicKeyFormat))) { - throw new Error(`unsupportedPublicKeyType: Unsupported format: ${publicKeyFormat}`); - } - - /** - * 8. If options.enableExperimentalPublicKeyTypes is set to false and - * publicKeyFormat is not Multikey, JsonWebKey2020, or - * X25519KeyAgreementKey2020, an invalidPublicKeyType error MUST be - * raised. - */ - const StandardPublicKeyTypes = ['Multikey', 'JsonWebKey2020', 'X25519KeyAgreementKey2020']; - if (enableExperimentalPublicKeyTypes === false - && !(StandardPublicKeyTypes.includes(publicKeyFormat))) { - throw new Error(`invalidPublicKeyType: Specified '${publicKeyFormat}' without setting enableExperimentalPublicKeyTypes to true.`); - } - - /** - * 9. Set verificationMethod.type to the publicKeyFormat value. - */ - verificationMethod.type = publicKeyFormat; - - /** - * 10. Set verificationMethod.controller to the identifier value. - * If verificationMethod.controller is not a valid DID, an invalidDid - * error MUST be raised. - */ - verificationMethod.controller = did; - if (!DidKeyMethod.validateIdentifier({ did })) { - throw new Error(`invalidDid: Invalid identifier format: ${did}`); - } - - /** - * 11. If publicKeyFormat is Multikey or X25519KeyAgreementKey2020, - * set the verificationMethod.publicKeyMultibase value to multibaseValue. - * - * Note: This implementation does not currently support the Multikey - * format. - */ - if (publicKeyFormat === 'X25519KeyAgreementKey2020') { - verificationMethod.publicKeyMultibase = kemMultibaseValue; - } - - /** - * 12. If publicKeyFormat is JsonWebKey2020, set the - * verificationMethod.publicKeyJwk value to the result of passing - * multicodecValue and rawPublicKeyBytes to a JWK encoding algorithm. - */ - if (publicKeyFormat === 'JsonWebKey2020') { - const jwkParams = await Jose.multicodecToJose({ code: multicodecValue }); - const jsonWebKey = await Jose.keyToJwk({ - keyMaterial : rawPublicKeyBytes, - keyType : 'public', - ...jwkParams - }); - // Ensure that "d" is NOT present. - if ('x' in jsonWebKey && !('d' in jsonWebKey)) { - verificationMethod.publicKeyJwk = jsonWebKey; - } - } - - /** - * 13. Return verificationMethod. - */ - return verificationMethod as VerificationMethod; - } - - /** - * Transform a multibase-encoded multicodec value to public encryption key - * components that are suitable for encrypting messages to a receiver. A - * mathematical proof elaborating on the safety of performing this operation - * is available in: - * {@link https://eprint.iacr.org/2021/509.pdf | On using the same key pair for Ed25519 and an X25519 based KEM} - */ - public static async deriveEncryptionKey(options: { - multibaseValue: string - }): Promise { - const { multibaseValue } = options; - - /** - * 1. Set publicEncryptionKey to an empty object. - */ - let publicEncryptionKey: Partial = {}; - - /** - * 2. Decode multibaseValue using the base58-btc multibase alphabet and - * set multicodecValue to the multicodec header for the decoded value. - * Implementers are cautioned to ensure that the multicodecValue is set - * to the result after performing varint decoding. - * - * 3. Set the rawPublicKeyBytes to the bytes remaining after the multicodec - * header. - */ - const { - key: rawPublicKeyBytes, - multicodecCode: multicodecValue - } = cryptoUtils.multibaseIdToKey({ multibaseKeyId: multibaseValue }); - - /** - * 4. If the multicodecValue is 0xed, derive a public X25519 encryption key - * by using the rawPublicKeyBytes and the algorithm defined in - * {@link https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm | Group OSCORE - Secure Group Communication for CoAP} - * for Curve25519 in Section 2.4.2: ECDH with Montgomery Coordinates and - * set generatedPublicEncryptionKeyBytes to the result. - */ - if (multicodecValue === 0xed) { - const generatedPublicEncryptionKeyBytes = await Ed25519.convertPublicKeyToX25519({ - publicKey: rawPublicKeyBytes - }); - - /** - * 5. Set multicodecValue in publicEncryptionKey to 0xec. - * - * 6. Set rawPublicKeyBytes in publicEncryptionKey to - * generatedPublicEncryptionKeyBytes. - */ - publicEncryptionKey = { - key : generatedPublicEncryptionKeyBytes, - multicodecCode : 0xec - }; - } - - /** - * 7. Return publicEncryptionKey. - */ - return publicEncryptionKey as DidKeyDeriveEncryptionKeyResult; - } - - /** - * Decodes a multibase-encoded multicodec value into a verification method - * that is suitable for verifying digital signatures. - * @param options - Signature method creation algorithm inputs. - * @returns - A verification method. - */ - public static async createSignatureMethod(options: { - did: string, - enableExperimentalPublicKeyTypes: boolean, - multibaseValue: string, - publicKeyFormat: DidVerificationMethodType - }): Promise { - const { did, enableExperimentalPublicKeyTypes, multibaseValue, publicKeyFormat } = options; - - /** - * 1. Initialize verificationMethod to an empty object. - */ - const verificationMethod: Partial = {}; - - /** - * 2. Set multicodecValue and rawPublicKeyBytes to the result of passing - * multibaseValue and options to a Decode Public Key algorithm. - */ - const { - key: rawPublicKeyBytes, - multicodecCode: multicodecValue, - multicodecName - } = cryptoUtils.multibaseIdToKey({ multibaseKeyId: multibaseValue }); - - /** - * 3. Ensure the proper key length of rawPublicKeyBytes based on the - * multicodecValue {@link https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm | table provided}. - * If the byte length of rawPublicKeyBytes does not match the expected - * public key length for the associated multicodecValue, an - * invalidPublicKeyLength error MUST be raised. - */ - const actualLength = rawPublicKeyBytes.byteLength; - const expectedLength = MULTICODEC_PUBLIC_KEY_LENGTH[multicodecValue]; - if (actualLength !== expectedLength) { - throw new Error(`invalidPublicKeyLength: Expected ${actualLength} bytes. Actual ${expectedLength} bytes.`); - } - - /** - * 4. Ensure the rawPublicKeyBytes are a proper encoding of the public - * key type as specified by the multicodecValue. This validation is often - * done by a cryptographic library when importing the public key by, - * for example, ensuring that an Elliptic Curve public key is a specific - * coordinate that exists on the elliptic curve. If an invalid public key - * value is detected, an invalidPublicKey error MUST be raised. - */ - let isValid = false; - switch (multicodecName) { - case 'secp256k1-pub': - isValid = await Secp256k1.validatePublicKey({ key: rawPublicKeyBytes }); - break; - case 'ed25519-pub': - isValid = await Ed25519.validatePublicKey({ key: rawPublicKeyBytes }); - break; - case 'x25519-pub': - // TODO: Validate key once/if X25519.validatePublicKey() is implemented. - // isValid = X25519.validatePublicKey({ key: rawPublicKeyBytes}) - isValid = true; - break; - } - if (!isValid) { - throw new Error('invalidPublicKey: Invalid public key detected.'); - } - - /** - * 5. Set the verificationMethod.id value by concatenating identifier, - * a hash character (#), and the multibaseValue. If verificationMethod.id - * is not a valid DID URL, an invalidDidUrl error MUST be raised. - */ - verificationMethod.id = `${did}#${multibaseValue}`; - try { - new URL(verificationMethod.id); - } catch (error: any) { - throw new Error('invalidDidUrl: Verification Method ID is not a valid DID URL.'); - } - - /** - * 6. Set the publicKeyFormat value to the options.publicKeyFormat value. - * 7. If publicKeyFormat is not known to the implementation, an - * unsupportedPublicKeyType error MUST be raised. - */ - if (!(SupportedPublicKeyFormats.includes(publicKeyFormat))) { - throw new Error(`unsupportedPublicKeyType: Unsupported format: ${publicKeyFormat}`); - } - - /** - * 8. If options.enableExperimentalPublicKeyTypes is set to false and - * publicKeyFormat is not Multikey, JsonWebKey2020, or - * Ed25519VerificationKey2020, an invalidPublicKeyType error MUST be - * raised. - */ - const StandardPublicKeyTypes = ['Multikey', 'JsonWebKey2020', 'Ed25519VerificationKey2020']; - if (enableExperimentalPublicKeyTypes === false - && !(StandardPublicKeyTypes.includes(publicKeyFormat))) { - throw new Error(`invalidPublicKeyType: Specified '${publicKeyFormat}' without setting enableExperimentalPublicKeyTypes to true.`); - } - - /** - * 9. Set verificationMethod.type to the publicKeyFormat value. - */ - verificationMethod.type = publicKeyFormat; - - /** - * 10. Set verificationMethod.controller to the identifier value. - * If verificationMethod.controller is not a valid DID, an invalidDid - * error MUST be raised. - */ - verificationMethod.controller = did; - if (!DidKeyMethod.validateIdentifier({ did })) { - throw new Error(`invalidDid: Invalid identifier format: ${did}`); - } - - /** - * 11. If publicKeyFormat is Multikey or Ed25519VerificationKey2020, - * set the verificationMethod.publicKeyMultibase value to multibaseValue. - * - * Note: This implementation does not currently support the Multikey - * format. - */ - if (publicKeyFormat === 'Ed25519VerificationKey2020') { - verificationMethod.publicKeyMultibase = multibaseValue; - } - - /** - * 12. If publicKeyFormat is JsonWebKey2020, set the - * verificationMethod.publicKeyJwk value to the result of passing - * multicodecValue and rawPublicKeyBytes to a JWK encoding algorithm. - */ - if (publicKeyFormat === 'JsonWebKey2020') { - const jwkParams = await Jose.multicodecToJose({ code: multicodecValue }); - const jsonWebKey = await Jose.keyToJwk({ - keyMaterial : rawPublicKeyBytes, - keyType : 'public', - ...jwkParams - }); - // Ensure that "d" is NOT present. - if ('x' in jsonWebKey && !('d' in jsonWebKey)) { - verificationMethod.publicKeyJwk = jsonWebKey; - } - } - - /** - * 13. Return verificationMethod. - */ - return verificationMethod as VerificationMethod; - } - - public static async generateKeySet(options?: { - keyAlgorithm?: typeof SupportedCryptoAlgorithms[number] - }): Promise { - // Generate Ed25519 keys, by default. - const { keyAlgorithm = 'Ed25519' } = options ?? {}; - - let keyPair: Web5Crypto.CryptoKeyPair; - - switch (keyAlgorithm) { - case 'Ed25519': { - keyPair = await new EdDsaAlgorithm().generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - break; - } - - case 'secp256k1': { - keyPair = await new EcdsaAlgorithm().generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - break; - } - - default: { - throw new Error(`Unsupported crypto algorithm: '${keyAlgorithm}'`); - } - } - - const publicKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.publicKey }) as PublicKeyJwk; - const privateKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.privateKey }) as PrivateKeyJwk; - - const keySet: DidKeyKeySet = { - verificationMethodKeys: [{ - publicKeyJwk, - privateKeyJwk, - relationships: ['authentication'] - }] - }; - - return keySet; - } - - /** - * Given the W3C DID Document of a `did:key` DID, return the identifier of - * the verification method key that will be used for signing messages and - * credentials, by default. - * - * @param document = DID Document to get the default signing key from. - * @returns Verification method identifier for the default signing key. - */ - public static async getDefaultSigningKey(options: { - didDocument: DidDocument - }): Promise { - const { didDocument } = options; - - if (didDocument.authentication - && Array.isArray(didDocument.authentication) - && didDocument.authentication.length > 0 - && typeof didDocument.authentication[0] === 'string') { - - const [verificationMethodId] = didDocument.authentication; - const signingKeyId = verificationMethodId; - - return signingKeyId; - } - } - - public static async resolve(options: { - didUrl: string, - resolutionOptions?: DidResolutionOptions - }): Promise { - const { didUrl, resolutionOptions: _ } = options; - // TODO: Implement resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution - - const parsedDid = parseDid({ didUrl }); - if (!parsedDid) { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'invalidDid', - errorMessage : `Cannot parse DID: ${didUrl}` - } - }; - } - - if (parsedDid.method !== 'key') { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'methodNotSupported', - errorMessage : `Method not supported: ${parsedDid.method}` - } - }; - } - - const didDocument = await DidKeyMethod.createDocument({ did: parsedDid.did }); - - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument, - didDocumentMetadata : {}, - didResolutionMetadata : { - did: { - didString : parsedDid.did, - methodSpecificId : parsedDid.id, - method : parsedDid.method - } - } - }; - } - - public static validateIdentifier(options: { - did: string - }): boolean { - const { did } = options; - - const { method, id: multibaseValue } = parseDid({ didUrl: did }); - const [scheme] = did.split(':', 1); - - /** - * Note: The W3C DID specification makes no mention of a version value - * being part of the DID syntax. Additionally, there does not - * appear to be any real-world usage of the version number. - * Consequently, this implementation will ignore the version - * related guidance in the did:key specification. - */ - const version = '1'; - - return ( - scheme !== 'did' || - method !== 'key' || - parseInt(version) > 0 || - universalTypeOf(multibaseValue) !== 'String' || - !multibaseValue.startsWith('z') - ); - } -} \ No newline at end of file diff --git a/packages/dids/src/did-resolver.ts b/packages/dids/src/did-resolver.ts deleted file mode 100644 index 1c0933b7f..000000000 --- a/packages/dids/src/did-resolver.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { - DidResource, - DidResolverCache, - DidMethodResolver, - DidResolutionResult, - DidResolutionOptions, - DidDereferencingResult, - DidDereferencingOptions, -} from './types.js'; - -import { parseDid } from './utils.js'; -import { DidResolverCacheNoop } from './resolver-cache-noop.js'; - -export type DidResolverOptions = { - didResolvers: DidMethodResolver[]; - cache?: DidResolverCache; -} - -/** - * The `DidResolver` class provides mechanisms for resolving Decentralized Identifiers (DIDs) to - * their corresponding DID documents. - * - * The class is designed to handle various DID methods by utilizing an array of `DidMethodResolver` - * instances, each responsible for a specific DID method. It also employs a caching mechanism to - * store and retrieve previously resolved DID documents for efficiency. - * - * Usage: - * - Construct the `DidResolver` with an array of `DidMethodResolver` instances and an optional cache. - * - Use `resolve` to resolve a DID to its DID Resolution Result. - * - Use `dereference` to extract specific resources from a DID URL, like service endpoints or verification methods. - * - * @example - * ```ts - * const resolver = new DidResolver({ - * didResolvers: [], - * cache: new DidResolverCacheNoop() - * }); - * - * const resolutionResult = await resolver.resolve('did:example:123456'); - * const dereferenceResult = await resolver.dereference({ didUrl: 'did:example:123456#key-1' }); - * ``` - */ -export class DidResolver { - /** - * A cache for storing resolved DID documents. - */ - private cache: DidResolverCache; - - /** - * A map to store method resolvers against method names. - */ - private didResolvers: Map = new Map(); - - /** - * Constructs a new `DidResolver`. - * - * @param options - The options for constructing the `DidResolver`. - * @param options.didResolvers - An array of `DidMethodResolver` instances. - * @param options.cache - Optional. A cache for storing resolved DID documents. If not provided, a no-operation cache is used. - */ - constructor(options: DidResolverOptions) { - this.cache = options.cache || DidResolverCacheNoop; - - for (const resolver of options.didResolvers) { - this.didResolvers.set(resolver.methodName, resolver); - } - } - - /** - * Resolves a DID to a DID Resolution Result. - * If the DID Resolution Result is present in the cache, it returns the cached - * result. Otherwise, it uses the appropriate method resolver to resolve - * the DID, stores the resolution result in the cache, and returns the - * resolultion result. - * - * Note: The method signature for resolve() in this implementation must match - * the `DidResolver` implementation in - * {@link https://github.com/TBD54566975/dwn-sdk-js | dwn-sdk-js} so that - * Web5 apps and the underlying DWN instance can share the same DID - * resolution cache. - * - * @param didUrl - The DID or DID URL to resolve. - * @returns A promise that resolves to the DID Resolution Result. - */ - async resolve(didUrl: string, resolutionOptions?: DidResolutionOptions): Promise { - - const parsedDid = parseDid({ didUrl }); - if (!parsedDid) { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'invalidDid', - errorMessage : `Cannot parse DID: ${didUrl}` - } - }; - } - - const resolver = this.didResolvers.get(parsedDid.method); - if (!resolver) { - return { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, - didDocumentMetadata : {}, - didResolutionMetadata : { - error : 'methodNotSupported', - errorMessage : `Method not supported: ${parsedDid.method}` - } - }; - } - - const cachedResolutionResult = await this.cache.get(parsedDid.did); - - if (cachedResolutionResult) { - return cachedResolutionResult; - } else { - const resolutionResult = await resolver.resolve({ - didUrl: parsedDid.did, - resolutionOptions - }); - - await this.cache.set(parsedDid.did, resolutionResult); - - return resolutionResult; - } - } - - /** - * Dereferences a DID (Decentralized Identifier) URL to a corresponding DID resource. This method - * interprets the DID URL's components, which include the DID method, method-specific identifier, - * path, query, and fragment, and retrieves the related resource as per the DID Core specifications. - * The dereferencing process involves resolving the DID contained in the DID URL to a DID document, - * and then extracting the specific part of the document identified by the fragment in the DID URL. - * If no fragment is specified, the entire DID document is returned. - * - * This method supports resolution of different components within a DID document such as service - * endpoints and verification methods, based on their IDs. It accommodates both full and - * DID URLs as specified in the DID Core specification. - * - * More information on DID URL dereferencing can be found in the - * {@link https://www.w3.org/TR/did-core/#did-url-dereferencing | DID Core specification}. - * - * @param didUrl - The DID URL string to dereference. - * @param [dereferenceOptions] - Input options to the dereference function. Optional. - * @returns a {@link DidDereferencingResult} - */ - async dereference({ didUrl }: { - didUrl: string, - dereferenceOptions?: DidDereferencingOptions - }): Promise { - const { didDocument, didResolutionMetadata = {}, didDocumentMetadata = {} } = await this.resolve(didUrl); - if (didResolutionMetadata.error) { - return { - dereferencingMetadata : { error: 'invalidDidUrl' }, - contentStream : null, - contentMetadata : {} - }; - } - - const parsedDid = parseDid({ didUrl }); - - // Return the entire DID Document if no fragment is present on the did url - if (!parsedDid.fragment) { - return { - dereferencingMetadata : { contentType: 'application/did+json' }, - contentStream : didDocument, - contentMetadata : didDocumentMetadata - }; - } - - const { service = [], verificationMethod = [] } = didDocument; - - // Create a set of possible id matches. The DID spec allows for an id to be the entire - // did#fragment or just #fragment. - // @see {@link }https://www.w3.org/TR/did-core/#relative-did-urls | Section 3.2.2, Relative DID URLs}. - // Using a Set for fast string comparison since some DID methods have long identifiers. - const idSet = new Set([didUrl, parsedDid.fragment, `#${parsedDid.fragment}`]); - - let didResource: DidResource; - for (let vm of verificationMethod) { - if (idSet.has(vm.id)) { - didResource = vm; - break; - } - } - - for (let svc of service) { - if (idSet.has(svc.id)) { - didResource = svc; - break; - } - } - if (didResource) { - return { - dereferencingMetadata : { contentType: 'application/did+json' }, - contentStream : didResource, - contentMetadata : didResolutionMetadata - }; - } else { - return { - dereferencingMetadata : { error: 'notFound' }, - contentStream : null, - contentMetadata : {}, - }; - } - } -} \ No newline at end of file diff --git a/packages/dids/src/did-uri.ts b/packages/dids/src/did-uri.ts new file mode 100644 index 000000000..22687abeb --- /dev/null +++ b/packages/dids/src/did-uri.ts @@ -0,0 +1,186 @@ +/** + * The `DidUri` class represents a Decentralized Identifier (DID) Uniform Resource Identifier (URI). + * + * This class provides a method for parsing a DID URI string into its component parts, as well as a + * method for serializing a DID URI object into a string. + * + * A DID URI is composed of the following components: + * - scheme + * - method + * - id + * - path + * - query + * - fragment + * - params + * + * @see {@link https://www.w3.org/TR/did-core/#did-syntax | DID Core Specification, § DID Syntax} + */ +export class DidUri { + /** Regular expression pattern for matching the method component of a DID URI. */ + static readonly METHOD_PATTERN = '([a-z0-9]+)'; + /** Regular expression pattern for matching percent-encoded characters in a method identifier. */ + static readonly PCT_ENCODED_PATTERN = '(?:%[0-9a-fA-F]{2})'; + /** Regular expression pattern for matching the characters allowed in a method identifier. */ + static readonly ID_CHAR_PATTERN = `(?:[a-zA-Z0-9._-]|${DidUri.PCT_ENCODED_PATTERN})`; + /** Regular expression pattern for matching the method identifier component of a DID URI. */ + static readonly METHOD_ID_PATTERN = `((?:${DidUri.ID_CHAR_PATTERN}*:)*(${DidUri.ID_CHAR_PATTERN}+))`; + /** Regular expression pattern for matching the path component of a DID URI. */ + static readonly PATH_PATTERN = `(/[^#?]*)?`; + /** Regular expression pattern for matching the query component of a DID URI. */ + static readonly QUERY_PATTERN = `([?][^#]*)?`; + /** Regular expression pattern for matching the fragment component of a DID URI. */ + static readonly FRAGMENT_PATTERN = `(#.*)?`; + /** Regular expression pattern for matching all of the components of a DID URI. */ + static readonly DID_URI_PATTERN = new RegExp( + `^did:(?${DidUri.METHOD_PATTERN}):(?${DidUri.METHOD_ID_PATTERN})(?${DidUri.PATH_PATTERN})(?${DidUri.QUERY_PATTERN})(?${DidUri.FRAGMENT_PATTERN})$` + ); + + /** + * A string representation of the DID. + * + * A DID is a URI composed of three parts: the scheme `did:`, a method identifier, and a unique, + * method-specific identifier specified by the DID method. + * + * @example + * did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o + */ + uri: string; + + /** + * The name of the DID method. + * + * Examples of DID method names are `dht`, `jwk`, and `web`, among others. + */ + method: string; + + /** + * The DID method identifier. + * + * @example + * h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o + */ + id: string; + + /** + * Optional path component of the DID URI. + * + * @example + * did:web:tbd.website/path + */ + path?: string; + + /** + * Optional query component of the DID URI. + * + * @example + * did:web:tbd.website?versionId=1 + */ + query?: string; + + /** + * Optional fragment component of the DID URI. + * + * @example + * did:web:tbd.website#key-1 + */ + fragment?: string; + + /** + * Optional query parameters in the DID URI. + * + * @example + * did:web:tbd.website?service=files&relativeRef=/whitepaper.pdf + */ + params?: Record; + + /** + * Constructs a new `DidUri` instance. + * + * @param params - An object containing the parameters to be included in the DID URI. + * @param params.method - The name of the DID method. + * @param params.id - The DID method identifier. + * @param params.path - Optional. The path component of the DID URI. + * @param params.query - Optional. The query component of the DID URI. + * @param params.fragment - Optional. The fragment component of the DID URI. + * @param params.params - Optional. The query parameters in the DID URI. + */ + constructor({ method, id, path, query, fragment, params }: { + method: string, + id: string, + path?: string, + query?: string, + fragment?: string, + params?: Record + }) { + this.uri = `did:${method}:${id}`; + this.method = method; + this.id = id; + this.path = path; + this.query = query; + this.fragment = fragment; + this.params = params; + } + + /** + * Parses a DID URI string into its individual components. + * + * @example + * ```ts + * const didUri = DidUri.parse('did:example:123?service=agent&relativeRef=/credentials#degree'); + * + * console.log(didUri.uri) // Output: 'did:example:123' + * console.log(didUri.method) // Output: 'example' + * console.log(didUri.id) // Output: '123' + * console.log(didUri.query) // Output: 'service=agent&relativeRef=/credentials' + * console.log(didUri.fragment) // Output: 'degree' + * console.log(didUri.params) // Output: { service: 'agent', relativeRef: '/credentials' } + * ``` + * + * @params didUriString - The DID URI string to be parsed. + * @returns A `DidUri` object representing the parsed DID URI, or `null` if the input string is not a valid DID URI. + */ + static parse(didUriString: string): DidUri | null { + // Return null if the input string is empty or not provided. + if (!didUriString) return null; + + // Execute the regex pattern on the input string to extract URI components. + const match = DidUri.DID_URI_PATTERN.exec(didUriString); + + // If the pattern does not match, or if the required groups are not found, return null. + if (!match || !match.groups) return null; + + // Extract the method, id, params, path, query, and fragment from the regex match groups. + const { method, id, path, query, fragment } = match.groups; + + // Initialize a new DidUri object with the uri, method and id. + const didUri: DidUri = { + uri: `did:${method}:${id}`, + method, + id, + }; + + // If path is present, add it to the DidUri object. + if (path) didUri.path = path; + + // If query is present, add it to the DidUri object, removing the leading '?'. + if (query) didUri.query = query.slice(1); + + // If fragment is present, add it to the DidUri object, removing the leading '#'. + if (fragment) didUri.fragment = fragment.slice(1); + + // If query params are present, parse them into a key-value object and add to the DidUri object. + if (query) { + const parsedParams = {} as Record; + // Split the query string by '&' to get individual parameter strings. + const paramPairs = query.slice(1).split('&'); + for (const pair of paramPairs) { + // Split each parameter string by '=' to separate keys and values. + const [key, value] = pair.split('='); + parsedParams[key] = value; + } + didUri.params = parsedParams; + } + + return didUri; + } +} \ No newline at end of file diff --git a/packages/dids/src/index.ts b/packages/dids/src/index.ts index f3ff1808b..6be03159d 100644 --- a/packages/dids/src/index.ts +++ b/packages/dids/src/index.ts @@ -1,9 +1,17 @@ -export * from './dht.js'; -export * from './did-dht.js'; -export * from './did-ion.js'; -export * from './did-key.js'; -export * from './did-resolver.js'; -export * from './resolver-cache-level.js'; -export * from './resolver-cache-noop.js'; -export * from './types.js'; -export * as utils from './utils.js'; \ No newline at end of file +export * from './did-error.js'; +export * from './did-uri.js'; + +export * from './methods/did-dht.js'; +export * from './methods/did-ion.js'; +export * from './methods/did-jwk.js'; +export * from './methods/did-key.js'; +export * from './methods/did-method.js'; +export * from './methods/did-web.js'; + +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 diff --git a/packages/dids/src/methods/did-dht.ts b/packages/dids/src/methods/did-dht.ts new file mode 100644 index 000000000..151b68205 --- /dev/null +++ b/packages/dids/src/methods/did-dht.ts @@ -0,0 +1,1550 @@ +import type { Packet, TxtAnswer, TxtData } from '@dnsquery/dns-packet'; +import type { + Jwk, + Signer, + KeyIdentifier, + KmsExportKeyParams, + KmsImportKeyParams, + KeyImporterExporter, + AsymmetricKeyConverter, +} from '@web5/crypto'; + +import bencode from 'bencode'; +import { Convert } from '@web5/common'; +import { CryptoApi, computeJwkThumbprint, Ed25519, LocalKeyManager, Secp256k1, Secp256r1 } from '@web5/crypto'; +import { AUTHORITATIVE_ANSWER, decode as dnsPacketDecode, encode as dnsPacketEncode } from '@dnsquery/dns-packet'; + +import type { Did, DidCreateOptions, DidCreateVerificationMethod, DidMetadata, PortableDid } from './did-method.js'; +import type { + DidService, + DidDocument, + DidResolutionResult, + DidResolutionOptions, + DidVerificationMethod, +} from '../types/did-core.js'; + +import { DidUri } from '../did-uri.js'; +import { DidMethod } from './did-method.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'; + +/** + * Represents a BEP44 message, which is used for storing and retrieving data in the Mainline DHT + * network. + * + * A BEP44 message is used primarily in the context of the DID DHT method for publishing and + * resolving DID documents in the DHT network. This type encapsulates the data structure required + * for such operations in accordance with BEP44. + * + * @see {@link https://www.bittorrent.org/beps/bep_0044.html | BEP44} + */ +interface Bep44Message { + /** + * The public key bytes of the Identity Key, which serves as the identifier in the DHT network for + * the corresponding BEP44 message. + */ + k: Uint8Array; + + /** + * The sequence number of the message, used to ensure the latest version of the data is retrieved + * and updated. It's a monotonically increasing number. + */ + seq: number; + + /** + * The signature of the message, ensuring the authenticity and integrity of the data. It's + * computed over the bencoded sequence number and value. + */ + sig: Uint8Array; + + /** + * The actual data being stored or retrieved from the DHT network, typically encoded in a format + * suitable for DNS packet representation of a DID Document. + */ + v: Uint8Array; +} + +/** + * Options for creating a Decentralized Identifier (DID) using the DID DHT method. + */ +export interface DidDhtCreateOptions extends DidCreateOptions { + /** + * Optionally specify that the DID Subject is also identified by one or more other DIDs or URIs. + * + * A DID subject can have multiple identifiers for different purposes, or at different times. + * The assertion that two or more DIDs (or other types of URI) refer to the same DID subject can + * be made using the `alsoKnownAs` property. + * + * @see {@link https://www.w3.org/TR/did-core/#also-known-as | DID Core Specification, § Also Known As} + * + * @example + * ```ts + * const did = await DidDht.create({ + * options: { + * alsoKnownAs: 'did:example:123' + * }; + * ``` + */ + alsoKnownAs?: string[]; + + /** + * Optionally specify which DID (or DIDs) is authorized to make changes to the DID document. + * + * A DID controller is an entity that is authorized to make changes to a DID document. Typically, + * only the DID Subject (i.e., the value of `id` property in the DID document) is authoritative. + * However, another DID (or DIDs) can be specified as the DID controller, and when doing so, any + * verification methods contained in the DID document for the other DID should be accepted as + * authoritative. In other words, proofs created by the controller DID should be considered + * equivalent to proofs created by the DID Subject. + * + * @see {@link https://www.w3.org/TR/did-core/#did-controller | DID Core Specification, § DID Controller} + * + * @example + * ```ts + * const did = await DidDht.create({ + * options: { + * controller: 'did:example:123' + * }; + * ``` + */ + controllers?: string | string[]; + + /** + * 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. + */ + gatewayUri?: string; + + /** + * Optional. Determines whether the created DID should be published to the DHT network. + * + * If set to `true` or omitted, the DID is publicly discoverable. If `false`, the DID is not + * published and cannot be resolved by others. By default, newly created DIDs are published. + * + * @see {@link https://did-dht.com | DID DHT Method Specification} + * + * @example + * ```ts + * const did = await DidDht.create({ + * options: { + * publish: false + * }; + * ``` + */ + publish?: boolean; + + /** + * Optional. An array of service endpoints associated with the DID. + * + * Services are used in DID documents to express ways of communicating with the DID subject or + * associated entities. A service can be any type of service the DID subject wants to advertise, + * including decentralized identity management services for further discovery, authentication, + * authorization, or interaction. + * + * @see {@link https://www.w3.org/TR/did-core/#services | DID Core Specification, § Services} + * + * @example + * ```ts + * const did = await DidDht.create({ + * options: { + * services: [ + * { + * id: 'did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y#dwn', + * type: 'DecentralizedWebNode', + * serviceEndpoint: ['https://example.com/dwn1', 'https://example/dwn2'] + * } + * ] + * }; + * ``` + */ + services?: DidService[]; + + /** + * Optionally specify one or more registered DID DHT types to make the DID discovereable. + * + * Type indexing is an OPTIONAL feature that enables DIDs to become discoverable. DIDs that wish + * to be discoverable and resolveable by type can include one or more types when publishing their + * DID document to a DID DHT Gateway. + * + * The registered DID types are published in the {@link https://did-dht.com/registry/index.html#indexed-types | DID DHT Registry}. + */ + types?: (DidDhtRegisteredDidType | keyof typeof DidDhtRegisteredDidType)[]; + + /** + * Optional. An array of verification methods to be included in the DID document. + * + * By default, a newly created DID DHT document will contain a single Ed25519 verification method, + * also known as the {@link https://did-dht.com/#term:identity-key | Identity Key}. Additional + * verification methods can be added to the DID document using the `verificationMethods` property. + * + * @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods} + * + * @example + * ```ts + * const did = await DidDht.create({ + * options: { + * verificationMethods: [ + * { + * algorithm: 'Ed25519', + * purposes: ['authentication', 'assertionMethod'] + * }, + * { + * algorithm: 'Ed25519', + * id: 'dwn-sig', + * purposes: ['authentication', 'assertionMethod'] + * } + * ] + * }; + * ``` + */ + verificationMethods?: DidCreateVerificationMethod[]; +} + +/** + * The default Pkarr relay server to use when publishing and resolving DID documents. + */ +const DEFAULT_PKARR_RELAY = 'https://diddht.tbddev.org'; + +/** + * The version of the DID DHT specification that is implemented by this library. + * + * When a DID DHT document is published to the DHT network, the version of the specification that + * was used to create the document is included in the DNS TXT record for the root record. This + * allows clients to determine whether the DID DHT document is compatible with the client's + * implementation of the DID DHT specification. The version number is not present in the + * corresponding DID document. + * + * @see {@link https://did-dht.com | DID DHT Method Specification} + */ +const DID_DHT_SPECIFICATION_VERSION = 0; + +/** + * The default TTL for DNS records published to the DHT network. + * + * The recommended TTL value is 7200 seconds (2 hours) since it matches the default TTL for + * Mainline DHT records. + */ +const DNS_RECORD_TTL = 7200; + +/** + * Character used to separate distinct elements or entries in the DNS packet representation + * of a DID Document. + * + * For example, verification methods, verification relationships, and services are separated by + * semicolons (`;`) in the root record: + * ``` + * vm=k1;auth=k1;asm=k2;inv=k3;del=k3;srv=s1 + * ``` + */ +const PROPERTY_SEPARATOR = ';'; + +/** + * Character used to separate distinct values within a single element or entry in the DNS packet + * representation of a DID Document. + * + * For example, multiple key references for the `authentication` verification relationships are + * separated by commas (`,`): + * ``` + * auth=0,1,2 + * ``` + */ +const VALUE_SEPARATOR = ','; + +/** + * Represents an optional extension to a DID Document’s DNS packet representation exposed as a + * type index. + * + * Type indexing is an OPTIONAL feature that enables DIDs to become discoverable. DIDs that wish to + * be discoverable and resolveable by type can include one or more types when publishing their DID + * document to a DID DHT Gateway. + * + * The registered DID types are published in the {@link https://did-dht.com/registry/index.html#indexed-types | DID DHT Registry}. + */ +export enum DidDhtRegisteredDidType { + /** + * Type 0 is reserved for DIDs that do not wish to associate themselves with a specific type but + * wish to make themselves discoverable. + */ + Discoverable = 0, + + /** + * Organization + * @see {@link https://schema.org/Organization | schema definition} + */ + Organization = 1, + + /** + * Government Organization + * @see {@link https://schema.org/GovernmentOrganization | schema definition} + */ + Government = 2, + + /** + * Corporation + * @see {@link https://schema.org/Corporation | schema definition} + */ + Corporation = 3, + + /** + * Corporation + * @see {@link https://schema.org/Corporation | schema definition} + */ + LocalBusiness = 4, + + /** + * Software Package + * @see {@link https://schema.org/SoftwareSourceCode | schema definition} + */ + SoftwarePackage = 5, + + /** + * Web App + * @see {@link https://schema.org/WebApplication | schema definition} + */ + WebApp = 6, + + /** + * Financial Institution + * @see {@link https://schema.org/FinancialService | schema definition} + */ + FinancialInstitution = 7 +} + +/** + * Enumerates the types of keys that can be used in a DID DHT document. + * + * The DID DHT method supports various cryptographic key types. These key types are essential for + * the creation and management of DIDs and their associated cryptographic operations like signing + * and encryption. The registered key types are published in the DID DHT Registry and each is + * assigned a unique numerical value for use by client and gateway implementations. + * + * The registered key types are published in the {@link https://did-dht.com/registry/index.html#key-type-index | DID DHT Registry}. + */ +export enum DidDhtRegisteredKeyType { + /** + * Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature + * Algorithm) and Curve25519. + */ + Ed25519 = 0, + + /** + * secp256k1: A cryptographic curve used for digital signatures in a range of decentralized + * systems. + */ + secp256k1 = 1, + + /** + * secp256r1: Also known as P-256 or prime256v1, this curve is used for cryptographic operations + * and is widely supported in various cryptographic libraries and standards. + */ + secp256r1 = 2 +} + +/** + * Maps {@link https://www.w3.org/TR/did-core/#verification-relationships | DID Core Verification Relationship} + * values to the corresponding record name in the DNS packet representation of a DHT DID document. + */ +export enum DidDhtVerificationRelationship { + /** + * Specifies how the DID subject is expected to be authenticated. + */ + authentication = 'auth', + + /** + * Specifies how the DID subject is expected to express claims, such as for issuing Verifiable + * Credentials. + */ + assertionMethod = 'asm', + + /** + * Specifies a mechanism used by the DID subject to delegate a cryptographic capability to another + * party + */ + capabilityDelegation = 'del', + + /** + * Specifies a verification method used by the DID subject to invoke a cryptographic capability. + */ + capabilityInvocation = 'inv', + + /** + * Specifies how an entity can generate encryption material to communicate confidentially with the + * DID subject. + */ + keyAgreement = 'agm' +} + +/** + * Private helper that maps algorithm identifiers to their corresponding DID DHT + * {@link DidDhtRegisteredKeyType | registered key type}. + */ +const AlgorithmToKeyTypeMap = { + Ed25519 : DidDhtRegisteredKeyType.Ed25519, + ES256K : DidDhtRegisteredKeyType.secp256k1, + ES256 : DidDhtRegisteredKeyType.secp256r1, + 'P-256' : DidDhtRegisteredKeyType.secp256r1, + secp256k1 : DidDhtRegisteredKeyType.secp256k1, + secp256r1 : DidDhtRegisteredKeyType.secp256r1 +} as const; + +/** + * The `DidDht` class provides an implementation of the `did:dht` DID method. + * + * Features: + * - DID Creation: Create new `did:dht` DIDs. + * - DID Key Management: Instantiate a DID object from an existing verification method keys or + * or a 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:dht` to its corresponding DID Document stored in the DHT network. + * - Signature Operations: Sign and verify messages using keys associated with a DID. + * + * @remarks + * The `did:dht` method leverages the distributed nature of the Mainline DHT network for + * decentralized identity management. This method allows DIDs to be resolved without relying on + * centralized registries or ledgers, enhancing privacy and control for users. The DID Document is + * stored and retrieved from the DHT network, and the method includes optional mechanisms for + * discovering DIDs by type. + * + * The DID URI in the `did:dht` method includes a method-specific identifier called the Identity Key + * which corresponds to the DID's entry in the DHT network. The Identity Key required to make + * changes to the DID Document since Mainline DHT nodes validate the signature of each message + * before storing the value in the DHT. + * + * @see {@link https://did-dht.com | DID DHT Method Specification} + * + * @example + * ```ts + * // DID Creation + * const did = await DidDht.create(); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidDht.create({ keyManager }); + * + * // DID Resolution + * const resolutionResult = await DidDht.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 }); + * + * // Key Management + * + * // Instantiate a DID object for a published DID with existing keys in a KMS + * const did = await DidDht.fromKeyManager({ + * didUri: 'did:dht:cf69rrqpanddbhkqecuwia314hfawfua9yr6zx433jmgm39ez57y', + * keyManager + * }); + * + * // 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); + * ``` + */ +export class DidDht extends DidMethod { + + /** + * Name of the DID method, as defined in the DID DHT specification. + */ + public static methodName = 'dht'; + + /** + * Creates a new DID using the `did:dht` method formed from a newly generated key. + * + * @remarks + * The DID URI is formed by z-base-32 encoding the Identity Key public key and prefixing with + * `did:dht:`. + * + * Notes: + * - If no `options` are given, by default a new Ed25519 key will be generated which serves as the + * Identity Key. + * + * @example + * ```ts + * // DID Creation + * const did = await DidDht.create(); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidDht.create({ keyManager }); + * ``` + * + * @param params - The parameters for the create operation. + * @param params.keyManager - Optionally specify a Key Management System (KMS) used to generate + * keys and sign data. + * @param params.options - Optional parameters that can be specified when creating a new DID. + * @returns A Promise resolving to a {@link Did} object representing the new DID. + */ + public static async create({ + keyManager = new LocalKeyManager(), + options = {} + }: { + keyManager?: TKms; + options?: DidDhtCreateOptions; + } = {}): Promise { + // Before processing the create operation, validate DID-method-specific requirements to prevent + // keys from being generated unnecessarily. + + // Check 1: Validate that the algorithm for any given verification method is supported by the + // DID DHT specification. + if (options.verificationMethods?.some(vm => !(vm.algorithm in AlgorithmToKeyTypeMap))) { + 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. + 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 and any additional verification methods. + const keySet = await DidDht.generateKeys({ + keyManager, + verificationMethods: options.verificationMethods ?? [] + }); + + // 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 }); + + // 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; + + return did; + } + + /** + * Instantiates a `Did` object for the DID DHT method from a given {@link PortableDid}. + * + * This method allows for the creation of a `Did` 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 `Did` 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. + * + * @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 }); + * ``` + * + * @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 `Did` 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. + */ + public static async fromKeys({ + keyManager = new LocalKeyManager(), + uri, + verificationMethods, + options = {} + }: { + 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`); + } + + if (!verificationMethods?.some(vm => vm.privateKeyJwk && vm.publicKeyJwk)) { + throw new Error(`All verification methods must contain a public and private key in JWK format`); + } + + // Import the private key material for every verification method into the key manager. + for (let vm of verificationMethods) { + await keyManager.importKey({ key: vm.privateKeyJwk! }); + } + + // 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; + } + } + + /** + * Given the W3C DID Document of a `did:dht` DID, return the verification method that will be used + * for signing messages and credentials. If given, the `methodId` parameter is used to select the + * verification method. If not given, the Identity Key's verification method with an ID fragment + * of '#0' is used. + * + * @param params - The parameters for the `getSigningMethod` operation. + * @param params.didDocument - DID Document to get the verification method from. + * @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' }: { + didDocument: DidDocument; + methodId?: string; + }): Promise { + + const parsedDid = DidUri.parse(didDocument.id); + if (parsedDid && parsedDid.method !== this.methodName) { + throw new Error(`Method not supported: ${parsedDid.method}`); + } + + const verificationMethod = didDocument.verificationMethod?.find(vm => vm.id.endsWith(methodId)); + + return verificationMethod; + } + + /** + * Publishes a DID to the DHT, making it publicly discoverable and resolvable. + * + * This method handles the publication of a DID Document associated with a `did:dht` DID to the + * Mainline DHT network. The publication process involves storing the DID Document in Mainline DHT + * via a Pkarr relay server. + * + * @remarks + * - This method is typically invoked automatically during the creation of a new DID unless the + * `publish` option is set to `false`. + * - 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 `Did` 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. + * ``` + */ + public static async publish({ did, gatewayUri = DEFAULT_PKARR_RELAY }: { + did: Did; + gatewayUri?: string; + }): Promise { + const isPublished = await DidDhtDocument.put({ did, relay: gatewayUri }); + + return isPublished; + } + + /** + * Resolves a `did:dht` identifier to its corresponding DID document. + * + * This method performs the resolution of a `did:dht` DID, retrieving its DID Document from the + * Mainline DHT network. The process involves querying the DHT network via a Pkarr relay server to + * retrieve the DID Document that corresponds to the given DID identifier. + * + * @remarks + * - If a `gatewayUri` option is not specified, a default Pkarr relay is used to access the DHT + * network. + * - It decodes the DID identifier and retrieves the associated DID Document and metadata. + * - In case of resolution failure, appropriate error information is returned. + * + * @example + * ```ts + * const resolutionResult = await DidDht.resolve('did:dht:example'); + * ``` + * + * @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. + */ + 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; + + 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 }); + + // If the DID document was retrieved successfully, return it. + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didDocument, + didDocumentMetadata: { published: true, ...didMetadata } + }; + + } catch (error: any) { + // Rethrow any unexpected errors that are not a `DidError`. + if (!(error instanceof DidError)) throw new Error(error); + + // Return a DID Resolution Result with the appropriate error code. + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { + error: error.code, + ...error.message && { errorMessage: error.message } + } + }; + } + } + + /** + * Instantiates a `Did` object for the DID DHT method using an array of public keys. + * + * This method is used to create a `Did` 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 `Did` 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 `Did` 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; + } +} + +/** + * The `DidDhtDocument` class provides functionality for interacting with the DID document stored in + * 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. + */ +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. + */ + public static async get({ didUri, relay }: { + didUri: string; + relay: string; + }): Promise<{ didDocument: DidDocument, didMetadata: DidMetadata }> { + // 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 }); + + // 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 }); + + return { didDocument, didMetadata }; + } + + /** + * Publishes a DID document to the DHT network. + * + * @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. + */ + public static async put({ did, relay }: { + did: Did; + relay: 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, + didMetadata : did.metadata + }); + + // Create a signed BEP44 put message from the DNS packet. + const bep44Message = await DidDhtUtils.createBep44PutMessage({ + dnsPacket, + publicKeyBytes : DidDhtUtils.identifierToIdentityKeyBytes({ didUri: did.uri }), + signer : await did.getSigner() + }); + + // Publish the DNS packet to the DHT network. + const putResult = await DidDhtDocument.pkarrPut({ relay, bep44Message }); + + return putResult; + } + + /** + * Retrieves a signed BEP44 message from a 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. + * @returns A promise resolving to a BEP44 message containing the signed DNS packet. + */ + private static async pkarrGet({ relay, publicKeyBytes }: { + publicKeyBytes: Uint8Array; + relay: 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; + + // Transmit the Get request to the Pkarr relay and get the response. + let response: Response; + try { + response = await fetch(url, { method: 'GET' }); + + if (!response.ok) { + throw new DidError(DidErrorCode.NotFound, `Pkarr record not found for: ${identifier}`); + } + + } catch (error: any) { + if (error instanceof DidError) throw error; + throw new DidError(DidErrorCode.InternalError, `Failed to fetch Pkarr record: ${error.message}`); + } + + // Read the Fetch Response stream into a byte array. + const messageBytes = await response.arrayBuffer(); + + if (messageBytes.byteLength < 72) { + throw new DidError(DidErrorCode.InvalidDidDocumentLength, `Pkarr response must be at least 72 bytes but got: ${messageBytes.byteLength}`); + } + + if (messageBytes.byteLength > 1072) { + throw new DidError(DidErrorCode.InvalidDidDocumentLength, `Pkarr response exceeds 1000 byte limit: ${messageBytes.byteLength}`); + } + + // Decode the BEP44 message from the byte array. + const bep44Message: Bep44Message = { + k : publicKeyBytes, + seq : Number(new DataView(messageBytes).getBigUint64(64)), + sig : new Uint8Array(messageBytes, 0, 64), + v : new Uint8Array(messageBytes, 72) + }; + + return bep44Message; + } + + /** + * Publishes a signed BEP44 message to a 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. + * @returns A promise resolving to `true` if the message was successfully published, otherwise `false`. + */ + private static async pkarrPut({ relay, bep44Message }: { + bep44Message: Bep44Message; + relay: 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; + + // Construct the body of the request according to the Pkarr relay specification. + const body = new Uint8Array(bep44Message.v.length + 72); + body.set(bep44Message.sig, 0); + new DataView(body.buffer).setBigUint64(bep44Message.sig.length, BigInt(bep44Message.seq)); + body.set(bep44Message.v, bep44Message.sig.length + 8); + + // Transmit the Put request to the Pkarr relay and get the response. + let response: Response; + try { + response = await fetch(url, { + method : 'PUT', + headers : { 'Content-Type': 'application/octet-stream' }, + body + }); + + } catch (error: any) { + throw new DidError(DidErrorCode.InternalError, `Failed to put Pkarr record: ${error.message}`); + } + + // Return `true` if the DHT request was successful, otherwise return `false`. + return response.ok; + } + + /** + * Converts a DNS packet to a DID document according to the DID DHT specification. + * + * @see {@link https://did-dht.com/#dids-as-dns-records | DID DHT Specification, § DIDs as DNS Records} + * + * @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. + */ + private static async fromDnsPacket({ didUri, dnsPacket }: { + didUri: string; + dnsPacket: Packet; + }): Promise<{ didDocument: DidDocument, didMetadata: DidMetadata }> { + // Begin constructing the DID Document. + const didDocument: DidDocument = { id: didUri }; + const didMetadata: DidMetadata = {}; + + const idLookup = new Map(); + + for (const answer of dnsPacket?.answers ?? []) { + // DID DHT properties are ONLY present in DNS TXT records. + if (answer.type !== 'TXT') continue; + + // Get the DID DHT record identifier (e.g., k0, aka, did, etc.) from the DNS resource name. + const dnsRecordId = answer.name.split('.')[0].substring(1); + + switch (true) { + // Process an also known as record. + case dnsRecordId.startsWith('aka'): { + // Decode the DNS TXT record data value to a string. + const data = DidDhtUtils.parseTxtDataToString(answer.data); + + // Add the 'alsoKnownAs' property to the DID document. + didDocument.alsoKnownAs = data.split(VALUE_SEPARATOR); + + break; + } + + // Process a controller record. + case dnsRecordId.startsWith('cnt'): { + // Decode the DNS TXT record data value to a string. + const data = DidDhtUtils.parseTxtDataToString(answer.data); + + // Add the 'controller' property to the DID document. + didDocument.controller = data.includes(VALUE_SEPARATOR) ? data.split(VALUE_SEPARATOR) : data; + + break; + } + + // Process verification methods. + case dnsRecordId.startsWith('k'): { + // Get the method ID fragment (id), key type (t), Base64URL-encoded public key (k), and + // optionally, controller (c) from the decoded TXT record data. + const { id, t, k, c } = DidDhtUtils.parseTxtDataToObject(answer.data); + + // Convert the public key from Base64URL format to a byte array. + const publicKeyBytes = Convert.base64Url(k).toUint8Array(); + + // Use the key type integer to look up the cryptographic curve name. + const namedCurve = DidDhtRegisteredKeyType[Number(t)]; + + // Convert the public key from a byte array to JWK format. + let publicKey = await DidDhtUtils.keyConverter(namedCurve).bytesToPublicKey({ publicKeyBytes }); + + // Initialize the `verificationMethod` array if it does not already exist. + didDocument.verificationMethod ??= []; + + // Prepend the DID URI to the ID fragment to form the full verification method ID. + const methodId = `${didUri}#${id}`; + + // Add the verification method to the DID document. + didDocument.verificationMethod.push({ + id : methodId, + type : 'JsonWebKey', + controller : c ?? didUri, + publicKeyJwk : publicKey, + }); + + // Add a mapping from the DNS record ID (e.g., 'k0', 'k1', etc.) to the verification + // method ID (e.g., 'did:dht:...#0', etc.). + idLookup.set(dnsRecordId, methodId); + + break; + } + + // Process services. + case dnsRecordId.startsWith('s'): { + // Get the service ID fragment (id), type (t), service endpoint (se), and optionally, + // other properties from the decoded TXT record data. + const { id, t, se, ...customProperties } = DidDhtUtils.parseTxtDataToObject(answer.data); + + // The service endpoint can either be a string or an array of strings. + const serviceEndpoint = se.includes(VALUE_SEPARATOR) ? se.split(VALUE_SEPARATOR) : se; + + // Initialize the `service` array if it does not already exist. + didDocument.service ??= []; + + didDocument.service.push({ + ...customProperties, + id : `${didUri}#${id}`, + type : t, + serviceEndpoint + }); + + break; + } + + // Process DID DHT types. + case dnsRecordId.startsWith('typ'): { + // Decode the DNS TXT record data value to an object. + 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)); + + break; + } + + // Process root record. + case dnsRecordId.startsWith('did'): { + // Helper function that maps verification relationship values to verification method IDs. + const recordIdsToMethodIds = (data: string): string[] => data + .split(VALUE_SEPARATOR) + .map(dnsRecordId => idLookup.get(dnsRecordId)) + .filter((id): id is string => typeof id === 'string'); + + // Decode the DNS TXT record data and destructure verification relationship properties. + const { auth, asm, del, inv, agm } = DidDhtUtils.parseTxtDataToObject(answer.data); + + // Add the verification relationships, if any, to the DID document. + if (auth) didDocument.authentication = recordIdsToMethodIds(auth); + if (asm) didDocument.assertionMethod = recordIdsToMethodIds(asm); + if (del) didDocument.capabilityDelegation = recordIdsToMethodIds(del); + if (inv) didDocument.capabilityInvocation = recordIdsToMethodIds(inv); + if (agm) didDocument.keyAgreement = recordIdsToMethodIds(agm); + + break; + } + } + } + + return { didDocument, didMetadata }; + } + + /** + * Converts a DID document to a DNS packet according to the DID DHT specification. + * + * @see {@link https://did-dht.com/#dids-as-dns-records | DID DHT Specification, § DIDs as DNS Records} + * + * @param params - The parameters to use when converting a DID document to a DNS packet. + * @param params.didDocument - The DID document to convert to a DNS packet. + * @param params.didMetadata - The DID metadata to include in the DNS packet. + * @returns A promise that resolves to a DNS packet. + */ + private static async toDnsPacket({ didDocument, didMetadata }: { + didDocument: DidDocument; + didMetadata: DidMetadata; + }): Promise { + const dnsAnswerRecords: TxtAnswer[] = []; + const idLookup = new Map(); + const serviceIds: string[] = []; + const verificationMethodIds: string[] = []; + + // Add DNS TXT records if the DID document contains an `alsoKnownAs` property. + if (didDocument.alsoKnownAs) { + dnsAnswerRecords.push({ + type : 'TXT', + name : '_aka.did.', + ttl : DNS_RECORD_TTL, + data : didDocument.alsoKnownAs.join(VALUE_SEPARATOR) + }); + } + + // Add DNS TXT records if the DID document contains a `controller` property. + if (didDocument.controller) { + const controller = Array.isArray(didDocument.controller) + ? didDocument.controller.join(VALUE_SEPARATOR) + : didDocument.controller; + dnsAnswerRecords.push({ + type : 'TXT', + name : '_cnt.did.', + ttl : DNS_RECORD_TTL, + data : controller + }); + } + + // Add DNS TXT records for each verification method. + for (const [index, vm] of didDocument.verificationMethod?.entries() ?? []) { + const dnsRecordId = `k${index}`; + verificationMethodIds.push(dnsRecordId); + let methodId = vm.id.split('#').pop()!; // Remove fragment prefix, if any. + idLookup.set(methodId, dnsRecordId); + + const publicKey = vm.publicKeyJwk; + + if (!(publicKey?.crv && publicKey.crv in AlgorithmToKeyTypeMap)) { + throw new DidError(DidErrorCode.InvalidPublicKeyType, `Verification method '${vm.id}' contains an unsupported key type: ${publicKey?.crv ?? 'undefined'}`); + } + + // Use the public key's `crv` property to get the DID DHT key type. + const keyType = DidDhtRegisteredKeyType[publicKey.crv as keyof typeof DidDhtRegisteredKeyType]; + + // Convert the public key from JWK format to a byte array. + const publicKeyBytes = await DidDhtUtils.keyConverter(publicKey.crv).publicKeyToBytes({ publicKey }); + + // Convert the public key from a byte array to Base64URL format. + const publicKeyBase64Url = Convert.uint8Array(publicKeyBytes).toBase64Url(); + + // Define the data for the DNS TXT record. + const txtData = [`id=${methodId}`, `t=${keyType}`, `k=${publicKeyBase64Url}`]; + + // Add the controller property, if set to a value other than the Identity Key (DID Subject). + if (vm.controller !== didDocument.id) txtData.push(`c=${vm.controller}`); + + // Add a TXT record for the verification method. + dnsAnswerRecords.push({ + type : 'TXT', + name : `_${dnsRecordId}._did.`, + ttl : DNS_RECORD_TTL, + data : txtData.join(PROPERTY_SEPARATOR) + }); + } + + // Add DNS TXT records for each service. + didDocument.service?.forEach((service, index) => { + const dnsRecordId = `s${index}`; + serviceIds.push(dnsRecordId); + const serviceId = service.id.split('#').pop()!; // Remove fragment prefix, if any. + const serviceEndpoint = Array.isArray(service.serviceEndpoint) + ? service.serviceEndpoint.join(',') + : service.serviceEndpoint; + + // Define the data for the DNS TXT record. + const txtData = [`id=${serviceId}`, `t=${service.type}`, `se=${serviceEndpoint}`]; + + // Add a TXT record for the verification method. + dnsAnswerRecords.push({ + type : 'TXT', + name : `_${dnsRecordId}._did.`, + ttl : DNS_RECORD_TTL, + data : txtData.join(PROPERTY_SEPARATOR) + }); + }); + + // Initialize the root DNS TXT record with the DID DHT specification version. + const rootRecord: string[] = [`v=${DID_DHT_SPECIFICATION_VERSION}`]; + + // Add verification methods to the root record. + if (verificationMethodIds.length) { + rootRecord.push(`vm=${verificationMethodIds.join(VALUE_SEPARATOR)}`); + } + + // Add verification relationships to the root record. + Object.keys(DidVerificationRelationship).forEach(relationship => { + // Collect the verification method IDs for the given relationship. + const dnsRecordIds = (didDocument[relationship as keyof DidDocument] as any[]) + ?.map(id => idLookup.get(id.split('#').pop())); + + // If the relationship includes verification methods, add them to the root record. + if (dnsRecordIds) { + const recordName = DidDhtVerificationRelationship[relationship as keyof typeof DidDhtVerificationRelationship]; + rootRecord.push(`${recordName}=${dnsRecordIds.join(VALUE_SEPARATOR)}`); + } + }); + + // Add services to the root record. + if (serviceIds.length) { + rootRecord.push(`svc=${serviceIds.join(VALUE_SEPARATOR)}`); + } + + // If defined, add a DNS TXT record for each registered DID type. + if (didMetadata.types?.length) { + // DID types can be specified as either a string or a number, so we need to normalize the + // values to integers. + const types = didMetadata.types as (DidDhtRegisteredDidType | keyof typeof DidDhtRegisteredDidType)[]; + const typeIntegers = types.map(type => typeof type === 'string' ? DidDhtRegisteredDidType[type] : type); + + dnsAnswerRecords.push({ + type : 'TXT', + name : '_typ._did.', + ttl : DNS_RECORD_TTL, + data : `id=${typeIntegers.join(VALUE_SEPARATOR)}` + }); + } + + // Add a DNS TXT record for the root record. + dnsAnswerRecords.push({ + type : 'TXT', + name : '_did.', + ttl : DNS_RECORD_TTL, + data : rootRecord.join(PROPERTY_SEPARATOR) + }); + + // Per the DID DHT specification, the method-specific identifier must be appended as the + // Origin of all records. + const [, , identifier] = didDocument.id.split(':'); + dnsAnswerRecords.forEach(record => record.name += identifier); + + // Create a DNS response packet with the authoritative answer flag set. + const dnsPacket: Packet = { + id : 0, + type : 'response', + flags : AUTHORITATIVE_ANSWER, + answers : dnsAnswerRecords + }; + + return dnsPacket; + } +} + +/** + * The `DidDhtUtils` class provides utility functions to support operations in the DID DHT method. + * This includes functions for creating and parsing BEP44 messages, handling identity keys, and + * converting between different formats and representations. + */ +class DidDhtUtils { + /** + * Creates a BEP44 put message, which is used to publish a DID document to the DHT network. + * + * @param params - The parameters to use when creating the BEP44 put message + * @param params.dnsPacket - The DNS packet to encode in the BEP44 message. + * @param params.publicKeyBytes - The public key bytes of the Identity Key. + * @param params.signer - Signer that can sign and verify data using the Identity Key. + * @returns A promise that resolves to a BEP44 put message. + */ + public static async createBep44PutMessage({ dnsPacket, publicKeyBytes, signer }: { + dnsPacket: Packet; + publicKeyBytes: Uint8Array; + signer: Signer; + }): Promise { + // BEP44 requires that the sequence number be a monotoically increasing integer, so we use the + // current time in seconds since Unix epoch as a simple solution. Higher precision is not + // recommended since DID DHT documents are not expected to change frequently and there are + // small differences in system clocks that can cause issues if multiple clients are publishing + // updates to the same DID document. + const sequenceNumber = Math.ceil(Date.now() / 1000); + + // Encode the DNS packet into a byte array containing a UDP payload. + const encodedDnsPacket = dnsPacketEncode(dnsPacket); + + // Encode the sequence and DNS byte array to bencode format. + const bencodedData = bencode.encode({ seq: sequenceNumber, v: encodedDnsPacket }).subarray(1, -1); + + if (bencodedData.length > 1000) { + throw new DidError(DidErrorCode.InvalidDidDocumentLength, `DNS packet exceeds the 1000 byte maximum size: ${bencodedData.length} bytes`); + } + + // Sign the BEP44 message. + const signature = await signer.sign({ data: bencodedData }); + + return { k: publicKeyBytes, seq: sequenceNumber, sig: signature, v: encodedDnsPacket }; + } + + /** + * Converts a DID URI to a JSON Web Key (JWK) representing the Identity Key. + * + * @param params - The parameters to use for the conversion. + * @param params.didUri - The DID URI containing the Identity Key. + * @returns A promise that resolves to a JWK representing the Identity Key. + */ + public static async identifierToIdentityKey({ didUri }: { + didUri: string + }): Promise { + // Decode the method-specific identifier from z-base-32 to a byte array. + let identityKeyBytes = DidDhtUtils.identifierToIdentityKeyBytes({ didUri }); + + // Convert the byte array to a JWK. + const identityKey = await Ed25519.bytesToPublicKey({ publicKeyBytes: identityKeyBytes }); + + return identityKey; + } + + /** + * Converts a DID URI to the byte array representation of the Identity Key. + * + * @param params - The parameters to use for the conversion. + * @param params.didUri - The DID URI containing the Identity Key. + * @returns A byte array representation of the Identity Key. + */ + public static identifierToIdentityKeyBytes({ didUri }: { + didUri: string + }): Uint8Array { + // Parse the DID URI. + const parsedDid = DidUri.parse(didUri); + + // Verify that the DID URI is valid. + if (!parsedDid) { + throw new DidError(DidErrorCode.InvalidDid, `Invalid DID URI: ${didUri}`); + } + + // Verify the DID method is supported. + if (parsedDid.method !== DidDht.methodName) { + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`); + } + + // Decode the method-specific identifier from z-base-32 to a byte array. + let identityKeyBytes: Uint8Array | undefined; + try { + identityKeyBytes = Convert.base32Z(parsedDid.id).toUint8Array(); + } catch { + throw new DidError(DidErrorCode.InvalidPublicKey, `Failed to decode method-specific identifier`); + } + + if (identityKeyBytes.length !== 32) { + throw new DidError(DidErrorCode.InvalidPublicKeyLength, `Invalid public key length: ${identityKeyBytes.length}`); + } + + return identityKeyBytes; + } + + /** + * Encodes a DID DHT Identity Key into a DID identifier. + * + * This method first z-base-32 encodes the Identity Key. The resulting string is prefixed with + * `did:dht:` to form the DID identifier. + * + * @param params - The parameters to use for the conversion. + * @param params.identityKey The Identity Key from which the DID identifier is computed. + * @returns A promise that resolves to a string containing the DID identifier. + */ + public static async identityKeyToIdentifier({ identityKey }: { + identityKey: Jwk + }): Promise { + // Convert the key from JWK format to a byte array. + const publicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey: identityKey }); + + // Encode the byte array as a z-base-32 string. + const identifier = Convert.uint8Array(publicKeyBytes).toBase32Z(); + + return `did:${DidDht.methodName}:${identifier}`; + } + + /** + * Returns the appropriate key converter for the specified cryptographic curve. + * + * @param curve - The cryptographic curve to use for the key conversion. + * @returns An `AsymmetricKeyConverter` for the specified curve. + */ + public static keyConverter(curve: string): AsymmetricKeyConverter { + const converters: Record = { + 'Ed25519' : Ed25519, + 'P-256' : Secp256r1, + 'secp256k1' : Secp256k1 + }; + + const converter = converters[curve]; + + if (!converter) throw new DidError(DidErrorCode.InvalidPublicKeyType, `Unsupported curve: ${curve}`); + + return converter; + } + + /** + * Parses and verifies a BEP44 Get message, converting it to a DNS packet. + * + * @param params - The parameters to use when verifying and parsing the BEP44 Get response message. + * @param params.bep44Message - The BEP44 message to verify and parse. + * @returns A promise that resolves to a DNS packet. + */ + public static async parseBep44GetMessage({ bep44Message }: { + bep44Message: Bep44Message; + }): Promise { + // Convert the public key byte array to JWK format. + const publicKey = await Ed25519.bytesToPublicKey({ publicKeyBytes: bep44Message.k }); + + // Encode the sequence and DNS byte array to bencode format. + const bencodedData = bencode.encode({ seq: bep44Message.seq, v: bep44Message.v }).subarray(1, -1); + + // Verify the signature of the BEP44 message. + const isValid = await Ed25519.verify({ + key : publicKey, + signature : bep44Message.sig, + data : bencodedData + }); + + if (!isValid) { + throw new DidError(DidErrorCode.InvalidSignature, `Invalid signature for DHT BEP44 message`); + } + + return dnsPacketDecode(bep44Message.v); + } + + /** + * Decodes and parses the data value of a DNS TXT record into a key-value object. + * + * @param txtData - The data value of a DNS TXT record. + * @returns An object containing the key/value pairs of the TXT record data. + */ + public static parseTxtDataToObject(txtData: TxtData): Record { + return this.parseTxtDataToString(txtData).split(PROPERTY_SEPARATOR).reduce((acc, pair) => { + const [key, value] = pair.split('='); + acc[key] = value; + return acc; + }, {} as Record); + } + + /** + * Decodes and parses the data value of a DNS TXT record into a string. + * + * @param txtData - The data value of a DNS TXT record. + * @returns A string representation of the TXT record data. + */ + public static parseTxtDataToString(txtData: TxtData): string { + if (typeof txtData === 'string') { + return txtData; + } else if (txtData instanceof Uint8Array) { + return Convert.uint8Array(txtData).toString(); + } else if (Array.isArray(txtData)) { + return txtData.map(item => this.parseTxtDataToString(item)).join(''); + } else { + throw new DidError(DidErrorCode.InternalError, 'Pkarr returned DNS TXT record with invalid data type'); + } + } +} \ No newline at end of file diff --git a/packages/dids/src/methods/did-ion.ts b/packages/dids/src/methods/did-ion.ts new file mode 100644 index 000000000..09881f7c7 --- /dev/null +++ b/packages/dids/src/methods/did-ion.ts @@ -0,0 +1,805 @@ +import type { CryptoApi, Jwk } from '@web5/crypto'; +import type { + JwkEs256k, + IonDocumentModel, + IonPublicKeyModel, + IonPublicKeyPurpose, +} from '@decentralized-identity/ion-sdk'; + +import { IonDid, IonRequest } from '@decentralized-identity/ion-sdk'; +import { LocalKeyManager, computeJwkThumbprint } from '@web5/crypto'; + +import type { + DidService, + DidDocument, + DidResolutionResult, + DidResolutionOptions, + DidVerificationMethod, +} from '../types/did-core.js'; +import type { + Did, + DidMetadata, + PortableDid, + DidCreateOptions, + DidCreateVerificationMethod, + PortableDidVerificationMethod, +} from '../methods/did-method.js'; + +import { DidUri } from '../did-uri.js'; +import { DidMethod } from '../methods/did-method.js'; +import { DidError, DidErrorCode } from '../did-error.js'; +import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; + +/** + * Options for creating a Decentralized Identifier (DID) using the DID ION method. + */ +export interface DidIonCreateOptions 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 Sidetree node. If not specified, a default + * gateway node is used. + */ + gatewayUri?: string; + + /** + * Optional. Determines whether the created DID should be published to a Sidetree node. + * + * If set to `true` or omitted, the DID is publicly discoverable. If `false`, the DID is not + * published and cannot be resolved by others. By default, newly created DIDs are published. + * + * @see {@link https://identity.foundation/sidetree/spec/#create | Sidetree Protocol Specification, § Create} + * + * @example + * ```ts + * const did = await DidIon.create({ + * options: { + * publish: false + * }; + * ``` + */ + publish?: boolean; + + /** + * Optional. An array of service endpoints associated with the DID. + * + * Services are used in DID documents to express ways of communicating with the DID subject or + * associated entities. A service can be any type of service the DID subject wants to advertise, + * including decentralized identity management services for further discovery, authentication, + * authorization, or interaction. + * + * @see {@link https://www.w3.org/TR/did-core/#services | DID Core Specification, § Services} + * + * @example + * ```ts + * const did = await DidIon.create({ + * options: { + * services: [ + * { + * id: 'dwn', + * type: 'DecentralizedWebNode', + * serviceEndpoint: ['https://example.com/dwn1', 'https://example/dwn2'] + * } + * ] + * }; + * ``` + */ + services?: DidService[]; + + /** + * Optional. An array of verification methods to be included in the DID document. + * + * By default, a newly created DID ION document will contain a single Ed25519 verification method. + * Additional verification methods can be added to the DID document using the + * `verificationMethods` property. + * + * @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods} + * + * @example + * ```ts + * const did = await DidIon.create({ + * options: { + * verificationMethods: [ + * { + * algorithm: 'Ed25519', + * purposes: ['authentication', 'assertionMethod'] + * }, + * { + * algorithm: 'Ed25519', + * id: 'dwn-sig', + * purposes: ['authentication', 'assertionMethod'] + * } + * ] + * }; + * ``` + */ + verificationMethods?: DidCreateVerificationMethod[]; +} + +/** + * Represents the request model for managing DID documents within the ION network, according to the + * Sidetree protocol specification. + */ +export interface DidIonCreateRequest { + /** The type of operation to perform, which is always 'create' for a Create Operation. */ + type: 'create'; + + /** Contains properties related to the initial state of the DID document. */ + suffixData: { + /** A hash of the `delta` object, representing the initial changes to the DID document. */ + deltaHash: string; + /** A commitment value used for future recovery operations, hashed for security. */ + recoveryCommitment: string; + }; + + /** Details the changes to be applied to the DID document in this operation. */ + delta: { + /** A commitment value used for the next update operation, hashed for security. */ + updateCommitment: string; + /** An array of patch objects specifying the modifications to apply to the DID document. */ + patches: { + /** The type of modification to perform (e.g., adding or removing public keys or service + * endpoints). */ + action: string; + /** The document state or partial state to apply with this patch. */ + document: IonDocumentModel; + }[]; + } +} + +/** + * `IonPortableDid` interface extends the {@link PortableDid} interface. + * + * It represents a Decentralized Identifier (DID) that is portable and can be used across different + * domains, including the ION specific recovery and update keys. + */ +export interface IonPortableDid extends PortableDid { + /** The JSON Web Key (JWK) used for recovery purposes. */ + recoveryKey: Jwk; + + /** The JSON Web Key (JWK) used for updating the DID. */ + updateKey: Jwk; +} + +/** + * Enumerates the types of keys that can be used in a DID ION document. + * + * The DID ION method supports various cryptographic key types. These key types are essential for + * the creation and management of DIDs and their associated cryptographic operations like signing + * and encryption. + */ +export enum DidIonRegisteredKeyType { + /** + * Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature + * Algorithm) and Curve25519. + */ + Ed25519 = 'Ed25519', + + /** + * secp256k1: A cryptographic curve used for digital signatures in a range of decentralized + * systems. + */ + secp256k1 = 'secp256k1', + + /** + * secp256r1: Also known as P-256 or prime256v1, this curve is used for cryptographic operations + * and is widely supported in various cryptographic libraries and standards. + */ + secp256r1 = 'secp256r1', + + /** + * X25519: A Diffie-Hellman key exchange algorithm using Curve25519. + */ + X25519 = 'X25519' +} + +/** + * Private helper that maps algorithm identifiers to their corresponding DID ION + * {@link DidIonRegisteredKeyType | registered key type}. + */ +const AlgorithmToKeyTypeMap = { + Ed25519 : DidIonRegisteredKeyType.Ed25519, + ES256K : DidIonRegisteredKeyType.secp256k1, + ES256 : DidIonRegisteredKeyType.secp256r1, + 'P-256' : DidIonRegisteredKeyType.secp256r1, + secp256k1 : DidIonRegisteredKeyType.secp256k1, + secp256r1 : DidIonRegisteredKeyType.secp256r1 +} as const; + +/** + * The default node to use as a gateway to the Sidetree newtork when anchoring, updating, and + * resolving DID documents. + */ +const DEFAULT_GATEWAY_URI = 'https://ion.tbd.engineering'; + +/** + * The `DidIon` class provides an implementation of the `did:ion` DID method. + * + * Features: + * - DID Creation: Create new `did:ion` DIDs. + * - 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. + * - Signature Operations: Sign and verify messages using keys associated with a DID. + * + * @see {@link https://identity.foundation/sidetree/spec/ | Sidetree Protocol Specification} + * @see {@link https://github.com/decentralized-identity/ion/blob/master/docs/design.md | ION Design Document} + * + * @example + * ```ts + * // DID Creation + * const did = await DidIon.create(); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidIon.create({ keyManager }); + * + * // DID Resolution + * const resolutionResult = await DidIon.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 }); + * + * // Key Management + * + * // Instantiate a DID object for a published DID with existing keys in a KMS + * const did = await DidIon.fromKeyManager({ + * didUri: 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug', + * keyManager + * }); + * + * // Convert a DID object to a portable format + * const portableDid = await DidIon.toKeys({ did }); + * ``` + */ + +export class DidIon extends DidMethod { + + /** + * Name of the DID method, as defined in the DID ION specification. + */ + public static methodName = 'ion'; + + /** + * Creates a new DID using the `did:ion` method formed from a newly generated key. + * + * Notes: + * - If no `options` are given, by default a new Ed25519 key will be generated. + * + * @example + * ```ts + * // DID Creation + * const did = await DidIon.create(); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidIon.create({ keyManager }); + * ``` + * + * @param params - The parameters for the create operation. + * @param params.keyManager - Optionally specify a Key Management System (KMS) used to generate + * keys and sign data. + * @param params.options - Optional parameters that can be specified when creating a new DID. + * @returns A Promise resolving to a {@link Did} object representing the new DID. + */ + public static async create({ + keyManager = new LocalKeyManager(), + options = {} + }: { + keyManager?: TKms; + options?: DidIonCreateOptions; + } = {}): Promise { + // Before processing the create operation, validate DID-method-specific requirements to prevent + // keys from being generated unnecessarily. + + // Check 1: Validate that the algorithm for any given verification method is supported by the + // DID ION specification. + if (options.verificationMethods?.some(vm => !(vm.algorithm in AlgorithmToKeyTypeMap))) { + 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. + 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 ?? [] + }); + + // 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 }); + + // 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; + + return did; + } + + /** + * Given the W3C DID Document of a `did:ion` DID, return the verification method that will be used + * for signing messages and credentials. If given, the `methodId` parameter is used to select the + * verification method. If not given, the first verification method in the authentication property + * in the DID Document is used. + * + * @param params - The parameters for the `getSigningMethod` operation. + * @param params.didDocument - DID Document to get the verification method from. + * @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 }: { + didDocument: DidDocument; + methodId?: string; + }): Promise { + // Verify the DID method is supported. + const parsedDid = DidUri.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. + const verificationMethod = didDocument.verificationMethod?.find( + vm => vm.id === (methodId ?? firstAuthMethodId) + ); + + return verificationMethod; + } + + /** + * Publishes a DID to a Sidetree node, making it publicly discoverable and resolvable. + * + * This method handles the publication of a DID Document associated with a `did:ion` DID to a + * Sidetree node. + * + * @remarks + * - This method is typically invoked automatically during the creation of a new DID unless the + * `publish` option is set to `false`. + * - For existing, unpublished DIDs, it can be used to publish the DID Document to a Sidetree node. + * - The method relies on the specified Sidetree node to interface with the network. + * + * @param params - The parameters for the `publish` operation. + * @param params.did - The `Did` 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 Sidetree node. If not + * specified, a default node 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 DidIon.create({ options: { publish: false } }); + * // Publish the DID to the Sidetree network. + * const isPublished = await DidIon.publish({ did }); + * // `isPublished` is true if the DID was successfully published. + * ``` + */ + public static async publish({ did, gatewayUri = DEFAULT_GATEWAY_URI }: { + did: Did; + gatewayUri?: string; + }): Promise { + // Create the ION document. + const ionDocument = await DidIonUtils.createIonDocument({ + services : did.didDocument.service ?? [], + verificationMethods : did.didDocument.verificationMethod ?? [] + }); + + // Construct the ION Create Operation request. + const createOperation = await DidIonUtils.constructCreateRequest({ + ionDocument, + recoveryKey : did.metadata.recoveryKey, + updateKey : did.metadata.updateKey + }); + + try { + // Construct the URL of the SideTree node's operations endpoint. + const operationsUrl = DidIonUtils.appendPathToUrl({ + baseUrl : gatewayUri, + path : `/operations` + }); + + // Submit the Create Operation to the operations endpoint. + const response = await fetch(operationsUrl, { + method : 'POST', + mode : 'cors', + headers : { 'Content-Type': 'application/json' }, + body : JSON.stringify(createOperation) + }); + + // Return true if the Create Operation was processed successfully; false otherwise. + return response.ok; + + } catch (error: any) { + throw new DidError(DidErrorCode.InternalError, `Failed to publish DID document for: ${did.uri}`); + } + } + + /** + * Resolves a `did:ion` identifier to its corresponding DID document. + * + * This method performs the resolution of a `did:ion` DID, retrieving its DID Document from the + * Sidetree-based DID overlay network. The process involves querying a Sidetree node to retrieve + * the DID Document that corresponds to the given DID identifier. + * + * @remarks + * - If a `gatewayUri` option is not specified, a default node is used to access the Sidetree + * network. + * - It decodes the DID identifier and retrieves the associated DID Document and metadata. + * - In case of resolution failure, appropriate error information is returned. + * + * @example + * ```ts + * const resolutionResult = await DidIon.resolve('did:ion:example'); + * ``` + * + * @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. + */ + public static async resolve(didUri: string, options: DidResolutionOptions = {}): Promise { + // Attempt to parse the DID URI. + const parsedDid = DidUri.parse(didUri); + + // If parsing failed, the DID is invalid. + if (!parsedDid) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { error: 'invalidDid' } + }; + } + + // If the DID method is not "ion", return an error. + if (parsedDid.method !== DidIon.methodName) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { error: 'methodNotSupported' } + }; + } + + // To execute the read method operation, use the given gateway URI or a default Sidetree node. + const gatewayUri = options?.gatewayUri ?? DEFAULT_GATEWAY_URI; + + try { + // Construct the URL to be used in the resolution request. + const resolutionUrl = DidIonUtils.appendPathToUrl({ + baseUrl : gatewayUri, + path : `/identifiers/${didUri}` + }); + + // Attempt to retrieve the DID document and metadata from the Sidetree node. + const response = await fetch(resolutionUrl); + + // If the DID document was not found, return an error. + if (!response.ok) { + throw new DidError(DidErrorCode.NotFound, `Unable to find DID document for: ${didUri}`); + } + + // If the DID document was retrieved successfully, return it. + const { didDocument, didDocumentMetadata } = await response.json() as DidResolutionResult; + return { + ...EMPTY_DID_RESOLUTION_RESULT, + ...didDocument && { didDocument }, + didDocumentMetadata: { + published: didDocumentMetadata?.method?.published, + ...didDocumentMetadata + } + }; + + } catch (error: any) { + // Rethrow any unexpected errors that are not a `DidError`. + if (!(error instanceof DidError)) throw new Error(error); + + // Return a DID Resolution Result with the appropriate error code. + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { + error: error.code, + ...error.message && { errorMessage: error.message } + } + }; + } + } + + /** + * Instantiates a `Did` object for the DID ION method using an array of public keys. + * + * This method is used to create a `Did` 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 `Did` 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 `Did` 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 { + /** + * Appends a specified path to a base URL, ensuring proper formatting of the resulting URL. + * + * This method is useful for constructing URLs for accessing various endpoints, such as Sidetree + * nodes in the ION network. It handles the nuances of URL path concatenation, including the + * addition or removal of leading/trailing slashes, to create a well-formed URL. + * + * @param params - The parameters for URL construction. + * @param params.baseUrl - The base URL to which the path will be appended. + * @param params.path - The path to append to the base URL. + * @returns The fully constructed URL string with the path appended to the base URL. + */ + public static appendPathToUrl({ baseUrl, path }: { + baseUrl: string; + path: string; + }): string { + const url = new URL(baseUrl); + url.pathname = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/'; + url.pathname += path.startsWith('/') ? path.substring(1) : path; + + return url.toString(); + } + + /** + * Computes the Long Form DID URI given an ION DID's recovery key, update key, services, and + * verification methods. + * + * @param params - The parameters for computing the Long Form DID URI. + * @param params.recoveryKey - The ION Recovery Key. + * @param params.updateKey - The ION Update Key. + * @param params.services - An array of services associated with the DID. + * @param params.verificationMethods - An array of verification methods associated with the DID. + * @returns A Promise resolving to the Long Form DID URI. + */ + public static async computeLongFormDidUri({ recoveryKey, updateKey, services, verificationMethods }: { + recoveryKey: Jwk; + services: DidService[]; + updateKey: Jwk; + verificationMethods: PortableDidVerificationMethod[]; + }): Promise { + // Create the ION document. + const ionDocument = await DidIonUtils.createIonDocument({ services, verificationMethods }); + + // Normalize JWK to onnly include specific members and in lexicographic order. + const normalizedRecoveryKey = DidIonUtils.normalizeJwk(recoveryKey); + const normalizedUpdateKey = DidIonUtils.normalizeJwk(updateKey); + + // Compute the Long Form DID URI. + const longFormDidUri = await IonDid.createLongFormDid({ + document : ionDocument, + recoveryKey : normalizedRecoveryKey as JwkEs256k, + updateKey : normalizedUpdateKey as JwkEs256k + }); + + return longFormDidUri; + } + + /** + * Constructs a Sidetree Create Operation request for a DID document within the ION network. + * + * This method prepares the necessary payload for submitting a Create Operation to a Sidetree + * node, encapsulating the details of the DID document, recovery key, and update key. + * + * @param params - Parameters required to construct the Create Operation request. + * @param params.ionDocument - The DID document model containing public keys and service endpoints. + * @param params.recoveryKey - The recovery public key in JWK format. + * @param params.updateKey - The update public key in JWK format. + * @returns A promise resolving to the ION Create Operation request model, ready for submission to a Sidetree node. + */ + public static async constructCreateRequest({ ionDocument, recoveryKey, updateKey }: { + ionDocument: IonDocumentModel, + recoveryKey: Jwk, + updateKey: Jwk + }): Promise { + // Create an ION DID create request operation. + const createRequest = await IonRequest.createCreateRequest({ + document : ionDocument, + recoveryKey : DidIonUtils.normalizeJwk(recoveryKey) as JwkEs256k, + updateKey : DidIonUtils.normalizeJwk(updateKey) as JwkEs256k + }) as DidIonCreateRequest; + + return createRequest; + } + + /** + * Assembles an ION document model from provided services and verification methods + * + * This model serves as the foundation for a DID document in the ION network, facilitating the + * creation and management of decentralized identities. It translates service endpoints and + * public keys into a format compatible with the Sidetree protocol, ensuring the resulting DID + * document adheres to the required specifications for ION DIDs. This method is essential for + * constructing the payload needed to register or update DIDs within the ION network. + * + * @param params - The parameters containing the services and verification methods to include in the ION document. + * @param params.services - A list of service endpoints to be included in the DID document, specifying ways to interact with the DID subject. + * @param params.verificationMethods - A list of verification methods to be included, detailing the cryptographic keys and their intended uses within the DID document. + * @returns A Promise resolving to an `IonDocumentModel`, ready for use in Sidetree operations like DID creation and updates. + */ + public static async createIonDocument({ services, verificationMethods }: { + services: DidService[]; + verificationMethods: PortableDidVerificationMethod[] + }): Promise { + /** + * STEP 1: Convert verification methods to ION SDK format. + */ + 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. + + // Convert public key JWK to ION format. + const publicKey: IonPublicKeyModel = { + id : methodId, + publicKeyJwk : DidIonUtils.normalizeJwk(vm.publicKeyJwk), + purposes : vm.purposes as IonPublicKeyPurpose[], + type : 'JsonWebKey2020' + }; + + ionPublicKeys.push(publicKey); + } + + /** + * STEP 2: Convert service entries, if any, to ION SDK format. + */ + const ionServices = services.map(service => ({ + ...service, + id: `${service.id.split('#').pop()}` // Remove fragment prefix, if any. + })); + + /** + * STEP 3: Format as ION document. + */ + const ionDocumentModel: IonDocumentModel = { + publicKeys : ionPublicKeys, + services : ionServices + }; + + return ionDocumentModel; + } + + /** + * Normalize the given JWK to include only specific members and in lexicographic order. + * + * @param jwk - The JWK to normalize. + * @returns The normalized JWK. + */ + private static normalizeJwk(jwk: Jwk): Jwk { + const keyType = jwk.kty; + let normalizedJwk: Jwk; + + if (keyType === 'EC') { + normalizedJwk = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }; + } else if (keyType === 'oct') { + normalizedJwk = { k: jwk.k, kty: jwk.kty }; + } else if (keyType === 'OKP') { + normalizedJwk = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }; + } else if (keyType === 'RSA') { + normalizedJwk = { e: jwk.e, kty: jwk.kty, n: jwk.n }; + } else { + throw new Error(`Unsupported key type: ${keyType}`); + } + + return normalizedJwk; + } +} \ No newline at end of file diff --git a/packages/dids/src/methods/did-jwk.ts b/packages/dids/src/methods/did-jwk.ts new file mode 100644 index 000000000..9294caf28 --- /dev/null +++ b/packages/dids/src/methods/did-jwk.ts @@ -0,0 +1,405 @@ +import type { + Jwk, + CryptoApi, + KeyIdentifier, + KmsExportKeyParams, + KmsImportKeyParams, + KeyImporterExporter, + InferKeyGeneratorAlgorithm, +} from '@web5/crypto'; + +import { Convert } from '@web5/common'; +import { LocalKeyManager } from '@web5/crypto'; + +import type { Did, DidCreateOptions, DidCreateVerificationMethod, DidMetadata, PortableDid } from './did-method.js'; +import type { DidDocument, DidResolutionOptions, DidResolutionResult, DidVerificationMethod } from '../types/did-core.js'; + +import { DidUri } from '../did-uri.js'; +import { DidMethod } from './did-method.js'; +import { DidError, DidErrorCode } from '../did-error.js'; +import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; + +/** + * Defines the set of options available when creating a new Decentralized Identifier (DID) with the + * 'did:jwk' method. + * + * Either the `algorithm` or `verificationMethods` option can be specified, but not both. + * - A new key will be generated using the algorithm identifier specified in either the `algorithm` + * property or the `verificationMethods` object's `algorithm` property. + * - If `verificationMethods` is given, it must contain exactly one entry since DID JWK only + * supports a single verification method. + * - If neither is given, the default is to generate a new Ed25519 key. + * + * @example + * ```ts + * // 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. + * const did = await DidJwk.create({ + * options: { algorithm = 'ES256K' } + * }); + * + * // Or, alternatively as a property of the verification method. + * const did = await DidJwk.create({ + * options: { + * verificationMethods: [{ algorithm = 'ES256K' }] + * } + * }); + * ``` + */ +export interface DidJwkCreateOptions extends DidCreateOptions { + /** + * Optionally specify the algorithm to be used for key generation. + */ + algorithm?: TKms extends CryptoApi + ? InferKeyGeneratorAlgorithm + : InferKeyGeneratorAlgorithm; + + /** + * Alternatively, specify the algorithm to be used for key generation of the single verification + * method in the DID Document. + */ + verificationMethods?: [DidCreateVerificationMethod]; +} + +/** + * The `DidJwk` class provides an implementation of the `did:jwk` DID method. + * + * Features: + * - DID Creation: Create new `did:jwk` DIDs. + * - DID Key Management: Instantiate a DID object from an existing verification method key set or + * or a 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:jwk` to its corresponding DID Document. + * - Signature Operations: Sign and verify messages using keys associated with a DID. + * + * @remarks + * The `did:jwk` DID method uses a single JSON Web Key (JWK) to generate a DID and does not rely + * on any external system such as a blockchain or centralized database. This characteristic makes + * it suitable for use cases where a assertions about a DID Subject can be self-verifiable by + * third parties. + * + * The DID URI is formed by Base64URL-encoding the JWK and prefixing with `did:jwk:`. The DID + * Document of a `did:jwk` DID contains a single verification method, which is the JWK used + * to generate the DID. The verification method is identified by the key ID `#0`. + * + * @see {@link https://github.com/quartzjer/did-jwk/blob/main/spec.md | DID JWK Specification} + * + * @example + * ```ts + * // DID Creation + * const did = await DidJwk.create(); + * + * // 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 }); + * + * // Key Management + * + * // Instantiate a DID object from an existing key in a KMS + * const did = await DidJwk.fromKeyManager({ + * didUri: 'did:jwk:eyJrIjoiT0tQIiwidCI6IkV1c2UyNTYifQ', + * keyManager + * }); + * + * // Instantiate a DID object from an existing verification method key + * const did = await DidJwk.fromKeys({ + * verificationMethods: [{ + * publicKeyJwk: { + * kty: 'OKP', + * crv: 'Ed25519', + * x: 'cHs7YMLQ3gCWjkacMURBsnEJBcEsvlsE5DfnsfTNDP4' + * }, + * privateKeyJwk: { + * kty: 'OKP', + * crv: 'Ed25519', + * x: 'cHs7YMLQ3gCWjkacMURBsnEJBcEsvlsE5DfnsfTNDP4', + * d: 'bdcGE4KzEaekOwoa-ee3gAm1a991WvNj_Eq3WKyqTnE' + * } + * }] + * }); + * + * // Convert a DID object to a portable format + * const portableDid = await DidJwk.toKeys({ did }); + * + * // Reconstruct a DID object from a portable format + * const did = await DidJwk.fromKeys(portableDid); + * ``` + */ +export class DidJwk extends DidMethod { + + /** + * Name of the DID method, as defined in the DID JWK specification. + */ + public static methodName = 'jwk'; + + /** + * Creates a new DID using the `did:jwk` method formed from a newly generated key. + * + * @remarks + * The DID URI is formed by Base64URL-encoding the JWK and prefixing with `did:jwk:`. + * + * Notes: + * - If no `options` are given, by default a new Ed25519 key will be generated. + * - The `algorithm` and `verificationMethods` options are mutually exclusive. If both are given, + * an error will be thrown. + * + * @example + * ```ts + * // DID Creation + * const did = await DidJwk.create(); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidJwk.create({ keyManager }); + * ``` + * + * @param params - The parameters for the create operation. + * @param params.keyManager - Optionally specify a Key Management System (KMS) used to generate + * keys and sign data. + * @param params.options - Optional parameters that can be specified when creating a new DID. + * @returns A Promise resolving to a {@link Did} object representing the new DID. + */ + public static async create({ + keyManager = new LocalKeyManager(), + options = {} + }: { + keyManager?: TKms; + options?: DidJwkCreateOptions; + } = {}): Promise { + if (options.algorithm && options.verificationMethods) { + throw new Error(`The 'algorithm' and 'verificationMethods' options are mutually exclusive`); + } + + // Default to Ed25519 key generation if an algorithm is not given. + const algorithm = options.algorithm ?? options.verificationMethods?.[0]?.algorithm ?? 'Ed25519'; + + // Generate a new key using the specified `algorithm`. + 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; + } + + /** + * Instantiates a `Did` object for the `did:jwk` method from a given {@link PortableDid}. + * + * This method allows for the creation of a `Did` 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 `Did` 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 `Did` 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 }); + + // 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 + }); + + return did; + } + + /** + * Given the W3C DID Document of a `did:jwk` DID, return the verification method that will be used + * for signing messages and credentials. If given, the `methodId` parameter is used to select the + * verification method. If not given, the first verification method in the DID Document is used. + * + * Note that for DID JWK, only one verification method can exist so specifying `methodId` could be + * considered redundant or unnecessary. The option is provided for consistency with other DID + * method implementations. + * + * @param params - The parameters for the `getSigningMethod` operation. + * @param params.didDocument - DID Document to get the verification method from. + * @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' }: { + didDocument: DidDocument; + methodId?: string; + }): Promise { + // Verify the DID method is supported. + const parsedDid = DidUri.parse(didDocument.id); + if (parsedDid && parsedDid.method !== this.methodName) { + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`); + } + + // Attempt to find the verification method in the DID Document. + const verificationMethod = didDocument.verificationMethod?.find(vm => vm.id.endsWith(methodId)); + + return verificationMethod; + } + + /** + * Resolves a `did:jwk` identifier to a DID Document. + * + * @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. + */ + public static async resolve(didUri: string, _options?: DidResolutionOptions): Promise { + // Attempt to parse the DID URI. + const parsedDid = DidUri.parse(didUri); + + // Attempt to decode the Base64URL-encoded JWK. + let publicKey: Jwk | undefined; + try { + publicKey = Convert.base64Url(parsedDid!.id).toObject() as Jwk; + } catch { /* Consume the error so that a DID resolution error can be returned later. */ } + + // If parsing or decoding failed, the DID is invalid. + if (!parsedDid || !publicKey) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { error: 'invalidDid' } + }; + } + + // If the DID method is not "jwk", return an error. + if (parsedDid.method !== DidJwk.methodName) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { error: 'methodNotSupported' } + }; + } + + const didDocument: DidDocument = { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1' + ], + id: parsedDid.uri + }; + + const keyUri = `${didDocument.id}#0`; + + // Set the Verification Method property. + didDocument.verificationMethod = [{ + id : keyUri, + type : 'JsonWebKey2020', + controller : didDocument.id, + publicKeyJwk : publicKey + }]; + + // Set the Verification Relationship properties. + didDocument.authentication = [keyUri]; + didDocument.assertionMethod = [keyUri]; + didDocument.capabilityInvocation = [keyUri]; + didDocument.capabilityDelegation = [keyUri]; + didDocument.keyAgreement = [keyUri]; + + // If the JWK contains a `use` property with the value "sig" then the `keyAgreement` property + // is not included in the DID Document. If the `use` value is "enc" then only the `keyAgreement` + // property is included in the DID Document. + switch (publicKey.use) { + case 'sig': { + delete didDocument.keyAgreement; + break; + } + + case 'enc': { + delete didDocument.authentication; + delete didDocument.assertionMethod; + delete didDocument.capabilityInvocation; + delete didDocument.capabilityDelegation; + break; + } + } + + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didDocument, + }; + } + + /** + * Instantiates a `Did` 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 `Did` 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 new file mode 100644 index 000000000..5a1476192 --- /dev/null +++ b/packages/dids/src/methods/did-key.ts @@ -0,0 +1,1334 @@ +import type { MulticodecCode, MulticodecDefinition, RequireOnly } from '@web5/common'; +import type { + Jwk, + CryptoApi, + KeyCompressor, + KeyIdentifier, + KmsExportKeyParams, + KmsImportKeyParams, + KeyImporterExporter, + AsymmetricKeyConverter, + InferKeyGeneratorAlgorithm, +} from '@web5/crypto'; + +import { Convert, Multicodec, universalTypeOf } from '@web5/common'; +import { + X25519, + Ed25519, + Secp256k1, + Secp256r1, + LocalKeyManager, +} from '@web5/crypto'; + +import type { Did, DidCreateOptions, DidCreateVerificationMethod, DidMetadata, PortableDid } from './did-method.js'; +import type { DidDocument, DidResolutionOptions, DidResolutionResult, DidVerificationMethod } from '../types/did-core.js'; + +import { DidUri } from '../did-uri.js'; +import { DidMethod } from './did-method.js'; +import { DidError, DidErrorCode } from '../did-error.js'; +import { getVerificationMethodTypes } from '../utils.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 +}; + +/** + * Defines the set of options available when creating a new Decentralized Identifier (DID) with the + * 'did:key' method. + * + * Either the `algorithm` or `verificationMethods` option can be specified, but not both. + * - A new key will be generated using the algorithm identifier specified in either the `algorithm` + * property or the `verificationMethods` object's `algorithm` property. + * - If `verificationMethods` is given, it must contain exactly one entry since DID Key only + * supports a single verification method. + * - If neither is given, the default is to generate a new Ed25519 key. + * + * @example + * ```ts + * // By default, when no options are given, a new Ed25519 key will be generated. + * const did = await DidKey.create(); + * + * // The algorithm to use for key generation can be specified as a top-level option. + * const did = await DidKey.create({ + * options: { algorithm = 'secp256k1' } + * }); + * + * // Or, alternatively as a property of the verification method. + * const did = await DidKey.create({ + * options: { + * verificationMethods: [{ algorithm = 'secp256k1' }] + * } + * }); + * ``` + */ +export interface DidKeyCreateOptions extends DidCreateOptions { + /** + * Optionally specify the algorithm to be used for key generation. + */ + algorithm?: TKms extends CryptoApi + ? InferKeyGeneratorAlgorithm + : InferKeyGeneratorAlgorithm; + + /** + * Optionally specify an array of JSON-LD context links for the @context property of the DID + * document. + * + * The @context property provides a JSON-LD processor with the information necessary to interpret + * the DID document JSON. The default context URL is 'https://www.w3.org/ns/did/v1'. + */ + defaultContext?: string; + + /** + * Optionally enable encryption key derivation during DID creation. + * + * By default, this option is set to `false`, which means encryption key derivation is not + * performed unless explicitly enabled. + * + * When set to `true`, an `X25519` key will be derived from the `Ed25519` public key used to + * create the DID. This feature enables the same DID to be used for encrypted communication, in + * addition to signature verification. + * + * Notes: + * - This option is ONLY applicable when the `algorithm` of the DID's public key is `Ed25519`. + * - Enabling this introduces specific cryptographic considerations that should be understood + * before using the same key pair for digital signatures and encrypted communication. See the following for more information: + */ + enableEncryptionKeyDerivation?: boolean; + + /** + * Optionally enable experimental public key types during DID creation. + * By default, this option is set to `false`, which means experimental public key types are not + * supported. + * + * Note: This implementation of the DID Key method does not support any experimental public key + * types. + */ + enableExperimentalPublicKeyTypes?: boolean; + + /** + * Optionally specify the format of the public key to be used for DID creation. + */ + publicKeyFormat?: keyof typeof DidKeyVerificationMethodType; + + /** + * Alternatively, specify the algorithm to be used for key generation of the single verification + * method in the DID Document. + */ + verificationMethods?: [DidCreateVerificationMethod]; +} + +/** + * Enumerates the types of keys that can be used in a DID Key document. + * + * The DID Key method supports various cryptographic key types. These key types are essential for + * the creation and management of DIDs and their associated cryptographic operations like signing + * and encryption. + */ +export enum DidKeyRegisteredKeyType { + /** + * Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature + * Algorithm) and Curve25519. + */ + Ed25519 = 'Ed25519', + + /** + * secp256k1: A cryptographic curve used for digital signatures in a range of decentralized + * systems. + */ + secp256k1 = 'secp256k1', + + /** + * secp256r1: Also known as P-256 or prime256v1, this curve is used for cryptographic operations + * and is widely supported in various cryptographic libraries and standards. + */ + secp256r1 = 'secp256r1', + + /** + * X25519: A Diffie-Hellman key exchange algorithm using Curve25519. + */ + X25519 = 'X25519' +} + +/** + * Enumerates the verification method types supported by the DID Key method. + * + * This enum defines the URIs associated with common verification methods used in DID Documents. + * These URIs represent cryptographic suites or key types standardized for use across decentralized + * identifiers (DIDs). + */ +export const DidKeyVerificationMethodType = { + /** Represents an Ed25519 public key used for digital signatures. */ + Ed25519VerificationKey2020: 'https://w3id.org/security/suites/ed25519-2020/v1', + + /** Represents a JSON Web Key (JWK) used for digital signatures and key agreement protocols. */ + JsonWebKey2020: 'https://w3id.org/security/suites/jws-2020/v1', + + /** Represents an X25519 public key used for key agreement protocols. */ + X25519KeyAgreementKey2020: 'https://w3id.org/security/suites/x25519-2020/v1', +} as const; + +/** + * Private helper that maps algorithm identifiers to their corresponding DID Key + * {@link DidKeyRegisteredKeyType | registered key type}. + */ +const AlgorithmToKeyTypeMap = { + Ed25519 : DidKeyRegisteredKeyType.Ed25519, + ES256K : DidKeyRegisteredKeyType.secp256k1, + ES256 : DidKeyRegisteredKeyType.secp256r1, + 'P-256' : DidKeyRegisteredKeyType.secp256r1, + secp256k1 : DidKeyRegisteredKeyType.secp256k1, + secp256r1 : DidKeyRegisteredKeyType.secp256r1, + X25519 : DidKeyRegisteredKeyType.X25519 +} as const; + +/** + * The `DidKey` class provides an implementation of the 'did:key' DID method. + * + * Features: + * - DID Creation: Create new `did:key` DIDs. + * - DID Key Management: Instantiate a DID object from an existing verification method key set or + * or a 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:key` to its corresponding DID Document. + * - Signature Operations: Sign and verify messages using keys associated with a DID. + * + * @remarks + * The `did:key` DID method uses a single public key to generate a DID and does not rely + * on any external system such as a blockchain or centralized database. This characteristic makes + * it suitable for use cases where a assertions about a DID Subject can be self-verifiable by + * third parties. + * + * The method-specific identifier is formed by + * {@link https://datatracker.ietf.org/doc/html/draft-multiformats-multibase#name-base-58-bitcoin-encoding | Multibase base58-btc} + * encoding the concatenation of the + * {@link https://github.com/multiformats/multicodec/blob/master/README.md | Multicodec} identifier + * for the public key type and the raw public key bytes. To form the DID URI, the method-specific + * identifier is prefixed with the string 'did:key:'. + * + * This method can optionally derive an encryption key from the public key used to create the DID + * if and only if the public key algorithm is `Ed25519`. This feature enables the same DID to be + * used for encrypted communication, in addition to signature verification. To enable this + * feature when calling {@link DidKey.create | `DidKey.create()`}, first specify an `algorithm` of + * `Ed25519` or provide a `keySet` referencing an `Ed25519` key and then set the + * `enableEncryptionKeyDerivation` option to `true`. + * + * Note: + * - The authors of the DID Key specification have indicated that use of this method for long-lived + * use cases is only recommended when accompanied with high confidence that private keys are + * securely protected by software or hardware isolation. + * + * @see {@link https://w3c-ccg.github.io/did-method-key/ | DID Key Specification} + * +* @example + * ```ts + * // DID Creation + * const did = await DidKey.create(); + * + * // 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 }); + * + * // Key Management + * + * // Instantiate a DID object from an existing key in a KMS + * const did = await DidKey.fromKeyManager({ + * didUri: 'did:key:z6MkpUzNmYVTGpqhStxK8yRKXWCRNm1bGYz8geAg2zmjYHKX', + * keyManager + * }); + * + * // Instantiate a DID object from an existing verification method key + * const did = await DidKey.fromKeys({ + * verificationMethods: [{ + * publicKeyJwk: { + * kty: 'OKP', + * crv: 'Ed25519', + * x: 'cHs7YMLQ3gCWjkacMURBsnEJBcEsvlsE5DfnsfTNDP4' + * }, + * privateKeyJwk: { + * kty: 'OKP', + * crv: 'Ed25519', + * x: 'cHs7YMLQ3gCWjkacMURBsnEJBcEsvlsE5DfnsfTNDP4', + * d: 'bdcGE4KzEaekOwoa-ee3gAm1a991WvNj_Eq3WKyqTnE' + * } + * }] + * }); + * + * // Convert a DID object to a portable format + * const portableDid = await DidKey.toKeys({ did }); + * + * // Reconstruct a DID object from a portable format + * const did = await DidKey.fromKeys(portableDid); + * ``` + */ +export class DidKey extends DidMethod { + + /** + * Name of the DID method, as defined in the DID Key specification. + */ + public static methodName = 'key'; + + /** + * Creates a new DID using the `did:key` method formed from a newly generated key. + * + * @remarks + * The DID URI is formed by + * {@link https://datatracker.ietf.org/doc/html/draft-multiformats-multibase#name-base-58-bitcoin-encoding | Multibase base58-btc} + * encoding the + * {@link https://github.com/multiformats/multicodec/blob/master/README.md | Multicodec}-encoded + * public key and prefixing with `did:key:`. + * + * This method can optionally derive an encryption key from the public key used to create the DID + * if and only if the public key algorithm is `Ed25519`. This feature enables the same DID to be + * used for encrypted communication, in addition to signature verification. To enable this + * feature, specify an `algorithm` of `Ed25519` as either a top-level option or in a + * `verificationMethod` and set the `enableEncryptionKeyDerivation` option to `true`. + * + * Notes: + * - If no `options` are given, by default a new Ed25519 key will be generated. + * - The `algorithm` and `verificationMethods` options are mutually exclusive. If both are given, + * an error will be thrown. + * + * @example + * ```ts + * // DID Creation + * const did = await DidKey.create(); + * + * // DID Creation with a KMS + * const keyManager = new LocalKeyManager(); + * const did = await DidKey.create({ keyManager }); + * ``` + * + * @param params - The parameters for the create operation. + * @param params.keyManager - Key Management System (KMS) used to generate keys and sign data. + * @param params.options - Optional parameters that can be specified when creating a new DID. + * @returns A Promise resolving to a {@link Did} object representing the new DID. + */ + public static async create({ + keyManager = new LocalKeyManager(), + options = {} + }: { + keyManager?: TKms; + options?: DidKeyCreateOptions; + } = {}): Promise { + if (options.algorithm && options.verificationMethods) { + throw new Error(`The 'algorithm' and 'verificationMethods' options are mutually exclusive`); + } + + // Default to Ed25519 key generation if an algorithm is not given. + const algorithm = options.algorithm ?? options.verificationMethods?.[0]?.algorithm ?? 'Ed25519'; + + // Generate a new key using the specified `algorithm`. + 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 `Did` object for the `did:jwk` method from a given {@link PortableDid}. + * + * This method allows for the creation of a `Did` 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 `Did` 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 `Did` 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`); + } + + // Store the private key in the key manager. + await keyManager.importKey({ key: verificationMethods[0].privateKeyJwk }); + + // 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 + }); + + return did; + } + + /** + * Given the W3C DID Document of a `did:key` DID, return the verification method that will be used + * for signing messages and credentials. With DID Key, the first verification method in the + * authentication property in the DID Document is used. + * + * Note that for DID Key, only one verification method intended for signing can exist so + * specifying `methodId` could be considered redundant or unnecessary. The option is provided for + * consistency with other DID method implementations. + * + * @param params - The parameters for the `getSigningMethod` operation. + * @param params.didDocument - DID Document to get the verification method from. + * @param params.methodId - ID of the verification method to use for signing. + * @returns Verification method to use for signing. + */ + public static async getSigningMethod({ didDocument }: { + didDocument: DidDocument; + methodId?: string; + }): Promise { + // Verify the DID method is supported. + const parsedDid = DidUri.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. + const verificationMethod = didDocument.verificationMethod?.find(vm => vm.id === methodId); + + return verificationMethod; + } + + /** + * Resolves a `did:key` identifier to a DID Document. + * + * @param didUri - The DID to be resolved. + * @param options - Optional parameters for resolving the DID. + * @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution. + */ + public static async resolve(didUri: string, options?: DidResolutionOptions): Promise { + try { + // Attempt to expand the DID URI string to a DID document. + const didDocument = await DidKey.createDocument({ didUri, options }); + + // If the DID document was created successfully, return it. + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didDocument, + }; + + } catch (error: any) { + // Rethrow any unexpected errors that are not a `DidError`. + if (!(error instanceof DidError)) throw new Error(error); + + // Return a DID Resolution Result with the appropriate error code. + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { + error: error.code, + ...error.message && { errorMessage: error.message } + } + }; + } + } + + /** + * Expands a did:key identifier to a DID Document. + * + * Reference: https://w3c-ccg.github.io/did-method-key/#document-creation-algorithm + * + * @param options + * @returns - A DID dodcument. + */ + private static async createDocument({ didUri, options = {}}: { + didUri: string; + options?: Exclude, 'algorithm' | 'verificationMethods'> | DidResolutionOptions; + }): Promise { + const { + defaultContext = 'https://www.w3.org/ns/did/v1', + enableEncryptionKeyDerivation = false, + enableExperimentalPublicKeyTypes = false, + publicKeyFormat = 'JsonWebKey2020' + } = options; + + /** + * 1. Initialize document to an empty object. + */ + const didDocument: DidDocument = { id: '' }; + + /** + * 2. Using a colon (:) as the delimiter, split the identifier into its + * components: a scheme, a method, a version, and a multibaseValue. + * If there are only three components set the version to the string + * value 1 and use the last value as the multibaseValue. + */ + const parsedDid = DidUri.parse(didUri); + if (!parsedDid) { + throw new DidError(DidErrorCode.InvalidDid, `Invalid DID URI: ${didUri}`); + } + const multibaseValue = parsedDid.id; + + /** + * 3. Check the validity of the input identifier. + * The scheme MUST be the value did. The method MUST be the value key. + * The version MUST be convertible to a positive integer value. The + * multibaseValue MUST be a string and begin with the letter z. If any + * of these requirements fail, an invalidDid error MUST be raised. + */ + if (parsedDid.method !== DidKey.methodName) { + throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`); + } + if (!DidKey.validateIdentifier(parsedDid)) { + throw new DidError(DidErrorCode.InvalidDid, `Invalid DID URI: ${didUri}`); + } + + /** + * 4. Initialize the signatureVerificationMethod to the result of passing + * identifier, multibaseValue, and options to a + * {@link https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm | Signature Method Creation Algorithm}. + */ + const signatureVerificationMethod = await DidKey.createSignatureMethod({ + didUri, + multibaseValue, + options: { enableExperimentalPublicKeyTypes, publicKeyFormat } + }); + + /** + * 5. Set document.id to identifier. If document.id is not a valid DID, + * an invalidDid error MUST be raised. + * + * Note: Identifier was already confirmed to be valid in Step 3, so + * skipping the redundant validation. + */ + didDocument.id = parsedDid.uri; + + /** + * 6. Initialize the verificationMethod property in document to an array + * where the first value is the signatureVerificationMethod. + */ + didDocument.verificationMethod = [signatureVerificationMethod]; + + /** + * 7. Initialize the authentication, assertionMethod, capabilityInvocation, + * and the capabilityDelegation properties in document to an array where + * the first item is the value of the id property in + * signatureVerificationMethod. + */ + didDocument.authentication = [signatureVerificationMethod.id]; + didDocument.assertionMethod = [signatureVerificationMethod.id]; + didDocument.capabilityInvocation = [signatureVerificationMethod.id]; + didDocument.capabilityDelegation = [signatureVerificationMethod.id]; + + /** + * 8. If options.enableEncryptionKeyDerivation is set to true: + * Add the encryptionVerificationMethod value to the verificationMethod + * array. Initialize the keyAgreement property in document to an array + * where the first item is the value of the id property in + * encryptionVerificationMethod. + */ + if (enableEncryptionKeyDerivation === true) { + /** + * Although not covered by the did:key method specification, a sensible + * default will be taken to use the 'X25519KeyAgreementKey2020' + * verification method type if the given publicKeyFormat is + * 'Ed25519VerificationKey2020' and 'JsonWebKey2020' otherwise. + */ + const encryptionPublicKeyFormat = + (publicKeyFormat === 'Ed25519VerificationKey2020') + ? 'X25519KeyAgreementKey2020' + : 'JsonWebKey2020'; + + /** + * 8.1 Initialize the encryptionVerificationMethod to the result of + * passing identifier, multibaseValue, and options to an + * {@link https://w3c-ccg.github.io/did-method-key/#encryption-method-creation-algorithm | Encryption Method Creation Algorithm}. + */ + const encryptionVerificationMethod = await this.createEncryptionMethod({ + didUri, + multibaseValue, + options: { enableExperimentalPublicKeyTypes, publicKeyFormat: encryptionPublicKeyFormat } + }); + + /** + * 8.2 Add the encryptionVerificationMethod value to the + * verificationMethod array. + */ + didDocument.verificationMethod.push(encryptionVerificationMethod); + + /** + * 8.3. Initialize the keyAgreement property in document to an array + * where the first item is the value of the id property in + * encryptionVerificationMethod. + */ + didDocument.keyAgreement = [encryptionVerificationMethod.id]; + } + + /** + * 9. Initialize the @context property in document to the result of passing document and options to the Context + * Creation algorithm. + */ + // Set contextArray to an array that is initialized to options.defaultContext. + const contextArray = [ defaultContext ]; + + // For every object in every verification relationship listed in document, + // add a string value to the contextArray based on the object type value, + // if it doesn't already exist, according to the following table: + // {@link https://w3c-ccg.github.io/did-method-key/#context-creation-algorithm | Context Type URL} + const verificationMethodTypes = getVerificationMethodTypes({ didDocument }); + verificationMethodTypes.forEach((typeName: string) => { + const typeUrl = DidKeyVerificationMethodType[typeName as keyof typeof DidKeyVerificationMethodType]; + contextArray.push(typeUrl); + }); + didDocument['@context'] = contextArray; + + /** + * 10. Return document. + */ + return didDocument; + } + + /** + * Decoding a multibase-encoded multicodec value into a verification method + * that is suitable for verifying that encrypted information will be + * received by the intended recipient. + */ + private static async createEncryptionMethod({ didUri, multibaseValue, options }: { + didUri: string; + multibaseValue: string; + options: Required, 'enableExperimentalPublicKeyTypes' | 'publicKeyFormat'>>; + }): Promise { + const { enableExperimentalPublicKeyTypes, publicKeyFormat } = options; + + /** + * 1. Initialize verificationMethod to an empty object. + */ + const verificationMethod: DidVerificationMethod = { id: '', type: '', controller: '' }; + + /** + * 2. Set multicodecValue and raw publicKeyBytes to the result of passing multibaseValue and + * options to a Derive Encryption Key algorithm. + */ + const { + keyBytes: publicKeyBytes, + multicodecCode: multicodecValue, + } = await DidKey.deriveEncryptionKey({ multibaseValue }); + + /** + * 3. Ensure the proper key length of raw publicKeyBytes based on the multicodecValue table + * provided below: + * + * Multicodec hexadecimal value: 0xec + * + * If the byte length of raw publicKeyBytes does not match the expected public key length for + * the associated multicodecValue, an invalidPublicKeyLength error MUST be raised. + */ + const actualLength = publicKeyBytes.byteLength; + const expectedLength = DidKeyUtils.MULTICODEC_PUBLIC_KEY_LENGTH[multicodecValue]; + if (actualLength !== expectedLength) { + throw new DidError(DidErrorCode.InvalidPublicKeyLength, `Expected ${actualLength} bytes. Actual: ${expectedLength}`); + } + + /** + * 4. Create the multibaseValue by concatenating the letter 'z' and the + * base58-btc encoding of the concatenation of the multicodecValue and + * the raw publicKeyBytes. + */ + const kemMultibaseValue = DidKeyUtils.keyBytesToMultibaseId({ + keyBytes : publicKeyBytes, + multicodecCode : multicodecValue + }); + + /** + * 5. Set the verificationMethod.id value by concatenating identifier, + * a hash character (#), and the multibaseValue. If verificationMethod.id + * is not a valid DID URL, an invalidDidUrl error MUST be raised. + */ + verificationMethod.id = `${didUri}#${kemMultibaseValue}`; + try { + new URL(verificationMethod.id); + } catch (error: any) { + throw new DidError(DidErrorCode.InvalidDidUrl, 'Verification Method ID is not a valid DID URL.'); + } + + /** + * 6. Set the publicKeyFormat value to the options.publicKeyFormat value. + * 7. If publicKeyFormat is not known to the implementation, an + * unsupportedPublicKeyType error MUST be raised. + */ + if (!(publicKeyFormat in DidKeyVerificationMethodType)) { + throw new DidError(DidErrorCode.UnsupportedPublicKeyType, `Unsupported format: ${publicKeyFormat}`); + } + + /** + * 8. If options.enableExperimentalPublicKeyTypes is set to false and publicKeyFormat is not + * Multikey, JsonWebKey2020, or X25519KeyAgreementKey2020, an invalidPublicKeyType error MUST be + * raised. + */ + const StandardPublicKeyTypes = ['Multikey', 'JsonWebKey2020', 'X25519KeyAgreementKey2020']; + if (enableExperimentalPublicKeyTypes === false + && !(StandardPublicKeyTypes.includes(publicKeyFormat))) { + throw new DidError(DidErrorCode.InvalidPublicKeyType, `Specified '${publicKeyFormat}' without setting enableExperimentalPublicKeyTypes to true.`); + } + + /** + * 9. Set verificationMethod.type to the publicKeyFormat value. + */ + verificationMethod.type = publicKeyFormat; + + /** + * 10. Set verificationMethod.controller to the identifier value. + */ + verificationMethod.controller = didUri; + + /** + * 11. If publicKeyFormat is Multikey or X25519KeyAgreementKey2020, set the verificationMethod.publicKeyMultibase + * value to multibaseValue. + * + * Note: This implementation does not currently support the Multikey + * format. + */ + if (publicKeyFormat === 'X25519KeyAgreementKey2020') { + verificationMethod.publicKeyMultibase = kemMultibaseValue; + } + + /** + * 12. If publicKeyFormat is JsonWebKey2020, set the verificationMethod.publicKeyJwk value to + * the result of passing multicodecValue and rawPublicKeyBytes to a JWK encoding algorithm. + */ + if (publicKeyFormat === 'JsonWebKey2020') { + const { crv } = await DidKeyUtils.multicodecToJwk({ code: multicodecValue }); + verificationMethod.publicKeyJwk = await DidKeyUtils.keyConverter(crv!).bytesToPublicKey({ publicKeyBytes }); + } + + /** + * 13. Return verificationMethod. + */ + return verificationMethod; + } + + /** + * Decodes a multibase-encoded multicodec value into a verification method + * that is suitable for verifying digital signatures. + * @param options - Signature method creation algorithm inputs. + * @returns - A verification method. + */ + private static async createSignatureMethod({ didUri, multibaseValue, options }: { + didUri: string; + multibaseValue: string; + options: Required, 'enableExperimentalPublicKeyTypes' | 'publicKeyFormat'>> + }): Promise { + const { enableExperimentalPublicKeyTypes, publicKeyFormat } = options; + + /** + * 1. Initialize verificationMethod to an empty object. + */ + const verificationMethod: DidVerificationMethod = { id: '', type: '', controller: '' }; + + /** + * 2. Set multicodecValue and publicKeyBytes to the result of passing + * multibaseValue and options to a Decode Public Key algorithm. + */ + const { + keyBytes: publicKeyBytes, + multicodecCode: multicodecValue, + multicodecName + } = DidKeyUtils.multibaseIdToKeyBytes({ multibaseKeyId: multibaseValue }); + + /** + * 3. Ensure the proper key length of publicKeyBytes based on the multicodecValue + * {@link https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm | table provided}. + * If the byte length of rawPublicKeyBytes does not match the expected public key length for the + * associated multicodecValue, an invalidPublicKeyLength error MUST be raised. + */ + const actualLength = publicKeyBytes.byteLength; + const expectedLength = DidKeyUtils.MULTICODEC_PUBLIC_KEY_LENGTH[multicodecValue]; + if (actualLength !== expectedLength) { + throw new DidError(DidErrorCode.InvalidPublicKeyLength, `Expected ${actualLength} bytes. Actual: ${expectedLength}`); + } + + /** + * 4. Ensure the publicKeyBytes are a proper encoding of the public key type as specified by + * the multicodecValue. If an invalid public key value is detected, an invalidPublicKey error + * MUST be raised. + */ + let isValid = false; + switch (multicodecName) { + case 'secp256k1-pub': + isValid = await Secp256k1.validatePublicKey({ publicKeyBytes }); + break; + case 'ed25519-pub': + isValid = await Ed25519.validatePublicKey({ publicKeyBytes }); + break; + case 'x25519-pub': + // TODO: Validate key once/if X25519.validatePublicKey() is implemented. + // isValid = X25519.validatePublicKey({ key: rawPublicKeyBytes}) + isValid = true; + break; + } + if (!isValid) { + throw new DidError(DidErrorCode.InvalidPublicKey, 'Invalid public key detected.'); + } + + /** + * 5. Set the verificationMethod.id value by concatenating identifier, a hash character (#), and + * the multibaseValue. If verificationMethod.id is not a valid DID URL, an invalidDidUrl error + * MUST be raised. + */ + verificationMethod.id = `${didUri}#${multibaseValue}`; + try { + new URL(verificationMethod.id); + } catch (error: any) { + throw new DidError(DidErrorCode.InvalidDidUrl, 'Verification Method ID is not a valid DID URL.'); + } + + /** + * 6. Set the publicKeyFormat value to the options.publicKeyFormat value. + * 7. If publicKeyFormat is not known to the implementation, an unsupportedPublicKeyType error + * MUST be raised. + */ + if (!(publicKeyFormat in DidKeyVerificationMethodType)) { + throw new DidError(DidErrorCode.UnsupportedPublicKeyType, `Unsupported format: ${publicKeyFormat}`); + } + + /** + * 8. If options.enableExperimentalPublicKeyTypes is set to false and publicKeyFormat is not + * Multikey, JsonWebKey2020, or Ed25519VerificationKey2020, an invalidPublicKeyType error MUST + * be raised. + */ + const StandardPublicKeyTypes = ['Multikey', 'JsonWebKey2020', 'Ed25519VerificationKey2020']; + if (enableExperimentalPublicKeyTypes === false + && !(StandardPublicKeyTypes.includes(publicKeyFormat))) { + throw new DidError(DidErrorCode.InvalidPublicKeyType, `Specified '${publicKeyFormat}' without setting enableExperimentalPublicKeyTypes to true.`); + } + + /** + * 9. Set verificationMethod.type to the publicKeyFormat value. + */ + verificationMethod.type = publicKeyFormat; + + /** + * 10. Set verificationMethod.controller to the identifier value. + */ + verificationMethod.controller = didUri; + + /** + * 11. If publicKeyFormat is Multikey or Ed25519VerificationKey2020, + * set the verificationMethod.publicKeyMultibase value to multibaseValue. + * + * Note: This implementation does not currently support the Multikey + * format. + */ + if (publicKeyFormat === 'Ed25519VerificationKey2020') { + verificationMethod.publicKeyMultibase = multibaseValue; + } + + /** + * 12. If publicKeyFormat is JsonWebKey2020, set the verificationMethod.publicKeyJwk value to + * the result of passing multicodecValue and rawPublicKeyBytes to a JWK encoding algorithm. + */ + if (publicKeyFormat === 'JsonWebKey2020') { + const { crv } = await DidKeyUtils.multicodecToJwk({ code: multicodecValue }); + verificationMethod.publicKeyJwk = await DidKeyUtils.keyConverter(crv!).bytesToPublicKey({ publicKeyBytes}); + } + + /** + * 13. Return verificationMethod. + */ + return verificationMethod; + } + + + /** + * Transform a multibase-encoded multicodec value to public encryption key + * components that are suitable for encrypting messages to a receiver. A + * mathematical proof elaborating on the safety of performing this operation + * is available in: + * {@link https://eprint.iacr.org/2021/509.pdf | On using the same key pair for Ed25519 and an X25519 based KEM} + */ + private static async deriveEncryptionKey({ multibaseValue }: { + multibaseValue: string + }): Promise> { + /** + * 1. Set publicEncryptionKey to an empty object. + */ + let publicEncryptionKey: RequireOnly = { + keyBytes : new Uint8Array(), + multicodecCode : 0 + }; + + /** + * 2. Decode multibaseValue using the base58-btc multibase alphabet and + * set multicodecValue to the multicodec header for the decoded value. + * Implementers are cautioned to ensure that the multicodecValue is set + * to the result after performing varint decoding. + * + * 3. Set the rawPublicKeyBytes to the bytes remaining after the multicodec + * header. + */ + const { + keyBytes: publicKeyBytes, + multicodecCode: multicodecValue + } = DidKeyUtils.multibaseIdToKeyBytes({ multibaseKeyId: multibaseValue }); + + /** + * 4. If the multicodecValue is 0xed (Ed25519 public key), derive a public X25519 encryption key + * by using the raw publicKeyBytes and the algorithm defined in + * {@link https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm | Group OSCORE - Secure Group Communication for CoAP} + * for Curve25519 in Section 2.4.2: ECDH with Montgomery Coordinates and set + * generatedPublicEncryptionKeyBytes to the result. + */ + if (multicodecValue === 0xed) { + const ed25519PublicKey = await DidKeyUtils.keyConverter('Ed25519').bytesToPublicKey({ + publicKeyBytes + }); + const generatedPublicEncryptionKey = await Ed25519.convertPublicKeyToX25519({ + publicKey: ed25519PublicKey + }); + const generatedPublicEncryptionKeyBytes = await DidKeyUtils.keyConverter('Ed25519').publicKeyToBytes({ + publicKey: generatedPublicEncryptionKey + }); + + /** + * 5. Set multicodecValue to 0xec. + * 6. Set raw public keyBytes to generatedPublicEncryptionKeyBytes. + */ + publicEncryptionKey = { + keyBytes : generatedPublicEncryptionKeyBytes, + multicodecCode : 0xec + }; + } + + /** + * 7. Return publicEncryptionKey. + */ + 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. + * + * @param parsedDid - An object representing the parsed components of a DID URI, including the + * scheme, method, and method-specific identifier. + * @returns `true` if the DID URI meets the `did:key` method's structural requirements, `false` otherwise. + * + */ + private static validateIdentifier(parsedDid: DidUri): boolean { + const { method, id: multibaseValue } = parsedDid; + const [ scheme ] = parsedDid.uri.split(':', 1); + + /** + * Note: The W3C DID specification makes no mention of a version value being part of the DID + * syntax. Additionally, there does not appear to be any real-world usage of the version + * number. Consequently, this implementation will ignore the version related guidance in + * the did:key specification. + */ + const version = '1'; + + return ( + scheme === 'did' && + method === 'key' && + Number(version) > 0 && + universalTypeOf(multibaseValue) === 'String' && + multibaseValue.startsWith('z') + ); + } +} + +/** + * The `DidKeyUtils` class provides utility functions to support operations in the DID Key method. + */ +export class DidKeyUtils { + /** + * A mapping from JSON Web Key (JWK) property descriptors to multicodec names. + * + * This mapping is used to convert keys in JWK (JSON Web Key) format to multicodec format. + * + * @remarks + * The keys of this object are strings that describe the JOSE key type and usage, + * such as 'Ed25519:public', 'Ed25519:private', etc. The values are the corresponding multicodec + * names used to represent these key types. + * + * @example + * ```ts + * const multicodecName = JWK_TO_MULTICODEC['Ed25519:public']; + * // Returns 'ed25519-pub', the multicodec name for an Ed25519 public key + * ``` + */ + private static JWK_TO_MULTICODEC: { [key: string]: string } = { + 'Ed25519:public' : 'ed25519-pub', + 'Ed25519:private' : 'ed25519-priv', + 'secp256k1:public' : 'secp256k1-pub', + 'secp256k1:private' : 'secp256k1-priv', + 'X25519:public' : 'x25519-pub', + 'X25519:private' : 'x25519-priv', + }; + + /** + * Defines the expected byte lengths for public keys associated with different cryptographic + * algorithms, indexed by their multicodec code values. + */ + public static MULTICODEC_PUBLIC_KEY_LENGTH: Record = { + // secp256k1-pub - Secp256k1 public key (compressed) - 33 bytes + 0xe7: 33, + + // x25519-pub - Curve25519 public key - 32 bytes + 0xec: 32, + + // ed25519-pub - Ed25519 public key - 32 bytes + 0xed: 32 + }; + + /** + * A mapping from multicodec names to their corresponding JOSE (JSON Object Signing and Encryption) + * representations. This mapping facilitates the conversion of multicodec key formats to + * JWK (JSON Web Key) formats. + * + * @remarks + * The keys of this object are multicodec names, such as 'ed25519-pub', 'ed25519-priv', etc. + * The values are objects representing the corresponding JWK properties for that key type. + * + * @example + * ```ts + * const joseKey = MULTICODEC_TO_JWK['ed25519-pub']; + * // Returns a partial JWK for an Ed25519 public key + * ``` + */ + private static MULTICODEC_TO_JWK: { [key: string]: Jwk } = { + 'ed25519-pub' : { crv: 'Ed25519', kty: 'OKP', x: '' }, + 'ed25519-priv' : { crv: 'Ed25519', kty: 'OKP', x: '', d: '' }, + 'secp256k1-pub' : { crv: 'secp256k1', kty: 'EC', x: '', y: ''}, + 'secp256k1-priv' : { crv: 'secp256k1', kty: 'EC', x: '', y: '', d: '' }, + 'x25519-pub' : { crv: 'X25519', kty: 'OKP', x: '' }, + 'x25519-priv' : { crv: 'X25519', kty: 'OKP', x: '', d: '' }, + }; + + /** + * Converts a JWK (JSON Web Key) to a Multicodec code and name. + * + * @example + * ```ts + * const jwk: Jwk = { crv: 'Ed25519', kty: 'OKP', x: '...' }; + * const { code, name } = await Jose.jwkToMulticodec({ jwk }); + * ``` + * + * @param params - The parameters for the conversion. + * @param params.jwk - The JSON Web Key to be converted. + * @returns A promise that resolves to a Multicodec definition. + */ + public static async jwkToMulticodec({ jwk }: { + jwk: Jwk + }): Promise> { + const params: string[] = []; + + if (jwk.crv) { + params.push(jwk.crv); + if (jwk.d) { + params.push('private'); + } else { + params.push('public'); + } + } + + const lookupKey = params.join(':'); + const name = DidKeyUtils.JWK_TO_MULTICODEC[lookupKey]; + + if (name === undefined) { + throw new Error(`Unsupported JWK to Multicodec conversion: '${lookupKey}'`); + } + + const code = Multicodec.getCodeFromName({ name }); + + 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. + * + * @param curve - The cryptographic curve to use for the key conversion. + * @returns A public key compressor for the specified curve. + */ + public static keyCompressor( + curve: string + ): KeyCompressor['compressPublicKey'] { + // ): ({ publicKeyBytes }: { publicKeyBytes: Uint8Array }) => Promise { + const compressors = { + 'P-256' : Secp256r1.compressPublicKey, + 'secp256k1' : Secp256k1.compressPublicKey + } as Record; + + const compressor = compressors[curve]; + + if (!compressor) throw new DidError(DidErrorCode.InvalidPublicKeyType, `Unsupported curve: ${curve}`); + + return compressor; + } + + /** + * Returns the appropriate key converter for the specified cryptographic curve. + * + * @param curve - The cryptographic curve to use for the key conversion. + * @returns An `AsymmetricKeyConverter` for the specified curve. + */ + public static keyConverter(curve: string): AsymmetricKeyConverter { + const converters: Record = { + 'Ed25519' : Ed25519, + 'P-256' : Secp256r1, + 'secp256k1' : Secp256k1, + 'X25519' : X25519 + }; + + const converter = converters[curve]; + + if (!converter) throw new DidError(DidErrorCode.InvalidPublicKeyType, `Unsupported curve: ${curve}`); + + 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' }); + * ``` + * + * @param params - The parameters for the conversion. + * @param params.code - Optional Multicodec code to convert. + * @param params.name - Optional Multicodec name to convert. + * @returns A promise that resolves to a JOSE format key. + */ + public static async multicodecToJwk({ code, name }: { + code?: MulticodecCode, + name?: string + }): Promise { + // Either code or name must be specified, but not both. + if (!(name ? !code : code)) { + throw new Error(`Either 'name' or 'code' must be defined, but not both.`); + } + + // If name is undefined, lookup by code. + name = (name === undefined ) ? Multicodec.getNameFromCode({ code: code! }) : name; + + const lookupKey = name; + const jose = DidKeyUtils.MULTICODEC_TO_JWK[lookupKey]; + + if (jose === undefined) { + throw new Error(`Unsupported Multicodec to JWK conversion`); + } + + return { ...jose }; + } + + /** + * Converts a public key in JWK (JSON Web Key) format to a multibase identifier. + * + * @remarks + * Note: All secp public keys are converted to compressed point encoding + * before the multibase identifier is computed. + * + * Per {@link https://github.com/multiformats/multicodec/blob/master/table.csv | Multicodec table}: + * Public keys for Elliptic Curve cryptography algorithms (e.g., secp256k1, + * secp256k1r1, secp384r1, etc.) are always represented with compressed point + * encoding (e.g., secp256k1-pub, p256-pub, p384-pub, etc.). + * + * Per {@link https://datatracker.ietf.org/doc/html/rfc8812#name-jose-and-cose-secp256k1-cur | RFC 8812}: + * "As a compressed point encoding representation is not defined for JWK + * elliptic curve points, the uncompressed point encoding defined there + * MUST be used. The x and y values represented MUST both be exactly + * 256 bits, with any leading zeros preserved." + * + * @example + * ```ts + * const publicKey = { crv: 'Ed25519', kty: 'OKP', x: '...' }; + * const multibaseId = await Jose.publicKeyToMultibaseId({ publicKey }); + * ``` + * + * @param params - The parameters for the conversion. + * @param params.publicKey - The public key in JWK format. + * @returns A promise that resolves to the multibase identifier. + */ + public static async publicKeyToMultibaseId({ publicKey }: { + publicKey: Jwk + }): Promise { + if (!(publicKey?.crv && publicKey.crv in AlgorithmToKeyTypeMap)) { + throw new DidError(DidErrorCode.InvalidPublicKeyType, `Public key contains an unsupported key type: ${publicKey?.crv ?? 'undefined'}`); + } + + // Convert the public key from JWK format to a byte array. + let publicKeyBytes = await DidKeyUtils.keyConverter(publicKey.crv).publicKeyToBytes({ publicKey }); + + // Compress the public key if it is an elliptic curve key. + if (/^(secp256k1|P-256|P-384|P-521)$/.test(publicKey.crv)) { + publicKeyBytes = await DidKeyUtils.keyCompressor(publicKey.crv)({ publicKeyBytes }); + } + + // Convert the JSON Web Key (JWK) parameters to a Multicodec name. + const { name: multicodecName } = await DidKeyUtils.jwkToMulticodec({ jwk: publicKey }); + + // Compute the multibase identifier based on the provided key. + const multibaseId = DidKeyUtils.keyBytesToMultibaseId({ + keyBytes: publicKeyBytes, + multicodecName + }); + + return multibaseId; + } +} \ No newline at end of file diff --git a/packages/dids/src/methods/did-method.ts b/packages/dids/src/methods/did-method.ts new file mode 100644 index 000000000..7746ded15 --- /dev/null +++ b/packages/dids/src/methods/did-method.ts @@ -0,0 +1,529 @@ +import type { + Jwk, + Signer, + CryptoApi, + LocalKeyManager, + EnclosedSignParams, + EnclosedVerifyParams, + InferKeyGeneratorAlgorithm, +} from '@web5/crypto'; + +import type { + DidDocument, + DidResolutionResult, + DidResolutionOptions, + 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 convenience functions. + */ +export interface Did { + /** + * 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 DidUri#uri} */ + uri: string; +} + +/** + * Represents options during the creation of a Decentralized Identifier (DID). + * + * Implementations of this interface may contain properties and methods that provide specific + * options or metadata during the DID creation processes following specific DID method + * specifications. + */ +export interface DidCreateOptions { + /** + * Optional. An array of verification methods to be included in the DID document. + */ + verificationMethods?: DidCreateVerificationMethod[]; +} + +/** + * Options for additional verification methods added to the DID Document during the creation of a + * new Decentralized Identifier (DID). + */ +export interface DidCreateVerificationMethod extends Pick, 'controller' | 'id' | 'type'> { + /** + * The name of the cryptographic algorithm to be used for key generation. + * + * Examples might include `Ed25519` and `ES256K` but will vary depending on the DID method + * specification and the key management system in use. + * + * @example + * ```ts + * const verificationMethod: DidCreateVerificationMethod = { + * algorithm: 'Ed25519' + * }; + * ``` + */ + algorithm: TKms extends CryptoApi + ? InferKeyGeneratorAlgorithm + : InferKeyGeneratorAlgorithm; + + /** + * 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. + * + * @example + * ```ts + * const verificationMethod: DidCreateVerificationMethod = { + * algorithm: 'Ed25519', + * controller: 'did:example:1234', + * purposes: ['authentication', 'assertionMethod'] + * }; + * ``` + */ + purposes?: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[]; +} + +/** + * Represents metadata about a DID resulting from create, update, or deactivate operations. + */ +export type DidMetadata = { + // Additional properties of any type. + [key: string]: any; +} + +/** + * Defines the API for a specific DID method. It includes functionalities for creating and resolving DIDs. + * + * @typeparam T - The type of the DID instance associated with this method. + * @typeparam O - The type of the options used for creating the DID. + */ +export interface DidMethodApi< + TDid extends Did, + TOptions extends DidCreateOptions, + TKms extends CryptoApi | undefined = undefined + > extends DidMethodResolver { + /** + * The name of the DID method. + * + * For example, in the DID `did:example:123456`, "example" would be the method name. + */ + methodName: string; + + new (): DidMethod; + + /** + * Creates a new DID. + * + * This function should generate a new DID in accordance with the DID method specification being + * implemented, using the provided `keyManager`, and optionally, any provided `options`. + * + * @param params - The parameters used to create the DID. + * @param params.keyManager - Optional. The cryptographic API used for key management. + * @param params.options - Optional. The options used for creating the DID. + * @returns A promise that resolves to the newly created DID instance. + */ + create(params: { keyManager?: TKms, options?: TOptions }): Promise; +} + +/** + * Defines the interface for resolving a DID using a specific DID method. + * + * A DID resolver takes a DID URI as input and returns a {@link DidResolutionResult} object. + * + * @property {string} methodName - The name of the DID method. + * @method resolve - Asynchronous method to resolve a DID URI. Takes the DID URI and optional resolution options. + */ +export interface DidMethodResolver { + /** + * The name of the DID method. + * + * For example, in the DID `did:example:123456`, "example" would be the method name. + */ + methodName: string; + + new (): DidMethod; + + /** + * Resolves a DID URI. + * + * This function should resolve the DID URI in accordance with the DID method specification being + * implemented, using the provided `options`. + * + * @param didUri - The DID URI to be resolved. + * @param options - Optional. The options used for resolving the DID. + * @returns A {@link DidResolutionResult} object containing the DID document and metadata or an error. + */ + resolve(didUri: string, options?: DidResolutionOptions): Promise; +} + +/** + * 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 the DID to a PortableDid. + * const portableDid = await DidExample.toKeys({ did }); + * + * // Instantiate a `Did` object from the PortableDid metadata. + * const didFromKeys = await DidExample.fromKeys({ ...portableDid }); + * // The `didFromKeys` object should be equivalent to the original `did` object. + * ``` + */ +export interface PortableDid { + /** {@inheritDoc DidUri#uri} */ + uri?: string; + + /** + * An array of verification methods, including the key material and key purpose, which are + * included in the DID document. + * + * @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods} + */ + verificationMethods: PortableDidVerificationMethod[]; +} + +/** + * Represents a verification method within a {@link PortableDid}, including the private key and + * the purposes for which the verification method can be used. + * + * This interface extends {@link DidVerificationMethod}, providing a structure to document the key + * material and metadata associated with a DID's verification methods. + */ +export interface PortableDidVerificationMethod extends Partial { + /** + * Express the private key in JWK format. + * + * (Optional) A JSON Web Key that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}. + */ + privateKeyJwk?: Jwk; + + /** + * 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. + * + * @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'] + * }; + * ``` + */ + purposes?: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[]; +} + +/** + * Base abstraction for all Decentralized Identifier (DID) method implementations. + * + * This base class serves as a foundational structure upon which specific DID methods + * can be implemented. Subclasses should furnish particular method and data models adherent + * to various DID methods, taking care to adhere to the + * {@link https://www.w3.org/TR/did-core/ | W3C DID Core specification} and the + * respective DID method specifications. + */ +export class DidMethod { + /** + * Instantiates a `Did` object from an existing DID using keys in an external Key Management + * System (KMS). + * + * This method returns a `Did` object by resolving an existing DID URI and verifying that all + * associated keys are present in the provided key manager. + * + * @remarks + * The method verifies the presence of key material for every verification method in the DID + * document within the given KMS. If any key is missing, an error is thrown. + * + * This approach ensures that the resulting `Did` object is fully operational with the provided + * key manager and that all cryptographic operations related to the DID can be performed. + * + * @param params - The parameters for the `fromKeyManager` operation. + * @param params.didUri - The URI of the DID to be instantiated. + * @param params.keyManager - The Key Management System to be used for key management operations. + * @returns A Promise resolving to the instantiated `Did` object. + * @throws An error if any key in the DID document is not present in the provided KMS. + * + * @example + * ```ts + * // Assuming keyManager already contains the key material for the DID. + * const didUri = 'did:method:example'; + * const did = await DidMethod.fromKeyManager({ didUri, keyManager }); + * // The 'did' is now an instance of Did, linked with the provided keyManager. + * ``` + */ + public static async fromKeyManager({ didUri, keyManager }: { + didUri: string; + keyManager: CryptoApi; + }): Promise { + // 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}. + * + * Given the W3C DID Document of a DID, return the verification method that will be used for + * signing messages and credentials. If given, the `methodId` parameter is used to select the + * verification method. If not given, each DID method implementation will select a default + * verification method from the DID Document. + * + * @param _params - The parameters for the `getSigningMethod` operation. + * @param _params.didDocument - DID Document to get the verification method from. + * @param _params.methodId - ID of the verification method to use for signing. + * @returns Verification method to use for signing. + */ + public static async getSigningMethod(_params: { + didDocument: DidDocument; + methodId?: string; + }): Promise { + throw new Error(`Not implemented: Classes extending DidMethod must implement getSigningMethod()`); + } + + /** + * MUST be implemented by all DID method implementations that extend {@link DidMethod}. + * + * Resolves a DID URI to a DID Document. + * + * @param _didUri - The DID to be resolved. + * @param _options - Optional parameters for resolving the DID. + * @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution. + */ + public static async resolve(_didUri: string, _options?: DidResolutionOptions): Promise { + throw new Error(`Not implemented: Classes extending DidMethod must implement resolve()`); + } + + /** + * Converts a `Did` 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 Did + * 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 `Did` 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: Did }): 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/methods/did-web.ts b/packages/dids/src/methods/did-web.ts new file mode 100644 index 000000000..4a2b2c2a6 --- /dev/null +++ b/packages/dids/src/methods/did-web.ts @@ -0,0 +1,96 @@ +import type { DidDocument, DidResolutionOptions, DidResolutionResult } from '../types/did-core.js'; + +import { DidUri } from '../did-uri.js'; +import { DidMethod } from './did-method.js'; +import { EMPTY_DID_RESOLUTION_RESULT } from '../resolver/did-resolver.js'; + +/** + * The `DidWeb` class provides an implementation of the `did:web` DID method. + * + * Features: + * - DID Resolution: Resolve a `did:web` to its corresponding DID Document. + * + * @remarks + * The `did:web` method uses a web domain's existing reputation and aims to integrate decentralized + * identities with the existing web infrastructure to drive adoption. It leverages familiar web + * security models and domain ownership to provide accessible, interoperable digital identity + * management. + * + * @see {@link https://w3c-ccg.github.io/did-method-web/ | DID Web Specification} + * + * @example + * ```ts + * // DID Resolution + * const resolutionResult = await DidWeb.resolve({ did: did.uri }); + * ``` + */ +export class DidWeb extends DidMethod { + + /** + * Name of the DID method, as defined in the DID Web specification. + */ + public static methodName = 'web'; + + /** + * Resolves a `did:web` identifier to a DID Document. + * + * @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. + */ + public static async resolve(didUri: string, _options?: DidResolutionOptions): Promise { + // Attempt to parse the DID URI. + const parsedDid = DidUri.parse(didUri); + + // If parsing failed, the DID is invalid. + if (!parsedDid) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { error: 'invalidDid' } + }; + } + + // If the DID method is not "web", return an error. + if (parsedDid.method !== DidWeb.methodName) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { error: 'methodNotSupported' } + }; + } + + // Replace ":" with "/" in the identifier and prepend "https://" to obtain the fully qualified + // domain name and optional path. + let baseUrl = `https://${parsedDid.id.replace(/:/g, '/')}`; + + // If the domain contains a percent encoded port value, decode the colon. + baseUrl = decodeURIComponent(baseUrl); + + // Append the expected location of the DID document depending on whether a path was specified. + const didDocumentUrl = parsedDid.id.includes(':') ? + `${baseUrl}/did.json` : + `${baseUrl}/.well-known/did.json`; + + try { + // Perform an HTTP GET request to obtain the DID document. + const response = await fetch(didDocumentUrl); + + // If the response status code is not 200, return an error. + if (!response.ok) throw new Error('HTTP error status code returned'); + + // Parse the DID document. + const didDocument = await response.json() as DidDocument; + + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didDocument, + }; + + } catch (error: any) { + // If the DID document could not be retrieved, return an error. + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { error: 'notFound' } + }; + } + } +} \ No newline at end of file diff --git a/packages/dids/src/resolver-cache-level.ts b/packages/dids/src/resolver-cache-level.ts deleted file mode 100644 index 973d5866e..000000000 --- a/packages/dids/src/resolver-cache-level.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { DidResolutionResult, DidResolverCache } from './types.js'; - -import ms from 'ms'; -import { Level } from 'level'; - -export type DidResolverCacheOptions = { - location?: string; - ttl?: string; -} - -type CacheWrapper = { - ttlMillis: number; - value: DidResolutionResult; -} - -/** - * Naive level-based cache for did resolution results. It just so happens that level aggressively keeps as much as it - * can in memory when possible while also writing to the filesystem (in node runtime) and indexedDB (in browser runtime). - * the persistent aspect is especially useful across page refreshes. - */ -export class DidResolverCacheLevel implements DidResolverCache { - private cache: Level; - private ttl: number; - - private static defaultOptions: Required = { - location : 'DATA/AGENT/DID_RESOLVERCACHE', - ttl : '15m' - }; - - constructor(options: DidResolverCacheOptions = {}) { - let { location, ttl } = options; - - location ??= DidResolverCacheLevel.defaultOptions.location; - ttl ??= DidResolverCacheLevel.defaultOptions.ttl; - - this.cache = new Level(location); - this.ttl = ms(ttl); - } - - async get(did: string): Promise { - try { - const str = await this.cache.get(did); - const cacheWrapper: CacheWrapper = JSON.parse(str); - - if (Date.now() >= cacheWrapper.ttlMillis) { - // defer deletion to be called in the next tick of the js event loop - this.cache.nextTick(() => this.cache.del(did)); - - return; - } else { - return cacheWrapper.value; - } - - } catch(error: any) { - // Don't throw when a key wasn't found. - if (error.code === 'LEVEL_NOT_FOUND') { - return; - } - - throw error; - } - } - - set(did: string, value: DidResolutionResult): Promise { - const cacheWrapper: CacheWrapper = { ttlMillis: Date.now() + this.ttl, value }; - const str = JSON.stringify(cacheWrapper); - - return this.cache.put(did, str); - } - - delete(did: string): Promise { - return this.cache.del(did); - } - - clear(): Promise { - return this.cache.clear(); - } - - close(): Promise { - return this.cache.close(); - } -} \ No newline at end of file diff --git a/packages/dids/src/resolver/did-resolver.ts b/packages/dids/src/resolver/did-resolver.ts new file mode 100644 index 000000000..6a3168479 --- /dev/null +++ b/packages/dids/src/resolver/did-resolver.ts @@ -0,0 +1,254 @@ +import type { KeyValueStore } from '@web5/common'; + +import type { DidMethodResolver } from '../methods/did-method.js'; +import type { DidDereferencingOptions, DidDereferencingResult, DidResolutionOptions, DidResolutionResult, DidResource } from '../types/did-core.js'; + +import { DidUri } from '../did-uri.js'; +import { DidErrorCode } from '../did-error.js'; +import { DidResolverCacheNoop } from './resolver-cache-noop.js'; + +/** + * Interface for cache implementations used by {@link DidResolver} to store resolved DID documents. + */ +export interface DidResolverCache extends KeyValueStore {} + +/** + * Parameters for configuring the `DidResolver` class, which is responsible for resolving + * decentralized identifiers (DIDs) to their corresponding DID documents. + * + * This type specifies the essential components required by the `DidResolver` to perform + * DID resolution and dereferencing. It includes an array of `DidMethodResolver` instances, + * each capable of resolving DIDs for a specific method, and optionally, a cache for storing + * resolved DID documents to improve resolution efficiency. + */ +export type DidResolverParams = { + /** + * An array of `DidMethodResolver` instances. + * + * Each resolver in this array is designed to handle a specific DID method, enabling the + * `DidResolver` to support multiple DID methods simultaneously. + */ + didResolvers: DidMethodResolver[]; + + /** + * An optional `DidResolverCache` instance used for caching resolved DID documents. + * + * Providing a cache implementation can significantly enhance resolution performance by avoiding + * redundant resolutions for previously resolved DIDs. If omitted, a no-operation cache is used, + * which effectively disables caching. + */ + cache?: DidResolverCache; +} + +/** + * A constant representing an empty DID Resolution Result. This object is used as the basis for a + * result of DID resolution and is typically augmented with additional properties by the + * DID method resolver. + */ +export const EMPTY_DID_RESOLUTION_RESULT: DidResolutionResult = { + '@context' : 'https://w3id.org/did-resolution/v1', + didResolutionMetadata : {}, + didDocument : null, + didDocumentMetadata : {}, +}; + +/** + * The `DidResolver` class provides mechanisms for resolving Decentralized Identifiers (DIDs) to + * their corresponding DID documents. + * + * The class is designed to handle various DID methods by utilizing an array of `DidMethodResolver` + * instances, each responsible for a specific DID method. + * + * Providing a cache implementation can significantly enhance resolution performance by avoiding + * redundant resolutions for previously resolved DIDs. If omitted, a no-operation cache is used, + * which effectively disables caching. + * + * Usage: + * - Construct the `DidResolver` with an array of `DidMethodResolver` instances and an optional cache. + * - Use `resolve` to resolve a DID to its DID Resolution Result. + * - Use `dereference` to extract specific resources from a DID URL, like service endpoints or verification methods. + * + * @example + * ```ts + * const resolver = new DidResolver({ + * didResolvers: [], + * cache: new DidResolverCacheNoop() + * }); + * + * const resolutionResult = await resolver.resolve('did:example:123456'); + * const dereferenceResult = await resolver.dereference({ didUri: 'did:example:123456#key-1' }); + * ``` + */ +export class DidResolver { + /** + * A cache for storing resolved DID documents. + */ + private cache: DidResolverCache; + + /** + * A map to store method resolvers against method names. + */ + private didResolvers: Map = new Map(); + + /** + * Constructs a new `DidResolver`. + * + * @param params - The parameters for constructing the `DidResolver`. + */ + constructor({ cache, didResolvers }: DidResolverParams) { + this.cache = cache || DidResolverCacheNoop; + + for (const resolver of didResolvers) { + this.didResolvers.set(resolver.methodName, resolver); + } + } + + /** + * Resolves a DID to a DID Resolution Result. + * + * If the DID Resolution Result is present in the cache, it returns the cached result. Otherwise, + * it uses the appropriate method resolver to resolve the DID, stores the resolution result in the + * cache, and returns the resolultion result. + * + * @param didUri - The DID or DID URL to resolve. + * @returns A promise that resolves to the DID Resolution Result. + */ + public async resolve(didUri: string, options?: DidResolutionOptions): Promise { + + const parsedDid = DidUri.parse(didUri); + if (!parsedDid) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { + error : DidErrorCode.InvalidDid, + errorMessage : `Invalid DID URI: ${didUri}` + } + }; + } + + const resolver = this.didResolvers.get(parsedDid.method); + if (!resolver) { + return { + ...EMPTY_DID_RESOLUTION_RESULT, + didResolutionMetadata: { + error : DidErrorCode.MethodNotSupported, + errorMessage : `Method not supported: ${parsedDid.method}` + } + }; + } + + const cachedResolutionResult = await this.cache.get(parsedDid.uri); + + if (cachedResolutionResult) { + return cachedResolutionResult; + } else { + const resolutionResult = await resolver.resolve(parsedDid.uri, options); + + await this.cache.set(parsedDid.uri, resolutionResult); + + return resolutionResult; + } + } + + /** + * Dereferences a DID (Decentralized Identifier) URL to a corresponding DID resource. + * + * + * This method interprets the DID URL's components, which include the DID method, method-specific + * identifier, path, query, and fragment, and retrieves the related resource as per the DID Core + * specifications. + * + * The dereferencing process involves resolving the DID contained in the DID URL to a DID document, + * and then extracting the specific part of the document identified by the fragment in the DID URL. + * If no fragment is specified, the entire DID document is returned. + * + * This method supports resolution of different components within a DID document such as service + * endpoints and verification methods, based on their IDs. It accommodates both full and + * DID URLs as specified in the DID Core specification. + * + * More information on DID URL dereferencing can be found in the + * {@link https://www.w3.org/TR/did-core/#did-url-dereferencing | DID Core specification}. + * + * TODO: This is a partial implementation and does not fully implement DID URL dereferencing. (https://github.com/TBD54566975/web5-js/issues/387) + * + * @param didUrl - The DID URL string to dereference. + * @param [_options] - Input options to the dereference function. Optional. + * @returns a {@link DidDereferencingResult} + */ + async dereference( + didUrl: string, + _options?: DidDereferencingOptions + ): Promise { + + // Validate the given `didUrl` confirms to the DID URL syntax. + const parsedDidUrl = DidUri.parse(didUrl); + + if (!parsedDidUrl) { + return { + dereferencingMetadata : { error: DidErrorCode.InvalidDidUrl }, + contentStream : null, + contentMetadata : {} + }; + } + + // Obtain the DID document for the input DID by executing DID resolution. + const { didDocument, didResolutionMetadata, didDocumentMetadata } = await this.resolve(parsedDidUrl.uri); + + if (!didDocument) { + return { + dereferencingMetadata : { error: didResolutionMetadata.error }, + contentStream : null, + contentMetadata : {} + }; + } + + // Return the entire DID Document if no query or fragment is present on the DID URL. + if (!parsedDidUrl.fragment || parsedDidUrl.query) { + return { + dereferencingMetadata : { contentType: 'application/did+json' }, + contentStream : didDocument, + contentMetadata : didDocumentMetadata + }; + } + + const { service = [], verificationMethod = [] } = didDocument; + + // Create a set of possible id matches. The DID spec allows for an id to be the entire + // did#fragment or just #fragment. + // @see {@link }https://www.w3.org/TR/did-core/#relative-did-urls | Section 3.2.2, Relative DID URLs}. + // Using a Set for fast string comparison since some DID methods have long identifiers. + const idSet = new Set([didUrl, parsedDidUrl.fragment, `#${parsedDidUrl.fragment}`]); + + let didResource: DidResource | undefined; + + // Find the first matching verification method in the DID document. + for (let vm of verificationMethod) { + if (idSet.has(vm.id)) { + didResource = vm; + break; + } + } + + // Find the first matching service in the DID document. + for (let svc of service) { + if (idSet.has(svc.id)) { + didResource = svc; + break; + } + } + + if (didResource) { + return { + dereferencingMetadata : { contentType: 'application/did+json' }, + contentStream : didResource, + contentMetadata : didResolutionMetadata + }; + } else { + return { + dereferencingMetadata : { error: DidErrorCode.NotFound }, + contentStream : null, + contentMetadata : {}, + }; + } + } +} \ No newline at end of file diff --git a/packages/dids/src/resolver/resolver-cache-level.ts b/packages/dids/src/resolver/resolver-cache-level.ts new file mode 100644 index 000000000..c7a7b71c5 --- /dev/null +++ b/packages/dids/src/resolver/resolver-cache-level.ts @@ -0,0 +1,163 @@ +import type { AbstractLevel } from 'abstract-level'; + +import ms from 'ms'; +import { Level } from 'level'; + +import type { DidResolverCache } from './did-resolver.js'; +import type { DidResolutionResult } from '../types/did-core.js'; + +/** + * Configuration parameters for creating a LevelDB-based cache for DID resolution results. + * + * Allows customization of the underlying database instance, storage location, and cache + * time-to-live (TTL) settings. + */ +export type DidResolverCacheLevelParams = { + /** + * Optional. An instance of `AbstractLevel` to use as the database. If not provided, a new + * LevelDB instance will be created at the specified `location`. + */ + db?: AbstractLevel; + + /** + * Optional. The file system path or IndexedDB name where the LevelDB store will be created. + * Defaults to 'DATA/DID_RESOLVERCACHE' if not specified. + */ + location?: string; + + /** + * Optional. The time-to-live for cache entries, expressed as a string (e.g., '1h', '15m'). + * Determines how long a cache entry should remain valid before being considered expired. Defaults + * to '15m' if not specified. + */ + ttl?: string; +} + +/** + * Encapsulates a DID resolution result along with its expiration information for caching purposes. + * + * This type is used internally by the `DidResolverCacheLevel` to store DID resolution results + * with an associated time-to-live (TTL) value. The TTL is represented in milliseconds and + * determines when the cached entry is considered expired and eligible for removal. + */ +type CacheWrapper = { + /** + * The expiration time of the cache entry in milliseconds since the Unix epoch. + * + * This value is used to calculate whether the cached entry is still valid or has expired. + */ + ttlMillis: number; + + /** + * The DID resolution result being cached. + * + * This object contains the resolved DID document and associated metadata. + */ + value: DidResolutionResult; +} + +/** + * A Level-based cache implementation for storing and retrieving DID resolution results. + * + * This cache uses LevelDB for storage, allowing data persistence across process restarts or + * browser refreshes. It's suitable for both Node.js and browser environments. + * + * @remarks + * The LevelDB cache keeps data in memory for fast access and also writes to the filesystem in + * Node.js or indexedDB in browsers. Time-to-live (TTL) for cache entries is configurable. + * + * @example + * ``` + * const cache = new DidResolverCacheLevel({ ttl: '15m' }); + * ``` + */ +export class DidResolverCacheLevel implements DidResolverCache { + /** The underlying LevelDB store used for caching. */ + private cache: AbstractLevel; + + /** The time-to-live for cache entries in milliseconds. */ + private ttl: number; + + constructor({ + db, + location = 'DATA/DID_RESOLVERCACHE', + ttl = '15m' + }: DidResolverCacheLevelParams = {}) { + this.cache = db ?? new Level(location); + this.ttl = ms(ttl); + } + + /** + * Retrieves a DID resolution result from the cache. + * + * If the cached item has exceeded its TTL, it's scheduled for deletion and undefined is returned. + * + * @param did - The DID string used as the key for retrieving the cached result. + * @returns The cached DID resolution result or undefined if not found or expired. + */ + async get(did: string): Promise { + try { + const str = await this.cache.get(did); + const cacheWrapper: CacheWrapper = JSON.parse(str); + + if (Date.now() >= cacheWrapper.ttlMillis) { + // defer deletion to be called in the next tick of the js event loop + this.cache.nextTick(() => this.cache.del(did)); + + return; + } else { + return cacheWrapper.value; + } + + } catch(error: any) { + // Don't throw when a key wasn't found. + if (error.notFound) { + return; + } + + throw error; + } + } + + /** + * Stores a DID resolution result in the cache with a TTL. + * + * @param did - The DID string used as the key for storing the result. + * @param value - The DID resolution result to be cached. + * @returns A promise that resolves when the operation is complete. + */ + set(did: string, value: DidResolutionResult): Promise { + const cacheWrapper: CacheWrapper = { ttlMillis: Date.now() + this.ttl, value }; + const str = JSON.stringify(cacheWrapper); + + return this.cache.put(did, str); + } + + /** + * Deletes a DID resolution result from the cache. + * + * @param did - The DID string used as the key for deletion. + * @returns A promise that resolves when the operation is complete. + */ + delete(did: string): Promise { + return this.cache.del(did); + } + + /** + * Clears all entries from the cache. + * + * @returns A promise that resolves when the operation is complete. + */ + clear(): Promise { + return this.cache.clear(); + } + + /** + * Closes the underlying LevelDB store. + * + * @returns A promise that resolves when the store is closed. + */ + close(): Promise { + return this.cache.close(); + } +} \ No newline at end of file diff --git a/packages/dids/src/resolver-cache-noop.ts b/packages/dids/src/resolver/resolver-cache-noop.ts similarity index 64% rename from packages/dids/src/resolver-cache-noop.ts rename to packages/dids/src/resolver/resolver-cache-noop.ts index 1823e30cb..dcbadf732 100644 --- a/packages/dids/src/resolver-cache-noop.ts +++ b/packages/dids/src/resolver/resolver-cache-noop.ts @@ -1,10 +1,11 @@ -import type { DidResolutionResult, DidResolverCache } from './types.js'; +import type { DidResolverCache } from './did-resolver.js'; +import type { DidResolutionResult } from '../types/did-core.js'; /** - * no-op cache that is used as the default cache for did-resolver. - * The motivation behind using a no-op cache as the default stems from - * the desire to maximize the potential for this library to be used - * in as many JS runtimes as possible + * No-op cache that is used as the default cache for did-resolver. + * + * The motivation behind using a no-op cache as the default stems from the desire to maximize the + * potential for this library to be used in as many JS runtimes as possible. */ export const DidResolverCacheNoop: DidResolverCache = { get: function (_key: string): Promise { diff --git a/packages/dids/src/types.ts b/packages/dids/src/types.ts deleted file mode 100644 index 06803ba50..000000000 --- a/packages/dids/src/types.ts +++ /dev/null @@ -1,365 +0,0 @@ -import type { KeyValueStore } from '@web5/common'; -import type { PrivateKeyJwk, PublicKeyJwk } from '@web5/crypto'; - -import { DidKeyKeySet } from './did-key.js'; -import { DidIonKeySet } from './did-ion.js'; -import { DidDhtKeySet } from './did-dht.js'; - -export type DidDereferencingMetadata = { - /** The Media Type of the returned contentStream SHOULD be expressed using this property if - * dereferencing is successful. */ - contentType?: string; - - /** - * The error code from the dereferencing process. This property is REQUIRED when there is an - * error in the dereferencing process. The value of this property MUST be a single keyword - * expressed as an ASCII string. The possible property values of this field SHOULD be registered - * in the {@link https://www.w3.org/TR/did-spec-registries/ | DID Specification Registries}. - * The DID Core specification defines the following common error values. - * - * @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-metadata | Section 7.2.2, DID URL Dereferencing Metadata} - */ - error?: - /** The DID URL supplied to the DID URL dereferencing function does not conform to valid - * syntax. */ - | 'invalidDidUrl' - - /** The DID URL dereferencer was unable to find the contentStream resulting from this - * dereferencing request. */ - | 'notFound' - | string; - - /** Additional output metadata generated during DID Resolution. */ - [key: string]: any -} - -/** - * A metadata structure consisting of input options to the dereference function. - * - * @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-options} - */ -export interface DidDereferencingOptions { - /** The Media Type that the caller prefers for contentStream. */ - accept?: string; - - /** Additional properties used during DID dereferencing. */ - [key: string]: any; -} - -export type DidDereferencingResult = { - /** - * A metadata structure consisting of values relating to the results of the DID URL dereferencing - * process. This structure is REQUIRED, and in the case of an error in the dereferencing process, - * this MUST NOT be empty. Properties defined by this specification are in 7.2.2 DID URL - * Dereferencing Metadata. If the dereferencing is not successful, this structure MUST contain an - * `error` property describing the error. - */ - dereferencingMetadata: DidDereferencingMetadata; - - /** - * If the `dereferencing` function was called and successful, this MUST contain a resource - * corresponding to the DID URL. The contentStream MAY be a resource such as: - * - a DID document that is serializable in one of the conformant representations - * - a Verification Method - * - a service. - * - any other resource format that can be identified via a Media Type and obtained through the - * resolution process. - * - * If the dereferencing is unsuccessful, this value MUST be empty. - */ - contentStream: DidResource | null; - - /** - * If the dereferencing is successful, this MUST be a metadata structure, but the structure MAY be - * empty. This structure contains metadata about the contentStream. If the contentStream is a DID - * document, this MUST be a didDocumentMetadata structure as described in DID Resolution. If the - * dereferencing is unsuccessful, this output MUST be an empty metadata structure. - */ - contentMetadata: DidDocumentMetadata; -} - -export type DidDocument = { - '@context'?: 'https://www.w3.org/ns/did/v1' | string | string[]; - id: string; - alsoKnownAs?: string[]; - controller?: string | string[]; - verificationMethod?: VerificationMethod[]; - service?: DidService[]; - assertionMethod?: VerificationMethod[] | string[]; - authentication?: VerificationMethod[] | string[]; - keyAgreement?: VerificationMethod[] | string[]; - capabilityDelegation?: VerificationMethod[] | string[]; - capabilityInvocation?: VerificationMethod[] | string[]; -} - -export type DidDocumentMetadata = { - // indicates the timestamp of the Create operation. ISO8601 timestamp - created?: string - // indicates the timestamp of the last Update operation for the document version which was - // resolved. ISO8601 timestamp - updated?: string - // indicates whether the DID has been deactivated - deactivated?: boolean - // indicates the version of the last Update operation for the document version which - // was resolved - versionId?: string - // indicates the timestamp of the next Update operation if the resolved document version - // is not the latest version of the document. - nextUpdate?: string - // indicates the version of the next Update operation if the resolved document version - // is not the latest version of the document. - nextVersionId?: string - // @see https://www.w3.org/TR/did-core/#dfn-equivalentid - equivalentId?: string - // @see https://www.w3.org/TR/did-core/#dfn-canonicalid - canonicalId?: string - // Additional output metadata generated during DID Resolution. - [key: string]: any -}; - -export type DidKeySet = DidKeyKeySet | DidIonKeySet | DidDhtKeySet; - -export type DidKeySetVerificationMethodKey = { - /** Unique identifier for the key in the KeyManager store. */ - keyManagerId?: string; - publicKeyJwk?: PublicKeyJwk; - privateKeyJwk?: PrivateKeyJwk; - relationships: VerificationRelationship[]; -} - -export type DidMetadata = { - /** - * Additional properties of any type. - */ - [key: string]: any; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DidMethod {} - -export interface DidMethodApi extends DidMethodOperator, DidMethodResolver { - new (): DidMethod; - methodName: string; -} - -export interface DidMethodResolver { - new (): DidMethod; - methodName: string; - - resolve(options: { - didUrl: string, - resolutionOptions?: DidResolutionOptions - }): Promise; -} - -export interface DidMethodOperator { - new (): DidMethod; - methodName: string; - - create(options: any): Promise; - - generateKeySet(): Promise; - - getDefaultSigningKey(options: { didDocument: DidDocument }): Promise; -} - -/** - * A DID Resource is either a DID Document, a DID Verification method or a DID Service - */ -export type DidResource = DidDocument | VerificationMethod | DidService - -/** - * Services are used in DID documents to express ways of communicating with the DID subject or associated entities. - * A service can be any type of service the DID subject wants to advertise. - * - * @see {@link https://www.w3.org/TR/did-core/#services} - */ -export type DidService = { - id: string; - type: string; - serviceEndpoint: string | DidServiceEndpoint | DidServiceEndpoint[]; - description?: string; -}; - -/** - * A service endpoint is a URI (Uniform Resource Identifier) that can be used to interact with the service. - * - * @see {@link https://www.w3.org/TR/did-core/#dfn-serviceendpoint} - */ -export interface DidServiceEndpoint { - [key: string]: any; -} - -export interface DwnServiceEndpoint extends DidServiceEndpoint { - encryptionKeys?: string[]; - nodes: string[]; - signingKeys: string[]; -} - -export type DidResolutionMetadata = { - /** - * The Media Type of the returned `didDocumentStream`. This property is REQUIRED if resolution is - * successful and if the `resolveRepresentation` function was called. This property MUST NOT be - * present if the `resolve` function was called. The value of this property MUST be an ASCII - * string that is the Media Type of the conformant representations. The caller of the - * `resolveRepresentation` function MUST use this value when determining how to parse and process - * the `didDocumentStream` returned by this function into the data model. - */ - contentType?: string - - error?: - /** - * When an unexpected error occurs during DID Resolution or DID URL dereferencing, the value of - * the DID Resolution or DID URL Dereferencing Metadata error property MUST be internalError. - */ - | 'internalError' - - /** - * If an invalid DID is detected during DID Resolution, the value of the - * DID Resolution Metadata error property MUST be invalidDid. - */ - | 'invalidDid' - - /** - * If a DID method is not supported during DID Resolution or DID URL - * dereferencing, the value of the DID Resolution or DID URL Dereferencing - * Metadata error property MUST be methodNotSupported. - */ - | 'methodNotSupported' - - /** - * If during DID Resolution or DID URL dereferencing a DID or DID URL - * doesn't exist, the value of the DID Resolution or DID URL dereferencing - * Metadata error property MUST be notFound. - */ - | 'notFound' - - /** - * If a DID document representation is not supported during DID Resolution - * or DID URL dereferencing, the value of the DID Resolution Metadata error - * property MUST be representationNotSupported. - */ - | 'representationNotSupported' - | string - - /** Additional output metadata generated during DID Resolution. */ - [key: string]: any -}; - -/** - * DID Resolution input metadata. - * - * @see {@link https://www.w3.org/TR/did-core/#did-resolution-options} - */ -export interface DidResolutionOptions { - accept?: string - - // Additional properties used during DID Resolution. - [key: string]: any -} - -export type DidResolutionResult = { - '@context'?: 'https://w3id.org/did-resolution/v1' | string | string[] - didResolutionMetadata: DidResolutionMetadata - didDocument?: DidDocument - didDocumentMetadata: DidDocumentMetadata -}; - -/** - * implement this interface to provide your own cache for did resolution results. can be plugged in through Web5 API - */ -export type DidResolverCache = KeyValueStore; - -/** - * 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. - */ -export interface PortableDid { - did: string; - - /** - * A DID method can define different forms of a DID that are logically - * equivalent. An example is when a DID takes one form prior to registration - * in a verifiable data registry and another form after such registration. - * This is the purpose of the canonicalId property. - * - * The `canonicalId` must be used as the primary ID for the DID subject, - * with all other equivalent values treated as secondary aliases. - * - * @see {@link https://www.w3.org/TR/did-core/#dfn-canonicalid | W3C DID Document Metadata} - */ - canonicalId?: string; - - /** - * A set of data describing the DID subject, including mechanisms, such as - * cryptographic public keys, that the DID subject or a DID delegate can use - * to authenticate itself and prove its association with the DID. - */ - document: DidDocument; - - /** - * A collection of cryptographic keys associated with the DID subject. The - * `keySet` encompasses various forms, such as recovery keys, update keys, - * and verification method keys, to enable authentication and verification - * of the DID subject's association with the DID. - */ - keySet: DidKeySet; - - /** - * This property can be used to store method specific data about - * each managed DID and additional properties of any type. - */ - metadata?: DidMetadata; -} - -export type VerificationMethod = { - id: string; - // one of the valid verification method types as per - // https://www.w3.org/TR/did-spec-registries/#verification-method-types - type: string; - // DID of the key's controller - controller: string; - // a JSON Web Key that conforms to https://datatracker.ietf.org/doc/html/rfc7517 - publicKeyJwk?: PublicKeyJwk; - // an encoded (e.g, base58) key with a Multibase-prefix that conforms to - // https://datatracker.ietf.org/doc/draft-multiformats-multibase/ - publicKeyMultibase?: string; -}; - -export type VerificationRelationship = - /** - * Used to specify how the DID subject is expected to express claims, such - * as for the purposes of issuing a Verifiable Credential - */ - | 'assertionMethod' - - /** - * Used to specify how the DID subject is expected to be authenticated, for - * purposes such as logging into a website or engaging in any sort of - * challenge-response protocol. - */ - | 'authentication' - - /** - * Used to specify how an entity can generate encryption material in order to - * transmit confidential information intended for the DID subject, such as - * for the purposes of establishing a secure communication channel with the - * recipient. - */ - | 'keyAgreement' - - /** - * Used to specify a mechanism that might be used by the DID subject to - * delegate a cryptographic capability to another party, such as delegating - * the authority to access a specific HTTP API to a subordinate. - */ - | 'capabilityDelegation' - - /** - * Used to specify a verification method that might be used by the DID - * subject to invoke a cryptographic capability, such as the authorization - * to update the DID Document. - */ - | 'capabilityInvocation'; \ No newline at end of file diff --git a/packages/dids/src/types/did-core.ts b/packages/dids/src/types/did-core.ts new file mode 100644 index 000000000..fbd8c893b --- /dev/null +++ b/packages/dids/src/types/did-core.ts @@ -0,0 +1,581 @@ +import { Jwk } from '@web5/crypto'; + +/** + * Represents metadata related to the process of DID dereferencing. + * + * This type includes fields that provide information about the outcome of a DID dereferencing operation, + * including the content type of the returned resource and any errors that occurred during the dereferencing process. + * + * @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-metadata | DID Core Specification, § DID URL Dereferencing Metadata} + */ +export type DidDereferencingMetadata = { + /** + * The Media Type of the returned contentStream SHOULD be expressed using this property if + * dereferencing is successful. + */ + contentType?: string; + + /** + * The error code from the dereferencing process. This property is REQUIRED when there is an + * error in the dereferencing process. The value of this property MUST be a single keyword + * expressed as an ASCII string. The possible property values of this field SHOULD be registered + * in the {@link https://www.w3.org/TR/did-spec-registries/ | DID Specification Registries}. + * The DID Core specification defines the following common error values: + * + * - `invalidDidUrl`: The DID URL supplied to the DID URL dereferencing function does not conform + * to valid syntax. + * - `notFound`: The DID URL dereferencer was unable to find the `contentStream` resulting from + * this dereferencing request. + * + * @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-metadata | DID Core Specification, § DID URL Dereferencing Metadata} + */ + error?: string; + + // Additional output metadata generated during DID Resolution. + [key: string]: any; +} + +/** + * Represents the options that can be used during the process of DID dereferencing. + * + * This interface allows the caller to specify preferences and additional parameters for the DID + * dereferencing operation. + * + * @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-options} + */ +export interface DidDereferencingOptions { + /** The Media Type that the caller prefers for contentStream. */ + accept?: string; + + /** Additional properties used during DID dereferencing. */ + [key: string]: any; +} + +/** + * Represents the result of a DID dereferencing operation. + * + * This type encapsulates the outcomes of the DID URL dereferencing process, including metadata + * about the dereferencing operation, the content stream retrieved (if any), and metadata about the + * content stream. + * + * @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing | DID Core Specification, § DID URL Dereferencing} + */ +export type DidDereferencingResult = { + /** + * A metadata structure consisting of values relating to the results of the DID URL dereferencing + * process. This structure is REQUIRED, and in the case of an error in the dereferencing process, + * this MUST NOT be empty. Properties defined by this specification are in 7.2.2 DID URL + * Dereferencing Metadata. If the dereferencing is not successful, this structure MUST contain an + * `error` property describing the error. + */ + dereferencingMetadata: DidDereferencingMetadata; + + /** + * If the `dereferencing` function was called and successful, this MUST contain a resource + * corresponding to the DID URL. The contentStream MAY be a resource such as: + * - a DID document that is serializable in one of the conformant representations + * - a Verification Method + * - a service. + * - any other resource format that can be identified via a Media Type and obtained through the + * resolution process. + * + * If the dereferencing is unsuccessful, this value MUST be empty. + */ + contentStream: DidResource | null; + + /** + * If the dereferencing is successful, this MUST be a metadata structure, but the structure MAY be + * empty. This structure contains metadata about the contentStream. If the contentStream is a DID + * document, this MUST be a didDocumentMetadata structure as described in DID Resolution. If the + * dereferencing is unsuccessful, this output MUST be an empty metadata structure. + */ + contentMetadata: DidDocumentMetadata; +} + +/** + * A set of data describing the Decentralized Identifierr (DID) subject. + * + * A DID Document contains information associated with the DID, such as cryptographic public keys + * and service endpoints, enabling trustable interactions associated with the DID subject. + * + * - Cryptographic public keys - Used by the DID subject or a DID delegate to authenticate itself + * and prove its association with the DID. + * - Service endpoints - Used to communicate or interact with the DID subject or associated + * entities. Examples include discovery, agent, social networking, file + * storage, and verifiable credential repository services. + * + * A DID Document can be retrieved by resolving a DID, as described in + * {@link https://www.w3.org/TR/did-core/#did-resolution | DID Core Specification, § DID Resolution}. + */ +export interface DidDocument { + /** + * A JSON-LD context link, which provides a JSON-LD processor with the information necessary to + * interpret the DID document JSON. The default context URL is 'https://www.w3.org/ns/did/v1'. + */ + '@context'?: 'https://www.w3.org/ns/did/v1' | string | (string | Record)[]; + + /** + * The DID Subject to which this DID Document pertains. + * + * The `id` property is REQUIRED and must be a valid DID. + * + * @see {@link https://www.w3.org/TR/did-core/#did-subject | DID Core Specification, § DID Subject} + */ + id: string; + + /** + * A DID subject can have multiple identifiers for different purposes, or at different times. + * The assertion that two or more DIDs (or other types of URI) refer to the same DID subject can + * be made using the `alsoKnownAs` property. + * + * @see {@link https://www.w3.org/TR/did-core/#also-known-as | DID Core Specification, § Also Known As} + */ + alsoKnownAs?: string[]; + + /** + * A DID controller is an entity that is authorized to make changes to a DID document. Typically, + * only the DID Subject (i.e., the value of `id` property in the DID document) is authoritative. + * However, another DID can be specified as the DID controller, and when doing so, any + * verification methods contained in the DID document for the other DID should be accepted as + * authoritative. In other words, proofs created by the controller DID should be considered + * equivalent to proofs created by the DID Subject. + * + * @see {@link https://www.w3.org/TR/did-core/#did-controller | DID Core Specification, § DID Controller} + */ + controller?: string | string[]; + + /** + * A DID document can express verification methods, such as cryptographic public keys, which can + * be used to authenticate or authorize interactions with the DID subject or associated parties. + * + * @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods} + */ + verificationMethod?: DidVerificationMethod[]; + + /** + * The `assertionMethod` verification relationship is used to specify how the DID subject is + * expected to express claims, such as for the purposes of issuing a Verifiable Credential. + * + * @see {@link https://www.w3.org/TR/did-core/#assertion | DID Core Specification, § Assertion} + */ + assertionMethod?: (DidVerificationMethod | string)[]; + + /** + * The `authentication` verification relationship is used to specify how the DID subject is expected + * to be authenticated, for purposes such as logging into a website or engaging in any sort of + * challenge-response protocol. + + * @see {@link https://www.w3.org/TR/did-core/#authentication | DID Core Specification, § Authentication} + */ + authentication?: (DidVerificationMethod | string)[]; + + /** + * The `keyAgreement` verification relationship is used to specify how an entity can generate + * encryption material in order to transmit confidential information intended for the DID + * subject, such as for the purposes of establishing a secure communication channel with the + * recipient. + * + * @see {@link https://www.w3.org/TR/did-core/#key-agreement | DID Core Specification, § Key Agreement} + */ + keyAgreement?: (DidVerificationMethod | string)[]; + + /** + * The `capabilityDelegation` verification relationship is used to specify a mechanism that might + * be used by the DID subject to delegate a cryptographic capability to another party, such as + * delegating the authority to access a specific HTTP API to a subordinate. + * + * @see {@link https://www.w3.org/TR/did-core/#capability-delegation | DID Core Specification, § Capability Delegation} + */ + capabilityDelegation?: (DidVerificationMethod | string)[]; + + /** + * The `capabilityInvocation` verification relationship is used to specify a verification method + * that might be used by the DID subject to invoke a cryptographic capability, such as the + * authorization to update the DID Document. + */ + capabilityInvocation?: (DidVerificationMethod | string)[]; + + /** + * Services are used in DID documents to express ways of communicating with the DID subject or + * associated entities. A service can be any type of service the DID subject wants to advertise, + * including decentralized identity management services for further discovery, authentication, + * authorization, or interaction. + * + * @see {@link https://www.w3.org/TR/did-core/#services | DID Core Specification, § Services} + */ + service?: DidService[]; +} + +/** + * Represents metadata about the DID document resulting from a DID resolution operation. + * + * This metadata typically does not change between invocations of the `resolve` and + * `resolveRepresentation` functions unless the DID document changes, as it represents metadata + * about the DID document. + * + * @see {@link https://www.w3.org/TR/did-core/#did-document-metadata | DID Core Specification, § DID Document Metadata} + */ +export type DidDocumentMetadata = { + /** + * Timestamp of the Create operation. + * + * The value of the property MUST be a string formatted as an XML Datetime normalized to + * UTC 00:00:00 and without sub-second decimal precision. For example: `2020-12-20T19:17:47Z`. + */ + created?: string; + + /** + * Timestamp of the last Update operation for the document version which was resolved. + * + * The value of the property MUST follow the same formatting rules as the `created` property. + * The `updated` property is omitted if an Update operation has never been performed on the DID + * document. If an `updated` property exists, it can be the same value as the `created` property + * when the difference between the two timestamps is less than one second. + */ + updated?: string; + + /** + * Whether the DID has been deactivated. + * + * If a DID has been deactivated, DID document metadata MUST include this property with the + * boolean value `true`. If a DID has not been deactivated, this properrty is OPTIONAL, but if + * present, MUST have the boolean value `false`. + */ + deactivated?: boolean; + + /** + * Version ID of the last Update operation for the document version which was resolved. + */ + versionId?: string; + + /** + * Timestamp of the next Update operation if the resolved document version is not the latest + * version of the document. + * + * The value of the property MUST follow the same formatting rules as the `created` property. + */ + nextUpdate?: string; + + /** + * Version ID of the next Update operation if the resolved document version is not the latest + * version of the document. + */ + nextVersionId?: string; + + /** + * A DID method can define different forms of a DID that are logically equivalent. An example is + * when a DID takes one form prior to registration in a verifiable data registry and another form + * after such registration. In this case, the DID method specification might need to express one + * or more DIDs that are logically equivalent to the resolved DID as a property of the DID + * document. This is the purpose of the `equivalentId` property. + * + * A requesting party is expected to retain the values from the id and equivalentId properties to + * ensure any subsequent interactions with any of the values they contain are correctly handled as + * logically equivalent (e.g., retain all variants in a database so an interaction with any one + * maps to the same underlying account). + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-equivalentid | DID Core Specification, § DID Document Metadata} + */ + equivalentId?: string[]; + + /** + * The `canonicalId` property is identical to the `equivalentId` property except: + * - it is associated with a single value rather than a set + * - the DID is defined to be the canonical ID for the DID subject within the scope of the + * containing DID document. + * + * A requesting party is expected to use the `canonicalId` value as its primary ID value for the + * DID subject and treat all other equivalent values as secondary aliases (e.g., update + * corresponding primary references in their systems to reflect the new canonical ID directive). + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-canonicalid | DID Core Specification, § DID Document Metadata} + */ + canonicalId?: string; + + // Additional output metadata generated during DID Resolution. + [key: string]: any; +}; + +/** + * Represents metadata related to the result of a DID resolution operation. + * + * This type includes fields that provide information about the outcome of a DID resolution process, + * including the content type of the returned DID document and any errors that occurred during the + * resolution process. + * + * This metadata typically changes between invocations of the `resolve` and `resolveRepresentation` + * functions, as it represents data about the resolution process itself. + * + * @see {@link https://www.w3.org/TR/did-core/#did-resolution-metadata | DID Core Specification, § DID Resolution Metadata} + */ +export type DidResolutionMetadata = { + /** + * The Media Type of the returned `didDocumentStream`. + * + * This property is REQUIRED if resolution is successful and if the `resolveRepresentation` + * function was called. This property MUST NOT be present if the `resolve` function was called. + * The value of this property MUST be an ASCII string that is the Media Type of the conformant + * representations. The caller of the `resolveRepresentation` function MUST use this value when + * determining how to parse and process the `didDocumentStream` returned by this function into the + * data model. + */ + contentType?: string; + + /** + * An error code indicating issues encountered during the DID Resolution or DID URL + * Dereferencing process. + * + * Defined error codes include: + * - `internalError`: An unexpected error occurred during DID Resolution or DID URL + * dereferencing process. + * - `invalidDid`: The provided DID is invalid. + * - `methodNotSupported`: The DID method specified is not supported. + * - `notFound`: The DID or DID URL does not exist. + * - `representationNotSupported`: The DID document representation is not supported. + * - Custom error codes can also be provided as strings. + * + * @see {@link https://www.w3.org/TR/did-core/#did-resolution-metadata | DID Core Specification, § DID Resolution Metadata} + * @see {@link https://www.w3.org/TR/did-spec-registries/#error | DID Specification Registries, § Error} + */ + error?: string; + + // Additional output metadata generated during DID Resolution. + [key: string]: any; +}; + +/** + * DID Resolution input metadata. +* +* The DID Core specification defines the following common properties: +* - `accept`: The Media Type that the caller prefers for the returned representation of the DID +* Document. +* +* The possible properties within this structure and their possible values are registered in the +* {@link https://www.w3.org/TR/did-spec-registries/#did-resolution-options | DID Specification Registries}. + * + * @see {@link https://www.w3.org/TR/did-core/#did-resolution-options | DID Core Specification, § DID Resolution Options} + */ +export interface DidResolutionOptions { + /** + * The Media Type that the caller prefers for the returned representation of the DID Document. + * + * This property is REQUIRED if the `resolveRepresentation` function was called. This property + * MUST NOT be present if the `resolve` function was called. + * + * The value of this property MUST be an ASCII string that is the Media Type of the conformant + * representations. The caller of the `resolveRepresentation` function MUST use this value when + * determining how to parse and process the `didDocumentStream` returned by this function into the + * data model. + * + * @see {@link https://www.w3.org/TR/did-core/#did-resolution-options | DID Core Specification, § DID Resolution Options} + */ + accept?: string; + + // Additional properties used during DID Resolution. + [key: string]: any; +} + +/** + * Represents the result of a Decentralized Identifier (DID) resolution operation. + * + * This type encapsulates the complete outcome of resolving a DID, including the resolution metadata, + * the DID document (if resolution is successful), and metadata about the DID document. + * + * @see {@link https://www.w3.org/TR/did-core/#did-resolution | DID Core Specification, § DID Resolution} + */ +export type DidResolutionResult = { + /** + * A JSON-LD context link, which provides the JSON-LD processor with the information necessary to + * interpret the resolution result JSON. The default context URL is + * 'https://w3id.org/did-resolution/v1'. + */ + '@context'?: 'https://w3id.org/did-resolution/v1' | string | (string | Record)[]; + + /** + * A metadata structure consisting of values relating to the results of the DID resolution + * process. + * + * This structure is REQUIRED, and in the case of an error in the resolution process, + * this MUST NOT be empty. If the resolution is not successful, this structure MUST contain an + * `error` property describing the error. + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-didresolutionmetadata | DID Core Specification, § DID Resolution Metadata} + */ + didResolutionMetadata: DidResolutionMetadata; + + /** + * The DID document resulting from the resolution process, if successful. + * + * If the `resolve` function was called and successful, this MUST contain a DID document + * corresponding to the DID. If the resolution is unsuccessful, this value MUST be empty. + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, § DID Document} + */ + didDocument: DidDocument | null; + + /** + * 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. + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-diddocumentmetadata | DID Core Specification, § DID Document Metadata} + */ + didDocumentMetadata: DidDocumentMetadata; +}; + +/** + * A DID Resource is either a DID Document, a DID Verification method or a DID Service + */ +export type DidResource = DidDocument | DidService | DidVerificationMethod; + +/** + * Services are used in DID documents to express ways of communicating with the DID subject or + * associated entities. A service can be any type of service the DID subject wants to advertise. + * + * @see {@link https://www.w3.org/TR/did-core/#services} + */ +export type DidService = { + /** + * Identifier of the service. + * + * The `id` property is REQUIRED. It MUST be a URI conforming to + * {@link https://datatracker.ietf.org/doc/html/rfc3986 | RFC3986} and MUST be unique within the + * DID document. + */ + id: string; + + /** + * The type of service being described. + * + * The `type` property is REQUIRED. It MUST be a string. To maximize interoperability, the value + * SHOULD be registered in the + * {@link https://www.w3.org/TR/did-spec-registries/ | DID Specification Registries}. Examples of + * service types can be found in + * {@link https://www.w3.org/TR/did-spec-registries/#service-types | § Service Types}. + */ + type: string; + + /** + * A URI that can be used to interact with the DID service. + * + * The value of the `serviceEndpoint` property MUST be a string, an object containing key/value + * pairs, or an array composed of strings or objects. All string values MUST be valid URIs + * conforming to {@link https://datatracker.ietf.org/doc/html/rfc3986 | RFC3986}. + */ + serviceEndpoint: DidServiceEndpoint | DidServiceEndpoint[]; + + // DID methods MAY include additional service properties. + [key: string]: any; +}; + +/** + * A service endpoint is a URI (Uniform Resource Identifier) that can be used to interact with the + * DID service. + * + * The value of the `serviceEndpoint` property MUST be a string or an object containing key/value + * pairs. All string values MUST be valid URIs conforming to + * {@link https://datatracker.ietf.org/doc/html/rfc3986 | RFC3986}. + * + * @see {@link https://www.w3.org/TR/did-core/#dfn-serviceendpoint | RFC3986, § 5.4 Services} + */ +export type DidServiceEndpoint = string | Record; + +/** + * Represents a verification method in the context of a DID document. + * + * A verification method is a mechanism by which a DID controller can cryptographically assert proof + * of ownership or control over a DID or DID document. This can include, but is not limited to, + * cryptographic public keys or other data that can be used to authenticate or authorize actions. + * + * @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods} + */ +export interface DidVerificationMethod { + /** + * The identifier of the verification method, which must be a URI. + */ + id: string; + + /** + * The type of the verification method. + * + * To maximize interoperability this value SHOULD be one of the valid verification method types + * registered in the {@link https://www.w3.org/TR/did-spec-registries/#verification-method-types | DID Specification Registries}. + */ + type: string; + + /** + * The DID of the entity that controls this verification method. + */ + controller: string; + + /** + * Express the public key in JWK format. + * + * (Optional) A JSON Web Key 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. + */ + // an encoded (e.g, base58) key with a Multibase-prefix that conforms to + // https://datatracker.ietf.org/doc/draft-multiformats-multibase/ + publicKeyMultibase?: string; +} + +/** + * Represents the various verification relationships defined in a DID document. + * + * These verification relationships indicate the intended usage of verification methods within a DID + * document. Each relationship signifies a different purpose or context in which a verification + * method can be used, such as authentication, assertionMethod, keyAgreement, capabilityDelegation, + * and capabilityInvocation. The array provides a standardized set of relationship names for + * consistent referencing and implementation across different DID methods. + * + * @see {@link https://www.w3.org/TR/did-core/#verification-relationships | DID Core Specification, § Verification Relationships} + */ +export enum DidVerificationRelationship { + /** + * Specifies how the DID subject is expected to be authenticated. This is commonly used for + * purposes like logging into a website or participating in challenge-response protocols. + * + * @see {@link https://www.w3.org/TR/did-core/#authentication | DID Core Specification, § Authentication} + */ + authentication = 'authentication', + + /** + * Specifies how the DID subject is expected to express claims, such as for issuing Verifiable + * Credentials. This relationship is typically used when the DID subject is the issuer of a + * credential. + * + * @see {@link https://www.w3.org/TR/did-core/#assertion | DID Core Specification, § Assertion} + */ + assertionMethod = 'assertionMethod', + + /** + * Specifies how an entity can generate encryption material to communicate confidentially with the + * DID subject. Often used in scenarios requiring secure communication channels. + * + * @see {@link https://www.w3.org/TR/did-core/#key-agreement | DID Core Specification, § Key Agreement} + */ + keyAgreement = 'keyAgreement', + + /** + * Specifies a mechanism used by the DID subject to delegate a cryptographic capability to another + * party. This can include delegating access to a specific resource or API. + * + * @see {@link https://www.w3.org/TR/did-core/#capability-delegation | DID Core Specification, § Capability Delegation} + */ + capabilityDelegation = 'capabilityDelegation', + + /** + * Specifies a verification method used by the DID subject to invoke a cryptographic capability. + * This is frequently associated with authorization actions, like updating the DID Document. + * + * @see {@link https://www.w3.org/TR/did-core/#capability-invocation | DID Core Specification, § Capability Invocation} + */ + capabilityInvocation = 'capabilityInvocation' +} \ No newline at end of file diff --git a/packages/dids/src/utils.ts b/packages/dids/src/utils.ts index d4ed0881f..fb9f3f54d 100644 --- a/packages/dids/src/utils.ts +++ b/packages/dids/src/utils.ts @@ -1,133 +1,368 @@ -import type { PublicKeyJwk } from '@web5/crypto'; -import { parse, type ParsedDID } from 'did-resolver'; - -import type { DidDocument, DidResource, VerificationMethod, DidService, DidServiceEndpoint, DwnServiceEndpoint } from './types.js'; - -export interface ParsedDid { - did: string - didUrl: string - method: string - id: string - path?: string - fragment?: string - query?: string - params?: ParsedDID['params'] -} +import type { Jwk } from '@web5/crypto'; + +import { computeJwkThumbprint } from '@web5/crypto'; -export const DID_REGEX = /^did:([a-z0-9]+):((?:(?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))*:)*((?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))+))((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\/[^#?]*)?([?][^#]*)?(#.*)?$/; +import { + DidService, + DidDocument, + DidVerificationMethod, + DidVerificationRelationship, +} from './types/did-core.js'; /** - * Retrieves services from a given DID document based on provided options. - * If no `id` or `type` filters are provided, all defined services are returned. + * Represents a Decentralized Web Node (DWN) service in a DID Document. * - * Note: The DID document must adhere to the W3C DID specification. + * A DWN DID service is a specialized type of DID service with the `type` set to + * `DecentralizedWebNode`. It includes specific properties `enc` and `sig` that are used to identify + * the public keys that can be used to interact with the DID Subject. The values of these properties + * are strings or arrays of strings containing one or more verification method `id` values present in + * the same DID document. If the `enc` and/or `sig` properties are an array of strings, an entity + * interacting with the DID subject is expected to use the verification methods in the order they + * are listed. * - * @param options - An object containing input parameters for retrieving services. - * @param options.didDocument - The DID document from which services are retrieved. - * @param options.id - Optional. A string representing the specific service ID to match. If provided, only the service with this ID will be returned. - * @param options.type - Optional. A string representing the specific service type to match. If provided, only the service(s) of this type will be returned. + * @example + * ```ts + * const service: DwnDidService = { + * id: 'did:example:123#dwn', + * type: 'DecentralizedWebNode', + * serviceEndpoint: 'https://dwn.tbddev.org/dwn0', + * enc: 'did:example:123#key-1', + * sig: 'did:example:123#key-2' + * } + * ``` * - * @returns An array of services. If no matching service is found, an empty array is returned. + * @see {@link https://identity.foundation/decentralized-web-node/spec/ | DIF Decentralized Web Node (DWN) Specification} + */ +export interface DwnDidService extends DidService { + /** + * One or more verification method `id` values that can be used to encrypt information + * intended for the DID subject. + */ + enc?: string | string[]; + + /** + * One or more verification method `id` values that will be used by the DID subject to sign data + * or by another entity to verify signatures created by the DID subject. + */ + sig: string | string[]; +} + +/** + * Retrieves services from a given DID document, optionally filtered by `id` or `type`. + * + * If no `id` or `type` filters are provided, all defined services are returned. + * + * The given DID Document must adhere to the + * {@link https://www.w3.org/TR/did-core/ | W3C DID Core Specification}. * * @example + * ```ts + * const didDocument = { ... }; // W3C DID document + * const services = getServices({ didDocument, type: 'DecentralizedWebNode' }); + * ``` * - * const didDoc = { ... }; // W3C DID document - * const services = getServices({ didDocument: didDoc, type: 'DecentralizedWebNode' }); + * @param params - An object containing input parameters for retrieving services. + * @param params.didDocument - The DID document from which services are retrieved. + * @param params.id - Optional. A string representing the specific service ID to match. If provided, only the service with this ID will be returned. + * @param params.type - Optional. A string representing the specific service type to match. If provided, only the service(s) of this type will be returned. + * @returns An array of services. If no matching service is found, an empty array is returned. */ -export function getServices(options: { - didDocument: DidDocument, - id?: string, - type?: string +export function getServices({ didDocument, id, type }: { + didDocument: DidDocument; + id?: string; + type?: string; }): DidService[] { - const { didDocument, id, type } = options ?? {}; - return didDocument?.service?.filter(service => { if (id && service.id !== id) return false; if (type && service.type !== type) return false; return true; - }) ?? [ ]; + }) ?? []; } -export function getVerificationMethodIds(options: { - didDocument: DidDocument, - publicKeyJwk?: PublicKeyJwk, - publicKeyMultibase?: string -}): string | undefined { - const { didDocument, publicKeyJwk, publicKeyMultibase } = options; - if (!didDocument) throw new Error(`Required parameter missing: 'didDocument'`); - if (!didDocument.verificationMethod) throw new Error('Given `didDocument` is missing `verificationMethod` entries.'); - - for (let method of didDocument.verificationMethod) { - if (publicKeyMultibase && 'publicKeyMultibase' in method) { - if (publicKeyMultibase === method.publicKeyMultibase) { - return method.id; +/** + * Retrieves a verification method object from a DID document if there is a match for the given + * public key. + * + * This function searches the verification methods in a given DID document for a match with the + * provided public key (either in JWK or multibase format). If a matching verification method is + * found it is returned. If no match is found `null` is returned. + * + * + * @example + * ```ts + * const didDocument = { + * // ... contents of a DID document ... + * }; + * const publicKeyJwk = { kty: 'OKP', crv: 'Ed25519', x: '...' }; + * + * const verificationMethod = await getVerificationMethodByKey({ + * didDocument, + * publicKeyJwk + * }); + * ``` + * + * @param params - An object containing input parameters for retrieving the verification method ID. + * @param params.didDocument - The DID document to search for the verification method. + * @param params.publicKeyJwk - The public key in JSON Web Key (JWK) format to match against the verification methods in the DID document. + * @param params.publicKeyMultibase - The public key as a multibase encoded string to match against the verification methods in the DID document. + * @returns A promise that resolves with the matching verification method, or `null` if no match is found. + * @throws Throws an `Error` if the `didDocument` parameter is missing or if the `didDocument` does not contain any verification methods. + */ +export async function getVerificationMethodByKey({ didDocument, publicKeyJwk, publicKeyMultibase }: { + didDocument: DidDocument; + publicKeyJwk?: Jwk; + publicKeyMultibase?: string; +}): Promise { + // Collect all verification methods from the DID document. + const verificationMethods = getVerificationMethods({ didDocument }); + + for (let method of verificationMethods) { + if (publicKeyJwk && method.publicKeyJwk) { + const publicKeyThumbprint = await computeJwkThumbprint({ jwk: publicKeyJwk }); + if (publicKeyThumbprint === await computeJwkThumbprint({ jwk: method.publicKeyJwk })) { + return method; } - } else if (publicKeyJwk && 'crv' in publicKeyJwk && - 'publicKeyJwk' in method && 'crv' in method.publicKeyJwk) { - if (publicKeyJwk.crv === method.publicKeyJwk.crv && - publicKeyJwk.x === method.publicKeyJwk.x) { - return method.id; + } else if (publicKeyMultibase && method.publicKeyMultibase) { + if (publicKeyMultibase === method.publicKeyMultibase) { + return method; } } } + + return null; +} + +/** + * Retrieves all verification methods from a given DID document, including embedded methods. + * + * This function consolidates all verification methods into a single array for easy access and + * processing. It checks both the primary `verificationMethod` array and the individual verification + * relationship properties `authentication`, `assertionMethod`, `keyAgreement`, + * `capabilityInvocation`, and `capabilityDelegation` for embedded methods. + * + * The given DID Document must adhere to the + * {@link https://www.w3.org/TR/did-core/ | W3C DID Core Specification}. + * + * @example + * ```ts + * const didDocument = { ... }; // W3C DID document + * const verificationMethods = getVerificationMethods({ didDocument }); + * ``` + * + * @param params - An object containing input parameters for retrieving verification methods. + * @param params.didDocument - The DID document from which verification methods are retrieved. + * @returns An array of `DidVerificationMethod`. If no verification methods are found, an empty array is returned. + * @throws Throws an `TypeError` if the `didDocument` parameter is missing. + */ +export function getVerificationMethods({ didDocument }: { + didDocument: DidDocument; +}): DidVerificationMethod[] { + if (!didDocument) throw new TypeError(`Required parameter missing: 'didDocument'`); + + const verificationMethods: DidVerificationMethod[] = []; + + // Check the 'verificationMethod' array. + verificationMethods.push(...didDocument.verificationMethod?.filter(isDidVerificationMethod) ?? []); + + // Check verification relationship properties for embedded verification methods. + Object.keys(DidVerificationRelationship).forEach((relationship) => { + verificationMethods.push( + ...(didDocument[relationship as keyof DidDocument] as (string | DidVerificationMethod)[]) + ?.filter(isDidVerificationMethod) ?? [] + ); + }); + + return verificationMethods; } /** - * Retrieves DID verification method types from a given DID document. + * Retrieves all DID verification method types from a given DID document. * - * Note: The DID document must adhere to the W3C DID specification. + * The given DID Document must adhere to the + * {@link https://www.w3.org/TR/did-core/ | W3C DID Core Specification}. * - * @param options - An object containing input parameters for retrieving types. - * @param options.didDocument - The DID document from which types are retrieved. + * @example + * ```ts + * const didDocument = { + * verificationMethod: [ + * { + * 'id' : 'did:example:123#key-0', + * 'type' : 'Ed25519VerificationKey2018', + * 'controller' : 'did:example:123', + * 'publicKeyBase58' : '3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J' + * }, + * { + * 'id' : 'did:example:123#key-1', + * 'type' : 'X25519KeyAgreementKey2019', + * 'controller' : 'did:example:123', + * 'publicKeyBase58' : 'FbQWLPRhTH95MCkQUeFYdiSoQt8zMwetqfWoxqPgaq7x' + * }, + * { + * 'id' : 'did:example:123#key-3', + * 'type' : 'JsonWebKey2020', + * 'controller' : 'did:example:123', + * 'publicKeyJwk' : { + * 'kty' : 'EC', + * 'crv' : 'P-256', + * 'x' : 'Er6KSSnAjI70ObRWhlaMgqyIOQYrDJTE94ej5hybQ2M', + * 'y' : 'pPVzCOTJwgikPjuUE6UebfZySqEJ0ZtsWFpj7YSPGEk' + * } + * } + * ] + * }, + * const vmTypes = getVerificationMethodTypes({ didDocument }); + * console.log(vmTypes); + * // Output: ['Ed25519VerificationKey2018', 'X25519KeyAgreementKey2019', 'JsonWebKey2020'] + * ``` * + * @param params - An object containing input parameters for retrieving types. + * @param params.didDocument - The DID document from which types are retrieved. * @returns An array of types. If no types were found, an empty array is returned. */ -export function getVerificationMethodTypes(options: { - didDocument: Record +export function getVerificationMethodTypes({ didDocument }: { + didDocument: DidDocument; }): string[] { - const { didDocument } = options; + // Collect all verification methods from the DID document. + const verificationMethods = getVerificationMethods({ didDocument }); - let types: string[] = []; + // Map to extract 'type' from each verification method. + const types = verificationMethods.map(method => method.type); - for (let key in didDocument) { - if (typeof didDocument[key] === 'object') { - types = types.concat(getVerificationMethodTypes({ - didDocument: didDocument[key] - })); + return [...new Set(types)]; // Return only unique types. +} - } else if (key === 'type') { - types.push(didDocument[key]); - } - } +/** + * Checks if a given object is a {@link DidService}. + * + * A {@link DidService} in the context of DID resources must include the properties `id`, `type`, + * and `serviceEndpoint`. The `serviceEndpoint` can be a `DidServiceEndpoint` or an array of + * `DidServiceEndpoint` objects. + * + * @example + * ```ts + * const service = { + * id: "did:example:123#service-1", + * type: "OidcService", + * serviceEndpoint: "https://example.com/oidc" + * }; + * + * if (isDidService(service)) { + * console.log('The object is a DidService'); + * } else { + * console.log('The object is not a DidService'); + * } + * ``` + * + * @param obj - The object to be checked. + * @returns `true` if `obj` is a `DidService`; otherwise, `false`. + */ +export function isDidService(obj: unknown): obj is DidService { + // Validate that the given value is an object. + if (!obj || typeof obj !== 'object' || obj === null) return false; - return [...new Set(types)]; // return only unique types + // Validate that the object has the necessary properties of DidService. + return 'id' in obj && 'type' in obj && 'serviceEndpoint' in obj; } /** - * Type guard function to check if the given endpoint is a DwnServiceEndpoint. + * Checks if a given object is a {@link DwnDidService}. + * + * A {@link DwnDidService} is defined as {@link DidService} object with a `type` of + * "DecentralizedWebNode" and `enc` and `sig` properties, where both properties are either strings + * or arrays of strings. + * + * @example + * ```ts + * const didDocument: DidDocument = { + * id: 'did:example:123', + * verificationMethod: [ + * { + * id: 'did:example:123#key-1', + * type: 'JsonWebKey2020', + * controller: 'did:example:123', + * publicKeyJwk: { ... } + * }, + * { + * id: 'did:example:123#key-2', + * type: 'JsonWebKey2020', + * controller: 'did:example:123', + * publicKeyJwk: { ... } + * } + * ], + * service: [ + * { + * id: 'did:example:123#dwn', + * type: 'DecentralizedWebNode', + * serviceEndpoint: 'https://dwn.tbddev.org/dwn0', + * enc: 'did:example:123#key-1', + * sig: 'did:example:123#key-2' + * } + * ] + * }; * - * @param key The endpoint to check. - * @returns True if the endpoint is a DwnServiceEndpoint, false otherwise. + * if (isDwnService(didDocument.service[0])) { + * console.log('The object is a DwnDidService'); + * } else { + * console.log('The object is not a DwnDidService'); + * } + * ``` + * + * @see {@link https://identity.foundation/decentralized-web-node/spec/ | Decentralized Web Node (DWN) Specification} + * + * @param obj - The object to be checked. + * @returns `true` if `obj` is a DwnDidService; otherwise, `false`. */ -export function isDwnServiceEndpoint(endpoint: string | DidServiceEndpoint | DidServiceEndpoint[]): endpoint is DwnServiceEndpoint { - return endpoint !== undefined && - typeof endpoint !== 'string' && - !Array.isArray(endpoint) && - 'nodes' in endpoint && - 'signingKeys' in endpoint; -} +export function isDwnDidService(obj: unknown): obj is DwnDidService { + // Validate that the given value is a {@link DidService}. + if (!isDidService(obj)) return false; + + // Validate that the `type` property is `DecentralizedWebNode`. + if (obj.type !== 'DecentralizedWebNode') return false; -export function parseDid({ didUrl }: { didUrl: string }): ParsedDid | undefined { - const parsedDid: ParsedDid = parse(didUrl); + // Validate that the given object has the `enc` and `sig` properties. + if (!('enc' in obj && 'sig' in obj)) return false; - return parsedDid; + // Validate that the `enc` and `sig` properties are either strings or arrays of strings. + const isStringOrStringArray = (prop: any): boolean => + typeof prop === 'string' || Array.isArray(prop) && prop.every(item => typeof item === 'string'); + return (isStringOrStringArray(obj.enc)) && (isStringOrStringArray(obj.sig)); } /** - * type guard for {@link VerificationMethod} - * @param didResource - the resource to check - * @returns true if the didResource is a `VerificationMethod` + * Checks if a given object is a DID Verification Method. + * + * A {@link DidVerificationMethod} in the context of DID resources must include the properties `id`, + * `type`, and `controller`. + * + * @example + * ```ts + * const resource = { + * id : "did:example:123#0", + * type : "JsonWebKey2020", + * controller : "did:example:123", + * publicKeyJwk : { ... } + * }; + * + * if (isDidVerificationMethod(resource)) { + * console.log('The resource is a DidVerificationMethod'); + * } else { + * console.log('The resource is not a DidVerificationMethod'); + * } + * ``` + * + * @param obj - The object to be checked. + * @returns `true` if `obj` is a `DidVerificationMethod`; otherwise, `false`. */ -export function isVerificationMethod(didResource: DidResource): didResource is VerificationMethod { - return didResource && 'id' in didResource && 'type' in didResource && 'controller' in didResource; +export function isDidVerificationMethod(obj: unknown): obj is DidVerificationMethod { + // Validate that the given value is an object. + if (!obj || typeof obj !== 'object' || obj === null) return false; + + // Validate that the object has the necessary properties of a DidVerificationMethod. + if (!('id' in obj && 'type' in obj && 'controller' in obj)) return false; + + if (typeof obj.id !== 'string') return false; + if (typeof obj.type !== 'string') return false; + if (typeof obj.controller !== 'string') return false; + + return true; } \ No newline at end of file diff --git a/packages/dids/tests/dht.spec.ts b/packages/dids/tests/dht.spec.ts deleted file mode 100644 index ea2f5cd7e..000000000 --- a/packages/dids/tests/dht.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import sinon from 'sinon'; -import { expect } from 'chai'; -import { Jose } from '@web5/crypto'; - -import type { DidKeySetVerificationMethodKey, DidService } from '../src/types.js'; - -import { DidDht } from '../src/dht.js'; -import { DidDhtMethod } from '../src/did-dht.js'; - -describe('DidDht', () => { - it('should create a put and parse a get request', async () => { - - const { document, keySet } = await DidDhtMethod.create(); - const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); - const publicCryptoKey = await Jose.jwkToCryptoKey({ key: identityKey.publicKeyJwk }); - const privateCryptoKey = await Jose.jwkToCryptoKey({ key: identityKey.privateKeyJwk }); - - const dhtPublishStub = sinon.stub(DidDht, 'publishDidDocument').resolves(true); - const dhtGetStub = sinon.stub(DidDht, 'getDidDocument').resolves(document); - - const published = await DidDht.publishDidDocument({ - keyPair: { - publicKey : publicCryptoKey, - privateKey : privateCryptoKey - }, - didDocument: document - }); - - expect(published).to.be.true; - - const gotDid = await DidDht.getDidDocument({ did: document.id }); - expect(gotDid.id).to.deep.equal(document.id); - expect(gotDid.capabilityDelegation).to.deep.equal(document.capabilityDelegation); - expect(gotDid.capabilityInvocation).to.deep.equal(document.capabilityInvocation); - expect(gotDid.keyAgreement).to.deep.equal(document.keyAgreement); - expect(gotDid.service).to.deep.equal(document.service); - expect(gotDid.verificationMethod.length).to.deep.equal(document.verificationMethod.length); - expect(gotDid.verificationMethod[0].id).to.deep.equal(document.verificationMethod[0].id); - expect(gotDid.verificationMethod[0].type).to.deep.equal(document.verificationMethod[0].type); - expect(gotDid.verificationMethod[0].controller).to.deep.equal(document.verificationMethod[0].controller); - expect(gotDid.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kid); - expect(gotDid.verificationMethod[0].publicKeyJwk.kty).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kty); - - expect(dhtPublishStub.calledOnce).to.be.true; - expect(dhtGetStub.calledOnce).to.be.true; - sinon.restore(); - }); - - describe('Codec', async () => { - it('encodes and decodes a DID Document as a DNS Packet', async () => { - const services: DidService[] = [{ - id : 'dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : 'https://example.com/dwn' - }]; - const secp = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'secp256k1'}); - const vm: DidKeySetVerificationMethodKey = { - publicKeyJwk : secp.publicKeyJwk, - privateKeyJwk : secp.privateKeyJwk, - relationships : ['authentication', 'assertionMethod'] - }; - const keySet = { - verificationMethodKeys: [vm], - }; - const { did, document } = await DidDhtMethod.create({ services: services, keySet: keySet }); - const encoded = await DidDht.toDnsPacket({ didDocument: document }); - const decoded = await DidDht.fromDnsPacket({ did, packet: encoded }); - - expect(document.id).to.deep.equal(decoded.id); - expect(document.capabilityDelegation).to.deep.equal(decoded.capabilityDelegation); - expect(document.capabilityInvocation).to.deep.equal(decoded.capabilityInvocation); - expect(document.keyAgreement).to.deep.equal(decoded.keyAgreement); - expect(document.service).to.deep.equal(decoded.service); - expect(document.verificationMethod.length).to.deep.equal(decoded.verificationMethod.length); - expect(document.verificationMethod[0].id).to.deep.equal(decoded.verificationMethod[0].id); - expect(document.verificationMethod[0].type).to.deep.equal(decoded.verificationMethod[0].type); - expect(document.verificationMethod[0].controller).to.deep.equal(decoded.verificationMethod[0].controller); - expect(document.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(decoded.verificationMethod[0].publicKeyJwk.kid); - expect(document.verificationMethod[0].publicKeyJwk.kty).to.deep.equal(decoded.verificationMethod[0].publicKeyJwk.kty); - expect(document.verificationMethod[1].id).to.deep.equal(decoded.verificationMethod[1].id); - expect(document.verificationMethod[1].type).to.deep.equal(decoded.verificationMethod[1].type); - expect(document.verificationMethod[1].controller).to.deep.equal(decoded.verificationMethod[1].controller); - expect(document.verificationMethod[1].publicKeyJwk.kid).to.deep.equal(decoded.verificationMethod[1].publicKeyJwk.kid); - expect(document.verificationMethod[1].publicKeyJwk.kty).to.deep.equal(decoded.verificationMethod[1].publicKeyJwk.kty); - }); - }); -}); \ No newline at end of file diff --git a/packages/dids/tests/did-dht.spec.ts b/packages/dids/tests/did-dht.spec.ts deleted file mode 100644 index 040f0be90..000000000 --- a/packages/dids/tests/did-dht.spec.ts +++ /dev/null @@ -1,512 +0,0 @@ -import type { PublicKeyJwk } from '@web5/crypto'; - -import sinon from 'sinon'; -import chai, { expect } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; - -import type { DidDhtKeySet } from '../src/did-dht.js'; -import type { DidDocument, DidKeySetVerificationMethodKey, DidService, PortableDid } from '../src/types.js'; - -import { DidDht } from '../src/dht.js'; -import { parseDid } from '../src/utils.js'; -import { DidDhtMethod } from '../src/did-dht.js'; -import { DidResolver } from '../src/did-resolver.js'; - -chai.use(chaiAsPromised); - -describe('DidDhtMethod', () => { - describe('generateJwkKeyPair()', () => { - it('generates Ed25519 JWK key pairs', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - - expect(ed25519KeyPair).to.exist; - expect(ed25519KeyPair).to.have.property('privateKeyJwk'); - expect(ed25519KeyPair).to.have.property('publicKeyJwk'); - expect(ed25519KeyPair.publicKeyJwk.kid).to.exist; - expect(ed25519KeyPair.publicKeyJwk.alg).to.equal('EdDSA'); - expect(ed25519KeyPair.publicKeyJwk.kty).to.equal('OKP'); - }); - - it('generates secp256k1 JWK key pairs', async () => { - const secp256k1KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'secp256k1' }); - - expect(secp256k1KeyPair).to.exist; - expect(secp256k1KeyPair).to.have.property('privateKeyJwk'); - expect(secp256k1KeyPair).to.have.property('publicKeyJwk'); - expect(secp256k1KeyPair.publicKeyJwk.kid).to.exist; - expect(secp256k1KeyPair.publicKeyJwk.alg).to.equal('ES256K'); - expect(secp256k1KeyPair.publicKeyJwk.kty).to.equal('EC'); - }); - - it('throws an error if an unsupported key algorithm is passed in', async () => { - await expect( - DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'unsupported' as any }) - ).to.be.rejectedWith(Error, 'unsupported'); - }); - }); - - describe('getDidIdentifierFragment()', () => { - it('should return the encoded identifier fragment for a given public key', async () => { - const testPublicKey: PublicKeyJwk = { - kty : 'OKP', - crv : 'Ed25519', - x : '9ZOlXQ7pZw7voYfQsrPPzvd1dA4ktXB5VbD1PWvl_jg', - ext : 'true', - 'key_ops' : ['verify'] - }; - - const result = await DidDhtMethod.getDidIdentifierFragment({ key: testPublicKey }); - - expect(result).to.equal('6sj4kzeq7fuo757bo9emfc6x355zk7yqr14zy6kisd4u449f9ahy'); - }); - }); - - describe('resolve()', () => { - it(`should return 'internalError' if DHT request throws error`, async () => { - const dhtDidResolutionStub = sinon.stub(DidDht, 'getDidDocument').rejects(new Error('Invalid SignedPacket bytes length, expected at least 72 bytes but got: 25')); - - const didResolutionResult = await DidDhtMethod.resolve({ didUrl: 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o' }); - const didResolutionMetadata = didResolutionResult.didResolutionMetadata; - expect(didResolutionMetadata.error).to.equal('internalError'); - - expect(dhtDidResolutionStub.calledOnce).to.be.true; - sinon.restore(); - }); - }); - - describe('key sets', () => { - it('should generate a key set with the identity key if no keys are passed in', async () => { - const keySet = await DidDhtMethod.generateKeySet(); - - expect(keySet).to.exist; - expect(keySet).to.have.property('verificationMethodKeys'); - expect(keySet).to.not.have.property('recoveryKey'); - expect(keySet).to.not.have.property('updateKey'); - expect(keySet).to.not.have.property('signingKey'); - expect(keySet.verificationMethodKeys).to.have.lengthOf(1); - expect(keySet.verificationMethodKeys[0].publicKeyJwk.kid).to.equal('0'); - }); - - it('should return the key set unmodified if only the identity key is passed in', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyId: '0', keyAlgorithm: 'Ed25519' }); - const identityKey: DidKeySetVerificationMethodKey = { - publicKeyJwk : ed25519KeyPair.publicKeyJwk, - privateKeyJwk : ed25519KeyPair.privateKeyJwk, - relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] - }; - - const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [identityKey] } }); - - expect(keySet).to.exist; - expect(keySet).to.have.property('verificationMethodKeys'); - expect(keySet).to.not.have.property('recoveryKey'); - expect(keySet).to.not.have.property('updateKey'); - expect(keySet).to.not.have.property('signingKey'); - expect(keySet.verificationMethodKeys).to.have.lengthOf(1); - expect(keySet.verificationMethodKeys[0]).to.deep.equal(identityKey); - }); - - it('should generate the identity key if non-identity keys are passed in', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - const vm: DidKeySetVerificationMethodKey = { - publicKeyJwk : ed25519KeyPair.publicKeyJwk, - privateKeyJwk : ed25519KeyPair.privateKeyJwk, - relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] - }; - - const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [vm] } }); - - expect(keySet).to.exist; - expect(keySet).to.have.property('verificationMethodKeys'); - expect(keySet).to.not.have.property('recoveryKey'); - expect(keySet).to.not.have.property('updateKey'); - expect(keySet).to.not.have.property('signingKey'); - expect(keySet.verificationMethodKeys).to.have.lengthOf(2); - - if (keySet.verificationMethodKeys[0].publicKeyJwk.kid === '0') { - expect(keySet.verificationMethodKeys[1].publicKeyJwk.kid).to.not.equal('0'); - } else { - expect(keySet.verificationMethodKeys[1].publicKeyJwk.kid).to.equal('0'); - } - }); - - it('should generate key ID values for provided keys, if missing', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - - // Remove the kid values from the key pair. - delete ed25519KeyPair.publicKeyJwk.kid; - delete ed25519KeyPair.privateKeyJwk.kid; - - const vm: DidKeySetVerificationMethodKey = { - publicKeyJwk : ed25519KeyPair.publicKeyJwk, - privateKeyJwk : ed25519KeyPair.privateKeyJwk, - relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] - }; - - const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [vm] } }); - - // Verify that the key ID values were generated. - expect(keySet.verificationMethodKeys[0].publicKeyJwk.kid).to.exist; - expect(keySet.verificationMethodKeys[0].privateKeyJwk.kid).to.exist; - expect(keySet.verificationMethodKeys[1].publicKeyJwk.kid).to.exist; - expect(keySet.verificationMethodKeys[1].privateKeyJwk.kid).to.exist; - }); - }); - - describe('DIDs', () => { - it('should generate a DID identifier given a public key jwk', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - const did = await DidDhtMethod.getDidIdentifier({ key: ed25519KeyPair.publicKeyJwk }); - - expect(did).to.exist; - expect(did).to.contain('did:dht:'); - }); - - it('should create a DID document without options', async () => { - const { document, keySet } = await DidDhtMethod.create(); - - expect(document).to.exist; - expect(document.id).to.contain('did:dht:'); - expect(document.verificationMethod).to.exist; - expect(document.verificationMethod).to.have.lengthOf(1); - expect(document.verificationMethod[0].id).to.equal(`${document.id}#0`); - expect(document.verificationMethod[0].publicKeyJwk).to.exist; - expect(document.verificationMethod[0].publicKeyJwk.kid).to.equal('0'); - - expect(document.service).to.not.exist; - expect(document.assertionMethod.length).to.equal(1); - expect(document.assertionMethod[0]).to.equal(`#0`); - expect(document.authentication.length).to.equal(1); - expect(document.authentication[0]).to.equal(`#0`); - expect(document.capabilityDelegation.length).to.equal(1); - expect(document.capabilityDelegation[0]).to.equal(`#0`); - expect(document.capabilityInvocation.length).to.equal(1); - expect(document.capabilityInvocation[0]).to.equal(`#0`); - - const ks = keySet as DidDhtKeySet; - expect(ks).to.exist; - const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); - expect(identityKey).to.exist; - expect(identityKey.publicKeyJwk).to.exist; - expect(identityKey.privateKeyJwk).to.exist; - expect(identityKey.publicKeyJwk.kid).to.equal('0'); - }); - - it('should create a DID document with a non identity key option', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - const keySet: DidDhtKeySet = { - verificationMethodKeys: [{ - publicKeyJwk : ed25519KeyPair.publicKeyJwk, - privateKeyJwk : ed25519KeyPair.privateKeyJwk, - relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] - }] - }; - - const { document } = await DidDhtMethod.create({ keySet }); - - expect(document).to.exist; - expect(document.id).to.contain('did:dht:'); - expect(document.verificationMethod).to.exist; - expect(document.verificationMethod).to.have.lengthOf(2); - expect(document.verificationMethod[1].id).to.equal(`${document.id}#0`); - expect(document.verificationMethod[1].publicKeyJwk).to.exist; - expect(document.verificationMethod[1].publicKeyJwk.kid).to.equal('0'); - - expect(document.service).to.not.exist; - expect(document.assertionMethod.length).to.equal(2); - expect(document.assertionMethod[1]).to.equal(`#0`); - expect(document.authentication.length).to.equal(2); - expect(document.authentication[1]).to.equal(`#0`); - expect(document.capabilityDelegation.length).to.equal(2); - expect(document.capabilityDelegation[1]).to.equal(`#0`); - expect(document.capabilityInvocation.length).to.equal(2); - expect(document.capabilityInvocation[1]).to.equal(`#0`); - - expect(keySet).to.exist; - const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); - expect(identityKey).to.exist; - expect(identityKey.publicKeyJwk).to.exist; - expect(identityKey.privateKeyJwk).to.exist; - expect(identityKey.publicKeyJwk.kid).to.equal('0'); - }); - - it('should create a DID document with services', async () => { - const services: DidService[] = [{ - id : 'agentId', - type : 'agent', - serviceEndpoint : 'https://example.com/agent' - }]; - const { document } = await DidDhtMethod.create({ services }); - - expect(document).to.exist; - expect(document.id).to.contain('did:dht:'); - expect(document.verificationMethod).to.exist; - expect(document.verificationMethod).to.have.lengthOf(1); - expect(document.verificationMethod[0].id).to.equal(`${document.id}#0`); - expect(document.verificationMethod[0].publicKeyJwk).to.exist; - expect(document.verificationMethod[0].publicKeyJwk.kid).to.equal('0'); - - expect(document.service).to.exist; - expect(document.service).to.have.lengthOf(1); - expect(document.service[0].id).to.equal(`${document.id}#agentId`); - expect(document.assertionMethod.length).to.equal(1); - expect(document.assertionMethod[0]).to.equal(`#0`); - expect(document.authentication.length).to.equal(1); - expect(document.authentication[0]).to.equal(`#0`); - expect(document.capabilityDelegation.length).to.equal(1); - expect(document.capabilityDelegation[0]).to.equal(`#0`); - expect(document.capabilityInvocation.length).to.equal(1); - expect(document.capabilityInvocation[0]).to.equal(`#0`); - }); - }); - - describe('DID publishing and resolving', function () { - it('should publish and DID should be resolvable', async () => { - const { document, keySet } = await DidDhtMethod.create(); - const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); - - const dhtDidPublishStub = sinon.stub(DidDht, 'publishDidDocument').resolves(true); - const dhtDidResolutionStub = sinon.stub(DidDhtMethod, 'resolve').resolves({ - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : document, - didDocumentMetadata : {}, - didResolutionMetadata : { - did: { - didString : document.id, - methodSpecificId : parseDid({ didUrl: document.id }).id, - method : 'dht' - } - } - }); - - const isPublished = await DidDhtMethod.publish({ identityKey, didDocument: document }); - expect(isPublished).to.be.true; - - const didResolutionResult = await DidDhtMethod.resolve({ didUrl: document.id }); - const didDocument = didResolutionResult.didDocument; - expect(didDocument.id).to.deep.equal(document.id); - - expect(dhtDidPublishStub.calledOnce).to.be.true; - expect(dhtDidResolutionStub.calledOnce).to.be.true; - sinon.restore(); - }); - - it('should create with publish and return a DID document', async () => { - const mockDocument: PortableDid = { - keySet : 'any' as any, - did : 'did:dht:123456789abcdefghi', - document : { - id : 'did:dht:123456789abcdefghi', - verificationMethod : [{ - id : 'did:dht:123456789abcdefghi#0', - type : 'JsonWebKey2020', - controller : 'did:dht:123456789abcdefghi', - publicKeyJwk : { - kty : 'OKP', - crv : 'Ed25519', - kid : '0', - x : 'O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik' - } - }], - assertionMethod : ['did:dht:123456789abcdefghi#0'], - authentication : ['did:dht:123456789abcdefghi#0'], - capabilityDelegation : ['did:dht:123456789abcdefghi#0'], - capabilityInvocation : ['did:dht:123456789abcdefghi#0'] - } - }; - const didDhtCreateStub = sinon.stub(DidDhtMethod, 'create').resolves(mockDocument); - - const { document } = await DidDhtMethod.create({ publish: true }); - const did = document.id; - - const dhtDidResolutionStub = sinon.stub(DidDhtMethod, 'resolve').resolves({ - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : document, - didDocumentMetadata : {}, - didResolutionMetadata : { - did: { - didString : 'did:dht:123456789abcdefgh', - methodSpecificId : '123456789abcdefgh', - method : 'dht' - } - } - }); - - const didResolutionResult = await DidDhtMethod.resolve({ didUrl: did }); - const resolvedDocument = didResolutionResult.didDocument; - expect(resolvedDocument.id).to.deep.equal(document.id); - expect(resolvedDocument.service).to.deep.equal(document.service); - expect(resolvedDocument.verificationMethod[0].id).to.deep.equal(document.verificationMethod[0].id); - expect(resolvedDocument.verificationMethod[0].type).to.deep.equal(document.verificationMethod[0].type); - expect(resolvedDocument.verificationMethod[0].controller).to.deep.equal(document.verificationMethod[0].controller); - expect(resolvedDocument.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kid); - - expect(didDhtCreateStub.calledOnce).to.be.true; - expect(dhtDidResolutionStub.calledOnce).to.be.true; - sinon.restore(); - }); - - it('should create with publish and DID should be resolvable', async () => { - const keySet: DidDhtKeySet = { - verificationMethodKeys: [{ - 'privateKeyJwk': { - 'd' : '2dPyiFL-vd21lxLKoyylz1nEK5EMByABqB2Fqio76sU', - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'ext' : 'true', - 'key_ops' : [ - 'sign' - ], - 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', - 'kid' : '0' - }, - 'publicKeyJwk': { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'ext' : 'true', - 'key_ops' : [ - 'verify' - ], - 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', - 'kid' : '0' - }, - 'relationships': [ - 'authentication', - 'assertionMethod', - 'capabilityInvocation', - 'capabilityDelegation' - ] - }] - }; - - const didDocument: DidDocument = { - 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', - 'verificationMethod' : [ - { - 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o#0', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'ext' : 'true', - 'key_ops' : [ - 'verify' - ], - 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', - 'kid' : '0' - } - } - ], - 'authentication': [ - '#0' - ], - 'assertionMethod': [ - '#0' - ], - 'capabilityInvocation': [ - '#0' - ], - 'capabilityDelegation': [ - '#0' - ] - }; - - const dhtDidPublishStub = sinon.stub(DidDhtMethod, 'publish').resolves(true); - const dhtDidResolutionStub = sinon.stub(DidDhtMethod, 'resolve').resolves({ - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument, - didDocumentMetadata : {}, - didResolutionMetadata : { - did: { - didString : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', - methodSpecificId : 'h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', - method : 'dht' - } - } - }); - - const portableDid = await DidDhtMethod.create({ publish: true, keySet: keySet }); - expect(portableDid).to.exist; - expect(portableDid.did).to.exist; - expect(portableDid.document).to.exist; - expect(portableDid.keySet).to.exist; - expect(portableDid.document.id).to.deep.equal(didDocument.id); - - const didResolutionResult = await DidDhtMethod.resolve({ didUrl: didDocument.id }); - expect(didDocument.id).to.deep.equal(didResolutionResult.didDocument.id); - - expect(dhtDidPublishStub.calledOnce).to.be.true; - expect(dhtDidResolutionStub.calledOnce).to.be.true; - sinon.restore(); - }); - }); - - describe('Integration with DidResolver', () => { - it('DidResolver resolves a did:dht DID', async () => { - // Previously published DID. - const did = 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o'; - const didDocument: DidDocument = { - 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', - 'verificationMethod' : [ - { - 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o#0', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'ext' : 'true', - 'key_ops' : [ - 'verify' - ], - 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', - 'kid' : '0' - } - } - ], - 'authentication': [ - '#0' - ], - 'assertionMethod': [ - '#0' - ], - 'capabilityInvocation': [ - '#0' - ], - 'capabilityDelegation': [ - '#0' - ] - }; - - const dhtDidResolutionStub = sinon.stub(DidDht, 'getDidDocument').resolves(didDocument); - - // Instantiate a DidResolver with the DidJwkMethod. - const didResolver = new DidResolver({ didResolvers: [DidDhtMethod] }); - - // Resolve the DID using the DidResolver. - const { didDocument: resolvedDocument } = await didResolver.resolve(did); - - // Verify that the resolved document matches the created document. - expect(resolvedDocument).to.deep.equal(didDocument); - - expect(dhtDidResolutionStub.calledOnce).to.be.true; - sinon.restore(); - }); - - it('returns an error for invalid didUrl', async () => { - const result = await DidDhtMethod.resolve({ didUrl: 'invalid' }); - expect(result).to.have.property('didResolutionMetadata').which.has.property('error', 'invalidDid'); - }); - - it('returns an error for unsupported method', async () => { - const result = await DidDhtMethod.resolve({ didUrl: 'did:unsupported:xyz' }); - expect(result).to.have.property('didResolutionMetadata').which.has.property('error', 'methodNotSupported'); - }); - }); - -}); \ No newline at end of file diff --git a/packages/dids/tests/did-ion.spec.ts b/packages/dids/tests/did-ion.spec.ts deleted file mode 100644 index 17b45de12..000000000 --- a/packages/dids/tests/did-ion.spec.ts +++ /dev/null @@ -1,805 +0,0 @@ -import type { JwkKeyPair } from '@web5/crypto'; - -import * as sinon from 'sinon'; -import chai, { expect } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; - -chai.use(chaiAsPromised); - -import type { - DidService, - DidDocument, - DwnServiceEndpoint, - DidKeySetVerificationMethodKey, -} from '../src/types.js'; - -import { didIonCreateTestVectors } from './fixtures/test-vectors/did-ion.js'; -import { DidIonCreateOptions, DidIonKeySet, DidIonMethod } from '../src/did-ion.js'; - -describe('DidIonMethod', () => { - let testRecoveryKey: JwkKeyPair; - let testUpdateKey: JwkKeyPair; - let testVerificationMethodKeys: DidKeySetVerificationMethodKey[]; - let testKeySet: DidIonKeySet; - - beforeEach(() => { - testRecoveryKey = structuredClone(didIonCreateTestVectors[0].input.keySet.recoveryKey) as JwkKeyPair; - testRecoveryKey.privateKeyJwk.kid = 'test-recovery-1'; - testRecoveryKey.publicKeyJwk.kid = 'test-recovery-1'; - - testUpdateKey = structuredClone(didIonCreateTestVectors[0].input.keySet.updateKey) as JwkKeyPair; - testUpdateKey.privateKeyJwk.kid = 'test-update-1'; - testUpdateKey.publicKeyJwk.kid = 'test-update-1'; - - testVerificationMethodKeys = structuredClone(didIonCreateTestVectors[0].input.keySet.verificationMethodKeys) as DidKeySetVerificationMethodKey[]; - testVerificationMethodKeys[0].publicKeyJwk!.kid = 'test-kid'; - - testKeySet = { - recoveryKey : testRecoveryKey, - updateKey : testUpdateKey, - verificationMethodKeys : testVerificationMethodKeys - }; - }); - - describe('anchor()', () => { - it('accepts a custom operations endpoint', async () => { - // Setup stub so that a mocked response is returned rather than calling over the network. - const mockResult = { mock: 'data' }; - const fetchStub = sinon.stub(global, 'fetch'); - // @ts-expect-error because we're only mocking ok and json() from global.fetch(). - fetchStub.returns(Promise.resolve({ - ok : true, - json : () => Promise.resolve(mockResult) - })); - - const resolutionResult = await DidIonMethod.anchor({ - challengeEnabled : false, - keySet : testKeySet, - operationsEndpoint : 'https://ion-service.com/operations', - services : [] - }); - fetchStub.restore(); - - expect(resolutionResult).to.deep.equal(mockResult); - expect(fetchStub.calledOnceWith( - 'https://ion-service.com/operations', - sinon.match({ - method : 'POST', - mode : 'cors', - body : sinon.match.string, - headers : { - 'Content-Type': 'application/json' - } - }) - )).to.be.true; - }); - - it('supports disabling POW/challenge', async () => { - // Setup stub so that a mocked response is returned rather than calling over the network. - const mockResult = { mock: 'data' }; - const fetchStub = sinon.stub(global, 'fetch'); - // @ts-expect-error because we're only mocking ok and json() from global.fetch(). - fetchStub.returns(Promise.resolve({ - ok : true, - json : () => Promise.resolve(mockResult) - })); - - const resolutionResult = await DidIonMethod.anchor({ - challengeEnabled : false, - keySet : testKeySet, - operationsEndpoint : 'https://ion-service.com/operations', - services : [] - }); - fetchStub.restore(); - - expect(resolutionResult).to.deep.equal(mockResult); - }); - }); - - describe('create()', () => { - it('creates a DID with Ed25519 keys, by default', async () => { - const portableDid = await DidIonMethod.create(); - - // Verify expected result. - expect(portableDid).to.have.property('did'); - expect(portableDid).to.have.property('canonicalId'); - expect(portableDid).to.have.property('document'); - expect(portableDid).to.have.property('keySet'); - - const keySet = portableDid.keySet as DidIonKeySet; - - expect(keySet).to.have.property('verificationMethodKeys'); - expect(keySet.verificationMethodKeys).to.have.length(1); - expect(keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); - expect(keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('alg', 'EdDSA'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('crv', 'Ed25519'); - - expect(keySet).to.have.property('recoveryKey'); - expect(keySet.recoveryKey).to.have.property('publicKeyJwk'); - expect(keySet.recoveryKey).to.have.property('privateKeyJwk'); - - expect(keySet).to.have.property('updateKey'); - expect(keySet.recoveryKey).to.have.property('publicKeyJwk'); - expect(keySet.recoveryKey).to.have.property('privateKeyJwk'); - }); - - it('creates a DID with secp256k1 keys, if specified', async () => { - const portableDid = await DidIonMethod.create({ keyAlgorithm: 'secp256k1' }); - - // Verify expected result. - expect(portableDid).to.have.property('did'); - expect(portableDid).to.have.property('canonicalId'); - expect(portableDid).to.have.property('document'); - expect(portableDid).to.have.property('keySet'); - - const keySet = portableDid.keySet as DidIonKeySet; - - expect(keySet).to.have.property('verificationMethodKeys'); - expect(keySet.verificationMethodKeys).to.have.length(1); - expect(keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); - expect(keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('alg', 'ES256K'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('crv', 'secp256k1'); - - expect(keySet).to.have.property('recoveryKey'); - expect(keySet.recoveryKey).to.have.property('publicKeyJwk'); - expect(keySet.recoveryKey).to.have.property('privateKeyJwk'); - - expect(keySet).to.have.property('updateKey'); - expect(keySet.recoveryKey).to.have.property('publicKeyJwk'); - expect(keySet.recoveryKey).to.have.property('privateKeyJwk'); - }); - - it('uses specified key ID values for key set, if given', async () => { - const portableDid = await DidIonMethod.create({ - keyAlgorithm : 'Ed25519', - keySet : testKeySet - }); - - const keySet = portableDid.keySet as DidIonKeySet; - expect(keySet.recoveryKey?.privateKeyJwk.kid).to.equal('test-recovery-1'); - expect(keySet.recoveryKey?.publicKeyJwk.kid).to.equal('test-recovery-1'); - expect(keySet.updateKey?.privateKeyJwk.kid).to.equal('test-update-1'); - expect(keySet.updateKey?.publicKeyJwk.kid).to.equal('test-update-1'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk?.kid).to.equal('test-kid'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk?.kid).to.equal('test-kid'); - }); - - it('generates key ID values for key set, if missing', async () => { - delete testRecoveryKey.privateKeyJwk.kid; - delete testRecoveryKey.publicKeyJwk.kid; - delete testUpdateKey.privateKeyJwk.kid; - delete testUpdateKey.publicKeyJwk.kid; - delete testVerificationMethodKeys[0].publicKeyJwk!.kid; - - const portableDid = await DidIonMethod.create({ - keyAlgorithm : 'Ed25519', - keySet : testKeySet - }); - - const keySet = portableDid.keySet as DidIonKeySet; - expect(keySet.recoveryKey?.privateKeyJwk.kid).to.equal('AEOG_sxXHhCA1Fel8fpheyLxAcW89D7V86lMcJXc500'); - expect(keySet.recoveryKey?.publicKeyJwk.kid).to.equal('AEOG_sxXHhCA1Fel8fpheyLxAcW89D7V86lMcJXc500'); - expect(keySet.updateKey?.privateKeyJwk.kid).to.equal('_1CySHVtk6tNXke3t_7NLI2nvaVlH5GFyuO9HjQCRKs'); - expect(keySet.updateKey?.publicKeyJwk.kid).to.equal('_1CySHVtk6tNXke3t_7NLI2nvaVlH5GFyuO9HjQCRKs'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk?.kid).to.equal('OAPj7ObrEJFgVNA2rrkPM5A-vYVsH_lyz4LgOUdJBa8'); - expect(keySet.verificationMethodKeys?.[0].publicKeyJwk?.kid).to.equal('OAPj7ObrEJFgVNA2rrkPM5A-vYVsH_lyz4LgOUdJBa8'); - }); - - it('given key IDs are automatically prefixed with hash symbol (#) in DID document', async () => { - testVerificationMethodKeys[0].publicKeyJwk!.kid = 'noPrefixInput'; - - const portableDid = await DidIonMethod.create({ - keyAlgorithm : 'Ed25519', - keySet : testKeySet - }); - - expect(portableDid.document.authentication).includes(`#noPrefixInput`); - expect(portableDid.document.assertionMethod).includes(`#noPrefixInput`); - expect(portableDid.document.verificationMethod![0].id).to.equal(`#noPrefixInput`); - }); - - it('accepts recovery and update key IDs that include a hash symbol (#)', async () => { - testRecoveryKey.privateKeyJwk.kid = '#test-recovery-1'; - testRecoveryKey.publicKeyJwk.kid = '#test-recovery-1'; - - await expect( - DidIonMethod.create({ keySet: { recoveryKey: testRecoveryKey } }) - ).to.eventually.be.fulfilled; - - testUpdateKey.privateKeyJwk.kid = '#test-update-1'; - testUpdateKey.publicKeyJwk.kid = '#test-update-1'; - - await expect( - DidIonMethod.create({ keySet: { updateKey: testUpdateKey } }) - ).to.eventually.eventually.be.fulfilled; - }); - - it('accepts verification method key IDs that start with a hash symbol (#)', async () => { - testVerificationMethodKeys[0].publicKeyJwk!.kid = '#prefixedKid'; - - const portableDid = await DidIonMethod.create({ - keyAlgorithm : 'Ed25519', - keySet : testKeySet - }); - - expect(portableDid.document.authentication).includes(`#prefixedKid`); - expect(portableDid.document.assertionMethod).includes(`#prefixedKid`); - expect(portableDid.document.verificationMethod![0].id).to.equal(`#prefixedKid`); - }); - - it('throws an error if verification method key IDs contain a hash symbol (#)', async () => { - testVerificationMethodKeys[0].publicKeyJwk!.kid = 'test#kid'; - - await expect( - DidIonMethod.create({ keySet: { verificationMethodKeys: testVerificationMethodKeys } }) - ).to.eventually.eventually.be.rejectedWith(Error, 'IdNotUsingBase64UrlCharacterSet'); - }); - - it('creates a DID with service entries, if specified', async () => { - const dwnEndpoints = [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1' - ]; - - const services: DidService[] = [{ - 'id' : 'dwn', - 'type' : 'DecentralizedWebNode', - 'serviceEndpoint' : { - 'nodes' : dwnEndpoints, - 'signingKeys' : ['#dwn-sig'], - 'encryptionKeys' : ['#dwn-enc'] - } - }]; - - const portableDid = await DidIonMethod.create({ services }); - - const dwnService = portableDid.document.service?.[0]; - expect(dwnService).to.have.property('type', 'DecentralizedWebNode'); - expect(dwnService?.serviceEndpoint).to.have.property('nodes'); - expect(dwnService?.serviceEndpoint).to.have.property('signingKeys'); - expect(dwnService?.serviceEndpoint).to.have.property('encryptionKeys'); - }); - - it('given service IDs are automatically prefixed with hash symbol (#) in DID document', async () => { - const dwnEndpoints = ['https://dwn.tbddev.test/dwn0']; - - const services: DidService[] = [{ - 'id' : 'dwn', - 'type' : 'DecentralizedWebNode', - 'serviceEndpoint' : { - 'nodes': dwnEndpoints - } - }]; - - const portableDid = await DidIonMethod.create({ services }); - - const dwnService = portableDid.document.service?.[0]; - expect(dwnService).to.have.property('id', '#dwn'); - }); - - it('accepts service IDs that start with a hash symbol (#)', async () => { - const services: DidService[] = [{ - 'id' : '#dwn', - 'type' : 'DecentralizedWebNode', - 'serviceEndpoint' : { } - }]; - - const portableDid = await DidIonMethod.create({ services }); - - const dwnService = portableDid.document.service?.[0]; - expect(dwnService).to.have.property('id', '#dwn'); - }); - - it('throws an error if verification method key IDs contain a hash symbol (#)', async () => { - const services = [{ - 'id' : 'foo#bar', - 'type' : 'DecentralizedWebNode', - 'serviceEndpoint' : { } - }]; - - await expect( - DidIonMethod.create({ services }) - ).to.eventually.eventually.be.rejectedWith(Error, 'IdNotUsingBase64UrlCharacterSet'); - }); - - for (const vector of didIonCreateTestVectors ) { - it(`passes test vector ${vector.id}`, async () => { - const portableDid = await DidIonMethod.create(vector.input as DidIonCreateOptions); - - expect(portableDid).to.deep.equal(vector.output); - }); - } - }); - - describe('decodeLongFormDid()', () => { - it('returns ION create request with services', async () => { - const longFormDid = 'did:ion:EiC94n5yoQEpRfmT6Co7Q4GCUWmuAK4UzDFpk5W4_BzP4A:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiSl9lek5jT2pqNTIxbWEtN18tanFWdC1JODRzendSRTJzMGFCN3h2R1ljYyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6eyJub2RlcyI6WyJodHRwczovL2R3bi50YmRkZXYudGVzdC9kd24wIl19LCJ0eXBlIjoiRGVjZW50cmFsaXplZFdlYk5vZGUifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUR2WHJNV3V0LTNIUWpsTm5JbHlKR2F0WVBsNWo2MFp3SnB4cG9wOHk2RGxBIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlEd2VVOG82clVZY1lCNHQzaHBoaXdtZFpxZWRVdm5zQ251a2xMdWVfOVFOUSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpREhIb3E0bFhoMndjWUZNSnNuNkZBN3otVk9sTHBiU20xcnRNOXlJMUQzd3cifX0'; - - const createRequest = await DidIonMethod.decodeLongFormDid({ didUrl: longFormDid}); - - expect(createRequest).to.have.property('delta'); - expect(createRequest).to.have.property('suffixData'); - expect(createRequest).to.have.property('type', 'create'); - - expect(createRequest.delta).to.have.property('updateCommitment', 'EiDvXrMWut-3HQjlNnIlyJGatYPl5j60ZwJpxpop8y6DlA'); - expect(createRequest.suffixData).to.have.property('recoveryCommitment', 'EiDHHoq4lXh2wcYFMJsn6FA7z-VOlLpbSm1rtM9yI1D3ww'); - - expect(createRequest.delta.patches[0].document.services).to.deep.equal([{ - 'id' : 'dwn', - 'type' : 'DecentralizedWebNode', - 'serviceEndpoint' : { - 'nodes': ['https://dwn.tbddev.test/dwn0'] - } - }]); - }); - - it('returns output that matches ION create request', async () => { - const services: DidService[] = [{ - 'id' : 'dwn', - 'type' : 'DecentralizedWebNode', - 'serviceEndpoint' : { - 'nodes': ['https://dwn.tbddev.test/dwn0'] - } - }]; - - const { did } = await DidIonMethod.create({ - keySet: testKeySet, - services - }); - - // @ts-expect-error because we're intentionally accessing a private method. - const ionDocument = await DidIonMethod.createIonDocument({ - keySet: testKeySet, - services - }); - - if (!testKeySet.recoveryKey) throw new Error('Type guard'); - if (!testKeySet.updateKey) throw new Error('Type guard'); - // @ts-expect-error because we're intentionally accessing a private method. - const createRequest = await DidIonMethod.getIonCreateRequest({ - ionDocument, - recoveryPublicKeyJwk : testKeySet.recoveryKey.publicKeyJwk, - updatePublicKeyJwk : testKeySet.updateKey.publicKeyJwk - }); - - if (!did) throw Error('Type guard'); - const decodedLongFormDid = await DidIonMethod.decodeLongFormDid({ didUrl: did }); - - expect(decodedLongFormDid).to.deep.equal(createRequest); - }); - }); - - describe('generateDwnOptions()', () => { - it('returns keys and services with two DWN URLs', async () => { - const ionCreateOptions = await DidIonMethod.generateDwnOptions({ - serviceEndpointNodes: [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1' - ]}); - - expect(ionCreateOptions).to.have.property('keySet'); - expect(ionCreateOptions.keySet.verificationMethodKeys).to.have.length(2); - const authorizationKey = ionCreateOptions.keySet.verificationMethodKeys.find(key => key.privateKeyJwk.kid === '#dwn-sig'); - expect(authorizationKey).to.exist; - const encryptionKey = ionCreateOptions.keySet.verificationMethodKeys.find(key => key.privateKeyJwk.kid === '#dwn-enc'); - expect(encryptionKey).to.exist; - - expect(ionCreateOptions).to.have.property('services'); - expect(ionCreateOptions.services).to.have.length(1); - - const [ service ] = ionCreateOptions.services; - expect(service.id).to.equal('#dwn'); - expect(service).to.have.property('serviceEndpoint'); - - const serviceEndpoint = service.serviceEndpoint as DwnServiceEndpoint; - expect(serviceEndpoint).to.have.property('nodes'); - expect(serviceEndpoint.nodes).to.have.length(2); - expect(serviceEndpoint).to.have.property('signingKeys'); - expect(serviceEndpoint.signingKeys[0]).to.equal(authorizationKey.publicKeyJwk.kid); - expect(serviceEndpoint).to.have.property('encryptionKeys'); - expect(serviceEndpoint.encryptionKeys[0]).to.equal(encryptionKey.publicKeyJwk.kid); - }); - - it('returns keys and services with one DWN URLs', async () => { - const ionCreateOptions = await DidIonMethod.generateDwnOptions({ - serviceEndpointNodes: [ - 'https://dwn.tbddev.test/dwn0' - ]}); - - const [ service ] = ionCreateOptions.services!; - expect(service.id).to.equal('#dwn'); - expect(service).to.have.property('serviceEndpoint'); - - const serviceEndpoint = service.serviceEndpoint as DwnServiceEndpoint; - expect(serviceEndpoint).to.have.property('nodes'); - expect(serviceEndpoint.nodes).to.have.length(1); - expect(serviceEndpoint).to.have.property('signingKeys'); - expect(serviceEndpoint).to.have.property('encryptionKeys'); - }); - - it('returns keys and services with 0 DWN URLs', async () => { - const ionCreateOptions = await DidIonMethod.generateDwnOptions({ serviceEndpointNodes: [] }); - - const [ service ] = ionCreateOptions.services!; - expect(service.id).to.equal('#dwn'); - expect(service).to.have.property('serviceEndpoint'); - - const serviceEndpoint = service.serviceEndpoint as DwnServiceEndpoint; - expect(serviceEndpoint).to.have.property('nodes'); - expect(serviceEndpoint.nodes).to.have.length(0); - expect(serviceEndpoint).to.have.property('signingKeys'); - expect(serviceEndpoint).to.have.property('encryptionKeys'); - }); - }); - - describe('generateJwkKeyPair()', () => { - it('generates an Ed25519 JwkKeyPair', async () => { - const jwkKeyPair = await DidIonMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - expect(jwkKeyPair).to.be.an('object'); - expect(jwkKeyPair.privateKeyJwk.kty).to.equal('OKP'); - if (!('crv' in jwkKeyPair.privateKeyJwk)) throw new Error('Type guard'); - expect(jwkKeyPair.privateKeyJwk.crv).to.equal('Ed25519'); - if (!('crv' in jwkKeyPair.publicKeyJwk)) throw new Error('Type guard'); - expect(jwkKeyPair.publicKeyJwk.kty).to.equal('OKP'); - expect(jwkKeyPair.publicKeyJwk.crv).to.equal('Ed25519'); - }); - - it('generates a secp256k1 JwkKeyPair', async () => { - const jwkKeyPair = await DidIonMethod.generateJwkKeyPair({ keyAlgorithm: 'secp256k1' }); - expect(jwkKeyPair).to.be.an('object'); - expect(jwkKeyPair.privateKeyJwk.kty).to.equal('EC'); - if (!('crv' in jwkKeyPair.privateKeyJwk)) throw new Error('Type guard'); - expect(jwkKeyPair.privateKeyJwk.crv).to.equal('secp256k1'); - expect(jwkKeyPair.publicKeyJwk.kty).to.equal('EC'); - if (!('crv' in jwkKeyPair.publicKeyJwk)) throw new Error('Type guard'); - expect(jwkKeyPair.publicKeyJwk.crv).to.equal('secp256k1'); - }); - - it('generates a JwkKeyPair with a custom key ID', async () => { - const keyId = 'custom-key-id'; - const jwkKeyPair = await DidIonMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519', keyId }); - expect(jwkKeyPair.privateKeyJwk.kid).to.equal(keyId); - expect(jwkKeyPair.publicKeyJwk.kid).to.equal(keyId); - }); - - it('throws an error for unsupported key algorithm', async () => { - await expect( - // @ts-expect-error because an invalid algorithm is being intentionally specified. - DidIonMethod.generateJwkKeyPair({ keyAlgorithm: 'unsupported-algorithm' }) - ).to.eventually.be.rejectedWith(Error, 'Unsupported crypto algorithm'); - }); - }); - - describe('getDefaultSigningKey()', () => { - it('returns the did:ion default signing key from long form DID, when present', async () => { - const partialDidDocument: Partial = { - id : 'did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUJOX1JaeXZka1lmb2tkRlV5MTNiWnFwR2gzdmhZU3IxVnh3MmVieE5uQzZRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ', - service : [ - { - id : '#dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : { - encryptionKeys: [ - '#dwn-enc' - ], - nodes: [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1' - ], - signingKeys: [ - '#dwn-sig' - ] - } - } - ], - }; - - const defaultSigningKeyId = await DidIonMethod.getDefaultSigningKey({ - didDocument: partialDidDocument as DidDocument - }); - - expect(defaultSigningKeyId).to.equal('did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUJOX1JaeXZka1lmb2tkRlV5MTNiWnFwR2gzdmhZU3IxVnh3MmVieE5uQzZRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ#dwn-sig'); - }); - - it('returns the did:ion default signing key from short form DID, when present', async () => { - const partialDidDocument: Partial = { - id : 'did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUJOX1JaeXZka1lmb2tkRlV5MTNiWnFwR2gzdmhZU3IxVnh3MmVieE5uQzZRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ', - service : [ - { - id : '#dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : { - encryptionKeys: [ - '#dwn-enc' - ], - nodes: [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1' - ], - signingKeys: [ - '#dwn-sig' - ] - } - } - ], - }; - - const defaultSigningKeyId = await DidIonMethod.getDefaultSigningKey({ - didDocument: partialDidDocument as DidDocument - }); - - expect(defaultSigningKeyId).to.equal('did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUJOX1JaeXZka1lmb2tkRlV5MTNiWnFwR2gzdmhZU3IxVnh3MmVieE5uQzZRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ#dwn-sig'); - }); - - it(`returns first 'authentication' key if DID document is missing 'signingKeys'`, async () => { - const partialDidDocument: Partial = { - id : 'did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUJOX1JaeXZka1lmb2tkRlV5MTNiWnFwR2gzdmhZU3IxVnh3MmVieE5uQzZRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ', - 'verificationMethod' : [ - { - id : '#OAPj7ObrEJFgVNA2rrkPM5A-vYVsH_lyz4LgOUdJBa8', - type : 'JsonWebKey2020', - controller : 'did:ion:EiBP6JaGhwYye4zz-wdeXR2JWl1JclaVDPA7FDgpzM8-ig:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJPQVBqN09ickVKRmdWTkEycnJrUE01QS12WVZzSF9seXo0TGdPVWRKQmE4IiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNUMEh4ZGNTRHkwQ0t5eHV4VkZ3d3A3N3YteEJkSkVRLUVtSXhZUGR4VnV3IiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ', - publicKeyJwk : { - crv : 'Ed25519', - kty : 'OKP', - x : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' - } - } - ], - service: [ - { - id : '#dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : { - encryptionKeys: [ - '#dwn-enc' - ], - nodes: [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1' - ] - } - } - ], - authentication: [ - '#OAPj7ObrEJFgVNA2rrkPM5A-vYVsH_lyz4LgOUdJBa8' - ], - }; - - const defaultSigningKeyId = await DidIonMethod.getDefaultSigningKey({ - didDocument: partialDidDocument as DidDocument - }); - - expect(defaultSigningKeyId).to.equal('did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUJOX1JaeXZka1lmb2tkRlV5MTNiWnFwR2gzdmhZU3IxVnh3MmVieE5uQzZRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ#OAPj7ObrEJFgVNA2rrkPM5A-vYVsH_lyz4LgOUdJBa8'); - }); - - it(`returns short form DID when DID has been anchored/published`, async () => { - let partialDidDocument: Partial = { - id : 'did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww', - service : [ - { - id : '#dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : { - encryptionKeys: [ - '#dwn-enc' - ], - nodes: [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1' - ], - signingKeys: [ - '#dwn-sig' - ] - } - } - ], - }; - - let defaultSigningKeyId = await DidIonMethod.getDefaultSigningKey({ - didDocument: partialDidDocument as DidDocument - }); - - expect(defaultSigningKeyId).to.equal('did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww#dwn-sig'); - - partialDidDocument = { - 'id' : 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ', - 'service' : [], - 'verificationMethod' : [ - { - 'id' : '#dwn-sig', - 'controller' : 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ', - 'type' : 'JsonWebKey2020', - 'publicKeyJwk' : { - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'Sy0lk6pMXC10WyIh4g8sLz1loL8ImzLcqmFW2267IXc' - } - } - ], - 'authentication': [ - '#dwn-sig' - ] - }; - - defaultSigningKeyId = await DidIonMethod.getDefaultSigningKey({ - didDocument: partialDidDocument as DidDocument - }); - - expect(defaultSigningKeyId).to.equal('did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ#dwn-sig'); - }); - - it(`returns undefined if DID document is missing 'signingKeys' and 'authentication'`, async () => { - const partialDidDocument: Partial = { - id : 'did:ion:EiAO3IAedMSHaGOZIuIVwLEBHd0SEuWwt2h00dbiGD7Hww:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRFZyOHUzVWxvOGtNVUx3WEh6VUdSMFdGdy1ROU14el8zRGQyQXEwVF9KR3cifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUJOX1JaeXZka1lmb2tkRlV5MTNiWnFwR2gzdmhZU3IxVnh3MmVieE5uQzZRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlEOEQtdjlsVjdqTzZ3ajVjSXVsRXRwZEFqaHE5NEFnTm54SlozWThVUnlrZyJ9fQ', - service : [ - { - id : '#dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : { - encryptionKeys: [ - '#dwn-enc' - ], - nodes: [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1' - ] - } - } - ], - }; - - const defaultSigningKeyId = await DidIonMethod.getDefaultSigningKey({ - didDocument: partialDidDocument as DidDocument - }); - - expect(defaultSigningKeyId).to.be.undefined; - }); - - it(`throws error if DID document is missing 'id' property`, async () => { - const partialDidDocument: Partial = {}; - - await expect( - DidIonMethod.getDefaultSigningKey({ - didDocument: partialDidDocument as DidDocument - }) - ).to.eventually.be.rejectedWith(Error, `DID document is missing 'id' property`); - }); - }); - - describe('resolve()', () => { - it('resolves published short form ION DIDs', async() => { - const did = 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ'; - const resolutionResult = await DidIonMethod.resolve({ didUrl: did }); - - expect(resolutionResult).to.have.property('@context'); - expect(resolutionResult).to.have.property('didDocument'); - expect(resolutionResult).to.have.property('didDocumentMetadata'); - - expect(resolutionResult.didDocument).to.have.property('id', did); - expect(resolutionResult.didDocumentMetadata.method).to.have.property('published', true); - }); - - it('returns internalError error with unpublished short form ION DIDs', async() => { - const did = 'did:ion:EiBCi7lnGtotBsFkbI_lQskQZLk_GPelU0C5-nRB4_nMfA'; - const resolutionResult = await DidIonMethod.resolve({ didUrl: did }); - - expect(resolutionResult).to.have.property('@context'); - expect(resolutionResult).to.have.property('didDocument'); - expect(resolutionResult).to.have.property('didDocumentMetadata'); - - expect(resolutionResult.didResolutionMetadata).to.have.property('error', 'internalError'); - }); - - it('resolves published long form ION DIDs', async() => { - const did = 'did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19'; - const resolutionResult = await DidIonMethod.resolve({ didUrl: did }); - - expect(resolutionResult).to.have.property('@context'); - expect(resolutionResult).to.have.property('didDocument'); - expect(resolutionResult).to.have.property('didDocumentMetadata'); - - expect(resolutionResult.didDocument).to.have.property('id', did); - expect(resolutionResult.didDocumentMetadata.method).to.have.property('published', true); - }); - - - it('resolves unpublished long form ION DIDs', async() => { - const did = 'did:ion:EiBCi7lnGtotBsFkbI_lQskQZLk_GPelU0C5-nRB4_nMfA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkd24tc2lnIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4Ijoib0xVQmdKUnA1dlVfSTdfOXB3UTFkb2IwSWg2VjUwT2FrenNOY2R6Uk1CbyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpQlRRYlV6cmlTU3FEVVpPb0JvUTZWek5wWFRvQWNtSjNHMlBIZzJ3ZXpFcHcifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaURLSFlkRFRpT3lCTWRORWtBcGJtUklHU1ExOFctUHFUeGlrZ0IzX1RpSlVBIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlBb2pZYzV6eTR2RFZFdElnS1lzWHgtdnBnZzNEeXBUOW0tRmtfMXZ0WHBkQSJ9fQ'; - const resolutionResult = await DidIonMethod.resolve({ didUrl: did }); - - expect(resolutionResult).to.have.property('@context'); - expect(resolutionResult).to.have.property('didDocument'); - expect(resolutionResult).to.have.property('didDocumentMetadata'); - - expect(resolutionResult.didDocument).to.have.property('id', did); - expect(resolutionResult.didDocumentMetadata.method).to.have.property('published', false); - }); - - it('returns internalError if custom DID resolver returns invalid response', async () => { - // Setup stub so that a mocked response is returned rather than calling over the network. - const mockResult = ` - 404 Not Found - -

404 Not Found

-
nginx/1.25.1
- - `; - const fetchStub = sinon.stub(global, 'fetch'); - // @ts-expect-error because we're only mocking ok and json() from global.fetch(). - fetchStub.returns(Promise.resolve({ - ok : false, - json : () => Promise.reject(JSON.parse(mockResult)) - })); - - const did = 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ'; - const resolutionResult = await DidIonMethod.resolve({ - didUrl : did, - resolutionOptions : { resolutionEndpoint: 'https://dev.uniresolver.io/7.5/identifiers' } - }); - fetchStub.restore(); - - expect(resolutionResult).to.have.property('@context'); - expect(resolutionResult).to.have.property('didDocument'); - expect(resolutionResult).to.have.property('didDocumentMetadata'); - - expect(resolutionResult.didResolutionMetadata).to.have.property('error', 'internalError'); - }); - - it(`returns methodNotSupported if DID method is not 'ion'`, async () => { - const did = 'did:key:z6MkvEvogvhMEv9bXLyDXdqSSvvh5goAMtUruYwCbFpuhDjx'; - const resolutionResult = await DidIonMethod.resolve({ didUrl: did }); - expect(resolutionResult).to.have.property('@context'); - expect(resolutionResult).to.have.property('didDocument'); - expect(resolutionResult).to.have.property('didDocumentMetadata'); - - expect(resolutionResult.didResolutionMetadata).to.have.property('error', 'methodNotSupported'); - }); - - it('accepts custom DID resolver with trailing slash', async () => { - const mockResult = { mock: 'data' }; - const fetchStub = sinon.stub(global, 'fetch'); - // @ts-expect-error because we're only mocking ok and json() from global.fetch(). - fetchStub.returns(Promise.resolve({ - ok : true, - json : () => Promise.resolve(mockResult) - })); - - const did = 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ'; - const resolutionResult = await DidIonMethod.resolve({ - didUrl : did, - resolutionOptions : { resolutionEndpoint: 'https://dev.uniresolver.io/1.0/identifiers/' } - }); - fetchStub.restore(); - - expect(resolutionResult).to.deep.equal(mockResult); - expect(fetchStub.calledOnceWith( - `https://dev.uniresolver.io/1.0/identifiers/${did}` - )).to.be.true; - }); - - it('accepts custom DID resolver without trailing slash', async () => { - const mockResult = { mock: 'data' }; - const fetchStub = sinon.stub(global, 'fetch'); - // @ts-expect-error because we're only mocking ok and json() from global.fetch(). - fetchStub.returns(Promise.resolve({ - ok : true, - json : () => Promise.resolve(mockResult) - })); - - const did = 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ'; - const resolutionResult = await DidIonMethod.resolve({ - didUrl : did, - resolutionOptions : { resolutionEndpoint: 'https://dev.uniresolver.io/1.0/identifiers' } - }); - fetchStub.restore(); - - expect(resolutionResult).to.deep.equal(mockResult); - expect(fetchStub.calledOnceWith( - `https://dev.uniresolver.io/1.0/identifiers/${did}` - )).to.be.true; - }); - }); -}); \ No newline at end of file diff --git a/packages/dids/tests/did-key.spec.ts b/packages/dids/tests/did-key.spec.ts deleted file mode 100644 index 682d2dbe4..000000000 --- a/packages/dids/tests/did-key.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { expect } from 'chai'; - -import type { DidKeyCreateOptions, DidKeyCreateDocumentOptions } from '../src/did-key.js'; - -import { DidKeyMethod } from '../src/did-key.js'; -import { didKeyCreateTestVectors, didKeyCreateDocumentTestVectors, } from './fixtures/test-vectors/did-key.js'; -import { DidDocument } from '../src/types.js'; - -describe('DidKeyMethod', () => { - describe('create()', () => { - it('creates a DID with Ed25519 keys, by default', async () => { - const portableDid = await DidKeyMethod.create(); - - // Verify expected result. - expect(portableDid).to.have.property('did'); - expect(portableDid).to.have.property('document'); - expect(portableDid).to.have.property('keySet'); - expect(portableDid.keySet).to.have.property('verificationMethodKeys'); - expect(portableDid.keySet.verificationMethodKeys).to.have.length(1); - expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); - expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); - expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('alg', 'EdDSA'); - expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('crv', 'Ed25519'); - }); - - it('creates a DID with secp256k1 keys, if specified', async () => { - const portableDid = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' }); - - // Verify expected result. - expect(portableDid).to.have.property('did'); - expect(portableDid).to.have.property('document'); - expect(portableDid).to.have.property('keySet'); - expect(portableDid.keySet).to.have.property('verificationMethodKeys'); - expect(portableDid.keySet.verificationMethodKeys).to.have.length(1); - expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); - expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); - expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('alg', 'ES256K'); - expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('crv', 'secp256k1'); - }); - - for (const vector of didKeyCreateTestVectors ) { - it(`passes test vector ${vector.id}`, async () => { - const portableDid = await DidKeyMethod.create(vector.input as DidKeyCreateOptions); - - expect(portableDid).to.deep.equal(vector.output); - }); - } - }); - - describe('createDocument()', () => { - it('accepts an alternate default context', async () => { - const didDocument = await DidKeyMethod.createDocument({ - did : 'did:key:z6MkjVM3rLLh9KCFBfKPNA5oEBq6KXXsPu72FDX7cZzYJN3y', - defaultContext : 'https://www.w3.org/ns/did/v99', - publicKeyFormat : 'JsonWebKey2020' - }); - - expect(didDocument['@context']).to.include('https://www.w3.org/ns/did/v99'); - }); - - for (const vector of didKeyCreateDocumentTestVectors ) { - it(`passes test vector ${vector.id}`, async () => { - const didDocument = await DidKeyMethod.createDocument(vector.input as DidKeyCreateDocumentOptions); - expect(didDocument).to.deep.equal(vector.output); - }); - } - }); - - describe('getDefaultSigningKey()', () => { - it('returns the did:key default signing key, when present', async () => { - const partialDidDocument = { - authentication: [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ] - } as unknown as DidDocument; - - const defaultSigningKeyId = await DidKeyMethod.getDefaultSigningKey({ - didDocument: partialDidDocument - }); - - expect(defaultSigningKeyId).to.equal('did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk'); - }); - - it('returns undefined if the did:key default signing key is not present', async () => { - const partialDidDocument = { - authentication: [{ - id : 'did:key:z6LSgmjjYTAffdKWLmBbYxe5d5fgLzuZxi6PEbHZNt3Cifvg#z6LSgmjjYTAffdKWLmBbYxe5d5fgLzuZxi6PEbHZNt3Cifvg', - type : 'JsonWebKey2020', - controller : 'did:key:z6LSgmjjYTAffdKWLmBbYxe5d5fgLzuZxi6PEbHZNt3Cifvg', - publicKeyJwk : { - kty : 'OKP', - crv : 'X25519', - x : 'S7cqN2_-PIPK6fVjR6PrQ1YZyyw61ajVnAJClFcXVhk' - } - }], - keyAgreement: [ - 'did:key:z6LSqCkip7X19obTwRpWc8ZLLCiXLzVQBFpcBAsTW38m6Rzs#z6LSqCkip7X19obTwRpWc8ZLLCiXLzVQBFpcBAsTW38m6Rzs' - ] - } as unknown as DidDocument; - - const defaultSigningKeyId = await DidKeyMethod.getDefaultSigningKey({ - didDocument: partialDidDocument - }); - - expect(defaultSigningKeyId).to.be.undefined; - }); - }); -}); \ No newline at end of file diff --git a/packages/dids/tests/did-resolver.spec.ts b/packages/dids/tests/did-resolver.spec.ts deleted file mode 100644 index 3aa79137c..000000000 --- a/packages/dids/tests/did-resolver.spec.ts +++ /dev/null @@ -1,201 +0,0 @@ -import * as sinon from 'sinon'; -import { expect } from 'chai'; - -import { DidKeyMethod } from '../src/did-key.js'; -import { DidResolver } from '../src/did-resolver.js'; -import { didResolverTestVectors } from './fixtures/test-vectors/did-resolver.js'; -import { DidResolverCacheLevel } from '../src/resolver-cache-level.js'; -import { DidResolverCache } from '../src/types.js'; -import { isVerificationMethod } from '../src/utils.js'; - -describe('DidResolver', () => { - describe('resolve()', () => { - let didResolver: DidResolver; - - describe('with no-op cache', () => { - beforeEach(() => { - const didMethodApis = [DidKeyMethod]; - didResolver = new DidResolver({ didResolvers: didMethodApis }); - }); - - it('returns an invalidDid error if the DID cannot be parsed', async () => { - const didResolutionResult = await didResolver.resolve('unparseable:did'); - expect(didResolutionResult).to.exist; - expect(didResolutionResult).to.have.property('@context'); - expect(didResolutionResult).to.have.property('didDocument'); - expect(didResolutionResult).to.have.property('didDocumentMetadata'); - expect(didResolutionResult).to.have.property('didResolutionMetadata'); - expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'invalidDid'); - }); - - it('returns a methodNotSupported error if the DID method is not supported', async () => { - const didResolutionResult = await didResolver.resolve('did:unknown:abc123'); - expect(didResolutionResult).to.exist; - expect(didResolutionResult).to.have.property('@context'); - expect(didResolutionResult).to.have.property('didDocument'); - expect(didResolutionResult).to.have.property('didDocumentMetadata'); - expect(didResolutionResult).to.have.property('didResolutionMetadata'); - expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'methodNotSupported'); - }); - - it('passes test vectors', async () => { - for (const vector of didResolverTestVectors) { - const didResolutionResult = await didResolver.resolve(vector.input); - expect(didResolutionResult.didDocument).to.deep.equal(vector.output); - } - }); - }); - - describe('with LevelDB cache', () => { - let cache: DidResolverCache; - - before(() => { - cache = new DidResolverCacheLevel(); - }); - - beforeEach(async () => { - await cache.clear(); - const didMethodApis = [DidKeyMethod]; - didResolver = new DidResolver({ cache, didResolvers: didMethodApis }); - }); - - after(async () => { - await cache.clear(); - }); - - it('should cache miss for the first resolution attempt', async () => { - const did = 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D'; - // Create a Sinon spy on the get method of the cache - const cacheGetSpy = sinon.spy(cache, 'get'); - - await didResolver.resolve(did); - - // Verify that cache.get() was called. - expect(cacheGetSpy.called).to.be.true; - - // Verify the cache returned undefined. - const getCacheResult = await cacheGetSpy.returnValues[0]; - expect(getCacheResult).to.be.undefined; - - cacheGetSpy.restore(); - }); - - it('should cache hit for the second resolution attempt', async () => { - const did = 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D'; - // Create a Sinon spy on the get method of the cache - const cacheGetSpy = sinon.spy(cache, 'get'); - const cacheSetSpy = sinon.spy(cache, 'set'); - - await didResolver.resolve(did); - - // Verify there was a cache miss. - expect(cacheGetSpy.calledOnce).to.be.true; - expect(cacheSetSpy.calledOnce).to.be.true; - - // Verify the cache returned undefined. - let getCacheResult = await cacheGetSpy.returnValues[0]; - expect(getCacheResult).to.be.undefined; - - // Resolve the same DID again. - await didResolver.resolve(did); - - // Verify that cache.get() was called. - expect(cacheGetSpy.called).to.be.true; - expect(cacheGetSpy.calledTwice).to.be.true; - - // Verify there was a cache hit this time. - getCacheResult = await cacheGetSpy.returnValues[1]; - expect(getCacheResult).to.not.be.undefined; - expect(getCacheResult).to.have.property('@context'); - expect(getCacheResult).to.have.property('didDocument'); - expect(getCacheResult).to.have.property('didDocumentMetadata'); - expect(getCacheResult).to.have.property('didResolutionMetadata'); - - cacheGetSpy.restore(); - }); - }); - }); - - describe('dereference()', () => { - let didResolver: DidResolver; - - beforeEach(() => { - const didMethodApis = [DidKeyMethod]; - didResolver = new DidResolver({ didResolvers: didMethodApis }); - }); - - it('returns a result with contentStream set to null and dereferenceMetadata.error set if resolution fails', async () => { - const result = await didResolver.dereference({ didUrl: 'abcd123;;;' }); - expect(result.contentStream).to.be.null; - expect(result.dereferencingMetadata.error).to.exist; - expect(result.dereferencingMetadata.error).to.equal('invalidDidUrl'); - }); - - it('returns a DID verification method resource as the value of contentStream if found', async () => { - const did = await DidKeyMethod.create(); - - const result = await didResolver.dereference({ didUrl: did.document.verificationMethod[0].id }); - expect(result.contentStream).to.be.not.be.null; - expect(result.dereferencingMetadata.error).to.not.exist; - - const didResource = result.contentStream; - expect(isVerificationMethod(didResource)).to.be.true; - }); - - it('returns a DID service resource as the value of contentStream if found', async () => { - // Create an instance of DidResolver - const resolver = new DidResolver({ didResolvers: [] }); - - // Stub the resolve method - const mockDidResolutionResult = { - '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : { - id : 'did:example:123456789abcdefghi', - service : [ - { - id : '#dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : { - nodes: [ 'https://dwn.tbddev.test/dwn0' ] - } - } - ], - }, - didDocumentMetadata : {}, - didResolutionMetadata : {} - }; - - const resolveStub = sinon.stub(resolver, 'resolve').resolves(mockDidResolutionResult); - - const testDidUrl = 'did:example:123456789abcdefghi#dwn'; - const result = await resolver.dereference({ didUrl: testDidUrl }); - - expect(resolveStub.calledOnce).to.be.true; - expect(result.contentStream).to.deep.equal(mockDidResolutionResult.didDocument.service[0]); - - // Restore the original resolve method - resolveStub.restore(); - }); - - it('returns the entire DID document as the value of contentStream if the DID URL contains no fragment', async () => { - const did = await DidKeyMethod.create(); - - const result = await didResolver.dereference({ didUrl: did.did }); - expect(result.contentStream).to.be.not.be.null; - expect(result.dereferencingMetadata.error).to.not.exist; - - const didResource = result.contentStream; - expect(didResource['@context']).to.exist; - expect(didResource['@context']).to.include('https://www.w3.org/ns/did/v1'); - }); - - it('returns contentStream set to null and dereferenceMetadata.error set to notFound if resource is not found', async () => { - const did = await DidKeyMethod.create(); - - const result = await didResolver.dereference({ didUrl: `${did.did}#0` }); - expect(result.contentStream).to.be.null; - expect(result.dereferencingMetadata.error).to.exist; - expect(result.dereferencingMetadata.error).to.equal('notFound'); - }); - }); -}); \ No newline at end of file diff --git a/packages/dids/tests/did-uri.spec.ts b/packages/dids/tests/did-uri.spec.ts new file mode 100644 index 000000000..8f9cf0772 --- /dev/null +++ b/packages/dids/tests/did-uri.spec.ts @@ -0,0 +1,188 @@ +import { expect } from 'chai'; + +import { DidUri } from '../src/did-uri.js'; + +describe('DidUri', () => { + it('constructor', () => { + const didUriComponents = { + method : 'example', + id : '123', + path : '/path', + query : 'versionId=1', + fragment : 'key1', + params : { versionId: '1' }, + }; + + const didUri = new DidUri(didUriComponents); + + expect(didUri).to.deep.equal({ ...didUriComponents, uri: 'did:example:123' }); + }); + + describe('parse()', () => { + it('parses a basic DID URI', () => { + const didUri = DidUri.parse('did:example:123'); + + expect(didUri).to.deep.equal({ + uri : 'did:example:123', + method : 'example', + id : '123', + }); + }); + + it('parses a DID URI with unusual identifier characters', () => { + let didUri = DidUri.parse('did:123:test::test2'); + expect(didUri).to.deep.equal({ + method : '123', + id : 'test::test2', + uri : 'did:123:test::test2', + }); + + didUri = DidUri.parse('did:method:%12%AF'); + expect(didUri).to.deep.equal({ + method : 'method', + id : '%12%AF', + uri : 'did:method:%12%AF', + }); + + didUri = DidUri.parse('did:web:example.com%3A8443'); + expect(didUri).to.deep.equal({ + uri : 'did:web:example.com%3A8443', + method : 'web', + id : 'example.com%3A8443', + }); + + didUri = DidUri.parse('did:web:example.com:path:some%2Bsubpath'); + expect(didUri).to.deep.equal({ + uri : 'did:web:example.com:path:some%2Bsubpath', + method : 'web', + id : 'example.com:path:some%2Bsubpath', + }); + }); + + it('parses a DID URI with a path', () => { + const didUri = DidUri.parse('did:example:123/path'); + + expect(didUri).to.deep.equal({ + uri : 'did:example:123', + method : 'example', + id : '123', + path : '/path', + }); + }); + + it('parses a DID URI with a query', () => { + const didUri = DidUri.parse('did:example:123?versionId=1'); + + expect(didUri).to.deep.equal({ + uri : 'did:example:123', + method : 'example', + id : '123', + query : 'versionId=1', + params : { versionId: '1' }, + }); + }); + + it('parses a DID URI with a fragment', () => { + const didUri = DidUri.parse('did:example:123#key-1'); + + expect(didUri).to.deep.equal({ + uri : 'did:example:123', + method : 'example', + id : '123', + fragment : 'key-1', + }); + }); + + it('parses a DID URI with an identifier containing an underscore', () => { + const didUri = DidUri.parse('did:example:abcdefg_123456790'); + + expect(didUri).to.deep.equal({ + uri : 'did:example:abcdefg_123456790', + method : 'example', + id : 'abcdefg_123456790', + }); + }); + + it('parses a complex DID URI with all components', () => { + const didUri = DidUri.parse('did:example:123/some/path?versionId=1#key1'); + + expect(didUri).to.deep.equal({ + uri : 'did:example:123', + method : 'example', + id : '123', + path : '/some/path', + query : 'versionId=1', + fragment : 'key1', + params : { versionId: '1' }, + }); + }); + + it('parses DID URIs with various combinations of components', () => { + expect( + DidUri.parse('did:uport:123/some/path#fragment=123') + ).to.deep.equal({ + uri : 'did:uport:123', + method : 'uport', + id : '123', + path : '/some/path', + fragment : 'fragment=123', + }); + + expect( + DidUri.parse('did:example:123?service=agent&relativeRef=/credentials#degree') + ).to.deep.equal({ + uri : 'did:example:123', + method : 'example', + id : '123', + query : 'service=agent&relativeRef=/credentials', + fragment : 'degree', + params : { + service : 'agent', + relativeRef : '/credentials', + }, + }); + + expect( + DidUri.parse('did:example:test:123/some/path?versionId=1#key1') + ).to.deep.equal({ + uri : 'did:example:test:123', + method : 'example', + id : 'test:123', + path : '/some/path', + query : 'versionId=1', + fragment : 'key1', + params : { versionId: '1' }, + }); + }); + + it('extracts ION DID long form identifier from a DID URI', async () => { + const { uri } = DidUri.parse( + 'did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19' + ) ?? {}; + + expect(uri).to.equal('did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19'); + }); + + it('extracts ION DID long form identifier from a DID URI with query and fragment', async () => { + const { uri } = DidUri.parse( + 'did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19?service=files&relativeRef=/credentials#degree' + ) ?? {}; + + expect(uri).to.equal('did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19'); + }); + + it('returns null for an invalid DID URI', () => { + expect(DidUri.parse('')).to.equal(null); + expect(DidUri.parse('not-a-did-uri')).to.equal(null); + expect(DidUri.parse('did:')).to.equal(null); + expect(DidUri.parse('did:uport')).to.equal(null); + expect(DidUri.parse('did:uport:')).to.equal(null); + expect(DidUri.parse('did:uport:1234_12313***')).to.equal(null); + expect(DidUri.parse('123')).to.equal(null); + expect(DidUri.parse('did:method:%12%1')).to.equal(null); + expect(DidUri.parse('did:method:%1233%Ay')).to.equal(null); + expect(DidUri.parse('did:CAP:id')).to.equal(null); + expect(DidUri.parse('did:method:id::anotherid%r9')).to.equal(null); + }); + }); +}); \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-ion/create.ts b/packages/dids/tests/fixtures/test-vectors/did-ion/create.ts new file mode 100644 index 000000000..a14e62727 --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/did-ion/create.ts @@ -0,0 +1,425 @@ +import type { Jwk } from '@web5/crypto'; + +import type { DidResolutionResult } from '../../../../src/types/did-core.js'; + +type TestVector = { + [key: string]: { + didUri: string; + privateKey: Jwk[]; + didResolutionResult: DidResolutionResult; + }; +}; + +export const vectors: TestVector = { + oneMethodNoServices: { + didUri : 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJFN1kyUUt1Zm9HUHhqWXJZSFl6MG51b1VtaEQxM1ctWGYxOVotdl9sTGNVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoibU9MYkxWVDQwR0lhTk13bjFqd05pdFhad05NTllIdmg5c0FOb0xvTjY5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRDlSN0M2enloakFFS25uT3RiRW1DU1d0RGJwQXFxbE1uMW4tNS04dlltYUEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNOazhJRHJkWHdMNng5Z1ZicHdvOEZpNDNTZUVzQjMxSm1UNzN5ODNOZ25BIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCUUNPclZBeVNHcXF2YVMzRU15c2RlNVhkUHhGTzFDZzlDN2lqOUs2NjhzQSJ9fQ', + privateKey : [ + { + crv : 'Ed25519', + d : 'kxXp_AYrMbkVvaWS_nDLIK1INI6Y_CpmUiZQemVCWI0', + kty : 'OKP', + x : 'mOLbLVT40GIaNMwn1jwNitXZwNMNYHvh9sANoLoN69A', + kid : 'E7Y2QKufoGPxjYrYHYz0nuoUmhD13W-Xf19Z-v_lLcU', + alg : 'EdDSA', + } + ], + didResolutionResult: { + didDocument: { + id : 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJFN1kyUUt1Zm9HUHhqWXJZSFl6MG51b1VtaEQxM1ctWGYxOVotdl9sTGNVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoibU9MYkxWVDQwR0lhTk13bjFqd05pdFhad05NTllIdmg5c0FOb0xvTjY5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRDlSN0M2enloakFFS25uT3RiRW1DU1d0RGJwQXFxbE1uMW4tNS04dlltYUEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNOazhJRHJkWHdMNng5Z1ZicHdvOEZpNDNTZUVzQjMxSm1UNzN5ODNOZ25BIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCUUNPclZBeVNHcXF2YVMzRU15c2RlNVhkUHhGTzFDZzlDN2lqOUs2NjhzQSJ9fQ', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJFN1kyUUt1Zm9HUHhqWXJZSFl6MG51b1VtaEQxM1ctWGYxOVotdl9sTGNVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoibU9MYkxWVDQwR0lhTk13bjFqd05pdFhad05NTllIdmg5c0FOb0xvTjY5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRDlSN0M2enloakFFS25uT3RiRW1DU1d0RGJwQXFxbE1uMW4tNS04dlltYUEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNOazhJRHJkWHdMNng5Z1ZicHdvOEZpNDNTZUVzQjMxSm1UNzN5ODNOZ25BIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCUUNPclZBeVNHcXF2YVMzRU15c2RlNVhkUHhGTzFDZzlDN2lqOUs2NjhzQSJ9fQ', + }, + ], + service: [ + ], + verificationMethod: [ + { + id : '#E7Y2QKufoGPxjYrYHYz0nuoUmhD13W-Xf19Z-v_lLcU', + controller : 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJFN1kyUUt1Zm9HUHhqWXJZSFl6MG51b1VtaEQxM1ctWGYxOVotdl9sTGNVIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoibU9MYkxWVDQwR0lhTk13bjFqd05pdFhad05NTllIdmg5c0FOb0xvTjY5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRDlSN0M2enloakFFS25uT3RiRW1DU1d0RGJwQXFxbE1uMW4tNS04dlltYUEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNOazhJRHJkWHdMNng5Z1ZicHdvOEZpNDNTZUVzQjMxSm1UNzN5ODNOZ25BIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCUUNPclZBeVNHcXF2YVMzRU15c2RlNVhkUHhGTzFDZzlDN2lqOUs2NjhzQSJ9fQ', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'mOLbLVT40GIaNMwn1jwNitXZwNMNYHvh9sANoLoN69A', + }, + }, + ], + authentication: [ + '#E7Y2QKufoGPxjYrYHYz0nuoUmhD13W-Xf19Z-v_lLcU', + ], + assertionMethod: [ + '#E7Y2QKufoGPxjYrYHYz0nuoUmhD13W-Xf19Z-v_lLcU', + ], + }, + didDocumentMetadata: { + method: { + published : false, + recoveryCommitment : 'EiBQCOrVAySGqqvaS3EMysde5XdPxFO1Cg9C7ij9K668sA', + updateCommitment : 'EiD9R7C6zyhjAEKnnOtbEmCSWtDbpAqqlMn1n-5-8vYmaA', + }, + equivalentId: [ + 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug', + ], + }, + didResolutionMetadata: {} + } + }, + + twoMethodsNoServices: { + didUri : 'did:ion:EiBya40LM6p4aaMyp2NtImm3yvtUOSXmmHNgcCt6JudUHQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJJZ2wwUE1Cam9tWDVyeHRCeUJZdDh4ZWpRbF9XQktCaXZaak9ydnhjVFAwIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOE1jWGdDN25ydjY3RTZBSG9SN2lDcjVaY0xRdml5aHo4M2NmSm5YLVY5MCJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifSx7ImlkIjoib0dTS1p6LUdZYW5CMTg4SnlPcXI4dmcxU0dIdzRHVnBBVWpkbGNKdmJvUSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJLT1lkNjRjaHBUZEc0bEtZczJlTXYtVkIzZ2E2b1FLcnFfYWFZdGtlWGZVIiwieSI6IkN2N3BfQnlCSHprZElja2gtcEUxSDZqcDFNVmJDUE9GMC1WQWVzZGN3dmMifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iLCJhc3NlcnRpb25NZXRob2QiXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUR2VjUtYjhDMEQ4NEZxMzh2M2ZxbEFBM2NZMUk5Q2RrU0NuZ1BTTU8zSnlnIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDRzNFSTVSeFlwRDBuYjdzR3hVdTBUWlBVTC02akJNdnFQNFZpd2p0TmF4dyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnB1UWQ0S1JwR0J0cC1Xb01oaGZGQnRpakptSDZhWklQaFI2a3oyRjFMamcifX0', + privateKey : [ + { + crv : 'Ed25519', + d : 'mXkHN5G6TRMBTVruPZ7iPbpyrq5-_wodVwxDKEVetCM', + kty : 'OKP', + x : '8McXgC7nrv67E6AHoR7iCr5ZcLQviyhz83cfJnX-V90', + kid : 'Igl0PMBjomX5rxtByBYt8xejQl_WBKBivZjOrvxcTP0', + alg : 'EdDSA', + }, + { + kty : 'EC', + crv : 'secp256k1', + d : 'HLFBI4JwQc8-kLP-3Yr5lsPz39XaGanOFi81MmixAXw', + x : 'KOYd64chpTdG4lKYs2eMv-VB3ga6oQKrq_aaYtkeXfU', + y : 'Cv7p_ByBHzkdIckh-pE1H6jp1MVbCPOF0-VAesdcwvc', + kid : 'oGSKZz-GYanB188JyOqr8vg1SGHw4GVpAUjdlcJvboQ', + alg : 'ES256K', + } + ], + didResolutionResult: { + didDocument: { + id : 'did:ion:EiBya40LM6p4aaMyp2NtImm3yvtUOSXmmHNgcCt6JudUHQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJJZ2wwUE1Cam9tWDVyeHRCeUJZdDh4ZWpRbF9XQktCaXZaak9ydnhjVFAwIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOE1jWGdDN25ydjY3RTZBSG9SN2lDcjVaY0xRdml5aHo4M2NmSm5YLVY5MCJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifSx7ImlkIjoib0dTS1p6LUdZYW5CMTg4SnlPcXI4dmcxU0dIdzRHVnBBVWpkbGNKdmJvUSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJLT1lkNjRjaHBUZEc0bEtZczJlTXYtVkIzZ2E2b1FLcnFfYWFZdGtlWGZVIiwieSI6IkN2N3BfQnlCSHprZElja2gtcEUxSDZqcDFNVmJDUE9GMC1WQWVzZGN3dmMifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iLCJhc3NlcnRpb25NZXRob2QiXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUR2VjUtYjhDMEQ4NEZxMzh2M2ZxbEFBM2NZMUk5Q2RrU0NuZ1BTTU8zSnlnIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDRzNFSTVSeFlwRDBuYjdzR3hVdTBUWlBVTC02akJNdnFQNFZpd2p0TmF4dyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnB1UWQ0S1JwR0J0cC1Xb01oaGZGQnRpakptSDZhWklQaFI2a3oyRjFMamcifX0', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiBya40LM6p4aaMyp2NtImm3yvtUOSXmmHNgcCt6JudUHQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJJZ2wwUE1Cam9tWDVyeHRCeUJZdDh4ZWpRbF9XQktCaXZaak9ydnhjVFAwIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOE1jWGdDN25ydjY3RTZBSG9SN2lDcjVaY0xRdml5aHo4M2NmSm5YLVY5MCJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifSx7ImlkIjoib0dTS1p6LUdZYW5CMTg4SnlPcXI4dmcxU0dIdzRHVnBBVWpkbGNKdmJvUSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJLT1lkNjRjaHBUZEc0bEtZczJlTXYtVkIzZ2E2b1FLcnFfYWFZdGtlWGZVIiwieSI6IkN2N3BfQnlCSHprZElja2gtcEUxSDZqcDFNVmJDUE9GMC1WQWVzZGN3dmMifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iLCJhc3NlcnRpb25NZXRob2QiXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUR2VjUtYjhDMEQ4NEZxMzh2M2ZxbEFBM2NZMUk5Q2RrU0NuZ1BTTU8zSnlnIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDRzNFSTVSeFlwRDBuYjdzR3hVdTBUWlBVTC02akJNdnFQNFZpd2p0TmF4dyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnB1UWQ0S1JwR0J0cC1Xb01oaGZGQnRpakptSDZhWklQaFI2a3oyRjFMamcifX0', + }, + ], + service: [ + ], + verificationMethod: [ + { + id : '#Igl0PMBjomX5rxtByBYt8xejQl_WBKBivZjOrvxcTP0', + controller : 'did:ion:EiBya40LM6p4aaMyp2NtImm3yvtUOSXmmHNgcCt6JudUHQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJJZ2wwUE1Cam9tWDVyeHRCeUJZdDh4ZWpRbF9XQktCaXZaak9ydnhjVFAwIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOE1jWGdDN25ydjY3RTZBSG9SN2lDcjVaY0xRdml5aHo4M2NmSm5YLVY5MCJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifSx7ImlkIjoib0dTS1p6LUdZYW5CMTg4SnlPcXI4dmcxU0dIdzRHVnBBVWpkbGNKdmJvUSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJLT1lkNjRjaHBUZEc0bEtZczJlTXYtVkIzZ2E2b1FLcnFfYWFZdGtlWGZVIiwieSI6IkN2N3BfQnlCSHprZElja2gtcEUxSDZqcDFNVmJDUE9GMC1WQWVzZGN3dmMifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iLCJhc3NlcnRpb25NZXRob2QiXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUR2VjUtYjhDMEQ4NEZxMzh2M2ZxbEFBM2NZMUk5Q2RrU0NuZ1BTTU8zSnlnIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDRzNFSTVSeFlwRDBuYjdzR3hVdTBUWlBVTC02akJNdnFQNFZpd2p0TmF4dyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnB1UWQ0S1JwR0J0cC1Xb01oaGZGQnRpakptSDZhWklQaFI2a3oyRjFMamcifX0', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : '8McXgC7nrv67E6AHoR7iCr5ZcLQviyhz83cfJnX-V90', + }, + }, + { + id : '#oGSKZz-GYanB188JyOqr8vg1SGHw4GVpAUjdlcJvboQ', + controller : 'did:ion:EiBya40LM6p4aaMyp2NtImm3yvtUOSXmmHNgcCt6JudUHQ:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJJZ2wwUE1Cam9tWDVyeHRCeUJZdDh4ZWpRbF9XQktCaXZaak9ydnhjVFAwIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiOE1jWGdDN25ydjY3RTZBSG9SN2lDcjVaY0xRdml5aHo4M2NmSm5YLVY5MCJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifSx7ImlkIjoib0dTS1p6LUdZYW5CMTg4SnlPcXI4dmcxU0dIdzRHVnBBVWpkbGNKdmJvUSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJLT1lkNjRjaHBUZEc0bEtZczJlTXYtVkIzZ2E2b1FLcnFfYWFZdGtlWGZVIiwieSI6IkN2N3BfQnlCSHprZElja2gtcEUxSDZqcDFNVmJDUE9GMC1WQWVzZGN3dmMifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iLCJhc3NlcnRpb25NZXRob2QiXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUR2VjUtYjhDMEQ4NEZxMzh2M2ZxbEFBM2NZMUk5Q2RrU0NuZ1BTTU8zSnlnIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDRzNFSTVSeFlwRDBuYjdzR3hVdTBUWlBVTC02akJNdnFQNFZpd2p0TmF4dyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnB1UWQ0S1JwR0J0cC1Xb01oaGZGQnRpakptSDZhWklQaFI2a3oyRjFMamcifX0', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'secp256k1', + kty : 'EC', + x : 'KOYd64chpTdG4lKYs2eMv-VB3ga6oQKrq_aaYtkeXfU', + y : 'Cv7p_ByBHzkdIckh-pE1H6jp1MVbCPOF0-VAesdcwvc', + }, + }, + ], + authentication: [ + '#Igl0PMBjomX5rxtByBYt8xejQl_WBKBivZjOrvxcTP0', + '#oGSKZz-GYanB188JyOqr8vg1SGHw4GVpAUjdlcJvboQ', + ], + assertionMethod: [ + '#Igl0PMBjomX5rxtByBYt8xejQl_WBKBivZjOrvxcTP0', + '#oGSKZz-GYanB188JyOqr8vg1SGHw4GVpAUjdlcJvboQ', + ], + }, + didDocumentMetadata: { + method: { + published : false, + recoveryCommitment : 'EiBpuQd4KRpGBtp-WoMhhfFBtijJmH6aZIPhR6kz2F1Ljg', + updateCommitment : 'EiDvV5-b8C0D84Fq38v3fqlAA3cY1I9CdkSCngPSMO3Jyg', + }, + equivalentId: [ + 'did:ion:EiBya40LM6p4aaMyp2NtImm3yvtUOSXmmHNgcCt6JudUHQ', + ], + }, + didResolutionMetadata: {} + } + }, + + oneMethodOneService: { + didUri : 'did:ion:EiDO7yuqY5ChgRW1BnsNlPlwcu2KQp_ZlroqbICLujPi7w:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJOTEJYZTdQbUloREVuamdWTHllamp2UTRrNmxHNzlIa0xtRS16N0xRcWZBIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaHBlYkRpYk82dDRUOVBybEZGU0NMUFQyYlhwMFRTY1VobzFvRk81bGRGcyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlDQ3cxcVYwYVk1Y2oydzNyQlhCWWVzOFRPN21aUHl3VzJZTXFTdHM3ZEVOdyJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQzhfN3VIcXpra2FBSVBlaDBLLURTVnVaOGd6dWVPNVB6WEpyM2R0VlBVSFEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUJWTDFxX0xlaXJfekFGV0l0M2pCRlhOSG5DU2hPU0Z6Z21xb0dzS2hjQkFnIn19', + privateKey : [ + { + crv : 'Ed25519', + d : 'cllAz3W1a3MnCFR3anZt6cZygIcRTHmSO2SJyKJzmQM', + kty : 'OKP', + x : 'hpebDibO6t4T9PrlFFSCLPT2bXp0TScUho1oFO5ldFs', + kid : 'NLBXe7PmIhDEnjgVLyejjvQ4k6lG79HkLmE-z7LQqfA', + alg : 'EdDSA', + } + ], + didResolutionResult: { + didDocument: { + id : 'did:ion:EiDO7yuqY5ChgRW1BnsNlPlwcu2KQp_ZlroqbICLujPi7w:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJOTEJYZTdQbUloREVuamdWTHllamp2UTRrNmxHNzlIa0xtRS16N0xRcWZBIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaHBlYkRpYk82dDRUOVBybEZGU0NMUFQyYlhwMFRTY1VobzFvRk81bGRGcyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlDQ3cxcVYwYVk1Y2oydzNyQlhCWWVzOFRPN21aUHl3VzJZTXFTdHM3ZEVOdyJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQzhfN3VIcXpra2FBSVBlaDBLLURTVnVaOGd6dWVPNVB6WEpyM2R0VlBVSFEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUJWTDFxX0xlaXJfekFGV0l0M2pCRlhOSG5DU2hPU0Z6Z21xb0dzS2hjQkFnIn19', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiDO7yuqY5ChgRW1BnsNlPlwcu2KQp_ZlroqbICLujPi7w:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJOTEJYZTdQbUloREVuamdWTHllamp2UTRrNmxHNzlIa0xtRS16N0xRcWZBIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaHBlYkRpYk82dDRUOVBybEZGU0NMUFQyYlhwMFRTY1VobzFvRk81bGRGcyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlDQ3cxcVYwYVk1Y2oydzNyQlhCWWVzOFRPN21aUHl3VzJZTXFTdHM3ZEVOdyJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQzhfN3VIcXpra2FBSVBlaDBLLURTVnVaOGd6dWVPNVB6WEpyM2R0VlBVSFEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUJWTDFxX0xlaXJfekFGV0l0M2pCRlhOSG5DU2hPU0Z6Z21xb0dzS2hjQkFnIn19', + }, + ], + service: [ + { + id : '#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + }, + ], + verificationMethod: [ + { + id : '#NLBXe7PmIhDEnjgVLyejjvQ4k6lG79HkLmE-z7LQqfA', + controller : 'did:ion:EiDO7yuqY5ChgRW1BnsNlPlwcu2KQp_ZlroqbICLujPi7w:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJOTEJYZTdQbUloREVuamdWTHllamp2UTRrNmxHNzlIa0xtRS16N0xRcWZBIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaHBlYkRpYk82dDRUOVBybEZGU0NMUFQyYlhwMFRTY1VobzFvRk81bGRGcyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlDQ3cxcVYwYVk1Y2oydzNyQlhCWWVzOFRPN21aUHl3VzJZTXFTdHM3ZEVOdyJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQzhfN3VIcXpra2FBSVBlaDBLLURTVnVaOGd6dWVPNVB6WEpyM2R0VlBVSFEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUJWTDFxX0xlaXJfekFGV0l0M2pCRlhOSG5DU2hPU0Z6Z21xb0dzS2hjQkFnIn19', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'hpebDibO6t4T9PrlFFSCLPT2bXp0TScUho1oFO5ldFs', + }, + }, + ], + authentication: [ + '#NLBXe7PmIhDEnjgVLyejjvQ4k6lG79HkLmE-z7LQqfA', + ], + assertionMethod: [ + '#NLBXe7PmIhDEnjgVLyejjvQ4k6lG79HkLmE-z7LQqfA', + ], + }, + didDocumentMetadata: { + method: { + published : false, + recoveryCommitment : 'EiBVL1q_Leir_zAFWIt3jBFXNHnCShOSFzgmqoGsKhcBAg', + updateCommitment : 'EiCCw1qV0aY5cj2w3rBXBYes8TO7mZPywW2YMqSts7dENw', + }, + equivalentId: [ + 'did:ion:EiDO7yuqY5ChgRW1BnsNlPlwcu2KQp_ZlroqbICLujPi7w', + ], + }, + didResolutionMetadata: {} + } + }, + + oneMethodTwoServices: { + didUri : 'did:ion:EiCBB3nlRtUcqBY8-vm3WLP12elafIJIbXeVh6CBjeAV8Q:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJoZENsb0lmQWM5TWpDYlhQTVF3RThOajREQzBPWDlqQUNuLVNsa2hldzU4IiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiZjN5SUxDNmd0dHRlTDAyZG5rVTBoT0FNbHV5V0dnRjJKcFpfUUV6bmM5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn0seyJpZCI6Im9pZDR2Y2kiLCJzZXJ2aWNlRW5kcG9pbnQiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInR5cGUiOiJPSUQ0VkNJIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCLS1HU0I1TXVFWUFPVE5MZHZoVFpTbkE0SVc2NHZOc2VlS09PUmd6TU1OUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQXVuN3F1QUF0d3lCQWJXNk54SG5rbTZhXzJWNDhfMGRnTW9fMWpvSmRzT0EiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUFjWGlFTi0tVGdzWVdwaWt0NjFmeHNlVzZpTHVMOFp4d2RmYjRNVzNES2RBIn19', + privateKey : [ + { + crv : 'Ed25519', + d : 'ADvv3DFjfZsezjo5W20UxUWUzVmUAwTI7HZg96l4rrY', + kty : 'OKP', + x : 'f3yILC6gttteL02dnkU0hOAMluyWGgF2JpZ_QEznc9A', + kid : 'hdCloIfAc9MjCbXPMQwE8Nj4DC0OX9jACn-Slkhew58', + alg : 'EdDSA', + } + ], + didResolutionResult: { + didDocument: { + id : 'did:ion:EiCBB3nlRtUcqBY8-vm3WLP12elafIJIbXeVh6CBjeAV8Q:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJoZENsb0lmQWM5TWpDYlhQTVF3RThOajREQzBPWDlqQUNuLVNsa2hldzU4IiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiZjN5SUxDNmd0dHRlTDAyZG5rVTBoT0FNbHV5V0dnRjJKcFpfUUV6bmM5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn0seyJpZCI6Im9pZDR2Y2kiLCJzZXJ2aWNlRW5kcG9pbnQiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInR5cGUiOiJPSUQ0VkNJIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCLS1HU0I1TXVFWUFPVE5MZHZoVFpTbkE0SVc2NHZOc2VlS09PUmd6TU1OUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQXVuN3F1QUF0d3lCQWJXNk54SG5rbTZhXzJWNDhfMGRnTW9fMWpvSmRzT0EiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUFjWGlFTi0tVGdzWVdwaWt0NjFmeHNlVzZpTHVMOFp4d2RmYjRNVzNES2RBIn19', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiCBB3nlRtUcqBY8-vm3WLP12elafIJIbXeVh6CBjeAV8Q:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJoZENsb0lmQWM5TWpDYlhQTVF3RThOajREQzBPWDlqQUNuLVNsa2hldzU4IiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiZjN5SUxDNmd0dHRlTDAyZG5rVTBoT0FNbHV5V0dnRjJKcFpfUUV6bmM5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn0seyJpZCI6Im9pZDR2Y2kiLCJzZXJ2aWNlRW5kcG9pbnQiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInR5cGUiOiJPSUQ0VkNJIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCLS1HU0I1TXVFWUFPVE5MZHZoVFpTbkE0SVc2NHZOc2VlS09PUmd6TU1OUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQXVuN3F1QUF0d3lCQWJXNk54SG5rbTZhXzJWNDhfMGRnTW9fMWpvSmRzT0EiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUFjWGlFTi0tVGdzWVdwaWt0NjFmeHNlVzZpTHVMOFp4d2RmYjRNVzNES2RBIn19', + }, + ], + service: [ + { + id : '#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + }, + { + id : '#oid4vci', + type : 'OID4VCI', + serviceEndpoint : 'https://issuer.example.com', + }, + ], + verificationMethod: [ + { + id : '#hdCloIfAc9MjCbXPMQwE8Nj4DC0OX9jACn-Slkhew58', + controller : 'did:ion:EiCBB3nlRtUcqBY8-vm3WLP12elafIJIbXeVh6CBjeAV8Q:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJoZENsb0lmQWM5TWpDYlhQTVF3RThOajREQzBPWDlqQUNuLVNsa2hldzU4IiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiZjN5SUxDNmd0dHRlTDAyZG5rVTBoT0FNbHV5V0dnRjJKcFpfUUV6bmM5QSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZHduIiwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn0seyJpZCI6Im9pZDR2Y2kiLCJzZXJ2aWNlRW5kcG9pbnQiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInR5cGUiOiJPSUQ0VkNJIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCLS1HU0I1TXVFWUFPVE5MZHZoVFpTbkE0SVc2NHZOc2VlS09PUmd6TU1OUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQXVuN3F1QUF0d3lCQWJXNk54SG5rbTZhXzJWNDhfMGRnTW9fMWpvSmRzT0EiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUFjWGlFTi0tVGdzWVdwaWt0NjFmeHNlVzZpTHVMOFp4d2RmYjRNVzNES2RBIn19', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'f3yILC6gttteL02dnkU0hOAMluyWGgF2JpZ_QEznc9A', + }, + }, + ], + authentication: [ + '#hdCloIfAc9MjCbXPMQwE8Nj4DC0OX9jACn-Slkhew58', + ], + assertionMethod: [ + '#hdCloIfAc9MjCbXPMQwE8Nj4DC0OX9jACn-Slkhew58', + ], + }, + didDocumentMetadata: { + method: { + published : false, + recoveryCommitment : 'EiAcXiEN--TgsYWpikt61fxseW6iLuL8Zxwdfb4MW3DKdA', + updateCommitment : 'EiB--GSB5MuEYAOTNLdvhTZSnA4IW64vNseeKOORgzMMNQ', + }, + equivalentId: [ + 'did:ion:EiCBB3nlRtUcqBY8-vm3WLP12elafIJIbXeVh6CBjeAV8Q', + ], + }, + didResolutionMetadata: {} + } + }, + + oneMethodCustomId: { + didUri : 'did:ion:EiDZjHxyHf-0llvciVAXm7BtYUIImm8WsDP2Wbfp737PvA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiIxIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaVV6SE8wMXlkYVk0Rmt5bjlmcDNYNWQ5cDR5TGtKaHJEcGpOU0VrcEVUZyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRGhoZDdCUkd3UmRReU05d1FOQlBHNVVtS3FGMmRaZFFaS3V6ekRtMFlSUmcifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUF3QUVNcTJGXzlIUDMwbkp6Rmp0V1NQMC10RlVwMHcxYzh4SG9NR2ZIY1JRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlERWJJQ1N6QU92ZWF1SVhMY1JpNURqbFpIRlhEaXQzRWNrT3FadWZRMUFTUSJ9fQ', + privateKey : [ + { + crv : 'Ed25519', + d : 'X0JFysSWp4eFAv9fk4ah8qVg3ClFNSiCy_Mdawz9ibo', + kty : 'OKP', + x : 'iUzHO01ydaY4Fkyn9fp3X5d9p4yLkJhrDpjNSEkpETg', + kid : 'HtE0w2iPtJOf4X3tOMci6FxidN5gsUDId_gIQ7X3iWU', + alg : 'EdDSA', + } + ], + didResolutionResult: { + didDocument: { + id : 'did:ion:EiDZjHxyHf-0llvciVAXm7BtYUIImm8WsDP2Wbfp737PvA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiIxIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaVV6SE8wMXlkYVk0Rmt5bjlmcDNYNWQ5cDR5TGtKaHJEcGpOU0VrcEVUZyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRGhoZDdCUkd3UmRReU05d1FOQlBHNVVtS3FGMmRaZFFaS3V6ekRtMFlSUmcifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUF3QUVNcTJGXzlIUDMwbkp6Rmp0V1NQMC10RlVwMHcxYzh4SG9NR2ZIY1JRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlERWJJQ1N6QU92ZWF1SVhMY1JpNURqbFpIRlhEaXQzRWNrT3FadWZRMUFTUSJ9fQ', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiDZjHxyHf-0llvciVAXm7BtYUIImm8WsDP2Wbfp737PvA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiIxIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaVV6SE8wMXlkYVk0Rmt5bjlmcDNYNWQ5cDR5TGtKaHJEcGpOU0VrcEVUZyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRGhoZDdCUkd3UmRReU05d1FOQlBHNVVtS3FGMmRaZFFaS3V6ekRtMFlSUmcifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUF3QUVNcTJGXzlIUDMwbkp6Rmp0V1NQMC10RlVwMHcxYzh4SG9NR2ZIY1JRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlERWJJQ1N6QU92ZWF1SVhMY1JpNURqbFpIRlhEaXQzRWNrT3FadWZRMUFTUSJ9fQ', + }, + ], + service: [ + ], + verificationMethod: [ + { + id : '#1', + controller : 'did:ion:EiDZjHxyHf-0llvciVAXm7BtYUIImm8WsDP2Wbfp737PvA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiIxIiwicHVibGljS2V5SndrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiaVV6SE8wMXlkYVk0Rmt5bjlmcDNYNWQ5cDR5TGtKaHJEcGpOU0VrcEVUZyJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRGhoZDdCUkd3UmRReU05d1FOQlBHNVVtS3FGMmRaZFFaS3V6ekRtMFlSUmcifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUF3QUVNcTJGXzlIUDMwbkp6Rmp0V1NQMC10RlVwMHcxYzh4SG9NR2ZIY1JRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlERWJJQ1N6QU92ZWF1SVhMY1JpNURqbFpIRlhEaXQzRWNrT3FadWZRMUFTUSJ9fQ', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'iUzHO01ydaY4Fkyn9fp3X5d9p4yLkJhrDpjNSEkpETg', + }, + }, + ], + authentication: [ + '#1', + ], + assertionMethod: [ + '#1', + ], + }, + didDocumentMetadata: { + method: { + published : false, + recoveryCommitment : 'EiDEbICSzAOveauIXLcRi5DjlZHFXDit3EckOqZufQ1ASQ', + updateCommitment : 'EiDhhd7BRGwRdQyM9wQNBPG5UmKqF2dZdQZKuzzDm0YRRg', + }, + equivalentId: [ + 'did:ion:EiDZjHxyHf-0llvciVAXm7BtYUIImm8WsDP2Wbfp737PvA', + ], + }, + didResolutionMetadata: {} + } + }, + + dwnService: { + didUri : 'did:ion:EiB9tsHcL4lXyGIZ71yZFoYl7FOUYpfUU0OHu0Auf2-AXg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWciLCJwdWJsaWNLZXlKd2siOnsiY3J2IjoiRWQyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJyOXVGMjRDYVZyZjhtLS1odkRsejEzX1otTWU1Q3VMaVNOUzE5bUM2SnVNIn0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJKc29uV2ViS2V5MjAyMCJ9LHsiaWQiOiJlbmMiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiRGtBWlB4OEJUTzFjNHQ4ZHQzN0Y1VldWdTNxd19tTnNReVVMaEo0a056dyIsInkiOiJUMm9INFhJRk53SmR3UmlDRURSX1VIckxRX3AxY3FHRzBHbnpoLVNONjJ3In0sInB1cnBvc2VzIjpbImtleUFncmVlbWVudCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6eyJlbmNyeXB0aW9uS2V5cyI6WyIjZW5jIl0sIm5vZGVzIjpbImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMCIsImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMSJdLCJzaWduaW5nS2V5cyI6WyIjc2lnIl19LCJ0eXBlIjoiRGVjZW50cmFsaXplZFdlYk5vZGUifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURHdVJGOUVJS1RLWFEzeUk5T3h4UVBZZXBwVElwMUdLaFNwdDhsT0YzcW1BIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDb3F3TDF1NFRCM1RUQ09vb0lySnlJbGw0ZkNTcUFGZlBnWUM2NmlwSUVfUSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ3MtaE5oajVxc3pWNVFPUXVuYkk2azdvTkdUZ0c1b2ZpTGcxR0RYb3p3VXcifX0', + privateKey : [ + { + crv : 'Ed25519', + d : 'ViSHL7fNc4IbltHb3wCGkxmXCL9DzVV9WL0NLvlMcAo', + kty : 'OKP', + x : 'r9uF24CaVrf8m--hvDlz13_Z-Me5CuLiSNS19mC6JuM', + kid : 'PRbAT8qKgVnVEaqqy5XOON5iu7EZIdKOBW1aG62J9GE', + alg : 'EdDSA', + }, + { + kty : 'EC', + crv : 'secp256k1', + d : 'VkaWP07BcUfcTKiK47l1WBYEq5QzakPcD4d1KCqWi_U', + x : 'DkAZPx8BTO1c4t8dt37F5VWVu3qw_mNsQyULhJ4kNzw', + y : 'T2oH4XIFNwJdwRiCEDR_UHrLQ_p1cqGG0Gnzh-SN62w', + kid : 'q514UkY8uFn9BeYykg2YyDMzewX2SQ1Gkqu-ceVpYOM', + alg : 'ES256K', + } + ], + didResolutionResult: { + didDocument: { + id : 'did:ion:EiB9tsHcL4lXyGIZ71yZFoYl7FOUYpfUU0OHu0Auf2-AXg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWciLCJwdWJsaWNLZXlKd2siOnsiY3J2IjoiRWQyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJyOXVGMjRDYVZyZjhtLS1odkRsejEzX1otTWU1Q3VMaVNOUzE5bUM2SnVNIn0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJKc29uV2ViS2V5MjAyMCJ9LHsiaWQiOiJlbmMiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiRGtBWlB4OEJUTzFjNHQ4ZHQzN0Y1VldWdTNxd19tTnNReVVMaEo0a056dyIsInkiOiJUMm9INFhJRk53SmR3UmlDRURSX1VIckxRX3AxY3FHRzBHbnpoLVNONjJ3In0sInB1cnBvc2VzIjpbImtleUFncmVlbWVudCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6eyJlbmNyeXB0aW9uS2V5cyI6WyIjZW5jIl0sIm5vZGVzIjpbImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMCIsImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMSJdLCJzaWduaW5nS2V5cyI6WyIjc2lnIl19LCJ0eXBlIjoiRGVjZW50cmFsaXplZFdlYk5vZGUifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURHdVJGOUVJS1RLWFEzeUk5T3h4UVBZZXBwVElwMUdLaFNwdDhsT0YzcW1BIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDb3F3TDF1NFRCM1RUQ09vb0lySnlJbGw0ZkNTcUFGZlBnWUM2NmlwSUVfUSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ3MtaE5oajVxc3pWNVFPUXVuYkk2azdvTkdUZ0c1b2ZpTGcxR0RYb3p3VXcifX0', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiB9tsHcL4lXyGIZ71yZFoYl7FOUYpfUU0OHu0Auf2-AXg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWciLCJwdWJsaWNLZXlKd2siOnsiY3J2IjoiRWQyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJyOXVGMjRDYVZyZjhtLS1odkRsejEzX1otTWU1Q3VMaVNOUzE5bUM2SnVNIn0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJKc29uV2ViS2V5MjAyMCJ9LHsiaWQiOiJlbmMiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiRGtBWlB4OEJUTzFjNHQ4ZHQzN0Y1VldWdTNxd19tTnNReVVMaEo0a056dyIsInkiOiJUMm9INFhJRk53SmR3UmlDRURSX1VIckxRX3AxY3FHRzBHbnpoLVNONjJ3In0sInB1cnBvc2VzIjpbImtleUFncmVlbWVudCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6eyJlbmNyeXB0aW9uS2V5cyI6WyIjZW5jIl0sIm5vZGVzIjpbImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMCIsImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMSJdLCJzaWduaW5nS2V5cyI6WyIjc2lnIl19LCJ0eXBlIjoiRGVjZW50cmFsaXplZFdlYk5vZGUifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURHdVJGOUVJS1RLWFEzeUk5T3h4UVBZZXBwVElwMUdLaFNwdDhsT0YzcW1BIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDb3F3TDF1NFRCM1RUQ09vb0lySnlJbGw0ZkNTcUFGZlBnWUM2NmlwSUVfUSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ3MtaE5oajVxc3pWNVFPUXVuYkk2azdvTkdUZ0c1b2ZpTGcxR0RYb3p3VXcifX0', + }, + ], + service: [ + { + id : '#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : { + encryptionKeys: [ + '#enc', + ], + nodes: [ + 'https://example.com/dwn0', + 'https://example.com/dwn1', + ], + signingKeys: [ + '#sig', + ], + }, + }, + ], + verificationMethod: [ + { + id : '#sig', + controller : 'did:ion:EiB9tsHcL4lXyGIZ71yZFoYl7FOUYpfUU0OHu0Auf2-AXg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWciLCJwdWJsaWNLZXlKd2siOnsiY3J2IjoiRWQyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJyOXVGMjRDYVZyZjhtLS1odkRsejEzX1otTWU1Q3VMaVNOUzE5bUM2SnVNIn0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJKc29uV2ViS2V5MjAyMCJ9LHsiaWQiOiJlbmMiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiRGtBWlB4OEJUTzFjNHQ4ZHQzN0Y1VldWdTNxd19tTnNReVVMaEo0a056dyIsInkiOiJUMm9INFhJRk53SmR3UmlDRURSX1VIckxRX3AxY3FHRzBHbnpoLVNONjJ3In0sInB1cnBvc2VzIjpbImtleUFncmVlbWVudCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6eyJlbmNyeXB0aW9uS2V5cyI6WyIjZW5jIl0sIm5vZGVzIjpbImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMCIsImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMSJdLCJzaWduaW5nS2V5cyI6WyIjc2lnIl19LCJ0eXBlIjoiRGVjZW50cmFsaXplZFdlYk5vZGUifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURHdVJGOUVJS1RLWFEzeUk5T3h4UVBZZXBwVElwMUdLaFNwdDhsT0YzcW1BIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDb3F3TDF1NFRCM1RUQ09vb0lySnlJbGw0ZkNTcUFGZlBnWUM2NmlwSUVfUSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ3MtaE5oajVxc3pWNVFPUXVuYkk2azdvTkdUZ0c1b2ZpTGcxR0RYb3p3VXcifX0', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'r9uF24CaVrf8m--hvDlz13_Z-Me5CuLiSNS19mC6JuM', + }, + }, + { + id : '#enc', + controller : 'did:ion:EiB9tsHcL4lXyGIZ71yZFoYl7FOUYpfUU0OHu0Auf2-AXg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWciLCJwdWJsaWNLZXlKd2siOnsiY3J2IjoiRWQyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJyOXVGMjRDYVZyZjhtLS1odkRsejEzX1otTWU1Q3VMaVNOUzE5bUM2SnVNIn0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJKc29uV2ViS2V5MjAyMCJ9LHsiaWQiOiJlbmMiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiRGtBWlB4OEJUTzFjNHQ4ZHQzN0Y1VldWdTNxd19tTnNReVVMaEo0a056dyIsInkiOiJUMm9INFhJRk53SmR3UmlDRURSX1VIckxRX3AxY3FHRzBHbnpoLVNONjJ3In0sInB1cnBvc2VzIjpbImtleUFncmVlbWVudCJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV0sInNlcnZpY2VzIjpbeyJpZCI6ImR3biIsInNlcnZpY2VFbmRwb2ludCI6eyJlbmNyeXB0aW9uS2V5cyI6WyIjZW5jIl0sIm5vZGVzIjpbImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMCIsImh0dHBzOi8vZXhhbXBsZS5jb20vZHduMSJdLCJzaWduaW5nS2V5cyI6WyIjc2lnIl19LCJ0eXBlIjoiRGVjZW50cmFsaXplZFdlYk5vZGUifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURHdVJGOUVJS1RLWFEzeUk5T3h4UVBZZXBwVElwMUdLaFNwdDhsT0YzcW1BIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlDb3F3TDF1NFRCM1RUQ09vb0lySnlJbGw0ZkNTcUFGZlBnWUM2NmlwSUVfUSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ3MtaE5oajVxc3pWNVFPUXVuYkk2azdvTkdUZ0c1b2ZpTGcxR0RYb3p3VXcifX0', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'secp256k1', + kty : 'EC', + x : 'DkAZPx8BTO1c4t8dt37F5VWVu3qw_mNsQyULhJ4kNzw', + y : 'T2oH4XIFNwJdwRiCEDR_UHrLQ_p1cqGG0Gnzh-SN62w', + }, + }, + ], + authentication: [ + '#sig', + ], + assertionMethod: [ + '#sig', + ], + keyAgreement: [ + '#enc', + ], + }, + didDocumentMetadata: { + method: { + published : false, + recoveryCommitment : 'EiCs-hNhj5qszV5QOQunbI6k7oNGTgG5ofiLg1GDXozwUw', + updateCommitment : 'EiDGuRF9EIKTKXQ3yI9OxxQPYeppTIp1GKhSpt8lOF3qmA', + }, + equivalentId: [ + 'did:ion:EiB9tsHcL4lXyGIZ71yZFoYl7FOUYpfUU0OHu0Auf2-AXg', + ], + }, + didResolutionMetadata: {} + } + }, +}; \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-ion/resolve.ts b/packages/dids/tests/fixtures/test-vectors/did-ion/resolve.ts new file mode 100644 index 000000000..cf087d303 --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/did-ion/resolve.ts @@ -0,0 +1,56 @@ +import type { Jwk } from '@web5/crypto'; + +import type { DidResolutionResult } from '../../../../src/types/did-core.js'; + +type TestVector = { + [key: string]: { + didUri: string; + privateKey: Jwk[]; + didResolutionResult: DidResolutionResult; + }; +}; + +export const vectors: TestVector = { + publishedDid: { + didUri : 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ', + privateKey : [], + didResolutionResult : { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : { + id : 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ', + '@context' : [ + 'https://www.w3.org/ns/did/v1', + { + '@base': 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ', + }, + ], + service: [ + ], + verificationMethod: [ + { + id : '#dwn-sig', + controller : 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ', + type : 'JsonWebKey2020', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'Sy0lk6pMXC10WyIh4g8sLz1loL8ImzLcqmFW2267IXc', + }, + }, + ], + authentication: [ + '#dwn-sig', + ], + }, + didDocumentMetadata: { + method: { + published : true, + recoveryCommitment : 'EiDzYFBF-EPcbt4sVdepmniqfg93wrh1dZTZY1ZI4m6enw', + updateCommitment : 'EiAp4ocUKXcYC3D-DaGiW2D01D3QVxqGegT1Fq42bDaPoQ', + }, + canonicalId: 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ', + }, + didResolutionMetadata: {} + } + }, +}; \ 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 new file mode 100644 index 000000000..ce4c46358 --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/did-ion/to-keys.ts @@ -0,0 +1,81 @@ +import type { Jwk, LocalKeyManager } from '@web5/crypto'; + +import sinon from 'sinon'; + +import type { Did } from '../../../../src/methods/did-method.js'; + +type TestVector = { + [key: string]: { + did: Did; + 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/fixtures/test-vectors/did-key.ts b/packages/dids/tests/fixtures/test-vectors/did-key.ts deleted file mode 100644 index 580ac1162..000000000 --- a/packages/dids/tests/fixtures/test-vectors/did-key.ts +++ /dev/null @@ -1,318 +0,0 @@ -export const didKeyCreateDocumentTestVectors = [ - { - id : 'did.createDocument.1', - input : { - did : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - publicKeyFormat : 'JsonWebKey2020' - }, - output: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'ZuVpK6HnahBtV1Y_jhnYK-fqHAz3dXmWXT_h-J7SL6I' - } - } - ], - 'assertionMethod': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'authentication': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityDelegation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityInvocation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ] - } - }, - { - id : 'did.createDocument.2', - input : { - did : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - publicKeyFormat : 'Ed25519VerificationKey2020' - }, - output: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/ed25519-2020/v1' - ], - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'type' : 'Ed25519VerificationKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyMultibase' : 'z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - } - ], - 'assertionMethod': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'authentication': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityDelegation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityInvocation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ] - } - }, - { - id : 'did.createDocument.3', - input : { - did : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - enableEncryptionKeyDerivation : true, - publicKeyFormat : 'JsonWebKey2020' - }, - output: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'ZuVpK6HnahBtV1Y_jhnYK-fqHAz3dXmWXT_h-J7SL6I' - } - }, - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyJwk' : { - 'crv' : 'X25519', - 'kty' : 'OKP', - 'x' : 'FrLpNU0FVX4oAByhAbU71h4yb-WMr6penULFCzbMtxo', - }, - } - ], - 'assertionMethod': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'authentication': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityDelegation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityInvocation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'keyAgreement': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd' - ] - } - }, - { - id : 'did.createDocument.4', - input : { - did : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - enableEncryptionKeyDerivation : true, - publicKeyFormat : 'Ed25519VerificationKey2020' - }, - output: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/ed25519-2020/v1', - 'https://w3id.org/security/suites/x25519-2020/v1' - ], - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'type' : 'Ed25519VerificationKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyMultibase' : 'z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - }, - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd', - 'type' : 'X25519KeyAgreementKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyMultibase' : 'z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd' - } - ], - 'assertionMethod': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'authentication': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityDelegation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityInvocation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'keyAgreement': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd' - ] - } - } -]; - -export const didKeyCreateTestVectors = [ - { - id : 'did.create.1', - input : { - keySet: { - verificationMethodKeys: [{ - 'publicKeyJwk': { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' - }, - relationships: ['authentication'] - }], - }, - publicKeyAlgorithm : 'Ed25519', - publicKeyFormat : 'JsonWebKey2020' - }, - output: { - did : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - document : { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], - 'id' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' - } - } - ], - 'assertionMethod': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - 'authentication': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - 'capabilityDelegation': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - 'capabilityInvocation': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - }, - keySet: { - verificationMethodKeys: [{ - 'publicKeyJwk': { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' - }, - relationships: ['authentication'] - }], - }, - } - }, - { - id : 'did.create.2', - input : { - enableEncryptionKeyDerivation : true, - keySet : { - verificationMethodKeys: [{ - publicKeyJwk: { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' - }, - relationships: ['authentication'] - }], - }, - publicKeyAlgorithm : 'Ed25519', - publicKeyFormat : 'JsonWebKey2020' - }, - output: { - did : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - document : { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], - 'id' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' - } - }, - { - 'controller' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk', - 'type' : 'JsonWebKey2020', - 'id' : 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6LSjqybG4FgDYHxo4v9tWzgTpCm9a3b9K3QYqicCabqWeHQ', - 'publicKeyJwk' : { - 'crv' : 'X25519', - 'kty' : 'OKP', - 'x' : 'eWA3oUNKm3nZN0vqiC_tClPCkBznN5R0Y9NofJkoaXM' - } - } - ], - 'assertionMethod': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - 'authentication': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - 'capabilityDelegation': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - 'capabilityInvocation': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk' - ], - 'keyAgreement': [ - 'did:key:z6MkrCigh4zugDVEieqt4WbtWParigHeH5TEYEuKcSyCykUk#z6LSjqybG4FgDYHxo4v9tWzgTpCm9a3b9K3QYqicCabqWeHQ' - ] - }, - keySet: { - verificationMethodKeys: [{ - publicKeyJwk: { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' - }, - relationships: ['authentication'] - }], - }, - } - } -]; \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-resolver.ts b/packages/dids/tests/fixtures/test-vectors/did-resolver.ts deleted file mode 100644 index e215406e9..000000000 --- a/packages/dids/tests/fixtures/test-vectors/did-resolver.ts +++ /dev/null @@ -1,38 +0,0 @@ -export const didResolverTestVectors = [ - { - id : 'did.resolve.1', - input : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - output : { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'ZuVpK6HnahBtV1Y_jhnYK-fqHAz3dXmWXT_h-J7SL6I' - } - } - ], - 'assertionMethod': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'authentication': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityDelegation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityInvocation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ] - } - }, -]; \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-utils.ts b/packages/dids/tests/fixtures/test-vectors/did-utils.ts deleted file mode 100644 index 0aed2d3de..000000000 --- a/packages/dids/tests/fixtures/test-vectors/did-utils.ts +++ /dev/null @@ -1,253 +0,0 @@ -const didDocumentForIdTestVectors = { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1', - 'https://w3id.org/security/suites/ed25519-2020/v1', - ], - id : 'did:method:alice', - verificationMethod : [ - { - id : 'did:method:alice#key-1', - type : 'JsonWebKey2020', - controller : 'did:method:alice', - publicKeyJwk : { - alg : 'EdDSA', - kty : 'OKP', - crv : 'Ed25519', - x : 'GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc' - } - }, - { - id : 'did:method:alice#key-2', - type : 'Ed25519VerificationKey2020', - controller : 'did:method:alice', - publicKeyMultibase : 'z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd' - }, - ], - 'authentication': [ - 'did:method:alice#key-1', - { - id : 'did:method:alice#key-3', - type : 'JsonWebKey2020', - controller : 'did:method:alice', - publicKeyJwk : { - alg : 'EdDSA', - kty : 'OKP', - crv : 'Ed25519', - x : 'k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI' - }, - }, - ], - 'keyAgreement': [ - { - id : 'did:method:alice#key-5', - type : 'JsonWebKey2020', - controller : 'did:method:alice', - publicKeyJwk : { - alg : 'EdDSA', - kty : 'OKP', - crv : 'X25519', - x : 'SOKzporeWqJMJxf1NgPtup3whiBLPLZxgLDORNzbXwA' - }, - }, - { - id : 'did:method:alice#key-6', - type : 'X25519KeyAgreementKey2020', - controller : 'did:method:alice', - publicKeyMultibase : 'z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d' - }, - ] -}; - -export const didDocumentIdTestVectors = [ - { - id : 'did.getIdByKey.1', - input : { - didDocument : didDocumentForIdTestVectors, - publicKeyJwk : { - kty : 'OKP', - crv : 'Ed25519', - x : 'GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc' - }, - }, - output: 'did:method:alice#key-1' - }, - { - id : 'did.getIdByKey.2', - input : { - didDocument : didDocumentForIdTestVectors, - publicKeyMultibase : 'z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd', - }, - output: 'did:method:alice#key-2' - }, - { - id : 'did.getIdByKey.3', - input : { - didDocument : didDocumentForIdTestVectors, - publicKeyJwk : { - kty : 'OKP', - crv : 'Ed25519', - x : 'k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI' - }, - publicKeyMultibase: 'z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd', - }, - output: 'did:method:alice#key-2' - }, - { - id : 'did.getIdByKey.4', - input : { - didDocument : didDocumentForIdTestVectors, - publicKeyJwk : { - kty : 'OKP', - crv : 'Ed25519', - x : 'k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI' - }, - }, - output: undefined - } -]; - -export const didDocumentTypeTestVectors = [ - { - id : 'did.getTypes.1', - input : { - didDocument: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/ed25519-2020/v1', - 'https://w3id.org/security/suites/x25519-2020/v1' - ], - 'id' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - 'type' : 'Ed25519VerificationKey2020', - 'controller' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - 'publicKeyMultibase' : 'z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp' - }, - { - 'id' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW', - 'type' : 'X25519KeyAgreementKey2020', - 'controller' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - 'publicKeyMultibase' : 'z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW' - } - ], - 'authentication': [ - 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - { - 'id' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#zH3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV', - 'type' : 'Ed25519VerificationKey2020', - 'controller' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - 'publicKeyMultibase' : 'zH3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV' - } - ], - 'assertionMethod': [ - 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - { - 'id' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf', - 'type' : 'Ed25519VerificationKey2020', - 'controller' : 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp', - 'publicKeyMultibase' : 'z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf' - } - ], - 'capabilityDelegation': [ - 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp' - ], - 'capabilityInvocation': [ - 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp' - ], - 'keyAgreement': [ - 'did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW' - ] - }, - }, - output: ['Ed25519VerificationKey2020', 'X25519KeyAgreementKey2020'] - }, - - { - id : 'did.getTypes.2', - input : { - didDocument: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'verificationMethod' : [ - { - 'id' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D', - 'publicKeyJwk' : { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'kty' : 'OKP', - 'x' : 'ZuVpK6HnahBtV1Y_jhnYK-fqHAz3dXmWXT_h-J7SL6I' - } - } - ], - 'assertionMethod': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'authentication': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityDelegation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ], - 'capabilityInvocation': [ - 'did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D' - ] - }, - }, - output: ['JsonWebKey2020'] - }, - - // Source: https://w3c.github.io/did-core/#example-did-document-with-different-verification-method-types - { - id : 'did.type.w3c.32', - input : { - didDocument: { - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - 'https://w3id.org/security/suites/secp256k1-2019/v1', - 'https://w3id.org/security/suites/jws-2020/v1' - ], - 'verificationMethod': [ - { - 'id' : 'did:example:123#key-0', - 'type' : 'Ed25519VerificationKey2018', - 'controller' : 'did:example:123', - 'publicKeyBase58' : '3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J' // external (property name) - }, - { - 'id' : 'did:example:123#key-1', - 'type' : 'X25519KeyAgreementKey2019', - 'controller' : 'did:example:123', - 'publicKeyBase58' : 'FbQWLPRhTH95MCkQUeFYdiSoQt8zMwetqfWoxqPgaq7x' // external (property name) - }, - { - 'id' : 'did:example:123#key-2', - 'type' : 'EcdsaSecp256k1VerificationKey2019', - 'controller' : 'did:example:123', - 'publicKeyBase58' : 'ns2aFDq25fEV1NUd3wZ65sgj5QjFW8JCAHdUJfLwfodt' // external (property name) - }, - { - 'id' : 'did:example:123#key-3', - 'type' : 'JsonWebKey2020', - 'controller' : 'did:example:123', - 'publicKeyJwk' : { - 'kty' : 'EC', // external (property name) - 'crv' : 'P-256', // external (property name) - 'x' : 'Er6KSSnAjI70ObRWhlaMgqyIOQYrDJTE94ej5hybQ2M', // external (property name) - 'y' : 'pPVzCOTJwgikPjuUE6UebfZySqEJ0ZtsWFpj7YSPGEk' // external (property name) - } - } - ] - }, - }, - output: ['Ed25519VerificationKey2018', 'X25519KeyAgreementKey2019', 'EcdsaSecp256k1VerificationKey2019', 'JsonWebKey2020'] - } -]; \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/utils/get-verification-method-by-key.json b/packages/dids/tests/fixtures/test-vectors/utils/get-verification-method-by-key.json new file mode 100644 index 000000000..6ff6b5122 --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/utils/get-verification-method-by-key.json @@ -0,0 +1,541 @@ +{ + "description": "DID Utils getVerificationMethodByKey test vectors", + "vectors": [ + { + "description": "returns verification method of given key in JWK format", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "verificationMethod": [ + { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0", + "controller": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "kid": "i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg", + "x": "vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U", + "y": "VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU", + "alg": "ES256K" + } + } + ], + "authentication": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "assertionMethod": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "capabilityInvocation": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "capabilityDelegation": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ] + }, + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "kid": "i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg", + "x": "vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U", + "y": "VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU", + "alg": "ES256K" + } + }, + "output": { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0", + "controller": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "kid": "i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg", + "x": "vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U", + "y": "VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU", + "alg": "ES256K" + } + }, + "errors": false + }, + { + "description": "returns verification method of given key in multibase format", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:method:alice", + "verificationMethod": [ + { + "id": "did:method:alice#key-1", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc" + } + }, + { + "id": "did:method:alice#key-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + } + ], + "authentication": [ + "did:method:alice#key-1", + { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + } + ], + "keyAgreement": [ + { + "id": "did:method:alice#key-5", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "X25519", + "x": "SOKzporeWqJMJxf1NgPtup3whiBLPLZxgLDORNzbXwA" + } + }, + { + "id": "did:method:alice#key-6", + "type": "X25519KeyAgreementKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + } + ] + }, + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + }, + "output": { + "id": "did:method:alice#key-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + }, + "errors": false + }, + { + "description": "returns embedded verification method of given key in JWK format", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:method:alice", + "verificationMethod": [ + { + "id": "did:method:alice#key-1", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc" + } + }, + { + "id": "did:method:alice#key-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + } + ], + "authentication": [ + "did:method:alice#key-1", + { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + } + ], + "keyAgreement": [ + { + "id": "did:method:alice#key-5", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "X25519", + "x": "SOKzporeWqJMJxf1NgPtup3whiBLPLZxgLDORNzbXwA" + } + }, + { + "id": "did:method:alice#key-6", + "type": "X25519KeyAgreementKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + } + ] + }, + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + }, + "output": { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + }, + "errors": false + }, + { + "description": "returns embedded verification method of given key in multibase format", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:method:alice", + "verificationMethod": [ + { + "id": "did:method:alice#key-1", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc" + } + }, + { + "id": "did:method:alice#key-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + } + ], + "authentication": [ + "did:method:alice#key-1", + { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + } + ], + "keyAgreement": [ + { + "id": "did:method:alice#key-5", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "X25519", + "x": "SOKzporeWqJMJxf1NgPtup3whiBLPLZxgLDORNzbXwA" + } + }, + { + "id": "did:method:alice#key-6", + "type": "X25519KeyAgreementKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + } + ] + }, + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + }, + "output": { + "id": "did:method:alice#key-6", + "type": "X25519KeyAgreementKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + }, + "errors": false + }, + { + "description": "returns publicKeyJwk match before publicKeyMultibase if both are given", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:method:alice", + "verificationMethod": [ + { + "id": "did:method:alice#key-1", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc" + } + }, + { + "id": "did:method:alice#key-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + } + ], + "authentication": [ + "did:method:alice#key-1", + { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + } + ], + "keyAgreement": [ + { + "id": "did:method:alice#key-5", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "X25519", + "x": "SOKzporeWqJMJxf1NgPtup3whiBLPLZxgLDORNzbXwA" + } + }, + { + "id": "did:method:alice#key-6", + "type": "X25519KeyAgreementKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + } + ] + }, + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + }, + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + }, + "output": { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + }, + "errors": false + }, + { + "description": "returns null if no match is found", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:method:alice", + "verificationMethod": [ + { + "id": "did:method:alice#key-1", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc" + } + }, + { + "id": "did:method:alice#key-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + } + ], + "authentication": [ + "did:method:alice#key-1", + { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + } + ], + "keyAgreement": [ + { + "id": "did:method:alice#key-5", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "X25519", + "x": "SOKzporeWqJMJxf1NgPtup3whiBLPLZxgLDORNzbXwA" + } + }, + { + "id": "did:method:alice#key-6", + "type": "X25519KeyAgreementKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + } + ] + }, + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "WAZghWazRgO-V858h_USUXWIeUZv1DfV_NmS_WIXkgU" + } + }, + "output": null, + "errors": false + }, + { + "description": "returns null if no match is found", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:method:alice", + "verificationMethod": [ + { + "id": "did:method:alice#key-1", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "GM_NcTChsLlfdODKG573OSWGO7wNwzhkHRPHPxdAYfc" + } + }, + { + "id": "did:method:alice#key-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSdCnN59MPkRCaVvXczoipz5tMcPpjrCnvqBcHHjCDohYd" + } + ], + "authentication": [ + "did:method:alice#key-1", + { + "id": "did:method:alice#key-3", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "k1GKchkMMp9nbYsShY1R2UVzPsQill6zv2De38ERkfI" + } + } + ], + "keyAgreement": [ + { + "id": "did:method:alice#key-5", + "type": "JsonWebKey2020", + "controller": "did:method:alice", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "X25519", + "x": "SOKzporeWqJMJxf1NgPtup3whiBLPLZxgLDORNzbXwA" + } + }, + { + "id": "did:method:alice#key-6", + "type": "X25519KeyAgreementKey2020", + "controller": "did:method:alice", + "publicKeyMultibase": "z6LSgah1r8rDCT2brDg7Vhh2LYmTkcEVgUHng1Ji68XBy4d" + } + ] + }, + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "WAZghWazRgO-V858h_USUXWIeUZv1DfV_NmS_WIXkgU" + } + }, + "output": null, + "errors": false + }, + { + "description": "error if didDocument is an empty object", + "input": { + "didDocument": {} + }, + "errors": true + }, + { + "description": "error if didDocument is missing", + "input": {}, + "errors": true + } + ] +} \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/utils/get-verification-method-types.json b/packages/dids/tests/fixtures/test-vectors/utils/get-verification-method-types.json new file mode 100644 index 000000000..59f3f4adf --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/utils/get-verification-method-types.json @@ -0,0 +1,169 @@ +{ + "description": "DID Utils getVerificationMethodTypes test vectors", + "vectors": [ + { + "description": "returns expected types from a DID document with only multibase keys", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "verificationMethod": [ + { + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "type": "Ed25519VerificationKey2020", + "controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "publicKeyMultibase": "z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + }, + { + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW", + "type": "X25519KeyAgreementKey2020", + "controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "publicKeyMultibase": "z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW" + } + ], + "authentication": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + { + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#zH3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV", + "type": "Ed25519VerificationKey2020", + "controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "publicKeyMultibase": "zH3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" + } + ], + "assertionMethod": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + { + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf", + "type": "Ed25519VerificationKey2020", + "controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "publicKeyMultibase": "z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf" + } + ], + "capabilityDelegation": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + ], + "capabilityInvocation": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + ], + "keyAgreement": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW" + ] + } + }, + "output": [ + "Ed25519VerificationKey2020", + "X25519KeyAgreementKey2020" + ], + "errors": false + }, + { + "description": "returns expected types from a DID document with only JWK keys", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D", + "verificationMethod": [ + { + "id": "did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D", + "publicKeyJwk": { + "alg": "EdDSA", + "crv": "Ed25519", + "kty": "OKP", + "x": "ZuVpK6HnahBtV1Y_jhnYK-fqHAz3dXmWXT_h-J7SL6I" + } + } + ], + "assertionMethod": [ + "did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D" + ], + "authentication": [ + "did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D" + ], + "capabilityDelegation": [ + "did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D" + ], + "capabilityInvocation": [ + "did:key:z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D#z6MkmNvXGmVuux5W63nXKEM8zoxFmDLNfe7siCKG2GM7Kd8D" + ] + } + }, + "output": [ + "JsonWebKey2020" + ], + "errors": false + }, + { + "description": "returns expected types from a DID document with a mix of JWK and multibase keys", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "verificationMethod": [ + { + "id": "did:example:123#key-0", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:123", + "publicKeyBase58": "3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J" + }, + { + "id": "did:example:123#key-1", + "type": "X25519KeyAgreementKey2019", + "controller": "did:example:123", + "publicKeyBase58": "FbQWLPRhTH95MCkQUeFYdiSoQt8zMwetqfWoxqPgaq7x" + }, + { + "id": "did:example:123#key-2", + "type": "EcdsaSecp256k1VerificationKey2019", + "controller": "did:example:123", + "publicKeyBase58": "ns2aFDq25fEV1NUd3wZ65sgj5QjFW8JCAHdUJfLwfodt" + }, + { + "id": "did:example:123#key-3", + "type": "JsonWebKey2020", + "controller": "did:example:123", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-256", + "x": "Er6KSSnAjI70ObRWhlaMgqyIOQYrDJTE94ej5hybQ2M", + "y": "pPVzCOTJwgikPjuUE6UebfZySqEJ0ZtsWFpj7YSPGEk" + } + } + ] + } + }, + "output": [ + "Ed25519VerificationKey2018", + "X25519KeyAgreementKey2019", + "EcdsaSecp256k1VerificationKey2019", + "JsonWebKey2020" + ], + "errors": false + }, + { + "description": "error if didDocument is an empty object", + "input": { + "didDocument": {} + }, + "errors": true + }, + { + "description": "error if didDocument is missing", + "input": {}, + "errors": true + } + ] +} \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/utils/get-verification-methods.json b/packages/dids/tests/fixtures/test-vectors/utils/get-verification-methods.json new file mode 100644 index 000000000..ead1d58ea --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/utils/get-verification-methods.json @@ -0,0 +1,175 @@ +{ + "description": "DID Utils getVerificationMethods test vectors", + "vectors": [ + { + "description": "returns expected result from a didDocument with one verificationMethod", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "verificationMethod": [ + { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0", + "controller": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "kid": "i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg", + "x": "vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U", + "y": "VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU", + "alg": "ES256K" + } + } + ], + "authentication": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "assertionMethod": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "capabilityInvocation": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "capabilityDelegation": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ] + } + }, + "output": [ + { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0", + "controller": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "kid": "i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg", + "x": "vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U", + "y": "VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU", + "alg": "ES256K" + } + } + ], + "errors": false + }, + { + "description": "returns expected result from a didDocument with one embedded method", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "verificationMethod": [ + { + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9#z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "M6cuSLXzNrcydvtfswnRxDhnictOvjyzXne6ljRVw9Q" + } + } + ] + } + }, + "output": [ + { + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9#z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "M6cuSLXzNrcydvtfswnRxDhnictOvjyzXne6ljRVw9Q" + } + } + ], + "errors": false + }, + { + "description": "returns expected result from a didDocument with verificationMethod and embedded methods", + "input": { + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "verificationMethod": [ + { + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9#z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "M6cuSLXzNrcydvtfswnRxDhnictOvjyzXne6ljRVw9Q" + } + } + ], + "authentication": [ + "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9#z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9" + ], + "keyAgreement": [ + { + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9#z6LSmYay64jcWvnDohtW3KKDUiKYs1XzFtJSbfBLuqBPgwjq", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "publicKeyJwk": { + "kty": "OKP", + "crv": "X25519", + "x": "kqMjAf2VRvY5jOgn3y-rJ9uII_ad2c5Ru3-mqmpTAAQ" + } + } + ] + } + }, + "output": [ + { + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9#z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "publicKeyJwk": { + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "M6cuSLXzNrcydvtfswnRxDhnictOvjyzXne6ljRVw9Q" + } + }, + { + "id": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9#z6LSmYay64jcWvnDohtW3KKDUiKYs1XzFtJSbfBLuqBPgwjq", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9", + "publicKeyJwk": { + "kty": "OKP", + "crv": "X25519", + "x": "kqMjAf2VRvY5jOgn3y-rJ9uII_ad2c5Ru3-mqmpTAAQ" + } + } + ], + "errors": false + }, + { + "description": "error if didDocument is an empty object", + "input": { + "didDocument": {} + }, + "errors": true + }, + { + "description": "error if didDocument is missing", + "input": {}, + "errors": true + } + ] +} \ 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 new file mode 100644 index 000000000..5e4088a28 --- /dev/null +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -0,0 +1,1108 @@ +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 { DidErrorCode } from '../../src/did-error.js'; +import { DidDht, DidDhtRegisteredDidType } from '../../src/methods/did-dht.js'; + +// Helper function to create a mocked fetch response that fails and returns a 404 Not Found. +const fetchNotFoundResponse = () => ({ + status : 404, + statusText : 'Not Found', + ok : false +}); + +// Helper function to create a mocked fetch response that is successful and returns the given +// response. +const fetchOkResponse = (response?: any) => ({ + status : 200, + statusText : 'OK', + ok : true, + arrayBuffer : async () => Promise.resolve(response) +}); + +describe('DidDht', () => { + let fetchStub: sinon.SinonStub; + + beforeEach(() => { + // Setup stub so that a mocked response is returned rather than calling over the network. + fetchStub = sinon.stub(globalThis as any, 'fetch'); + + // By default, return a 200 OK response when fetch is called by publish(). + fetchStub.resolves(fetchOkResponse()); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + 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('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); + }); + + it('handles creating DIDs with additional Ed25519 verification methods', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.verificationMethod).to.have.length(2); + expect(did.didDocument.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'Ed25519'); + }); + + it('handles creating DIDs with additional secp256k1 verification methods', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'secp256k1', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.verificationMethod).to.have.length(2); + expect(did.didDocument.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'secp256k1'); + }); + + it('handles creating DIDs with additional secp256r1 verification methods', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'secp256r1', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.verificationMethod).to.have.length(2); + expect(did.didDocument.verificationMethod?.[1].publicKeyJwk).to.have.property('crv', 'P-256'); + }); + + it('allows one or more DID controller identifiers to be specified', async () => { + let did = await DidDht.create({ + options: { + controllers: 'did:example:1234' + } + }); + + expect(did.didDocument).to.have.property('controller', 'did:example:1234'); + + did = await DidDht.create({ + options: { + controllers: ['did:example:1234', 'did:example:5678'] + } + }); + + expect(did.didDocument.controller).to.deep.equal(['did:example:1234', 'did:example:5678']); + }); + + it('allows one or more Also Known As identifiers to be specified', async () => { + let did = await DidDht.create({ + options: { + alsoKnownAs: ['did:example:1234'] + } + }); + + expect(did.didDocument.alsoKnownAs).to.deep.equal(['did:example:1234']); + + did = await DidDht.create({ + options: { + alsoKnownAs: ['did:example:1234', 'did:example:5678'] + } + }); + + expect(did.didDocument.alsoKnownAs).to.deep.equal(['did:example:1234', 'did:example:5678']); + }); + + it('handles creating DIDs with additional verification methods', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.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'); + }); + + it('uses the JWK thumbprint as the ID for additional verification methods, by default', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.verificationMethod?.[1].id).to.include(`#${did?.didDocument?.verificationMethod?.[1]?.publicKeyJwk?.kid}`); + }); + + it('allows a custom ID to be specified for additional verification methods', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + id : '1', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.verificationMethod?.[1]).to.have.property('id', `${did.uri}#1`); + }); + + it('handles creating DIDs with one service', async () => { + const did = await DidDht.create({ + options: { + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + } + ] + } + }); + + 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'); + }); + + it('handles creating DIDs with multiple services', async () => { + const did = await DidDht.create({ + options: { + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + }, + { + id : 'oid4vci', + type : 'OID4VCI', + serviceEndpoint : 'https://issuer.example.com', + } + ] + } + }); + + 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`); + }); + + it('accepts a custom controller for the Identity Key verification method', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + id : '0', + controller : 'did:example:1234', + } + ] + } + }); + + const identityKeyVerificationMethod = did.didDocument?.verificationMethod?.find( + (method) => method.id.endsWith('#0') + ); + expect(identityKeyVerificationMethod).to.have.property('controller', 'did:example:1234'); + }); + + it('accepts custom properties for services', async () => { + const did = await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + id : 'sig', + purposes : ['authentication', 'assertionMethod'] + }, + { + algorithm : 'secp256k1', + id : 'enc', + purposes : ['keyAgreement'] + } + ], + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + enc : '#enc', + sig : '#sig' + } + ] + } + }); + + 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'); + }); + + it('accepts one or more DID DHT registered types', async () => { + const did = await DidDht.create({ + options: { + types: [DidDhtRegisteredDidType.FinancialInstitution, DidDhtRegisteredDidType.WebApp] + } + }); + + expect(did.metadata).to.have.property('types'); + expect(did.metadata.types).to.have.length(2); + expect(did.metadata.types).to.include(DidDhtRegisteredDidType.FinancialInstitution); + expect(did.metadata.types).to.include(DidDhtRegisteredDidType.WebApp); + }); + + it('publishes DIDs, by default', async () => { + const did = await DidDht.create(); + + expect(did.metadata).to.have.property('published', true); + expect(fetchStub.calledOnce).to.be.true; + }); + + it('allows DID publishing to optionally be disabled', async () => { + const did = await DidDht.create({ options: { publish: false } }); + + expect(did.metadata).to.have.property('published', false); + expect(fetchStub.called).to.be.false; + }); + + it('returns a DID with a getSigner function that can sign and verify data', async () => { + const did = await DidDht.create(); + + 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('throws an error if publishing fails', async () => { + // Simulate a network error when attempting to publish the DID. + fetchStub.rejects(new Error('Network error')); + + try { + await DidDht.create(); + + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.code).to.equal(DidErrorCode.InternalError); + expect(error.message).to.include('Failed to put Pkarr record'); + } + }); + + it('throws an error if a verification method algorithm is not supported', async () => { + try { + await DidDht.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + purposes : ['authentication', 'assertionMethod'] + }, + { + // @ts-expect-error - Testing invalid algorithm. + algorithm : 'Ed448', + id : 'dwn-sig', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('algorithms are not supported'); + } + }); + + it('throws an error if services are missing required properties', async () => { + try { + // @ts-expect-error - Testing service with missing 'id' property. + await DidDht.create({ options: { services: [{ type: 'b', serviceEndpoint: 'c' }] } }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('services are missing required properties'); + } + + try { + // @ts-expect-error - Testing service with missing 'type' property. + await DidDht.create({ options: { services: [{ id: 'a', serviceEndpoint: 'c' }] } }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('services are missing required properties'); + } + + try { + // @ts-expect-error - Testing service with missing 'serviceEndpoint' property. + await DidDht.create({ options: { services: [{ id: 'a', type: 'b' }] } }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('services are missing required properties'); + } + }); + + it('throws an error if the resulting DID document would exceed the 1000 byte maximum', async () => { + try { + // Attempt to create a DID with 6 verification methods (Identity Key plus 5 additional). + await DidDht.create({ + options: { + verificationMethods: [ + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' } + ] + } + }); + + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.code).to.equal(DidErrorCode.InvalidDidDocumentLength); + } + }); + }); + + 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 () => { + try { + await DidDht.fromKeyManager({ didUri: 'did:example:1234', keyManager }); + 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); + } + }); + + 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:...'; + try { + await DidDht.fromKeyManager({ didUri, keyManager }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('does not contain a public key'); + } finally { + sinon.restore(); + } + }); + }); + + describe('fromKeys', () => { + 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: { + id : 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo', + verificationMethod : [ + { + 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', + }, + }, + ], + authentication: [ + 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#0', + ], + assertionMethod: [ + 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#0', + ], + capabilityDelegation: [ + 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#0', + ], + capabilityInvocation: [ + 'did:dht:urex8kbn3ewbdrjq36obf3rpg8joomzpu1gb4cfkhj3ey4y9fqgo#0', + ], + }, + didDocumentMetadata : {}, + didResolutionMetadata : {} + }; + + // 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); + + 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', 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: { + 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)); + + const did = await DidDht.fromKeys(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('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'); + } + }); + + it('throws an error if the given key set is missing a public key', async () => { + delete portableDid.verificationMethods![0].publicKeyJwk; + + 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'); + } + }); + + it('throws an error if the given key set is missing a private key', async () => { + delete portableDid.verificationMethods![0].privateKeyJwk; + + 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'); + } + }); + + 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'; + + try { + await DidDht.fromKeys(portableDid); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('missing an Identity Key'); + } + }); + }); + + 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( + Convert.hex('5f011403ca8a3dbf0935a4f598b47c965b66bc67c86c7b665fbbfa6a31013075f512bbf68ca5' + + 'c1b6f6ddde45b6645366a7234e204ae6f7c2d0bf4b9b99efae050000000065b0123100008400' + + '0000000200000000035f6b30045f64696434706a6969773769626e3674396b316d6b6b6e6b6f' + + '776a6b6574613863686b7367777a6b7435756b3837393865707578313338366f000010000100' + + '001c2000373669643d303b743d303b6b3d616d7461647145586f5f564a616c4356436956496a' + + '67374f4b73616c3152334e522d5f4f68733379796630045f64696434706a6969773769626e36' + + '74396b316d6b6b6e6b6f776a6b6574613863686b7367777a6b7435756b383739386570757831' + + '3338366f000010000100001c20002726763d303b766d3d6b303b617574683d6b303b61736d3d' + + '6b303b64656c3d6b303b696e763d6b30').toArrayBuffer() + )); + + const did = 'did:dht:pjiiw7ibn6t9k1mkknkowjketa8chksgwzkt5uk8798epux1386o'; + const didResolutionResult = await DidDht.resolve(did); + + expect(didResolutionResult).to.have.property('didDocument'); + expect(didResolutionResult).to.have.property('didDocumentMetadata'); + expect(didResolutionResult).to.have.property('didResolutionMetadata'); + + expect(didResolutionResult.didDocument).to.have.property('id', did); + expect(didResolutionResult.didDocument?.verificationMethod).to.have.length(1); + }); + + it('resolves a published DID with services', async () => { + // Mock the response from the Pkarr relay rather than calling over the network. + fetchStub.resolves(fetchOkResponse( + Convert.hex('19c356a57605e7be8d101e211137dec2bbb875f076a60866529eff68372380c63e435c852bf3' + + 'dbc6fa4bbda014c561af361cace90c91350477c010769a9910060000000065b035ce00008400' + + '0000000300000000035f6b30045f646964343177696161616f61677a63656767736e77667a6d' + + '78356377656f67356d736734753533366d627938737179336d6b703377796b6f000010000100' + + '001c2000373669643d303b743d303b6b3d6c53754d5968673132494d6177714675742d325552' + + '413231324e7165382d574542374f426c616d356f4255035f7330045f64696434317769616161' + + '6f61677a63656767736e77667a6d78356377656f67356d736734753533366d62793873717933' + + '6d6b703377796b6f000010000100001c2000393869643d64776e3b743d446563656e7472616c' + + '697a65645765624e6f64653b73653d68747470733a2f2f6578616d706c652e636f6d2f64776e' + + '045f646964343177696161616f61677a63656767736e77667a6d78356377656f67356d736734' + + '753533366d627938737179336d6b703377796b6f000010000100001c20002e2d763d303b766d' + + '3d6b303b617574683d6b303b61736d3d6b303b64656c3d6b303b696e763d6b303b7376633d73' + + '30').toArrayBuffer() + )); + + const did = 'did:dht:1wiaaaoagzceggsnwfzmx5cweog5msg4u536mby8sqy3mkp3wyko'; + const didResolutionResult = await DidDht.resolve(did); + + expect(didResolutionResult.didDocument?.service).to.have.length(1); + expect(didResolutionResult.didDocument?.service?.[0]).to.have.property('id', `${did}#dwn`); + }); + + it('resolves a published DID with a DID Controller identifier', async () => { + fetchStub.resolves(fetchOkResponse( + Convert.hex('980110156ea686d159d62952c43a151e9fc8f69d9edf0ed38ae78505a3a340f4508de2adad29' + + '342e4acf9f3149b976234c6157272b28937e9b217a03e5a66e0f0000000065b0f2db00008400' + + '0000000300000000045f636e740364696434663464366267336331676a7368716f31656b3364' + + '3935347a336d79316f65686f6e31746b6a6863366a3466356d3666646839346f000010000100' + + '001c200011106469643a6578616d706c653a31323334035f6b30045f64696434663464366267' + + '336331676a7368716f31656b33643935347a336d79316f65686f6e31746b6a6863366a346635' + + '6d3666646839346f000010000100001c2000373669643d303b743d303b6b3d4c6f66676d7979' + + '526b323436456b4b79502d39587973456f493541556f7154786e6b364c7466696a355f55045f' + + '64696434663464366267336331676a7368716f31656b33643935347a336d79316f65686f6e31' + + '746b6a6863366a3466356d3666646839346f000010000100001c20002726763d303b766d3d6b' + + '303b617574683d6b303b61736d3d6b303b64656c3d6b303b696e763d6b30').toArrayBuffer() + )); + + const did = 'did:dht:f4d6bg3c1gjshqo1ek3d954z3my1oehon1tkjhc6j4f5m6fdh94o'; + const didResolutionResult = await DidDht.resolve(did); + + expect(didResolutionResult.didDocument).to.have.property('controller'); + }); + + it('resolves a published DID with an Also Known As identifier', async () => { + fetchStub.resolves(fetchOkResponse( + Convert.hex('802d44499e456cdee25fef5ffe6f6fbc56201be836d8d44bcb1332a6414529a5503e514230e0' + + 'd0ec63a33d12a79aa06c3b8212160f514e40c9ac1b0f479128040000000065b0f37c00008400' + + '0000000300000000045f616b6103646964346b6e66356e37713568666e657a356b636d6d3439' + + '67346b6e716a356d727261393737337266756e73776f3578747269656f716d6f000010000100' + + '001c200011106469643a6578616d706c653a31323334035f6b30045f646964346b6e66356e37' + + '713568666e657a356b636d6d343967346b6e716a356d727261393737337266756e73776f3578' + + '747269656f716d6f000010000100001c2000373669643d303b743d303b6b3d55497578646476' + + '68524976745446723138326c43636e617945785f76636b4c4d567151322d4a4b6f673563045f' + + '646964346b6e66356e37713568666e657a356b636d6d343967346b6e716a356d727261393737' + + '337266756e73776f3578747269656f716d6f000010000100001c20002726763d303b766d3d6b' + + '303b617574683d6b303b61736d3d6b303b64656c3d6b303b696e763d6b30').toArrayBuffer() + )); + + const did = 'did:dht:knf5n7q5hfnez5kcmm49g4knqj5mrra9773rfunswo5xtrieoqmo'; + const didResolutionResult = await DidDht.resolve(did); + + expect(didResolutionResult.didDocument).to.have.property('alsoKnownAs'); + }); + + it('resolves a published DID with types', 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('types'); + expect(didResolutionResult.didDocumentMetadata.types).to.have.length(2); + expect(didResolutionResult.didDocumentMetadata.types).to.include(DidDhtRegisteredDidType.FinancialInstitution); + expect(didResolutionResult.didDocumentMetadata.types).to.include(DidDhtRegisteredDidType.WebApp); + }); + + 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()); + + const did = 'did:dht:5634graogy41ow91cc78up6i45a9mcscccruwer9o4ah5wcc1xmy'; + const didResolutionResult = await DidDht.resolve(did); + + expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'notFound'); + }); + + it('returns a invalidDidDocumentLength error if the Pkarr relay returns smaller than the 72 byte minimum', async () => { + // Mock the response from the Pkarr relay rather than calling over the network. + fetchStub.resolves(fetchOkResponse( + new Uint8Array(71).buffer + )); + + const did = 'did:dht:pjiiw7ibn6t9k1mkknkowjketa8chksgwzkt5uk8798epux1386o'; + const didResolutionResult = await DidDht.resolve(did); + + expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'invalidDidDocumentLength'); + }); + + it('returns a invalidDidDocumentLength error if the Pkarr relay returns larger than the 1072 byte maximum', async () => { + // Mock the response from the Pkarr relay rather than calling over the network. + fetchStub.resolves(fetchOkResponse( + new Uint8Array(1073).buffer + )); + + const did = 'did:dht:pjiiw7ibn6t9k1mkknkowjketa8chksgwzkt5uk8798epux1386o'; + const didResolutionResult = await DidDht.resolve(did); + + 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 new file mode 100644 index 000000000..e1f73dc9b --- /dev/null +++ b/packages/dids/tests/methods/did-ion.spec.ts @@ -0,0 +1,641 @@ +import type { Jwk } from '@web5/crypto'; + +import sinon from 'sinon'; +import { expect } from 'chai'; +import { LocalKeyManager, computeJwkThumbprint } from '@web5/crypto'; + +import type { DidDocument } from '../../src/types/did-core.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. +const fetchNotFoundResponse = () => ({ + status : 404, + statusText : 'Not Found', + ok : false +}); + +// Helper function to create a mocked fetch response that is successful and returns the given +// response. +const fetchOkResponse = (response?: any) => ({ + status : 200, + statusText : 'OK', + ok : true, + json : async () => Promise.resolve(response) +}); + +const ION_OPERATIONS_ENDPOINT = 'https://ion.tbd.engineering/operations'; +const ION_RESOLUTION_ENDPOINT = 'https://ion.tbd.engineering/identifiers'; + +describe('DidIon', () => { + let fetchStub: sinon.SinonStub; + + beforeEach(() => { + // Setup stub so that a mocked response is returned rather than calling over the network. + fetchStub = sinon.stub(globalThis as any, 'fetch'); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + describe('create', () => { + it('creates a DID with one verification method, by default', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodNoServices.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create(); + + 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'); + + expect(fetchStub.calledTwice).to.be.true; + + expect(did.didDocument).to.have.property('verificationMethod'); + expect(did.didDocument.verificationMethod).to.have.length(1); + expect(did.metadata).to.have.property('canonicalId'); + }); + + it('handles creating DIDs with multiple verification methods', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.twoMethodsNoServices.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + purposes : ['authentication', 'assertionMethod'] + }, + { + algorithm : 'secp256k1', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + 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'); + }); + + it('uses the JWK thumbprint as the ID for verification methods, by default', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodNoServices.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create(); + + const expectedKeyId = await computeJwkThumbprint({ jwk: did.didDocument.verificationMethod![0]!.publicKeyJwk! }); + expect(did.didDocument.verificationMethod?.[0].id).to.include(expectedKeyId); + }); + + it('allows a custom ID to be specified for additional verification methods', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodCustomId.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + id : '1', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.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 () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodCustomId.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + id : 'someprefix#1', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect(did.didDocument.verificationMethod?.[0]).to.have.property('id', '#1'); + }); + + it('handles creating DIDs with one service', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodOneService.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + } + ] + } + }); + + 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'); + }); + + it('handles creating DIDs with multiple services', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodTwoServices.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + }, + { + id : 'oid4vci', + type : 'OID4VCI', + serviceEndpoint : 'https://issuer.example.com', + } + ] + } + }); + + 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`); + }); + + it('given service IDs are automatically prefixed with hash symbol (#) in DID document', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodOneService.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + } + ] + } + }); + + expect(did.didDocument.service).to.have.length(1); + expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); + }); + + it('accepts service IDs that start with a hash symbol (#)', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodOneService.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + services: [ + { + id : '#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + } + ] + } + }); + + expect(did.didDocument.service).to.have.length(1); + expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); + }); + + it('retains only the ID fragment if service IDs contain a prefix before the hash symbol (#)', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodOneService.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + services: [ + { + id : 'someprefix#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + } + ] + } + }); + + expect(did.didDocument.service).to.have.length(1); + expect(did.didDocument.service?.[0]).to.have.property('id', `#dwn`); + }); + + it('accepts custom properties for services', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.dwnService.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + id : 'sig', + purposes : ['authentication', 'assertionMethod'] + }, + { + algorithm : 'secp256k1', + id : 'enc', + purposes : ['keyAgreement'] + } + ], + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : { + 'nodes' : ['https://example.com/dwn0', 'https://example.com/dwn1'], + 'signingKeys' : ['#sig'], + 'encryptionKeys' : ['#enc'] + } + } + ] + } + }); + + 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'); + }); + + it('publishes DIDs, by default', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodNoServices.didResolutionResult)); + } else if (url.startsWith(ION_OPERATIONS_ENDPOINT)) { + return Promise.resolve(fetchOkResponse()); + } + }); + + const did = await DidIon.create(); + + expect(fetchStub.calledTwice).to.be.true; + expect(did.metadata).to.have.property('published', true); + }); + + it('allows publishing of DIDs to optionally be disabled', async () => { + fetchStub.callsFake((url: string) => { + if (url.startsWith(ION_RESOLUTION_ENDPOINT)) { + return Promise.resolve(fetchOkResponse(CreateTestVector.oneMethodNoServices.didResolutionResult)); + } + }); + + const did = await DidIon.create({ options: { publish: false } }); + + expect(fetchStub.calledOnce).to.be.true; + expect(did.metadata).to.have.property('published', false); + }); + + it('throws an error if a verification method algorithm is not supported', async () => { + try { + await DidIon.create({ + options: { + verificationMethods: [ + { + algorithm : 'Ed25519', + purposes : ['authentication', 'assertionMethod'] + }, + { + // @ts-expect-error - Testing invalid algorithm. + algorithm : 'Ed448', + id : 'dwn-sig', + purposes : ['authentication', 'assertionMethod'] + } + ] + } + }); + + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('algorithms are not supported'); + } + }); + + it('throws an error if services are missing required properties', async () => { + try { + // @ts-expect-error - Testing service with missing 'id' property. + await DidIon.create({ options: { services: [{ type: 'b', serviceEndpoint: 'c' }] } }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('services are missing required properties'); + } + + try { + // @ts-expect-error - Testing service with missing 'type' property. + await DidIon.create({ options: { services: [{ id: 'a', serviceEndpoint: 'c' }] } }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('services are missing required properties'); + } + + try { + // @ts-expect-error - Testing service with missing 'serviceEndpoint' property. + await DidIon.create({ options: { services: [{ id: 'a', type: 'b' }] } }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('services are missing required properties'); + } + }); + }); + + 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 () { + 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: ['did:ion:123#0'] + } + }); + + expect(verificationMethod).to.exist; + 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('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('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 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 DidIon.getSigningMethod({ didDocument }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.equal('Method not supported: example'); + } + }); + }); + + describe('resolve()', () => { + it('resolves published short form ION DIDs', async() => { + fetchStub.returns(Promise.resolve(fetchOkResponse(ResolveTestVector.publishedDid.didResolutionResult))); + + const resolutionResult = await DidIon.resolve(ResolveTestVector.publishedDid.didUri); + + expect(resolutionResult).to.have.property('didDocument'); + expect(resolutionResult).to.have.property('didDocumentMetadata'); + expect(resolutionResult).to.have.property('didResolutionMetadata'); + + expect(resolutionResult.didDocument).to.have.property('id', ResolveTestVector.publishedDid.didUri); + expect(resolutionResult.didDocumentMetadata).to.have.property('canonicalId', ResolveTestVector.publishedDid.didUri); + expect(resolutionResult.didDocumentMetadata).to.have.property('published', true); + }); + + it('returns notFound error with unpublished short form ION DIDs', async() => { + fetchStub.returns(Promise.resolve(fetchNotFoundResponse())); + + const didUri = 'did:ion:EiBCi7lnGtotBsFkbI_lQskQZLk_GPelU0C5-nRB4_nMfA'; + const resolutionResult = await DidIon.resolve(didUri); + + expect(resolutionResult).to.have.property('@context'); + expect(resolutionResult).to.have.property('didDocument'); + expect(resolutionResult).to.have.property('didDocumentMetadata'); + + expect(resolutionResult.didResolutionMetadata).to.have.property('error', 'notFound'); + }); + + it(`returns methodNotSupported error if DID method is not 'ion'`, async () => { + const didUri = 'did:key:z6MkvEvogvhMEv9bXLyDXdqSSvvh5goAMtUruYwCbFpuhDjx'; + const resolutionResult = await DidIon.resolve(didUri); + expect(resolutionResult).to.have.property('@context'); + expect(resolutionResult).to.have.property('didDocument'); + expect(resolutionResult).to.have.property('didDocumentMetadata'); + + expect(resolutionResult.didResolutionMetadata).to.have.property('error', 'methodNotSupported'); + }); + + it('accepts custom DID resolver with trailing slash', async () => { + const mockResult = { + '@context' : 'https://w3id.org/did-resolution/v1', + 'didDocument' : null, + 'didDocumentMetadata' : { + 'published': undefined + }, + 'didResolutionMetadata': {} + }; + fetchStub.returns(Promise.resolve({ + ok : true, + json : () => Promise.resolve(mockResult) + })); + + const didUri = 'did:ion:EiCab9QRUcUTKKIM-W2SMCwnOPxa4y0q7emoWJDSOSz3HQ'; + const resolutionResult = await DidIon.resolve(didUri, { + gatewayUri: 'https://dev.uniresolver.io/1.0/' + }); + + expect(resolutionResult).to.deep.equal(mockResult); + expect(fetchStub.calledOnceWith( + `https://dev.uniresolver.io/1.0/identifiers/${didUri}` + )).to.be.true; + }); + }); + + 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 new file mode 100644 index 000000000..fc8c78c04 --- /dev/null +++ b/packages/dids/tests/methods/did-jwk.spec.ts @@ -0,0 +1,590 @@ +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 { DidErrorCode } from '../../src/did-error.js'; +import { DidJwk } from '../../src/methods/did-jwk.js'; +import DidJwkResolveTestVector from '../../../../web5-spec/test-vectors/did_jwk/resolve.json' assert { type: 'json' }; + +describe('DidJwk', () => { + let keyManager: LocalKeyManager; + + before(() => { + keyManager = new LocalKeyManager(); + }); + + describe('create()', () => { + it('creates a did:jwk DID', async () => { + const did = await DidJwk.create({ keyManager, options: { algorithm: 'Ed25519' } }); + + 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'); + expect(did.uri.startsWith('did:jwk:')).to.be.true; + expect(did.didDocument.verificationMethod).to.have.length(1); + }); + + it('uses a default key manager and key generation algorithm if neither is given', async () => { + // Create a DID with no params. + let did = await DidJwk.create(); + expect(did.uri.startsWith('did:jwk:')).to.be.true; + + // Create a DID with an empty options object. + did = await DidJwk.create({ options: {} }); + expect(did.uri.startsWith('did:jwk:')).to.be.true; + + // Create a DID with an empty options object and undefined key manager. + did = await DidJwk.create({}); + expect(did.uri.startsWith('did:jwk:')).to.be.true; + }); + + it('creates a DID using the top-level algorithm property, if given', async () => { + 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 publicKey = await keyManager.getPublicKey({ keyUri }); + + // Verify the public key is an secp256k1 key. + expect(publicKey).to.have.property('crv', 'secp256k1'); + }); + + it('creates a DID using the verificationMethods algorithm property, if given', async () => { + 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 publicKey = await keyManager.getPublicKey({ keyUri }); + + // Verify the public key is an secp256k1 key. + expect(publicKey).to.have.property('crv', 'secp256k1'); + }); + + it('creates a DID with an Ed25519 key, by default', async () => { + 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 publicKey = await keyManager.getPublicKey({ keyUri }); + + // Verify the public key is an Ed25519 key. + expect(publicKey).to.have.property('crv', 'Ed25519'); + }); + + it('creates a DID using any signature algorithm supported by the provided KMS', async () => { + expect( + await DidJwk.create({ keyManager, options: { algorithm: 'secp256k1' } }) + ).to.have.property('uri'); + + expect( + await DidJwk.create({ keyManager, options: { algorithm: 'Ed25519' } }) + ).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({ + keyManager, + options: { + algorithm : 'Ed25519', + verificationMethods : [{ algorithm: 'Ed25519' }] + } + }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + 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:...'; + 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'); + } + }); + + it('throws an error if an unsupported DID method is given', async () => { + try { + await DidJwk.fromKeyManager({ didUri: 'did:example:e30', keyManager }); + 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); + } + }); + + 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 : {} + })); + + 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(); + } + + // 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 : {} + })); + + 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(); + } + }); + + 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 : {} + })); + + 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('does not contain a public key'); + } finally { + sinon.restore(); + } + }); + }); + + describe('fromKeys()', () => { + let portableDid: PortableDid; + + beforeEach(() => { + // Define a DID to use for the test. + portableDid = { + uri : 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjNFQmFfRUxvczJhbHZMb2pxSVZjcmJLcGlyVlhqNmNqVkQ1djJWaHdMejgifQ', + verificationMethods : [{ + publicKeyJwk: { + kty : 'OKP', + crv : 'Ed25519', + x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8' + }, + privateKeyJwk: { + kty : 'OKP', + crv : 'Ed25519', + x : '3EBa_ELos2alvLojqIVcrbKpirVXj6cjVD5v2VhwLz8', + d : 'hMqv-FAvhVWz2nxobesO7TzI0-GN0kvzkUGYdnZt_TA' + }, + purposes: ['authentication'] + }] + }; + }); + + it('returns a DID JWK from the given set of verification method keys', async () => { + const did = await DidJwk.fromKeys(portableDid); + + 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', portableDid.uri); + }); + + it('returns a DID with a getSigner function that can sign and verify data', async () => { + const did = await DidJwk.fromKeys(portableDid); + 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 () => { + 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; + + try { + await DidJwk.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'); + } + }); + + it('throws an error if the given key set is missing a private key', async () => { + delete portableDid.verificationMethods[0].privateKeyJwk; + + try { + await DidJwk.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'); + } + }); + + 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'] + }; + + try { + await DidJwk.fromKeys({ + verificationMethods: [verificationMethod, verificationMethod] + }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.message).to.include('one verification method'); + } + }); + }); + + describe('getSigningMethod()', () => { + it('handles didDocuments missing verification methods', async function () { + const result = await DidJwk.getSigningMethod({ + didDocument: { id: 'did:jwk:123' } + }); + + expect(result).to.be.undefined; + }); + + 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 + } + ], + }; + + try { + await DidJwk.getSigningMethod({ didDocument }); + expect.fail('Error should have been thrown'); + } catch (error: any) { + expect(error.message).to.equal('Method not supported: example'); + } + }); + }); + + describe('resolve()', () => { + it('returns an error due to DID parsing failing', async function () { + const invalidDidUri = 'did:invalidFormat'; + const resolutionResult = await DidJwk.resolve(invalidDidUri); + expect(resolutionResult.didResolutionMetadata.error).to.equal('invalidDid'); + }); + + it('returns an error due to failing to decode the publicKeyJwk', async function () { + const didUriWithInvalidEncoding = 'did:jwk:invalidEncoding'; + const resolutionResult = await DidJwk.resolve(didUriWithInvalidEncoding); + expect(resolutionResult.didResolutionMetadata.error).to.equal('invalidDid'); + }); + + it('returns an error because the DID method is not "jwk"', async function () { + const didUriWithDifferentMethod = 'did:notjwk:eyJmb28iOiJiYXIifQ'; + const resolutionResult = await DidJwk.resolve(didUriWithDifferentMethod); + expect(resolutionResult.didResolutionMetadata.error).to.equal('methodNotSupported'); + }); + }); + + 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 = { + description: string; + input: Parameters[0]; + output: UnwrapPromise>; + errors: boolean; + }; + + for (const vector of DidJwkResolveTestVector.vectors as unknown as TestVector[]) { + const didResolutionResult = await DidJwk.resolve(vector.input); + + expect(didResolutionResult).to.deep.equal(vector.output); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/dids/tests/methods/did-key.spec.ts b/packages/dids/tests/methods/did-key.spec.ts new file mode 100644 index 000000000..cae7b5b25 --- /dev/null +++ b/packages/dids/tests/methods/did-key.spec.ts @@ -0,0 +1,900 @@ +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 { DidErrorCode } from '../../src/did-error.js'; +import { DidKey, DidKeyUtils } from '../../src/methods/did-key.js'; + +describe('DidKey', () => { + let keyManager: LocalKeyManager; + + before(() => { + keyManager = new LocalKeyManager(); + }); + + describe('create()', () => { + 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('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); + }); + + it('uses a default key manager and key generation algorithm if neither is given', async () => { + // Create a DID with no params. + let did = await DidKey.create(); + expect(did.uri.startsWith('did:key:')).to.be.true; + + // Create a DID with an empty options object. + did = await DidKey.create({ options: {} }); + expect(did.uri.startsWith('did:key:')).to.be.true; + + // Create a DID with an empty options object and undefined key manager. + did = await DidKey.create({}); + expect(did.uri.startsWith('did:key:')).to.be.true; + }); + + it('creates a DID using the top-level algorithm property, if given', async () => { + 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 publicKey = await keyManager.getPublicKey({ keyUri }); + + // Verify the public key is an secp256k1 key. + expect(publicKey).to.have.property('crv', 'secp256k1'); + }); + + it('creates a DID using the verificationMethods algorithm property, if given', async () => { + 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 publicKey = await keyManager.getPublicKey({ keyUri }); + + // Verify the public key is an secp256k1 key. + expect(publicKey).to.have.property('crv', 'secp256k1'); + }); + + it('creates a DID with an Ed25519 key, by default', async () => { + 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 publicKey = await keyManager.getPublicKey({ keyUri }); + + // Verify the public key is an Ed25519 key. + expect(publicKey).to.have.property('crv', 'Ed25519'); + }); + + it('creates a DID using any signature algorithm supported by the provided KMS', async () => { + expect( + await DidKey.create({ keyManager, options: { algorithm: 'secp256k1' } }) + ).to.have.property('uri'); + + expect( + await DidKey.create({ keyManager, options: { algorithm: 'Ed25519' } }) + ).to.have.property('uri'); + }); + + 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; + + 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; + }); + + it('accepts an alternate default context', async () => { + const did = await DidKey.create({ + options: { + defaultContext : 'https://www.w3.org/ns/did/v99', + publicKeyFormat : 'JsonWebKey2020' + } + }); + + 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'); + }); + + it('throws an error if both algorithm and verificationMethods are provided', async () => { + try { + await DidKey.create({ + keyManager, + options: { + algorithm : 'Ed25519', + verificationMethods : [{ algorithm: 'Ed25519' }] + } + }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + 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:key:z6MkqBvAA4RBFFATVs7TXxEf4FcL1QY3JntYvwAYJMptDt5D'; + + privateKey = { + kty : 'OKP', + crv : 'Ed25519', + x : 'n4JbpJYkl77eGav9miqxHJsf-hoZl7GrbcrTmLJ9NBA', + d : 'JZPFC1MVj65ZUnj1HWTUDqvdQU6W2yBdZXMrRxDSqVA' + }; + }); + + 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 }); + + const did = await DidKey.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 DidKey.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 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 }); + + expect(signature).to.have.length(64); + expect(isValid).to.be.true; + }); + + it('throws an error if the given DID URI cannot be resolved', async () => { + 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'); + } + }); + + it('throws an error if an unsupported DID method is given', async () => { + try { + await DidKey.fromKeyManager({ didUri: 'did:example:z6Mk', keyManager }); + expect.fail('Expected an error to be thrown.'); + } catch (error: any) { + expect(error.code).to.equal(DidErrorCode.MethodNotSupported); + } + }); + + 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 : {} + })); + + 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(); + } + }); + + 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 : {} + })); + + 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('does not contain a public key'); + } finally { + sinon.restore(); + } + }); + }); + + describe('fromKeys()', () => { + let portableDid: PortableDid; + + beforeEach(() => { + // Define a DID to use for the test. + portableDid = { + uri : 'did:key:z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN', + verificationMethods : [{ + publicKeyJwk: { + kty : 'OKP', + crv : 'Ed25519', + x : 'VnSOQ-n7kRcYd0XGW2MNCv7DDY5py5XhNcjM7-Y1HVM' + }, + privateKeyJwk: { + kty : 'OKP', + crv : 'Ed25519', + x : 'VnSOQ-n7kRcYd0XGW2MNCv7DDY5py5XhNcjM7-Y1HVM', + d : 'iTD5DIOKZNkwgzsND-I8CLIXmgTxfQ1HUzl9fpMktAo' + }, + purposes: ['authentication'] + }] + }; + }); + + it('returns a DID Key from the given set of verification method keys', async () => { + const did = await DidKey.fromKeys(portableDid); + + 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', portableDid.uri); + }); + + it('returns a DID with a getSigner function that can sign and verify data', async () => { + const did = await DidKey.fromKeys(portableDid); + 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 () => { + 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 given key set is empty', async () => { + try { + await DidKey.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; + + 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'); + } + }); + + it('throws an error if the given key set is missing a private key', async () => { + delete portableDid.verificationMethods[0].privateKeyJwk; + + 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'); + } + }); + + 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'] + }; + + try { + await DidKey.fromKeys({ + verificationMethods: [verificationMethod, verificationMethod] + }); + 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'); + } + }); + }); + + describe('resolve()', () => { + it('derives a key agreement verification method when enableEncryptionKeyDerivation is true', async function () { + const did = 'did:key:z6MkpUzNmYVTGpqhStxK8yRKXWCRNm1bGYz8geAg2zmjYHKX'; + const resolutionResult = await DidKey.resolve(did, { enableEncryptionKeyDerivation: true }); + + expect(resolutionResult.didDocument?.verificationMethod).to.have.length(2); + expect(resolutionResult.didDocument?.verificationMethod![0]!.publicKeyJwk).to.have.property('crv', 'Ed25519'); + expect(resolutionResult.didDocument?.verificationMethod![1]!.publicKeyJwk).to.have.property('crv', 'X25519'); + expect(resolutionResult.didDocument?.verificationMethod![1]!.id).to.equal(resolutionResult.didDocument?.keyAgreement![0]); + }); + + it('returns an error due to DID parsing failing', async function () { + const invalidDidUri = 'did:invalidFormat'; + const resolutionResult = await DidKey.resolve(invalidDidUri); + expect(resolutionResult.didResolutionMetadata.error).to.equal('invalidDid'); + }); + + it('returns an error due to failing to decode the multibase identifier', async function () { + const didUriWithInvalidEncoding = 'did:key:invalidEncoding'; + const resolutionResult = await DidKey.resolve(didUriWithInvalidEncoding); + expect(resolutionResult.didResolutionMetadata.error).to.equal('invalidDid'); + }); + + it('returns an error because the DID method is not "key"', async function () { + const didUriWithDifferentMethod = 'did:notkey:eyJmb28iOiJiYXIifQ'; + const resolutionResult = await DidKey.resolve(didUriWithDifferentMethod); + expect(resolutionResult.didResolutionMetadata.error).to.equal(DidErrorCode.MethodNotSupported); + }); + }); + + describe('DidKeyUtils', () => { + describe('joseToMulticodec()', () => { + it('supports Ed25519 public keys', async () => { + const multicoded = await DidKeyUtils.jwkToMulticodec({ + jwk: { + alg : 'EdDSA', + crv : 'Ed25519', + kty : 'OKP', + x : 'lSPJrpccK4uv3f7IUCVYDz5qcUhSjiPHFyRcr5Z5VYg', + } + }); + + expect(multicoded).to.deep.equal({ code: 237, name: 'ed25519-pub' }); + }); + + it('supports Ed25519 private keys', async () => { + const multicoded = await DidKeyUtils.jwkToMulticodec({ + jwk: { + d : 'fbGqifMN3h7tjLMd2gi5dggG-A2s7paNkBbdFAyGZyU', + alg : 'EdDSA', + crv : 'Ed25519', + kty : 'OKP', + x : 'lSPJrpccK4uv3f7IUCVYDz5qcUhSjiPHFyRcr5Z5VYg', + } + }); + + expect(multicoded).to.deep.equal({ code: 4864, name: 'ed25519-priv' }); + }); + + it('supports secp256k1 public keys', async () => { + const multicoded = await DidKeyUtils.jwkToMulticodec({ + jwk: { + alg : 'ES256K', + crv : 'secp256k1', + kty : 'EC', + x : 'hEpfKD1BpSyoP9CYULUxD8JoTGB6Y8NNxe2cX0p_bQY', + y : 'SNP8nyU4iDWeu7nfcjpJ04htOgF8u94pFUzBYiPw75g', + } + }); + + expect(multicoded).to.deep.equal({ code: 231, name: 'secp256k1-pub' }); + }); + + it('supports secp256k1 private keys', async () => { + const multicoded = await DidKeyUtils.jwkToMulticodec({ + jwk: { + d : 'KvnTJGCOHzsUHEaIj1gy5uOE22K-3Shpl6NYLG7TRGQ', + alg : 'ES256K', + crv : 'secp256k1', + kty : 'EC', + x : 'hEpfKD1BpSyoP9CYULUxD8JoTGB6Y8NNxe2cX0p_bQY', + y : 'SNP8nyU4iDWeu7nfcjpJ04htOgF8u94pFUzBYiPw75g', + } + }); + + expect(multicoded).to.deep.equal({ code: 4865, name: 'secp256k1-priv' }); + }); + + it('supports X25519 public keys', async () => { + const multicoded = await DidKeyUtils.jwkToMulticodec({ + jwk: { + crv : 'X25519', + kty : 'OKP', + x : 'Uszsfy4vkz9MKeflgUpQot7sJhDyco2aYWCRXKTrcQg', + } + }); + + expect(multicoded).to.deep.equal({ code: 236, name: 'x25519-pub' }); + }); + + it('supports X25519 private keys', async () => { + const multicoded = await DidKeyUtils.jwkToMulticodec({ + jwk: { + d : 'MJf4AAqcwfBC68Wkb8nRbmnIdHb07zYM7vU_TAOgmtM', + crv : 'X25519', + kty : 'OKP', + x : 'Uszsfy4vkz9MKeflgUpQot7sJhDyco2aYWCRXKTrcQg', + } + }); + + expect(multicoded).to.deep.equal({ code: 4866, name: 'x25519-priv' }); + }); + + it('throws an error if unsupported JOSE has been passed', async () => { + await expect( + // @ts-expect-error because parameters are intentionally omitted to trigger an error. + DidKeyUtils.jwkToMulticodec({ jwk: { crv: '123' } }) + ).to.eventually.be.rejectedWith(Error, `Unsupported JWK to Multicodec conversion: '123:public'`); + }); + }); + + 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' }); + expect(result).to.deep.equal({ + crv : 'Ed25519', + kty : 'OKP', + x : '' // x value would be populated with actual key material in real use + }); + }); + + it('converts ed25519 private key multicodec to JWK', async () => { + const result = await DidKeyUtils.multicodecToJwk({ name: 'ed25519-priv' }); + expect(result).to.deep.equal({ + crv : 'Ed25519', + kty : 'OKP', + x : '', // x value would be populated with actual key material in real use + d : '' // d value would be populated with actual key material in real use + }); + }); + + it('converts secp256k1 public key multicodec to JWK', async () => { + const result = await DidKeyUtils.multicodecToJwk({ name: 'secp256k1-pub' }); + expect(result).to.deep.equal({ + crv : 'secp256k1', + kty : 'EC', + x : '', // x value would be populated with actual key material in real use + y : '' // y value would be populated with actual key material in real use + }); + }); + + it('converts secp256k1 private key multicodec to JWK', async () => { + const result = await DidKeyUtils.multicodecToJwk({ name: 'secp256k1-priv' }); + expect(result).to.deep.equal({ + crv : 'secp256k1', + kty : 'EC', + x : '', // x value would be populated with actual key material in real use + y : '', // y value would be populated with actual key material in real use + d : '' // d value would be populated with actual key material in real use + }); + }); + + it('converts x25519 public key multicodec to JWK', async () => { + const result = await DidKeyUtils.multicodecToJwk({ name: 'x25519-pub' }); + expect(result).to.deep.equal({ + crv : 'X25519', + kty : 'OKP', + x : '' // x value would be populated with actual key material in real use + }); + }); + + it('converts x25519 private key multicodec to JWK', async () => { + const result = await DidKeyUtils.multicodecToJwk({ name: 'x25519-priv' }); + expect(result).to.deep.equal({ + crv : 'X25519', + kty : 'OKP', + x : '', // x value would be populated with actual key material in real use + d : '' // d value would be populated with actual key material in real use + }); + }); + + it('throws an error when name is undefined and code is not provided', async () => { + try { + await DidKeyUtils.multicodecToJwk({}); + expect.fail('Should have thrown an error for undefined name and code'); + } catch (e: any) { + expect(e.message).to.equal('Either \'name\' or \'code\' must be defined, but not both.'); + } + }); + + it('throws an error when both name and code are provided', async () => { + try { + await DidKeyUtils.multicodecToJwk({ name: 'ed25519-pub', code: 0xed }); + expect.fail('Should have thrown an error for both name and code being defined'); + } catch (e: any) { + expect(e.message).to.equal('Either \'name\' or \'code\' must be defined, but not both.'); + } + }); + + it('throws an error for unsupported multicodec name', async () => { + try { + await DidKeyUtils.multicodecToJwk({ name: 'unsupported-key-type' }); + expect.fail('Should have thrown an error for unsupported multicodec name'); + } catch (e: any) { + expect(e.message).to.include('Unsupported Multicodec to JWK conversion'); + } + }); + + it('throws an error for unsupported multicodec code', async () => { + try { + await DidKeyUtils.multicodecToJwk({ code: 0x9999 }); + expect.fail('Should have thrown an error for unsupported multicodec code'); + } catch (e: any) { + expect(e.message).to.include('Unsupported multicodec'); + } + }); + }); + + 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 = { + crv : 'Ed25519', + kty : 'OKP', + x : 'wwk7wOlocpOHDopgc0cZVCnl_7zFrp-JpvZe9vr5500' + }; + + const multibaseId = await DidKeyUtils.publicKeyToMultibaseId({ publicKey }); + + expect(multibaseId).to.equal('z6MksabiHWJ5wQqJGDzxw1EiV5zi6BE6QRENTnHBcKHSqLaQ'); + }); + + it('supports secp256k1', async () => { + const publicKey: Jwk = { + crv : 'secp256k1', + kty : 'EC', + x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', + y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', + }; + + const multibaseId = await DidKeyUtils.publicKeyToMultibaseId({ publicKey }); + + expect(multibaseId).to.equal('zQ3sheTFzDvGpXAc9AXtwGF3MW1CusKovnwM4pSsUamqKCyLB'); + }); + + it('supports X25519', async () => { + const publicKey: Jwk = { + crv : 'X25519', + kty : 'OKP', + x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', + }; + + const multibaseId = await DidKeyUtils.publicKeyToMultibaseId({ publicKey }); + + expect(multibaseId).to.equal('z6LSjQhGhqqYgrFsNFoZL9wzuKpS1xQ7YNE6fnLgSyW2hUt2'); + }); + + it('throws an error for an unsupported public key type', async () => { + await expect( + DidKeyUtils.publicKeyToMultibaseId({ + publicKey: { + kty : 'RSA', + n : 'r0YDzIV4GPJ1wFb1Gftdd3C3VE6YeknVq1C7jGypq5WTTmX0yRDBqzL6mBR3_c-mKRuE5Z5VMGniA1lFnFmv8m0A2engKfALXHPJqoL6WzqN1SyjSM2aI6v8JVTj4H0RdYV9R4jxIB-zK5X-ZyL6CwHx-3dKZkCvZSEp8b-5I8c2Fz8E8Hl7qKkD_qEz6ZOmKVhJLGiEag1qUQYJv2TcRdiyZfwwVsV3nI3IcVfMCTjDZTw2jI0YHJgLi7-MkP4DO7OJ4D4AFtL-7CkZ7V2xG0piBz4b02_-ZGnBZ5zHJxGoUZnTY6HX4V9bPQI_ME8qCjFXf-TcwCfDFcwMm70L2Q', + e : 'AQAB', + alg : 'RS256' + } + }) + ).to.eventually.be.rejectedWith(Error, `unsupported key type`); + }); + + it('throws an error for an unsupported public key curve', async () => { + await expect( + DidKeyUtils.publicKeyToMultibaseId({ + publicKey: { + kty : 'EC', + crv : 'BLS12381_G1', + x : 'mIT3NuXBB_VeJUaV15hwBbMtBrMaTWcN4gnDfkzX-VuUZg3vnpB9RxxaC6vkTgJ2' + } + }) + ).to.eventually.be.rejectedWith(Error, `unsupported key type`); + }); + }); + }); +}); diff --git a/packages/dids/tests/methods/did-method.spec.ts b/packages/dids/tests/methods/did-method.spec.ts new file mode 100644 index 000000000..300af0d29 --- /dev/null +++ b/packages/dids/tests/methods/did-method.spec.ts @@ -0,0 +1,308 @@ +import type { CryptoApi, Jwk } from '@web5/crypto'; + +import sinon from 'sinon'; +import { expect } from 'chai'; +import { LocalKeyManager } from '@web5/crypto'; + +import type { Did } 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; + }); + + 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 }); + 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'); + } + }); + }); + + describe('toKeys()', () => { + let didJwk: Did; + + 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; + + try { + await DidMethod.toKeys({ did: didJwk }); + 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: Did = { + 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'); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/dids/tests/methods/did-web.spec.ts b/packages/dids/tests/methods/did-web.spec.ts new file mode 100644 index 000000000..24f1681f2 --- /dev/null +++ b/packages/dids/tests/methods/did-web.spec.ts @@ -0,0 +1,81 @@ +import type { UnwrapPromise } from '@web5/common'; + +import sinon from 'sinon'; +import { expect } from 'chai'; + +import { DidWeb } from '../../src/methods/did-web.js'; +import DidWebResolveTestVector from '../../../../web5-spec/test-vectors/did_web/resolve.json' assert { type: 'json' }; + +// Helper function to create a mocked fetch response that fails and returns a 404 Not Found. +const fetchNotFoundResponse = () => ({ + status : 404, + statusText : 'Not Found', + ok : false +}); + +// Helper function to create a mocked fetch response that is successful and returns the given +// response. +const fetchOkResponse = (response: any) => ({ + status : 200, + statusText : 'OK', + ok : true, + json : async () => Promise.resolve(response) +}); + +describe('DidWeb', () => { + describe('resolve()', () => { + it(`returns a 'notFound' error if the HTTP GET response is not status code 200`, async () => { + // Setup stub so that a mocked response is returned rather than calling over the network. + let fetchStub = sinon.stub(globalThis as any, 'fetch'); + fetchStub.callsFake(() => Promise.resolve(fetchNotFoundResponse())); + + const resolutionResult = await DidWeb.resolve('did:web:non-existent-domain.com'); + + expect(resolutionResult.didResolutionMetadata.error).to.equal('notFound'); + + fetchStub.restore(); + }); + }); + + describe('Web5TestVectorsDidWeb', () => { + let fetchStub: sinon.SinonStub; + + beforeEach(() => { + // Setup stub so that a mocked response is returned rather than calling over the network. + fetchStub = sinon.stub(globalThis as any, 'fetch'); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + it('resolve', async () => { + type TestVector = { + description: string; + input: { + didUri: Parameters[0]; + mockServer: { [url: string]: any }; + }; + output: UnwrapPromise>; + errors: boolean; + }; + + for (const vector of DidWebResolveTestVector.vectors as unknown as TestVector[]) { + + // Only mock the response if the test vector contains a `mockServer` property. + if (vector.input.mockServer) { + const mockResponses = vector.input.mockServer; + fetchStub.callsFake((url: string) => { + if (url in mockResponses) return Promise.resolve(fetchOkResponse(mockResponses[url])); + }); + } + + const didResolutionResult = await DidWeb.resolve(vector.input.didUri); + + expect(didResolutionResult.didDocument).to.deep.equal(vector.output.didDocument); + expect(didResolutionResult.didDocumentMetadata).to.deep.equal(vector.output.didDocumentMetadata); + expect(didResolutionResult.didResolutionMetadata).to.deep.equal(vector.output.didResolutionMetadata); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/dids/tests/resolver/did-resolver.spec.ts b/packages/dids/tests/resolver/did-resolver.spec.ts new file mode 100644 index 000000000..c31b8c1d1 --- /dev/null +++ b/packages/dids/tests/resolver/did-resolver.spec.ts @@ -0,0 +1,147 @@ +import type { UnwrapPromise } from '@web5/common'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { DidJwk } from '../../src/methods/did-jwk.js'; +import { DidResource } from '../../src/types/did-core.js'; +import { isDidVerificationMethod } from '../../src/utils.js'; +import { DidResolver } from '../../src/resolver/did-resolver.js'; +import DidJwkResolveTestVector from '../../../../web5-spec/test-vectors/did_jwk/resolve.json' assert { type: 'json' }; + +describe('DidResolver', () => { + describe('resolve()', () => { + let didResolver: DidResolver; + + beforeEach(() => { + const didMethodApis = [DidJwk]; + didResolver = new DidResolver({ didResolvers: didMethodApis }); + }); + + it('returns an invalidDid error if the DID cannot be parsed', async () => { + const didResolutionResult = await didResolver.resolve('unparseable:did'); + expect(didResolutionResult).to.exist; + expect(didResolutionResult).to.have.property('@context'); + expect(didResolutionResult).to.have.property('didDocument'); + expect(didResolutionResult).to.have.property('didDocumentMetadata'); + expect(didResolutionResult).to.have.property('didResolutionMetadata'); + expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'invalidDid'); + }); + + it('returns a methodNotSupported error if the DID method is not supported', async () => { + const didResolutionResult = await didResolver.resolve('did:unknown:abc123'); + expect(didResolutionResult).to.exist; + expect(didResolutionResult).to.have.property('@context'); + expect(didResolutionResult).to.have.property('didDocument'); + expect(didResolutionResult).to.have.property('didDocumentMetadata'); + expect(didResolutionResult).to.have.property('didResolutionMetadata'); + expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'methodNotSupported'); + }); + + it('pass DID JWK resolve test vectors', async () => { + type TestVector = { + description: string; + input: Parameters[0]; + output: UnwrapPromise>; + errors: boolean; + }; + + for (const vector of DidJwkResolveTestVector.vectors as unknown as TestVector[]) { + const didResolutionResult = await DidJwk.resolve(vector.input); + + expect(didResolutionResult).to.deep.equal(vector.output); + } + }); + }); + + describe('dereference()', () => { + let didResolver: DidResolver; + + beforeEach(() => { + const didMethodApis = [DidJwk]; + didResolver = new DidResolver({ didResolvers: didMethodApis }); + }); + + it('returns a result with contentStream set to null and dereferenceMetadata.error set to invalidDidUrl, if the DID URL is invalid', async () => { + const result = await didResolver.dereference('abcd123;;;'); + expect(result.contentStream).to.be.null; + expect(result.dereferencingMetadata.error).to.exist; + expect(result.dereferencingMetadata.error).to.equal('invalidDidUrl'); + }); + + it('returns a result with contentStream set to null and dereferenceMetadata.error set to invalidDid, if the DID is invalid', async () => { + const result = await didResolver.dereference('did:jwk:abcd123'); + expect(result.contentStream).to.be.null; + expect(result.dereferencingMetadata.error).to.exist; + expect(result.dereferencingMetadata.error).to.equal('invalidDid'); + }); + + 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); + expect(result.contentStream).to.be.not.be.null; + expect(result.dereferencingMetadata.error).to.not.exist; + + const didResource = result.contentStream; + expect(isDidVerificationMethod(didResource)).to.be.true; + }); + + it('returns a DID service resource as the value of contentStream if found', async () => { + // Create an instance of DidResolver + const resolver = new DidResolver({ didResolvers: [] }); + + // Stub the resolve method + const mockDidResolutionResult = { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : { + id : 'did:example:123456789abcdefghi', + service : [ + { + id : '#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : { + nodes: [ 'https://dwn.tbddev.test/dwn0' ] + } + } + ], + }, + didDocumentMetadata : {}, + didResolutionMetadata : {} + }; + + const resolveStub = sinon.stub(resolver, 'resolve').resolves(mockDidResolutionResult); + + const testDidUrl = 'did:example:123456789abcdefghi#dwn'; + const result = await resolver.dereference(testDidUrl); + + expect(resolveStub.calledOnce).to.be.true; + expect(result.contentStream).to.deep.equal(mockDidResolutionResult.didDocument.service[0]); + + // Restore the original resolve method + resolveStub.restore(); + }); + + it('returns the entire DID document as the value of contentStream if the DID URL contains no fragment', async () => { + const did = await DidJwk.create(); + + const result = await didResolver.dereference(did.uri); + expect(result.contentStream).to.be.not.be.null; + expect(result.dereferencingMetadata.error).to.not.exist; + + const didResource = result.contentStream as DidResource; + if (!(!isDidVerificationMethod(didResource) && !isDidVerificationMethod(didResource))) throw new Error('Expected DidResource to be a DidDocument'); + expect(didResource['@context']).to.exist; + expect(didResource['@context']).to.include('https://www.w3.org/ns/did/v1'); + }); + + it('returns contentStream set to null and dereferenceMetadata.error set to notFound if resource is not found', async () => { + const did = await DidJwk.create(); + + const result = await didResolver.dereference(`${did.uri}#1`); + expect(result.contentStream).to.be.null; + expect(result.dereferencingMetadata.error).to.exist; + expect(result.dereferencingMetadata.error).to.equal('notFound'); + }); + }); +}); \ No newline at end of file diff --git a/packages/dids/tests/resolver-cache-level.spec.ts b/packages/dids/tests/resolver/resolver-cache-level.spec.ts similarity index 62% rename from packages/dids/tests/resolver-cache-level.spec.ts rename to packages/dids/tests/resolver/resolver-cache-level.spec.ts index e038f37df..99f49da32 100644 --- a/packages/dids/tests/resolver-cache-level.spec.ts +++ b/packages/dids/tests/resolver/resolver-cache-level.spec.ts @@ -2,7 +2,10 @@ import sinon from 'sinon'; import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { DidResolverCacheLevel } from '../src/resolver-cache-level.js'; +import { Level } from 'level'; +import { DidJwk } from '../../src/methods/did-jwk.js'; +import { DidResolver, DidResolverCache } from '../../src/resolver/did-resolver.js'; +import { DidResolverCacheLevel } from '../../src/resolver/resolver-cache-level.js'; chai.use(chaiAsPromised); @@ -29,6 +32,13 @@ describe('DidResolverCacheLevel', () => { expect(cache).to.exist; }); + it('should initialize with a custom database', async function() { + const db = new Level('__TESTDATA__/customLocation'); + const cache = new DidResolverCacheLevel({ db }); + expect(cache).to.be.an.instanceof(DidResolverCacheLevel); + await cache.close(); + }); + it('uses a 15 minute TTL, by default', async () => { cache = new DidResolverCacheLevel({ location: cacheStoreLocation }); @@ -155,12 +165,84 @@ describe('DidResolverCacheLevel', () => { cache = new DidResolverCacheLevel({ location: cacheStoreLocation }); await expect( + // @ts-expect-error - Test invalid input. cache.get(null) ).to.eventually.be.rejectedWith(Error, 'Key cannot be null or undefine'); await expect( + // @ts-expect-error - Test invalid input. cache.get(undefined) ).to.eventually.be.rejectedWith(Error, 'Key cannot be null or undefine'); }); }); + + describe('with DidResolver', () => { + let cache: DidResolverCache; + let didResolver: DidResolver; + + before(() => { + cache = new DidResolverCacheLevel(); + }); + + beforeEach(async () => { + await cache.clear(); + const didMethodApis = [DidJwk]; + didResolver = new DidResolver({ cache, didResolvers: didMethodApis }); + }); + + after(async () => { + await cache.clear(); + }); + + it('should cache miss for the first resolution attempt', async () => { + const did = 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjNFQmFfRUxvczJhbHZMb2pxSVZjcmJLcGlyVlhqNmNqVkQ1djJWaHdMejgifQ'; + // Create a Sinon spy on the get method of the cache + const cacheGetSpy = sinon.spy(cache, 'get'); + + await didResolver.resolve(did); + + // Verify that cache.get() was called. + expect(cacheGetSpy.called).to.be.true; + + // Verify the cache returned undefined. + const getCacheResult = await cacheGetSpy.returnValues[0]; + expect(getCacheResult).to.be.undefined; + + cacheGetSpy.restore(); + }); + + it('should cache hit for the second resolution attempt', async () => { + const did = 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IjNFQmFfRUxvczJhbHZMb2pxSVZjcmJLcGlyVlhqNmNqVkQ1djJWaHdMejgifQ'; + // Create a Sinon spy on the get method of the cache + const cacheGetSpy = sinon.spy(cache, 'get'); + const cacheSetSpy = sinon.spy(cache, 'set'); + + await didResolver.resolve(did); + + // Verify there was a cache miss. + expect(cacheGetSpy.calledOnce).to.be.true; + expect(cacheSetSpy.calledOnce).to.be.true; + + // Verify the cache returned undefined. + let getCacheResult = await cacheGetSpy.returnValues[0]; + expect(getCacheResult).to.be.undefined; + + // Resolve the same DID again. + await didResolver.resolve(did); + + // Verify that cache.get() was called. + expect(cacheGetSpy.called).to.be.true; + expect(cacheGetSpy.calledTwice).to.be.true; + + // Verify there was a cache hit this time. + getCacheResult = await cacheGetSpy.returnValues[1]; + expect(getCacheResult).to.not.be.undefined; + expect(getCacheResult).to.have.property('@context'); + expect(getCacheResult).to.have.property('didDocument'); + expect(getCacheResult).to.have.property('didDocumentMetadata'); + expect(getCacheResult).to.have.property('didResolutionMetadata'); + + cacheGetSpy.restore(); + }); + }); }); \ No newline at end of file diff --git a/packages/dids/tests/resolver/resolver-cache-noop.spec.ts b/packages/dids/tests/resolver/resolver-cache-noop.spec.ts new file mode 100644 index 000000000..b1fd7e4ea --- /dev/null +++ b/packages/dids/tests/resolver/resolver-cache-noop.spec.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import { DidResolverCacheNoop } from '../../src/resolver/resolver-cache-noop.js'; + +describe('DidResolverCacheNoop', function() { + it('returns null for get method', async function() { + const result = await DidResolverCacheNoop.get('someKey'); + expect(result).to.be.null; + }); + + it('returns null for set method', async function() { + const result = await DidResolverCacheNoop.set('someKey', { + didResolutionMetadata : {}, + didDocument : null, + didDocumentMetadata : {}, + }); + expect(result).to.be.null; + }); + + it('returns null for delete method', async function() { + const result = await DidResolverCacheNoop.delete('someKey'); + expect(result).to.be.null; + }); + + it('returns null for clear method', async function() { + const result = await DidResolverCacheNoop.clear(); + expect(result).to.be.null; + }); + + it('returns null for close method', async function() { + const result = await DidResolverCacheNoop.close(); + expect(result).to.be.null; + }); +}); diff --git a/packages/dids/tests/tsconfig.json b/packages/dids/tests/tsconfig.json index ee97c2267..7c6d2c8e7 100644 --- a/packages/dids/tests/tsconfig.json +++ b/packages/dids/tests/tsconfig.json @@ -7,7 +7,6 @@ }, "include": [ "../src", - "../typings", ".", ], "exclude": [ diff --git a/packages/dids/tests/utils.spec.ts b/packages/dids/tests/utils.spec.ts index 59160fa12..c6791a63d 100644 --- a/packages/dids/tests/utils.spec.ts +++ b/packages/dids/tests/utils.spec.ts @@ -1,55 +1,432 @@ import { expect } from 'chai'; +import type { DidDocument } from '../src/types/did-core.js'; + import { - getVerificationMethodIds, + getServices, + isDidService, + isDwnDidService, + getVerificationMethodByKey, + isDidVerificationMethod, + getVerificationMethods, getVerificationMethodTypes, - parseDid, } from '../src/utils.js'; -import { didDocumentIdTestVectors, didDocumentTypeTestVectors } from './fixtures/test-vectors/did-utils.js'; + +import DidUtilsgetVerificationMethodsTestVector from './fixtures/test-vectors/utils/get-verification-methods.json' assert { type: 'json' }; +import DidUtilsGetVerificationMethodTypesTestVector from './fixtures/test-vectors/utils/get-verification-method-types.json' assert { type: 'json' }; +import DidUtilsGetVerificationMethodByKeyTestVector from './fixtures/test-vectors/utils/get-verification-method-by-key.json' assert { type: 'json' }; describe('DID Utils', () => { - describe('getVerificationMethodIds()', () => { - for (const vector of didDocumentIdTestVectors) { - it(`passes test vector ${vector.id}`, () => { - const methodIds = getVerificationMethodIds(vector.input as any); - expect(methodIds).to.deep.equal(vector.output); + describe('getServices()', () => { + let didDocument: DidDocument = { + id : 'did:example:123', + service : [ + { id: 'service1', type: 'TypeA', serviceEndpoint: 'http://example.com/service1' }, + { id: 'service2', type: 'TypeB', serviceEndpoint: 'http://example.com/service2' }, + { id: 'service3', type: 'TypeA', serviceEndpoint: 'http://example.com/service3' } + ] + }; + + it('returns all services if no id or type filter is provided', () => { + const services = getServices({ didDocument }); + expect(services).to.have.lengthOf(3); + }); + + it('should filter services by id', () => { + const services = getServices({ didDocument, id: 'service1' }); + expect(services).to.have.lengthOf(1); + expect(services[0].id).to.equal('service1'); + }); + + it('returns an empty array if no services are present', () => { + const emptyDidDocument = {} as DidDocument; + const services = getServices({ didDocument: emptyDidDocument }); + expect(services).to.be.an('array').that.is.empty; + }); + + it('should filter services by type', () => { + const services = getServices({ didDocument, type: 'TypeA' }); + expect(services).to.have.lengthOf(2); + services.forEach(service => expect(service.type).to.equal('TypeA')); + }); + + it('returns an empty array if no service matches the specified type', () => { + const services = getServices({ didDocument, type: 'NonExistingType' }); + expect(services).to.be.an('array').that.is.empty; + }); + + it('should filter services by both id and type', () => { + const services = getServices({ didDocument, id: 'service3', type: 'TypeA' }); + expect(services).to.have.lengthOf(1); + expect(services[0].id).to.equal('service3'); + expect(services[0].type).to.equal('TypeA'); + }); + + it('returns an empty array if no service matches both the specified id and type', () => { + const services = getServices({ didDocument, id: 'service3', type: 'TypeB' }); + expect(services).to.be.an('array').that.is.empty; + }); + + it('returns an empty array if didDocument is null', () => { + // @ts-expect-error - Testing invalid input + const services = getServices({ didDocument: null }); + expect(services).to.be.an('array').that.is.empty; + }); + + it('returns an empty array if didDocument is undefined', () => { + // @ts-expect-error - Testing invalid input + const services = getServices({ didDocument: undefined }); + expect(services).to.be.an('array').that.is.empty; + }); + }); + + describe('getVerificationMethodByKey()', () => { + type TestVector = { + description: string; + input: Parameters[0]; + output: ReturnType; + errors: boolean; + }; + + for (const vector of DidUtilsGetVerificationMethodByKeyTestVector.vectors as unknown as TestVector[]) { + it(vector.description, async () => { + let errorOccurred = false; + try { + const verificationMethods = await getVerificationMethodByKey(vector.input); + + expect(verificationMethods).to.deep.equal(vector.output, vector.description); + + } catch { errorOccurred = true; } + expect(errorOccurred).to.equal(vector.errors, `Expected '${vector.description}' to${vector.errors ? ' ' : ' not '}throw an error`); + }); + } + }); + + describe('getVerificationMethods()', () => { + type TestVector = { + description: string; + input: Parameters[0]; + output: ReturnType; + errors: boolean; + }; + + for (const vector of DidUtilsgetVerificationMethodsTestVector.vectors as unknown as TestVector[]) { + it(vector.description, async () => { + let errorOccurred = false; + try { + const verificationMethods = getVerificationMethods({ + didDocument: vector.input.didDocument as DidDocument + }); + + expect(verificationMethods).to.deep.equal(vector.output, vector.description); + + } catch { errorOccurred = true; } + expect(errorOccurred).to.equal(vector.errors, `Expected '${vector.description}' to${vector.errors ? ' ' : ' not '}throw an error`); }); } }); - describe('getTypesFromDocument()', () => { - for (const vector of didDocumentTypeTestVectors) { - it(`passes test vector ${vector.id}`, () => { - const types = getVerificationMethodTypes(vector.input); - expect(types).to.deep.equal(vector.output); + describe('getVerificationMethodTypes()', () => { + type TestVector = { + description: string; + input: Parameters[0]; + output: ReturnType; + errors: boolean; + }; + + for (const vector of DidUtilsGetVerificationMethodTypesTestVector.vectors as unknown as TestVector[]) { + it(vector.description, async () => { + let errorOccurred = false; + try { + const types = getVerificationMethodTypes(vector.input); + + expect(types).to.deep.equal(vector.output, vector.description); + + } catch { errorOccurred = true; } + expect(errorOccurred).to.equal(vector.errors, `Expected '${vector.description}' to${vector.errors ? ' ' : ' not '}throw an error`); }); } + + it('returns an empty array if no verification methods are present', () => { + const emptyDidDocument = {} as DidDocument; + const types = getVerificationMethodTypes({ didDocument: emptyDidDocument }); + expect(types).to.be.an('array').that.is.empty; + }); + + it('throws an error when didDocument is not provided', async () => { + try { + // @ts-expect-error - Testing invalid input + getVerificationMethodTypes({ }); + throw new Error('Test failed - error not thrown'); + } catch (error: any) { + expect(error.message).to.include('parameter missing'); + } + }); + }); + + describe('isDidService', () => { + it('returns true for a valid DidService object', () => { + const validService = { + id : 'did:example:123#service-1', + type : 'OidcService', + serviceEndpoint : 'https://example.com/oidc' + }; + expect(isDidService(validService)).to.be.true; + }); + + it('returns false for an object missing the id property', () => { + const noIdService = { + type : 'OidcService', + serviceEndpoint : 'https://example.com/oidc' + }; + expect(isDidService(noIdService)).to.be.false; + }); + + it('returns false for an object missing the type property', () => { + const noTypeService = { + id : 'did:example:123#service-1', + serviceEndpoint : 'https://example.com/oidc' + }; + expect(isDidService(noTypeService)).to.be.false; + }); + + it('returns false for an object missing the serviceEndpoint property', () => { + const noEndpointService = { + id : 'did:example:123#service-1', + type : 'OidcService' + }; + expect(isDidService(noEndpointService)).to.be.false; + }); + + it('returns false for a null object', () => { + expect(isDidService(null)).to.be.false; + }); + + it('returns false for an undefined object', () => { + expect(isDidService(undefined)).to.be.false; + }); + + it('returns false for a non-object value', () => { + expect(isDidService('string')).to.be.false; + expect(isDidService(123)).to.be.false; + expect(isDidService(true)).to.be.false; + }); + + it('returns false for an empty object', () => { + expect(isDidService({})).to.be.false; + }); + + it('returns false for an object with extra properties', () => { + const extraPropsService = { + id : 'did:example:123#service-1', + type : 'OidcService', + serviceEndpoint : 'https://example.com/oidc', + extraProp : 'extraValue' + }; + expect(isDidService(extraPropsService)).to.be.true; // Note: Extra properties do not invalidate a DidService. + }); + }); + + describe('isDwnDidService', () => { + it('returns true for a valid DwnDidService object', () => { + const validDwnService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://dwn.example.org', + enc : 'did:example:123#key-1', + sig : 'did:example:123#key-2' + }; + expect(isDwnDidService(validDwnService)).to.be.true; + }); + + it('returns false for a non-DwnDidService type', () => { + const nonDwnService = { + id : 'did:example:123#service', + type : 'SomeOtherType', + serviceEndpoint : 'https://service.example.org', + enc : 'did:example:123#key-1', + sig : 'did:example:123#key-2' + }; + expect(isDwnDidService(nonDwnService)).to.be.false; + }); + + it('returns false for missing enc property', () => { + const missingEncService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://dwn.example.org', + sig : 'did:example:123#key-2' + }; + expect(isDwnDidService(missingEncService)).to.be.false; + }); + + it('returns false for missing sig property', () => { + const missingSigService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://dwn.example.org', + enc : 'did:example:123#key-1' + }; + expect(isDwnDidService(missingSigService)).to.be.false; + }); + + it('returns false for invalid enc property type', () => { + const invalidEncService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://dwn.example.org', + enc : 123, + sig : 'did:example:123#key-2' + }; + expect(isDwnDidService(invalidEncService)).to.be.false; + }); + + it('returns false for invalid sig property type', () => { + const invalidSigService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://dwn.example.org', + enc : 'did:example:123#key-1', + sig : true + }; + expect(isDwnDidService(invalidSigService)).to.be.false; + }); + + it('returns false for an array of non-string in enc', () => { + const arrayEncService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://dwn.example.org', + enc : [123, 'did:example:123#key-1'], + sig : 'did:example:123#key-2' + }; + expect(isDwnDidService(arrayEncService)).to.be.false; + }); + + it('returns false for an array of non-string in sig', () => { + const arraySigService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://dwn.example.org', + enc : 'did:example:123#key-1', + sig : ['did:example:123#key-2', null] + }; + expect(isDwnDidService(arraySigService)).to.be.false; + }); + + it('returns false for a null object', () => { + expect(isDwnDidService(null)).to.be.false; + }); + + it('returns false for an undefined object', () => { + expect(isDwnDidService(undefined)).to.be.false; + }); + + it('returns false for a non-object value', () => { + expect(isDwnDidService('string')).to.be.false; + expect(isDwnDidService(123)).to.be.false; + expect(isDwnDidService(true)).to.be.false; + }); + + it('returns false for an object not adhering to DidService structure', () => { + const invalidStructureService = { + id : 'did:example:123#dwn', + type : 'DecentralizedWebNode', + wrongProperty : 'https://dwn.example.org', + enc : 'did:example:123#key-1', + sig : 'did:example:123#key-2' + }; + expect(isDwnDidService(invalidStructureService)).to.be.false; + }); + + it('returns false for an empty object', () => { + expect(isDwnDidService({})).to.be.false; + }); }); - describe('parseDid()', () => { - it('extracts ION DID long form identifier from DID URL', async () => { - const { did } = parseDid({ - didUrl: 'did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19' - }) ?? {}; + describe('isDidVerificationMethod', () => { + it('returns true for a valid DidVerificationMethod object', () => { + const validVerificationMethod = { + id : 'did:example:123#0', + type : 'JsonWebKey2020', + controller : 'did:example:123', + publicKeyJwk : {} + }; + expect(isDidVerificationMethod(validVerificationMethod)).to.be.true; + }); + + it('returns false for an object missing the id property', () => { + const missingId = { + type : 'JsonWebKey2020', + controller : 'did:example:123', + publicKeyJwk : {} + }; + expect(isDidVerificationMethod(missingId)).to.be.false; + }); + + it('returns false for an object missing the type property', () => { + const missingType = { + id : 'did:example:123#0', + controller : 'did:example:123', + publicKeyJwk : {} + }; + expect(isDidVerificationMethod(missingType)).to.be.false; + }); + + it('returns false for an object missing the controller property', () => { + const missingController = { + id : 'did:example:123#0', + type : 'JsonWebKey2020', + publicKeyJwk : {} + }; + expect(isDidVerificationMethod(missingController)).to.be.false; + }); + + it('returns false for an object with incorrect property types', () => { + expect(isDidVerificationMethod({ + id : 123, + type : {}, + controller : false + })).to.be.false; + expect(isDidVerificationMethod({ + id : 'did:example:123', + type : {}, + controller : false + })).to.be.false; + expect(isDidVerificationMethod({ + id : 'did:example:123', + type : 'JsonWebKey2020', + controller : false + })).to.be.false; + }); - expect(did).to.equal('did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19'); + it('returns false for a null object', () => { + expect(isDidVerificationMethod(null)).to.be.false; }); - it('extracts ION DID long form identifier from DID URL with query and fragment', async () => { - const { did } = parseDid({ - didUrl: 'did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19?service=agent&relativeRef=/credentials#degree' - }) ?? {}; + it('returns false for an undefined object', () => { + expect(isDidVerificationMethod(undefined)).to.be.false; + }); - expect(did).to.equal('did:ion:EiAi68p2irCNQIzaui8gTjGDeOqSUusZS8jWVHfseSWZ5g:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiI2MWlQWXVHZWZ4b3R6QmRRWnREdnY2Y1dIWm1YclRUc2NZLXU3WTJwRlpjIiwieSI6Ijg4blBDVkxmckFZOWktd2c1T1Jjd1ZiSFdDX3RiZUFkMUpFMmUwY28wbFUifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoiZHduIiwic2VydmljZUVuZHBvaW50Ijp7Im5vZGVzIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4NSJdfSwidHlwZSI6IkRlY2VudHJhbGl6ZWRXZWJOb2RlIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCb1c2dGs4WlZRTWs3YjFubkF2R3F3QTQ2amlaaUc2dWNYemxyNTZDWWFiUSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ3Y2cUhEMFV4TTBadmZlTHU4dDR4eU5DVjNscFBSaTl6a3paU3h1LW8wWUEiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUN0STM0ckdGNU9USkJETXRUYm14a1lQeC0ydFd3MldZLTU2UTVPNHR0WWJBIn19'); + it('returns false for a non-object value', () => { + expect(isDidVerificationMethod('string')).to.be.false; + expect(isDidVerificationMethod(123)).to.be.false; + expect(isDidVerificationMethod(true)).to.be.false; }); - it('extracts query and fragment from DID URL', () => { - const { fragment, query } = parseDid({ - didUrl: 'did:example:123?service=agent&relativeRef=/credentials#degree' - }) ?? {}; + it('returns false for an empty object', () => { + expect(isDidVerificationMethod({})).to.be.false; + }); - expect(fragment).to.equal('degree'); - expect(query).to.equal('service=agent&relativeRef=/credentials'); + it('returns true for an object with extra properties', () => { + const extraProps = { + id : 'did:example:123#0', + type : 'JsonWebKey2020', + controller : 'did:example:123', + publicKeyJwk : {}, + extra : 'extraValue' + }; + expect(isDidVerificationMethod(extraProps)).to.be.true; }); }); }); \ No newline at end of file diff --git a/packages/dids/tsconfig.cjs.json b/packages/dids/tsconfig.cjs.json index 63f6f61a4..966f80c2b 100644 --- a/packages/dids/tsconfig.cjs.json +++ b/packages/dids/tsconfig.cjs.json @@ -14,7 +14,6 @@ "downlevelIteration": true }, "include": [ - "src", - "typings" + "src" ] } \ No newline at end of file diff --git a/packages/dids/tsconfig.json b/packages/dids/tsconfig.json index 3226cdc66..23ed4ccf6 100644 --- a/packages/dids/tsconfig.json +++ b/packages/dids/tsconfig.json @@ -1,13 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "strict": false, "declarationDir": "dist/types", "outDir": "dist/esm" }, "include": [ - "src", - "typings" + "src" ], "exclude": [ "node_modules" diff --git a/packages/dids/typings/decentralized-identity__ion-pow-sdk.d.ts b/packages/dids/typings/decentralized-identity__ion-pow-sdk.d.ts deleted file mode 100644 index 03fc84a06..000000000 --- a/packages/dids/typings/decentralized-identity__ion-pow-sdk.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module '@decentralized-identity/ion-pow-sdk' { - export default class IonProofOfWork { - static randomHexString(): string; - static submitIonRequestUntilSuccess(getChallengeUri: string, solveChallengeUri: string, requestBody: string): Promise; - static submitIonRequest(getChallengeUri: string, solveChallengeUri: string, requestBody: string): Promise; - } -} \ No newline at end of file diff --git a/packages/identity-agent/.c8rc.json b/packages/identity-agent/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/identity-agent/.c8rc.json +++ b/packages/identity-agent/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/identity-agent/package.json b/packages/identity-agent/package.json index f10a0c30d..9293c452a 100644 --- a/packages/identity-agent/package.json +++ b/packages/identity-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/identity-agent", - "version": "0.2.5", + "version": "0.2.6", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -68,8 +68,8 @@ "node": ">=18.0.0" }, "dependencies": { - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4" }, @@ -84,7 +84,7 @@ "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", "@web5/api": "0.8.4", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", diff --git a/packages/proxy-agent/.c8rc.json b/packages/proxy-agent/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/proxy-agent/.c8rc.json +++ b/packages/proxy-agent/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/proxy-agent/package.json b/packages/proxy-agent/package.json index 1d7411b7d..91fc854ce 100644 --- a/packages/proxy-agent/package.json +++ b/packages/proxy-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/proxy-agent", - "version": "0.2.5", + "version": "0.2.6", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -68,8 +68,8 @@ "node": ">=18.0.0" }, "dependencies": { - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4" }, @@ -77,13 +77,14 @@ "@playwright/test": "1.40.1", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", + "@types/dns-packet": "5.6.4", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@typescript-eslint/eslint-plugin": "6.4.0", "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8", diff --git a/packages/user-agent/.c8rc.json b/packages/user-agent/.c8rc.json index ab680f663..c44b67aba 100644 --- a/packages/user-agent/.c8rc.json +++ b/packages/user-agent/.c8rc.json @@ -5,12 +5,12 @@ ".js" ], "include": [ - "tests/compiled/src/**" + "tests/compiled/**/src/**" ], "exclude": [ - "tests/compiled/src/index.js", - "tests/compiled/src/types.js", - "tests/compiled/src/types/**" + "tests/compiled/**/src/index.js", + "tests/compiled/**/src/types.js", + "tests/compiled/**/src/types/**" ], "reporter": [ "cobertura", diff --git a/packages/user-agent/package.json b/packages/user-agent/package.json index 12b899413..c3fb84c7a 100644 --- a/packages/user-agent/package.json +++ b/packages/user-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/user-agent", - "version": "0.2.5", + "version": "0.2.6", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -68,8 +68,8 @@ "node": ">=18.0.0" }, "dependencies": { - "@web5/agent": "0.2.5", - "@web5/common": "0.2.2", + "@web5/agent": "0.2.6", + "@web5/common": "0.2.3", "@web5/crypto": "0.2.2", "@web5/dids": "0.2.4" }, @@ -77,13 +77,14 @@ "@playwright/test": "1.40.1", "@types/chai": "4.3.6", "@types/chai-as-promised": "7.1.5", + "@types/dns-packet": "5.6.4", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", "@typescript-eslint/eslint-plugin": "6.4.0", "@typescript-eslint/parser": "6.4.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", - "c8": "8.0.1", + "c8": "9.0.0", "chai": "4.3.10", "chai-as-promised": "7.1.1", "esbuild": "0.19.8",