diff --git a/packages/did-provider-ion/CHANGELOG.md b/packages/did-provider-ion/CHANGELOG.md new file mode 100644 index 000000000..e4d87c4d4 --- /dev/null +++ b/packages/did-provider-ion/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/did-provider-ion/LICENSE b/packages/did-provider-ion/LICENSE new file mode 100644 index 000000000..bcfcb2701 --- /dev/null +++ b/packages/did-provider-ion/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Consensys AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/did-provider-ion/README.md b/packages/did-provider-ion/README.md new file mode 100644 index 000000000..6d33a2976 --- /dev/null +++ b/packages/did-provider-ion/README.md @@ -0,0 +1,290 @@ +# Veramo did:ion provider + +This package contains an implementation of the `AbstractIdentifierProvider` for the `did:ion` method. +Enabling creating, updating, deactivating and resolving of `did:ion` entities. The ION Network is an implementation of +the +[Sidetree](https://identity.foundation/sidetree/spec/) protocol +and [specification](https://identity.foundation/sidetree/spec/) using the Bitcoin blockchain. +An explorer can be found [here](https://identity.foundation/ion/explorer/). + +_WARNING: Although the update and removal functions for Keys and Services seem to yield a request with a valid response +from the ION +node, right now these update and delete operations are not reflected in the eventual DID Document after the anchor time +has passed when using Microsoft's Public ION node. We are actively looking to solve this issue_ + +## Long form and short form ION DIDs + +ION has the concept of short form DIDs. a Short form DID is a DID, you also typically encounter when using other DID +methods. They are resolved using a resolver, as long as the DID is anchored or stored. + +Since ION uses the Bitcoin network it takes time to propagate changes (see next chapter). To ensure that ION DIDs can +already be resolved directly after creation a long form DID is available. The long form DID starts with the short form +DID and then a signed string is appended after a colon. This means that a long form DID is self certifying as it is +signed. It basically is a representation of the DID Document directly within the DID, similar to for instance did:key. +Long form DIDs are resolvable immediately. +More info about long and short form ION DIDs can be found in +the [Sidetree specification](https://identity.foundation/sidetree/spec/#did-uri-composition) + +This ION Provider creates Veramo Identifiers using Long form as the identifier's 'did' value, whilst using the short +form as it's 'alias' value. + +## Timing and limitations of current ION Network + +ION uses the Bitcoin network to anchor DIDs. As a result it typically also takes at least 10 minutes before requests are +actually reflected in the ION Network. +The net result is that you will not see your updates reflected in for instance +the [ION Explorer](https://identity.foundation/ion/explorer/) or when resolving the DID, until this time has passed. + +Currently, you can only use one operation for the same Identifier in between anchoring on the network. This typically is +roughly 10-15 minutes. +The net-result is that you will receive an error in case you are trying to add or remove keys and services for the same +identifier within this period. + +## Update key(s) and rotation + +The ION DID provider uses Update keys when updating the DID document. The provider creates new update keys internally +for every update. These are stored ordered by timestamp. When updating the DID Document if first resolves the current +DID document to look at the ION/Sidetree update commitment value. It then looks up the local key with the same +commitment value. The update request will be signed using the matched update key. At the same time a new update key is +generated, and the new key's update commitment will also be part of the update request, so that this new key needs to be used +next time. The provider takes care of both the rotation and the selection of the correct update key. + +## Recovery key(s) and rotation + +Recovery Keys are needed for deleting the Identifier and deactivating the DID. It uses a similar mechanism as described +above for the update keys. +The ION update keys could also be used in case of loss of update keys. Currently, this provider does not expose methods +to do this, although most of the methods and infra to do so should be present. + +## NodeJS and Browser notice + +When using the ION DID Provider in the browser or a Node environment, you can ignore the warning about the missing peer +dependency for `@sphereon/react-native-argon2`. Obviously these are not needed for non react-native +projects. + +## React Native notice + +Next to NodeJS and Browser support, the ION DID Provider also works with react-native. You do need to install the +following +package using your package manager. This has to do with auto-linking not being available for transitive dependencies. We +need some native Argon2 Android/IOS modules on React Native because WASM isn't available. As such you will have to +install the dependency directly into your app. You do not need to change settings or anything. The dependency will be +picked up and used automatically. +See [this ticket](https://github.com/react-native-community/cli/issues/870) for a discussion about this issue. + +npm: `npm install @sphereon/react-native-argon2` + +yarn: `yarn add @sphereon/react-native-argon2` + +## Different ION node and challenge/response settings + +The ION DID provider by default uses Microsoft's publicly hosted ION node(s) +at: https://beta.ion.msidentity.com/api/v1.0/operations + +This Node uses a challenge/response system to prevent spam. The `challengeEnabled` option, defaults to true with +a `challengeEndpoint` value of https://beta.ion.msidentity.com/api/v1.0/proof-of-work-challenge. +If you are running your own node, you might want to change the `solutionEndpoint` constructor option, pointing to your +ION node, together with setting the `challengeEnabled` constructor option to false. + +## Creating an identifier + +When creating a new Veramo Identifier you can choose to import your own keys or have them generated for you. You can +also choose to use specific Key IDs for your key, regardless of generation or import. +The options object when creating an identifier is as follows: + +```typescript +export interface ICreateIdentifierOpts { + verificationMethods?: VerificationMethod[] // The Verification method to add + recoveryKey?: KeyOpts // Recovery key options + updateKey?: KeyOpts // Update key options + services?: IService[] // Service(s) to add + actionTimestamp?: number // Unique number denoting the action. Used for ordering internally. Suggested to use current timestamp + anchor?: boolean // Whether the DID should be anchored on ION or not. Handy for testing or importing an ID +} + +export interface VerificationMethod extends KeyOpts { + purposes: IonPublicKeyPurpose[] // In sidetree these are called purposes, but in DID-Core Verification Relationships +} + +export interface KeyOpts { + kid?: string // Key ID to assign in case we are importing or creating a key + key?: MinimalImportableKey // Optional key to import. If not specified a key with random kid will be created + type?: KeyType // The key type. Defaults to Secp256k1 +} + +export interface IService { + id: string // ID + type: string // Service type + serviceEndpoint: string // Endpoint URL + description?: string //Optional description +} +``` + +### Creating an Identifier using auto-generated keys + +The example below generates an update key, a recover key and creates one DID Verification Method as part of the DID +Document with id `did-generated`, with accompanying key. +The `anchor: true`, means to propagate the anchor to the ION Network. You typically want to use `true`, unless you are +importing keys for an existing DID. +The purposes are the Verification Method Relationships in the DID Document. ION/Sidetree calls these purposes. + +```typescript +const identifier: IIdentifier = await agent.didManagerCreate({ + options: { + anchor: true, + recoveryKey: { + kid: 'recovery-generated', + }, + updateKey: { + kid: 'update-generated', + }, + verificationMethods: [ + { + kid: 'did-generated', + purposes: [IonPublicKeyPurpose.Authentication, IonPublicKeyPurpose.AssertionMethod], + }, + ], + }, +}) +``` + +### Creating an Identifier using imported keys + +The example below generates an update key with random Key ID. Notice the absence of the recoveryKey property. It imports +an existing recovery key and creates one DID Verification Method as part of the DID Document with id `did-imported1`, +using an imported key. +The `anchor: true`, means to propagate the anchor to the ION Network. You typically want to use `true`, unless you are +importing keys for an existing DID. +The purposes are the Verification Method Relationships in the DID Document. ION/Sidetree calls these purposes. + +```typescript +const identifier: IIdentifier = await agent.didManagerCreate({ + options: { + anchor: true, + updateKey: { + kid: 'update-imported', + }, + verificationMethods: [ + { + kid: 'did-imported1', + purposes: [IonPublicKeyPurpose.Authentication, IonPublicKeyPurpose.AssertionMethod], + }, + ], + }, +}) +``` + +## Adding and removing Keys + +Adding and removing keys typically use the below options: + +```typescript +export interface IUpdateOpts { + actionTimestamp?: number // Unique number denoting the action. Used for ordering internally. Suggested to use current timestamp + anchor?: boolean // Whether the DID should be anchored on ION or not. Handy for testing or importing an ID +} + +export interface VerificationMethod extends KeyOpts { + purposes: IonPublicKeyPurpose[] // In sidetree these are called purposes, but in DID-Core Verification Relationships +} + +export interface KeyOpts { + kid?: string // Key ID to assign in case we are importing or creating a key + key?: MinimalImportableKey // Optional key to import. If not specified a key with random kid will be created + type?: KeyType // The key type. Defaults to Secp256k1 +} +``` + +### Adding a Key + +Adding a key always has to happen using a previously created or imported key in Veramo. Unlike the create Identifier +function we do not support generating the key on the fly. This has to do with the interface of Veramo requiring a key. + +```typescript +const key = await agent.keyManagerCreate({ kms: 'mem', type: 'Secp256k1' }) +const result = agent.didManagerAddKey({ + did: identifier.did, + key, + kid: 'my-newly-added-key', + options: { + purposes: [IonPublicKeyPurpose.AssertionMethod], + anchor: true, + }, +}) +``` + +### Removing a Key + +Removing a key always has to happen using a previously created or imported key in Veramo. + +```typescript +const result = await agent.didManagerRemoveKey({ + did: identifier.did, + kid: 'my-newly-added-key', + options: { anchor: true }, +}) +``` + +## Adding and removing Services + +Adding and removing DID Document Services involves the below interfaces: + +```typescript +export interface IUpdateOpts { + actionTimestamp?: number // Unique number denoting the action. Used for ordering internally. Suggested to use current timestamp + anchor?: boolean // Whether the DID should be anchored on ION or not. Handy for testing or importing an ID +} + +export interface VerificationMethod extends KeyOpts { + purposes: IonPublicKeyPurpose[] // In sidetree these are called purposes, but in DID-Core Verification Relationships +} + +export interface IService { + id: string // ID + type: string // Service type + serviceEndpoint: string // Endpoint URL + description?: string //Optional description +} +``` + +### Adding a service + +Adding a service is straightforward. It requires a Service object which will end up in the DID Document, and it needs +the +DID value. + +```typescript +const service: IService = { + type: 'LinkedDomains', + id: 'example-domain', + serviceEndpoint: 'https://test-example.com', +} + +const result = await agent.didManagerAddService({ + did: identifier.did, + service, + options: { anchor: true }, +}) +``` + +### Removing a service + +Removing a service is straightforward. The example has no options, which equates to anchor being true. + +```typescript +const result = await agent.didManagerRemoveService({ + did: identifier.did, + id: 'example-domain', +}) +``` + +## Removing the Identifier and DID + +_WARNING: Currently deleting the identifier will always be propagated to the ION Network. Reason is that Veramo doesn't +expose an options parameter for deleting an identifier._ + +Deleting an identifier is straightforward: + +```typescript +const deleted = await agent.didManagerDelete({ did: identifier.did }) +``` diff --git a/packages/did-provider-ion/__tests__/canonicalizer.test.ts b/packages/did-provider-ion/__tests__/canonicalizer.test.ts new file mode 100644 index 000000000..84646dc3d --- /dev/null +++ b/packages/did-provider-ion/__tests__/canonicalizer.test.ts @@ -0,0 +1,55 @@ +import { JsonCanonicalizer } from '../src/json-canonicalizer' + +describe('canonicalizer result should be', () => { + it('throwing an error on null input', () => { + expect(() => JsonCanonicalizer.asString(null)).toThrow('Null content received in canonicalizer') + }) + it('quoted empty string on empty string', () => { + expect(JsonCanonicalizer.asString('')).toEqual('""') + }) + it('empty json string on empty json', () => { + expect(JsonCanonicalizer.asString({})).toEqual('{}') + }) + it('a json string exactly matching input', () => { + expect( + JsonCanonicalizer.asString({ + test1: 'test1', + test2: 'test2', + }) + ).toEqual('{"test1":"test1","test2":"test2"}') + }) + it('a json string with properties reordered', () => { + expect( + JsonCanonicalizer.asString({ + test2: 'test2', + test1: 'test1', + }) + ).toEqual('{"test1":"test1","test2":"test2"}') + }) + it('a json string with null values intact', () => { + expect( + JsonCanonicalizer.asString({ + test1: 'test1', + test2: null, + }) + ).toEqual('{"test1":"test1","test2":null}') + }) + it('a json string with undefined property values removed', () => { + expect( + JsonCanonicalizer.asString({ + test1: 'test1', + test2: undefined, + }) + ).toEqual('{"test1":"test1"}') + }) + it('a json string with undefined property values removed if nested', () => { + expect( + JsonCanonicalizer.asString({ + test1: { + test2: 'test2', + test3: undefined, + }, + }) + ).toEqual('{"test1":{"test2":"test2"}}') + }) +}) diff --git a/packages/did-provider-ion/__tests__/functions.test.ts b/packages/did-provider-ion/__tests__/functions.test.ts new file mode 100644 index 000000000..34c12cb93 --- /dev/null +++ b/packages/did-provider-ion/__tests__/functions.test.ts @@ -0,0 +1,90 @@ +import { ManagedKeyInfo } from '../../core/src' +import { generatePrivateKeyHex, tempMemoryKey, toIonPrivateKeyJwk } from '../src/functions' +import { KeyIdentifierRelation, KeyType } from '../src/types/ion-provider-types' + +const PRIVATE_RECOVERY_KEY_HEX = '7c90c0575643d09a370c35021c91e9d8af2c968c5f3a4bf73802693511a55b9f' +const PRIVATE_UPDATE_KEY_HEX = '7288a92f6219c873446abd1f8d26fcbbe1caa5274b47f6f086ef3e7e75dcad8b' +const PRIVATE_DID_KEY_HEX = '06eb9e64569203679b36f834a4d9725c989d32a7fb52c341eae3517b3aff8ee6' + +describe('functions: key generator', () => { + it('Secp256k1 should generate random keys', () => { + const key1 = generatePrivateKeyHex(KeyType.Secp256k1) + const key2 = generatePrivateKeyHex(KeyType.Secp256k1) + const key3 = generatePrivateKeyHex(KeyType.Secp256k1) + expect(key1).toBeDefined() + expect(key2).toBeDefined() + expect(key3).toBeDefined() + expect(key1).not.toBe(key2) + expect(key2).not.toBe(key3) + }) + it('Secp256k1 should result in hex length 64', () => { + expect(generatePrivateKeyHex(KeyType.Secp256k1).length).toBe(64) + }) + + it('Ed25519 should generate random keys', () => { + const key1 = generatePrivateKeyHex(KeyType.Ed25519) + const key2 = generatePrivateKeyHex(KeyType.Ed25519) + const key3 = generatePrivateKeyHex(KeyType.Ed25519) + expect(key1).toBeDefined() + expect(key2).toBeDefined() + expect(key3).toBeDefined() + expect(key1).not.toBe(key2) + expect(key2).not.toBe(key3) + }) + it('Ed25519 should result in hex length 128', () => { + expect(generatePrivateKeyHex(KeyType.Ed25519).length).toBe(128) + }) +}) + +describe('functions: ionKeys', () => { + it('toPrivateKeyJwk should be deterministic', () => { + const privateKeyJwk = toIonPrivateKeyJwk(PRIVATE_DID_KEY_HEX) + expect(privateKeyJwk).toEqual({ + crv: 'secp256k1', + d: 'BuueZFaSA2ebNvg0pNlyXJidMqf7UsNB6uNRezr_juY', + kty: 'EC', + x: 'aMjNCWMdeXJRg3PDzE7TE9P2xFpoL9fRkJ0toVBMB8E', + y: 'Qz7vj0zUj6S4daGIuEMbB_Ua6Q6wOTGAo46tXLi3SxE', + }) + }) + it('temp recovery Memory Key should be deterministic and have a commitment ', async () => { + const tmpKey = await tempMemoryKey(KeyType.Secp256k1, PRIVATE_RECOVERY_KEY_HEX, 'test-recovery-kid', 'test-recovery-kms', { + relation: KeyIdentifierRelation.RECOVERY, + actionTimestamp: 2, + }) + expect(tmpKey).toMatchObject({ + kid: 'test-recovery-kid', + kms: 'test-recovery-kms', + meta: { + ion: { + actionTimestamp: 2, + commitment: 'EiDAQXSi7HcjJVBYAKdO2zrM4HfybmBBCWsl6PQPJ_jklA', + relation: 'recovery', + }, + }, + publicKeyHex: + '04d530f20a7b3aa14a1dd4ca0aa67fc36138b6547bc91f454bda8d37f9021e0f5c24eeb53256a81d1b26ac342b00b0e7346b38f25a47c3cf233a0601714ae63b8b', + type: 'Secp256k1', + }) + }) + it('temp update Memory Key should be deterministic and have a commitment ', async () => { + const tmpKey = await tempMemoryKey(KeyType.Secp256k1, PRIVATE_UPDATE_KEY_HEX, 'test-update-kid', 'test-update-kms', { + relation: KeyIdentifierRelation.UPDATE, + actionTimestamp: 4, + }) + expect(tmpKey).toMatchObject({ + kid: 'test-update-kid', + kms: 'test-update-kms', + meta: { + ion: { + actionTimestamp: 4, + commitment: 'EiBzp7YhN9mhUcZsFdxnf-lwkRU-hVbBtZWsVoJHV6jkwA', + relation: 'update', + }, + }, + publicKeyHex: + '04826d51d96e9accdef1b13d9acfab61a15d5b5a6b0c1050acb68d58070a3baa0402dae4c63691a6d094537930aef1fa9116af81a6796015edb67013a2792e0b09', + type: 'Secp256k1', + }) + }) +}) diff --git a/packages/did-provider-ion/__tests__/ion-did-provider.test.ts b/packages/did-provider-ion/__tests__/ion-did-provider.test.ts new file mode 100644 index 000000000..27a7d254d --- /dev/null +++ b/packages/did-provider-ion/__tests__/ion-did-provider.test.ts @@ -0,0 +1,230 @@ +import { createAgent, IIdentifier, IKey, IKeyManager, IService } from '../../core/src' +import { DIDManager, MemoryDIDStore } from '../../did-manager/src' +import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../../key-manager/src' +import { IonPublicKeyPurpose } from '@decentralized-identity/ion-sdk' +import { KeyManagementSystem } from '../../kms-local/src' +import { IonDIDProvider } from '../src' +import { ICreateIdentifierOpts } from '../src/types/ion-provider-types' + +const ionDIDProvider = new IonDIDProvider({ + defaultKms: 'mem', +}) +const agent = createAgent({ + plugins: [ + new KeyManager({ + store: new MemoryKeyStore(), + kms: { + mem: new KeyManagementSystem(new MemoryPrivateKeyStore()), + }, + }), + new DIDManager({ + providers: { + 'did:ion': ionDIDProvider, + }, + defaultProvider: 'did:ion', + store: new MemoryDIDStore(), + }), + ], +}) + +const PRIVATE_RECOVERY_KEY_HEX = '7c90c0575643d09a370c35021c91e9d8af2c968c5f3a4bf73802693511a55b9f' +const PRIVATE_UPDATE_KEY_HEX = '7288a92f6219c873446abd1f8d26fcbbe1caa5274b47f6f086ef3e7e75dcad8b' +const PRIVATE_DID1_KEY_HEX = '06eb9e64569203679b36f834a4d9725c989d32a7fb52c341eae3517b3aff8ee6' +const PRIVATE_DID2_KEY_HEX = '42f5d6cbb8af0b484453e19193b6d89e814f1ce66d2c1428271c94ff5465d627' +const PRIVATE_DID3_KEY_HEX = 'abebf433281c5bb86ff8a271d2a464e528437041322a58fb8c14815763cfc189' +const PRIVATE_DID4_KEY_HEX = '7dd923e40f4615ac496119f7e793cc2899e99b64b88ca8603db986700089532b' + +// Generate a new private key in hex format if needed, using the following method: +// console.log(generatePrivateKeyHex(KeyType.Secp256k1)) + +describe('@sphereon/ion-did-provider', () => { + it('should create identifier', async () => { + const options: ICreateIdentifierOpts = createIdentifierOpts + const identifier: IIdentifier = await agent.didManagerCreate({ options }) + + expect(identifier).toBeDefined() + expect(identifier.keys.length).toBe(4) + expect(identifier.services.length).toBe(1) + + expect(identifier.keys[0]).toMatchObject>({ + kms: 'mem', + kid: 'recovery-test', + meta: { ion: { relation: 'recovery' } }, + }) + }) + + it('should add key', async () => { + // This DID is known in ION, hence no anchoring + const identifier: IIdentifier = await agent.didManagerCreate(existingDidConfig(false, 'did1-test2', PRIVATE_DID1_KEY_HEX)) + expect(identifier.alias).toEqual('did:ion:EiCprjAMfWpp7zYXDZV2TGNDV6U4AEBN2Jr6sVsuzL7qhA') + expect(identifier.did).toEqual( + 'did:ion:EiCprjAMfWpp7zYXDZV2TGNDV6U4AEBN2Jr6sVsuzL7qhA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkaWQxLXRlc3QyIiwicHVibGljS2V5SndrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6ImFNak5DV01kZVhKUmczUER6RTdURTlQMnhGcG9MOWZSa0owdG9WQk1COEUiLCJ5IjoiUXo3dmowelVqNlM0ZGFHSXVFTWJCX1VhNlE2d09UR0FvNDZ0WExpM1N4RSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiRWNkc2FTZWNwMjU2azFWZXJpZmljYXRpb25LZXkyMDE5In1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCenA3WWhOOW1oVWNac0ZkeG5mLWx3a1JVLWhWYkJ0WldzVm9KSFY2amt3QSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpRDl4NFJOekEtRGRpRHJUMGd1UU9vLXAwWDh2RTRNcUpvcEVTelZ2ZUtEQnciLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaURBUVhTaTdIY2pKVkJZQUtkTzJ6ck00SGZ5Ym1CQkNXc2w2UFFQSl9qa2xBIn19' + ) + + const newKey = await agent.keyManagerCreate({ kms: 'mem', type: 'Secp256k1' }) + const resultPromise = agent.didManagerAddKey({ + did: identifier.did, + key: newKey, + kid: 'test-add-key-' + Date.now(), + options: { purposes: [IonPublicKeyPurpose.AssertionMethod, IonPublicKeyPurpose.Authentication], anchor: true }, + }) + try { + await expect(resultPromise).resolves.toMatchObject({}) + } catch (error) { + expect(JSON.stringify(error)).toMatch('An operation request already exists in queue for DID') + } + }) + + it('should add service', async () => { + // This DID is known in ION, hence no anchoring + const identifier: IIdentifier = await agent.didManagerCreate(existingDidConfig(false, 'test-kid2', PRIVATE_DID2_KEY_HEX)) + expect(identifier.alias).toEqual('did:ion:EiADHIE9lE5oyd1XAx4xI_WvUaQBr0oYSCUJTGO1czkLKg') + expect(identifier.did).toEqual( + 'did:ion:EiADHIE9lE5oyd1XAx4xI_WvUaQBr0oYSCUJTGO1czkLKg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJ0ZXN0LWtpZDIiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiZFdxTzVyYWRQNXJGdVV6ZnY0T204a1Bnem11MThLVEJ4eEpaRnlJNHhlNCIsInkiOiJYYjl6b1Y5aG9FM2puc2ZXR05iOEZKaWpyNTVZQ0dqYUpsa3FUYnpJZ1ZJIn0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUJ6cDdZaE45bWhVY1pzRmR4bmYtbHdrUlUtaFZiQnRaV3NWb0pIVjZqa3dBIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlBeXhSSHdvWndTbnAtSTllNVZHTmJMcWNxVi15eGtGaTVZMllEc1B1UmhXUSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpREFRWFNpN0hjakpWQllBS2RPMnpyTTRIZnlibUJCQ1dzbDZQUVBKX2prbEEifX0' + ) + + const service: IService = { + type: 'LinkedDomains', + id: 'test' + Date.now(), + serviceEndpoint: 'https://test-example.com', + } + + const resultPromise = agent.didManagerAddService({ + did: identifier.did, + service, + options: { anchor: true }, + }) + try { + await expect(resultPromise).resolves.toMatchObject({}) + } catch (error) { + expect(JSON.stringify(error)).toMatch('An operation request already exists in queue for DID') + } + }) + + it('should remove key', async () => { + // This DID is known in ION, hence no anchoring + const identifier: IIdentifier = await agent.didManagerCreate(existingDidConfig(false, 'did3-test3', PRIVATE_DID3_KEY_HEX)) + expect(identifier.alias).toEqual('did:ion:EiCkiD0CYfwNWupjNPycPi7WbTbMpDgt8KzVHboaUoitdw') + expect(identifier.did).toEqual( + 'did:ion:EiCkiD0CYfwNWupjNPycPi7WbTbMpDgt8KzVHboaUoitdw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkaWQzLXRlc3QzIiwicHVibGljS2V5SndrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6IktYYXp6U21PUzBvQWo4VEd4a3VnaS1QTzFxYWwyREJJemNWcUV6MjRzYkEiLCJ5IjoiQnVTaDJDVFQ2SV9IRmtVaXhaTkkwemstNjNvZEVKR1E5NkZ4RWxvZG1XayJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiRWNkc2FTZWNwMjU2azFWZXJpZmljYXRpb25LZXkyMDE5In1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCenA3WWhOOW1oVWNac0ZkeG5mLWx3a1JVLWhWYkJ0WldzVm9KSFY2amt3QSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ1N6N0FxV2FyWk5ISmV3ZTR5ZUsxMkxVdHBfNmpaVXhzNzY5ZkZfcXZ1aWciLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaURBUVhTaTdIY2pKVkJZQUtkTzJ6ck00SGZ5Ym1CQkNXc2w2UFFQSl9qa2xBIn19' + ) + + const resultPromise = agent.didManagerRemoveKey({ + did: identifier.did, + kid: 'did3-test3', + options: { anchor: false }, + }) + try { + await expect(resultPromise).resolves.toMatchObject({}) + } catch (error) { + expect(JSON.stringify(error)).toMatch('An operation request already exists in queue for DID') + } + }) + + it('should remove service', async () => { + // This DID is known in ION, hence no anchoring + const identifier: IIdentifier = await agent.didManagerCreate(existingDidConfig(false, 'did3-test3', PRIVATE_DID3_KEY_HEX)) + expect(identifier.alias).toEqual('did:ion:EiCkiD0CYfwNWupjNPycPi7WbTbMpDgt8KzVHboaUoitdw') + expect(identifier.did).toEqual( + 'did:ion:EiCkiD0CYfwNWupjNPycPi7WbTbMpDgt8KzVHboaUoitdw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJkaWQzLXRlc3QzIiwicHVibGljS2V5SndrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6IktYYXp6U21PUzBvQWo4VEd4a3VnaS1QTzFxYWwyREJJemNWcUV6MjRzYkEiLCJ5IjoiQnVTaDJDVFQ2SV9IRmtVaXhaTkkwemstNjNvZEVKR1E5NkZ4RWxvZG1XayJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiIsImFzc2VydGlvbk1ldGhvZCJdLCJ0eXBlIjoiRWNkc2FTZWNwMjU2azFWZXJpZmljYXRpb25LZXkyMDE5In1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlCenA3WWhOOW1oVWNac0ZkeG5mLWx3a1JVLWhWYkJ0WldzVm9KSFY2amt3QSJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQ1N6N0FxV2FyWk5ISmV3ZTR5ZUsxMkxVdHBfNmpaVXhzNzY5ZkZfcXZ1aWciLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaURBUVhTaTdIY2pKVkJZQUtkTzJ6ck00SGZ5Ym1CQkNXc2w2UFFQSl9qa2xBIn19' + ) + + const service: IService = { + type: 'LinkedDomains', + id: 'remove-test', + serviceEndpoint: 'https://test-example.com', + } + + const addPromise = agent.didManagerAddService({ + did: identifier.did, + service, + options: { anchor: false }, + }) + try { + await expect(addPromise).resolves.toMatchObject({}) + } catch (error) { + expect(JSON.stringify(error)).toMatch('An operation request already exists in queue for DID') + } + + const removePromise = agent.didManagerRemoveService({ + did: identifier.did, + id: 'remove-test', + options: { anchor: false }, + }) + try { + await expect(removePromise).resolves.toMatchObject({}) + } catch (error) { + expect(JSON.stringify(error)).toMatch('An operation request already exists in queue for DID') + } + }) + + it('should remove identifier', async () => { + const options = existingDidConfig(false, 'remove-test', PRIVATE_DID4_KEY_HEX) + const identifier: IIdentifier = await agent.didManagerCreate({ options }) + + expect(identifier).toBeDefined() + + const deletePromise = agent.didManagerDelete({ did: identifier.did, options: { anchor: false } }) + try { + await expect(deletePromise).resolves.toBeTruthy() + } catch (error) { + expect(JSON.stringify(error)).toMatch('An operation request already exists in queue for DID') + } + }) +}) + +function existingDidConfig(anchor: boolean = false, kid: string, privateDIDKeyHex: String) { + return { + options: { + anchor, + recoveryKey: { + kid: 'recovery-test2', + key: { + privateKeyHex: PRIVATE_RECOVERY_KEY_HEX, + }, + }, + updateKey: { + kid: 'update-test2', + key: { + privateKeyHex: PRIVATE_UPDATE_KEY_HEX, + }, + }, + verificationMethods: [ + { + kid, + purposes: [IonPublicKeyPurpose.Authentication, IonPublicKeyPurpose.AssertionMethod], + key: { + privateKeyHex: privateDIDKeyHex, + }, + }, + ], + }, + } +} + +const createIdentifierOpts = { + anchor: false, + recoveryKey: { + kid: 'recovery-test', + }, + updateKey: { + kid: 'update-test', + }, + verificationMethods: [ + { + kid: 'did1-test', + purposes: [IonPublicKeyPurpose.Authentication, IonPublicKeyPurpose.AssertionMethod], + }, + { + kid: 'did2-test', + purposes: [IonPublicKeyPurpose.KeyAgreement], + }, + ], + services: [ + { + id: 'bar', + type: 'LinkedDomains', + serviceEndpoint: 'https://bar.example.com', + }, + ], +} diff --git a/packages/did-provider-ion/api-extractor.json b/packages/did-provider-ion/api-extractor.json new file mode 100644 index 000000000..409d7f16c --- /dev/null +++ b/packages/did-provider-ion/api-extractor.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "apiReport": { + "enabled": true, + "reportFolder": "./api", + "reportTempFolder": "./api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "./api/.api.json" + }, + + "dtsRollup": { + "enabled": false + }, + "mainEntryPointFilePath": "/build/index.d.ts" +} diff --git a/packages/did-provider-ion/package.json b/packages/did-provider-ion/package.json new file mode 100644 index 000000000..08f97a981 --- /dev/null +++ b/packages/did-provider-ion/package.json @@ -0,0 +1,45 @@ +{ + "name": "@veramo/did-provider-ion", + "description": "Veramo ion-did based identity controller plugin.", + "version": "4.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "extract-api": "yarn veramo dev extract-api" + }, + "dependencies": { + "@decentralized-identity/ion-sdk": "^0.5.0", + "@sphereon/ion-pow": "^0.2.0", + "@transmute/did-key-secp256k1": "0.2.1-unstable.42", + "@ethersproject/random": "^5.6.1", + "@stablelib/ed25519": "^1.0.2", + "uint8arrays": "^3.0.0", + "base64url": "^3.0.1", + "@veramo/core": "^4.0.0", + "@veramo/did-manager": "^4.0.0", + "@veramo/key-manager": "^4.0.0", + "@veramo/kms-local": "^4.0.0", + "debug": "^4.3.3" + }, + "devDependencies": { + "@types/debug": "4.1.7", + "typescript": "4.4.3" + }, + "peerDependencies": { + "@sphereon/react-native-argon2": "^2.0.7" + }, + "files": [ + "dist/**/*", + "src/**/*", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:Sphereon-OpenSource/ssi-sdk.git", + "author": "Sphereon ", + "license": "Apache-2.0", + "keywords": [] +} diff --git a/packages/did-provider-ion/src/functions.ts b/packages/did-provider-ion/src/functions.ts new file mode 100644 index 000000000..cb9ea0d39 --- /dev/null +++ b/packages/did-provider-ion/src/functions.ts @@ -0,0 +1,284 @@ +import { IonKeyMetadata, KeyIdentifierRelation, KeyType } from './types/ion-provider-types' +import { IonDid, IonDocumentModel, IonPublicKeyModel, IonPublicKeyPurpose, JwkEs256k } from '@decentralized-identity/ion-sdk' +import { IKey, ManagedKeyInfo } from '@veramo/core' +import { keyUtils as secp256k1KeyUtils } from '@transmute/did-key-secp256k1' + +import { randomBytes } from '@ethersproject/random' +import * as u8a from 'uint8arrays' +import { generateKeyPair as generateSigningKeyPair } from '@stablelib/ed25519' +import Debug from 'debug' +import { JsonCanonicalizer } from './json-canonicalizer' +import crypto from 'crypto' +import base64url from 'base64url' +import { MemoryPrivateKeyStore } from '@veramo/key-manager' +import { KeyManagementSystem } from '@veramo/kms-local' + +const multihashes = require('multihashes') + +const debug = Debug('veramo:ion-did-provider') + +const MULTI_HASH_SHA256_LITERAL = 18 + +/** + * Ensures we only return Jwk properties that ION can handle + * + * @param jwk The input JWK + * @return The sanitized JWK + */ +export const toJwkEs256k = (jwk: any): JwkEs256k => { + if (jwk.d) { + return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y, d: jwk.d } + } else { + return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y } + } +} + +/** + * Create a JWK from a hex private Key + * @param privateKeyHex The private key in hex form + * @return The JWK + */ +export const toIonPrivateKeyJwk = (privateKeyHex: string): JwkEs256k => { + return toJwkEs256k(secp256k1KeyUtils.privateKeyJwkFromPrivateKeyHex(privateKeyHex)) +} + +/** + * Create a JWK from a hex public Key + * @param privateKeyHex The public key in hex form + * @return The JWK + */ +export const toIonPublicKeyJwk = (publicKeyHex: string): JwkEs256k => { + return toJwkEs256k(secp256k1KeyUtils.publicKeyJwkFromPublicKeyHex(publicKeyHex)) +} + +/** + * Computes the ION Commitment value from a ION public key + * + * @param ionKey The ion public key + * @return The ION commitment value + */ +export const computeCommitmentFromIonPublicKey = (ionKey: IonPublicKeyModel): string => { + return computeCommitmentFromJwk(toJwkEs256k(ionKey.publicKeyJwk)) +} + +/** + * Computes the ION Commitment from a JWK + * + * @param jwk The JWK to computate the commitment for + */ +export const computeCommitmentFromJwk = (jwk: JwkEs256k): string => { + const data = JsonCanonicalizer.asString(jwk) + debug(`canonicalized JWK: ${data}`) + const singleHash = crypto.createHash('sha256').update(data).digest() + const doubleHash = crypto.createHash('sha256').update(singleHash).digest() + + const multiHash = multihashes.encode(Buffer.from(doubleHash), MULTI_HASH_SHA256_LITERAL) + debug(`commitment: ${base64url.encode(multiHash)}`) + return base64url.encode(multiHash) +} + +/** + * Get the action timestamp if present. Use current date/timestamp otherwise + * @param timestamp An optional provided timestamp, useful only in case a key needs to be inserted in between other keys + * @return The action timestamp + */ +export const getActionTimestamp = (timestamp = Date.now()): number => { + return timestamp +} + +/** + * Gets a specific recovery key matching the commitment value, typically coming from a DID document, or the latest recovery key in case it is not provided + * + * @param keys The actual keys related to an identifier + * @param commitment An optional commitment value to match the recovery key against. Typically comes from a DID Document + * @return The ION Recovery Public Key + */ +export const getVeramoRecoveryKey = (keys: IKey[], commitment?: string): IKey => { + const typedKeys = veramoKeysOfType(keys, KeyIdentifierRelation.RECOVERY, commitment) + return commitment === 'genesis' ? typedKeys[0] : typedKeys[typedKeys.length - 1] +} + +/** + * Gets a specific update key matching the commitment value, typically coming from a DID document, or the latest update key in case it is not provided + * + * @param keys The actual keys related to an identifier + * @param commitment An optional commitment value to match the update key against. Typically comes from a DID Document + * @return The Veramo Update Key + */ +export const getVeramoUpdateKey = (keys: IKey[], commitment?: string): IKey => { + const typedKeys = veramoKeysOfType(keys, KeyIdentifierRelation.UPDATE, commitment) + return commitment === 'genesis' ? typedKeys[0] : typedKeys[typedKeys.length - 1] +} + +/** + * Get the Ion Public keys from Veramo which have a specific relation type. If the commitment value is set the Key belonging to that value will be returned, otherwise the latest key + * @param keys The Veramo Keys to inspect + * @param relation The Key relation ship type + * @param commitment An optional commitment value, typically coming from a DID Document. The value 'genisis' returns the first key found + */ +export const ionKeysOfType = (keys: IKey[], relation: KeyIdentifierRelation, commitment?: string): IonPublicKeyModel[] => { + return veramoKeysOfType(keys, relation, commitment).flatMap((key) => { + return toIonPublicKey(key) + }) +} + +/** + * Get the Veramo keys which have a specific relation type. If the commitment value is set the Key belonging to that value will be returned, otherwise the latest key + * @param keys The Veramo Keys to inspect + * @param relation The Key relation ship type + * @param commitment An optional commitment value, typically coming from a DID Document. The value 'genisis' returns the first key found + */ +export const veramoKeysOfType = (keys: IKey[], relation: KeyIdentifierRelation, commitment?: string): IKey[] => { + return keys + .sort((key1, key2) => { + const opId1 = key1.meta?.ion?.operationId + const opId2 = key2.meta?.ion?.operationId + return !opId1 ? 1 : !opId2 ? -1 : opId1 - opId2 + }) + .filter((key) => !commitment || commitment === 'genesis' || !key.meta?.ion.commitment || key.meta?.ion.commitment === commitment) + .filter((key) => key.meta?.ion.relation === relation) + .flatMap((keys) => keys) +} + +/** + * Ion/Sidetree only supports kid with a maximum length of 50 characters. This method truncates longer keys when create ION requests + * + * @param kid The Veramo kid + * @return A truncated kid if the input kid contained more than 50 characters + */ +export const truncateKidIfNeeded = (kid: string): string => { + const id = kid.substring(0, 50) // ION restricts the id to 50 chars. Ideally we can also provide kids for key creation in Veramo + if (id.length != kid.length) { + debug(`Key kid ${kid} has been truncated to 50 chars to support ION!`) + } + return id +} + +/** + * Creates an Ion Public Key (Verification Method), used in ION request from a Veramo Key + * @param key The Veramo Key + * @param createKeyPurposes The verification relationships (Sidetree calls them purposes) to explicitly set. Used during key creation + * @return An Ion Public Key which can be used in Ion request objects + */ +export const toIonPublicKey = (key: ManagedKeyInfo, createKeyPurposes?: IonPublicKeyPurpose[]): IonPublicKeyModel => { + const purposes: IonPublicKeyPurpose[] = createKeyPurposes ? createKeyPurposes : key.meta?.ion?.purposes ? key.meta.ion.purposes : [] + const publicKeyJwk = toIonPublicKeyJwk(key.publicKeyHex) + const id = truncateKidIfNeeded(key.kid) + + return { + id, + type: 'EcdsaSecp256k1VerificationKey2019', + publicKeyJwk, + purposes, + } +} + +/** + * Generates a random Private Hex Key for the specified key type + * @param type The key type + * @return The private key in Hex form + */ +export const generatePrivateKeyHex = (type: KeyType): string => { + let privateKeyHex: string + + switch (type) { + case KeyType.Ed25519: { + const keyPairEd25519 = generateSigningKeyPair() + privateKeyHex = u8a.toString(keyPairEd25519.secretKey, 'base16') + break + } + case KeyType.Secp256k1: { + const privateBytes = randomBytes(32) + privateKeyHex = u8a.toString(privateBytes, 'base16') + break + } + default: + throw Error('not_supported: Key type not supported: ' + type) + } + return privateKeyHex +} + +/** + * Create a Veramo Key entirely in Memory. It is not stored + * + * Didn't want to recreate the logic to extract the pub key for the different key types + * So let's create a temp in-mem kms to do it for us + * + * @param type + * @param privateKeyHex + * @param kid + * @param kms + * @param ionMeta + */ +export const tempMemoryKey = async ( + type: KeyType.Ed25519 | KeyType.Secp256k1 | KeyType, + privateKeyHex: string, + kid: string, + kms: string, + ionMeta: IonKeyMetadata +): Promise => { + const tmpKey = (await new KeyManagementSystem(new MemoryPrivateKeyStore()).importKey({ + type, + privateKeyHex, + kid, + })) as IKey + tmpKey.meta!.ion = JSON.parse(JSON.stringify(ionMeta)) + tmpKey.meta!.ion.commitment = computeCommitmentFromJwk(toIonPublicKeyJwk(tmpKey.publicKeyHex)) + tmpKey.kms = kms + // tmpKey.privateKeyHex = privateKeyHex + return tmpKey +} + +/** + * Generate a deterministic Ion Long form DID from the creation keys + * + * @param input The creation keys + * @return The Ion Long form DID + */ +export const ionLongFormDidFromCreation = (input: { recoveryKey: JwkEs256k; updateKey: JwkEs256k; document: IonDocumentModel }): string => { + return IonDid.createLongFormDid(input) +} + +/** + * Generate a deterministic Ion Short form DID from the creation keys + * + * @param input The creation keys + * @return The Ion Short form DID + */ +export const ionShortFormDidFromCreation = (input: { recoveryKey: JwkEs256k; updateKey: JwkEs256k; document: IonDocumentModel }): string => { + return ionShortFormDidFromLong(ionLongFormDidFromCreation(input)) +} + +/** + * Convert an Ion Long form DID into a short form DID. Be awaer that the input really needs to be a long Form DID! + * @param longFormDid The Ion Long form DID + * @return An Ion Short form DID + */ +export const ionShortFormDidFromLong = (longFormDid: string): string => { + // Only call this from a long form DID! + + // todo: Add min length check + return longFormDid.split(':').slice(0, -1).join(':') +} + +/** + * Gets the method specific Short form Id from the Long form DID. So the did: prefix and Ion Long Form suffix have been removed + * @param longFormDid The Ion Long form DID + * @return The Short form method specific Id + */ +export const ionDidSuffixFromLong = (longFormDid: string): string => { + return ionDidSuffixFromShort(ionShortFormDidFromLong(longFormDid)) +} + +/** + * Gets the method specific Short form Id from the Short form DID. So the did: prefix has been removed + * @param shortFormDid The Ion Short form DID + * @return The Short form method specific Id + */ +export const ionDidSuffixFromShort = (shortFormDid: string): string => { + const suffix = shortFormDid.split(':').pop() + if (!suffix) { + throw new Error(`Could not extract ion DID suffix from short form DID ${shortFormDid}`) + } + return suffix +} diff --git a/packages/did-provider-ion/src/index.ts b/packages/did-provider-ion/src/index.ts new file mode 100644 index 000000000..a1d600d01 --- /dev/null +++ b/packages/did-provider-ion/src/index.ts @@ -0,0 +1,8 @@ +/** + * Provides `did:ion` {@link @veramo/did-provider-ion#IonDIDProvider | identifier provider } + * for the {@link @veramo/did-manager#DIDManager} + * + * @packageDocumentation + */ +export { IonDIDProvider } from './ion-did-provider' +export { getDidIonResolver } from './ion-did-resolver' diff --git a/packages/did-provider-ion/src/ion-did-provider.ts b/packages/did-provider-ion/src/ion-did-provider.ts new file mode 100644 index 000000000..95b972ca3 --- /dev/null +++ b/packages/did-provider-ion/src/ion-did-provider.ts @@ -0,0 +1,445 @@ +import { DIDResolutionResult, IAgentContext, IIdentifier, IKey, IKeyManager, IService, ManagedKeyInfo } from '@veramo/core' +import { AbstractIdentifierProvider } from '@veramo/did-manager' + +import Debug from 'debug' +import { + IAddKeyOpts, + IContext, + ICreateIdentifierOpts, + IKeyRotation, + IonDidForm, + IonKeyMetadata, + IUpdateOpts, + KeyIdentifierRelation, + KeyOpts, + KeyType, + VerificationMethod, +} from './types/ion-provider-types' + +import { IonSigner } from './ion-signer' +import { resolveDidIonFromIdentifier } from './ion-did-resolver' + +import { IonPublicKeyModel, IonPublicKeyPurpose, IonRequest } from '@decentralized-identity/ion-sdk' +import { + computeCommitmentFromIonPublicKey, + generatePrivateKeyHex, + getActionTimestamp, + getVeramoRecoveryKey, + getVeramoUpdateKey, + ionDidSuffixFromLong, + ionLongFormDidFromCreation, + ionShortFormDidFromLong, + tempMemoryKey, + toIonPublicKey, + toIonPublicKeyJwk, + toJwkEs256k, + truncateKidIfNeeded, +} from './functions' +import { IonPoW } from '@sphereon/ion-pow' + +const debug = Debug('veramo:ion-did-provider') + +/** + * {@link @veramo/did-manager#DIDManager} identifier provider for `did:ion` identifiers + * @public + */ +export class IonDIDProvider extends AbstractIdentifierProvider { + private readonly defaultKms: string + private readonly ionPoW: IonPoW + + constructor(options: { defaultKms: string, challengeEnabled?: boolean, challengeEndpoint?: string, solutionEndpoint?: string}) { + super() + this.defaultKms = options.defaultKms + const challengeEnabled = options?.challengeEnabled === undefined ? true : options.challengeEnabled; + const challengeEndpoint = options?.challengeEndpoint + const solutionEndpoint = options?.solutionEndpoint + this.ionPoW = new IonPoW({challengeEnabled, challengeEndpoint, solutionEndpoint}) + } + + /** {@inheritDoc @veramo/core#IDIDManager.didManagerCreate} */ + async createIdentifier( + { kms, options, alias }: { kms?: string; alias?: string; options?: ICreateIdentifierOpts }, + context: IAgentContext + ): Promise> { + const actionTimestamp = getActionTimestamp(options?.actionTimestamp) + + const recoveryKey = await this.importProvidedOrGeneratedKey( + { + kms, + actionTimestamp: actionTimestamp, + relation: KeyIdentifierRelation.RECOVERY, + options: options?.recoveryKey, + }, + context + ) + const updateKey = await this.importProvidedOrGeneratedKey( + { + kms, + actionTimestamp: actionTimestamp, + relation: KeyIdentifierRelation.UPDATE, + options: options?.updateKey, + }, + context + ) + + // No options or no verification method options, results in us generating a single key as the only authentication verification method in the DID + const verificationMethods = options?.verificationMethods + ? options.verificationMethods + : [ + { + type: KeyType.Secp256k1, + purposes: [IonPublicKeyPurpose.Authentication], + }, + ] + + const veramoKeys: ManagedKeyInfo[] = [recoveryKey, updateKey] + const ionPublicKeys: IonPublicKeyModel[] = [] + for (const verificationMethod of verificationMethods) { + const key = await this.importProvidedOrGeneratedKey( + { + kms, + actionTimestamp: actionTimestamp, + relation: KeyIdentifierRelation.DID, + options: verificationMethod, + }, + context + ) + veramoKeys.push(key) + ionPublicKeys.push(toIonPublicKey(key, verificationMethod.purposes)) + } + + const services = options?.services ? options.services : undefined + + const createRequest = { + recoveryKey: toIonPublicKeyJwk(recoveryKey.publicKeyHex), + updateKey: toIonPublicKeyJwk(updateKey.publicKeyHex), + document: { + publicKeys: ionPublicKeys, + services, + }, + } + const longFormDid = ionLongFormDidFromCreation(createRequest) + const shortFormDid = ionShortFormDidFromLong(longFormDid) + + const request = IonRequest.createCreateRequest(createRequest) + await this.anchorRequest(request, options?.anchor) + + const identifier: Omit = { + did: longFormDid, + controllerKeyId: updateKey.kid, + alias: shortFormDid, + keys: veramoKeys, + services: services ? services : [], + } + + debug('Created DID (short, long form): ', identifier.alias, identifier.did) + return identifier + } + + async updateIdentifier(args: { did: string; kms?: string | undefined; alias?: string | undefined; options?: any }, context: IAgentContext): Promise { + throw new Error('IonDIDProvider updateIdentifier not supported yet.') + } + + /** {@inheritDoc @veramo/core#IDIDManager.didManagerDelete} */ + async deleteIdentifier(identifier: IIdentifier, context: IContext): Promise { + const didResolution = await this.getAssertedDidDocument(identifier, IonDidForm.LONG) + const recoveryKey = getVeramoRecoveryKey(identifier.keys, didResolution.didDocumentMetadata.method.recoveryCommitment) + const request = await IonRequest.createDeactivateRequest({ + didSuffix: ionDidSuffixFromLong(identifier.did), + recoveryPublicKey: toJwkEs256k(toIonPublicKeyJwk(recoveryKey.publicKeyHex)), + signer: new IonSigner(context, recoveryKey.kid), + }) + + await this.anchorRequest(request, true) + + return true + } + + /** {@inheritDoc @veramo/core#IDIDManager.didManagerAddKey} */ + async addKey({ identifier, key, options }: { identifier: IIdentifier; key: IKey; options?: IAddKeyOpts }, context: IContext): Promise { + if (!options) { + throw Error('Add key needs options, since we need to know the purpose(s) of the key') + } + const rotation = await this.rotateVeramoKey({ identifier, options, kms: key.kms, context }) + + const request = await IonRequest.createUpdateRequest({ + didSuffix: ionDidSuffixFromLong(identifier.did), + updatePublicKey: rotation.currentJwk, + nextUpdatePublicKey: rotation.nextJwk, + signer: new IonSigner(context, rotation.currentVeramoKey.kid), + publicKeysToAdd: [ + { + ...toIonPublicKey(key, options.purposes), + }, + ], + }) + + try { + await this.anchorRequest(request, options.anchor) + return request + } catch (error) { + // It would have been nicer if we hadn't stored the new update key yet + await context.agent.keyManagerDelete({ kid: rotation.nextVeramoKey.kid }) + throw error + } + } + + /** {@inheritDoc @veramo/core#IDIDManager.didManagerAddService} */ + async addService( + { identifier, service, options }: { identifier: IIdentifier; service: IService; options?: IUpdateOpts }, + context: IContext + ): Promise { + const rotation = await this.rotateVeramoKey({ identifier, options, context }) + + const request = await IonRequest.createUpdateRequest({ + didSuffix: ionDidSuffixFromLong(identifier.did), + updatePublicKey: rotation.currentJwk, + nextUpdatePublicKey: rotation.nextJwk, + signer: new IonSigner(context, rotation.currentVeramoKey.kid), + servicesToAdd: [ + { + ...service, + }, + ], + }) + + try { + await this.anchorRequest(request, options?.anchor) + return request + } catch (error) { + // It would have been nicer if we hadn't stored the new update key yet + await context.agent.keyManagerDelete({ kid: rotation.nextVeramoKey.kid }) + throw error + } + } + + /** {@inheritDoc @veramo/core#IDIDManager.didManagerRemoveKey} */ + async removeKey({ identifier, kid, options }: { identifier: IIdentifier; kid: string; options?: IUpdateOpts }, context: IContext): Promise { + const rotation = await this.rotateVeramoKey({ identifier, options, context }) + + const request = await IonRequest.createUpdateRequest({ + didSuffix: ionDidSuffixFromLong(identifier.did), + updatePublicKey: rotation.currentJwk, + nextUpdatePublicKey: rotation.nextJwk, + signer: new IonSigner(context, rotation.currentVeramoKey.kid), + idsOfPublicKeysToRemove: [truncateKidIfNeeded(kid)], + }) + + try { + await this.anchorRequest(request, options?.anchor) + return request + } catch (error) { + // It would have been nicer if we hadn't stored the new update key yet + await context.agent.keyManagerDelete({ kid: rotation.nextVeramoKey.kid }) + throw error + } + } + + /** {@inheritDoc @veramo/core#IDIDManager.didManagerRemoveService} */ + async removeService({ identifier, id, options }: { identifier: IIdentifier; id: string; options?: IUpdateOpts }, context: IContext): Promise { + const rotation = await this.rotateVeramoKey({ identifier, options, context }) + + const request = await IonRequest.createUpdateRequest({ + didSuffix: ionDidSuffixFromLong(identifier.did), + updatePublicKey: rotation.currentJwk, + nextUpdatePublicKey: rotation.nextJwk, + signer: new IonSigner(context, rotation.currentVeramoKey.kid), + idsOfServicesToRemove: [truncateKidIfNeeded(id)], + }) + + try { + await this.anchorRequest(request, options?.anchor) + return request + } catch (error) { + // It would have been nicer if we hadn't stored the new update key yet + await context.agent.keyManagerDelete({ kid: rotation.nextVeramoKey.kid }) + throw error + } + } + + /** + * Gets as DID document from the identifier in either short or long form + * + * @param identifier - The Identifier (DID) to use + * @param didForm - Use short or long form (the default) for resolution + * @return - A DID Document promise + * @private + */ + private async getAssertedDidDocument(identifier: IIdentifier, didForm: IonDidForm = IonDidForm.LONG): Promise { + const didDocument = await resolveDidIonFromIdentifier(identifier, didForm) + if (!didDocument) { + return Promise.reject(Error(`Could not resolve existing DID document for did ${identifier.did}`)) + } + return didDocument + } + + /** + * Rotate an update or recovery key. Meaning a new key will be generated, which will be used from that moment on for recoveries or updates. + * It returns an object which is used internally to get access to current en next update/recovery keys, which are transformed in different types (Veramo, JWK, ION Public Key) + * + * @param identifier - The identifier (DID) for which to update the recovery/update key + * @param commitment - The current commitment value for either the update or recovery key from the DID document + * @param relation - Whether it is an update key or a recovery key + * @param kms - The KMS to use + * @param options - Allows to set a kid for the new key or to import a key for the new update/recovery key + * @param actionTimestamp - The action Timestamp. These are used to order keys in chronological order. Normally you will want to use Date.now() for these + * @param context - The Veramo Agent context + * @private + */ + private async rotateUpdateOrRecoveryKey( + { + identifier, + commitment, + relation, + kms, + options, + actionTimestamp, + }: { + identifier: IIdentifier + commitment: string + actionTimestamp: number + relation: KeyIdentifierRelation + kms?: string + alias?: string + options?: KeyOpts + }, + context: IAgentContext + ): Promise { + const currentVeramoKey = + relation == KeyIdentifierRelation.UPDATE ? getVeramoUpdateKey(identifier.keys, commitment) : getVeramoRecoveryKey(identifier.keys, commitment) + const currentIonKey = toIonPublicKey(currentVeramoKey) + const currentJwk = toIonPublicKeyJwk(currentVeramoKey.publicKeyHex) + //todo alias? + const nextVeramoKey = await this.importProvidedOrGeneratedKey( + { + kms, + actionTimestamp: actionTimestamp, + relation, + options, + }, + context + ) + const nextIonKey = toIonPublicKey(nextVeramoKey) + const nextJwk = toIonPublicKeyJwk(nextVeramoKey.publicKeyHex) + + return { currentIonKey, currentVeramoKey, currentJwk, nextJwk, nextIonKey, nextVeramoKey } + } + + /** + * Rotates an actual update/recovery key in Veramo + * + * @param kms - The KMS to use + * @param context - The Veramo agent context + * @param options - options Allows to set a kid for the new key or to import a key for the new update/recovery key + * @param identifier - The identifier (DID) for which to update the recovery/update key + * @private + */ + private async rotateVeramoKey({ + kms, + context, + options, + identifier, + }: { + identifier: IIdentifier + options?: IUpdateOpts + kms?: string + context: IContext + }) { + const didResolution = await this.getAssertedDidDocument(identifier, IonDidForm.LONG) + const currentUpdateKey = getVeramoUpdateKey(identifier.keys, didResolution.didDocumentMetadata.method.updateCommitment) + const commitment = computeCommitmentFromIonPublicKey(toIonPublicKey(currentUpdateKey)) + const actionId = getActionTimestamp(options?.actionTimestamp) + + const rotation = await this.rotateUpdateOrRecoveryKey( + { + identifier, + commitment, + relation: KeyIdentifierRelation.UPDATE, + actionTimestamp: actionId, + kms: kms ? kms : this.defaultKms, + options: {}, + }, + context + ) + return rotation + } + + /** + * We optionally generate and then import our own keys. + * + * Reason for this is that we want to be able to assign Key IDs (kid), which Veramo supports on import, but not creation. The net result is that we do not support keys which have been created from keyManagerCreate + * + * @param kms - The KMS to use + * @param actionTimestamp - The action Timestamp. These are used to order keys in chronological order. Normally you will want to use Date.now() for these + * @param relation - Whether it is a DID Verification Method key, an update key or a recovery key + * @param options - Allows to set a kid for the new key or to import a key for the new update/recovery key + * @param context - The Veramo agent context + * @private + */ + private async importProvidedOrGeneratedKey( + { + kms, + actionTimestamp, + relation, + options, + }: { kms?: string; actionTimestamp: number; relation: KeyIdentifierRelation; options?: KeyOpts | VerificationMethod }, + context: IAgentContext + ): Promise { + const kid = options?.kid ? options.kid : options?.key?.kid + const type = options?.type ? options.type : options?.key?.type ? (options.key.type as KeyType) : KeyType.Secp256k1 + + const meta = options?.key?.meta ? options.key.meta : {} + const ionMeta: IonKeyMetadata = { + relation, + actionTimestamp: actionTimestamp, + } + if (options && 'purposes' in options) { + ionMeta.purposes = options.purposes + } + let privateKeyHex: string + if (options?.key) { + if (!options.key.privateKeyHex) { + throw new Error(`We need to have a private key when importing a recovery or update key. Key ${kid} did not have one`) + } + privateKeyHex = options.key.privateKeyHex + } else { + privateKeyHex = generatePrivateKeyHex(type) + } + if (relation === KeyIdentifierRelation.RECOVERY || relation === KeyIdentifierRelation.UPDATE) { + // We need a commitment for these keys. As they are based on the publicKey let's create an in mem key + const tmpKey = await tempMemoryKey(type, privateKeyHex, kid!, kms ? kms : this.defaultKms, ionMeta) + ionMeta.commitment = tmpKey.meta!.ion.commitment + } + meta.ion = ionMeta + + const key: IKey = await context.agent.keyManagerImport({ + kms: kms || this.defaultKms, + type, + privateKeyHex, + kid, + meta, + }) + // We need it in case we are importing it again in the same call + // key.privateKeyHex = privateKeyHex + + debug('Created key', type, relation, kid, key.publicKeyHex) + + return key + } + + /** + * Whether to actually anchor the request on the ION network + * + * @param request - The ION request + * @param anchor - Whether to anchor or not (defaults to true) + * @return - The anchor request + * @private + */ + private async anchorRequest(request: IonRequest, anchor?: boolean) { + if (anchor !== false) { + await this.ionPoW.submit(JSON.stringify(request)) + } else { + debug(`Not anchoring as anchoring was not enabled`) + } + } +} diff --git a/packages/did-provider-ion/src/ion-did-resolver.ts b/packages/did-provider-ion/src/ion-did-resolver.ts new file mode 100644 index 000000000..a53348511 --- /dev/null +++ b/packages/did-provider-ion/src/ion-did-resolver.ts @@ -0,0 +1,30 @@ +import { fetch } from 'cross-fetch' +import { IIdentifier } from '@veramo/core' +import { DIDResolutionOptions, DIDResolutionResult, DIDResolver } from 'did-resolver' +import { IonDidForm } from './types/ion-provider-types' + +export const resolveDidIonFromIdentifier = async ( + identifier: IIdentifier, + ionDidForm: IonDidForm = IonDidForm.LONG, + options?: DIDResolutionOptions +): Promise => { + return await resolve(ionDidForm == IonDidForm.LONG ? identifier.did : identifier.alias!, options) +} + +export const resolveDidIon: DIDResolver = async (didUrl: string, options?: DIDResolutionOptions): Promise => { + return resolve(didUrl, options) +} + +const resolve = async (didUrl: string, options?: DIDResolutionOptions) => { + return fetch((options?.nodeEndpoint || 'https://beta.discover.did.microsoft.com/1.0/identifiers/') + didUrl).then((response) => { + if (response.status >= 400) throw new Error('Not Found') + return response.json() + }) +} + +/** + * @public + */ +export function getDidIonResolver() { + return { ion: resolveDidIon } +} diff --git a/packages/did-provider-ion/src/ion-signer.ts b/packages/did-provider-ion/src/ion-signer.ts new file mode 100644 index 000000000..8cc48a43f --- /dev/null +++ b/packages/did-provider-ion/src/ion-signer.ts @@ -0,0 +1,48 @@ +import crypto from 'crypto' +import { IContext } from './types/ion-provider-types' +import * as u8a from 'uint8arrays' +import base64url from 'base64url' + +/** + * This class is responsible for signing the JWT when sending in Anchor requests to an ION node. It is using the update or recovery key denoted by 'kid' + */ +export class IonSigner { + private readonly kid: string + + /** + * Construct the signer object + * + * @param context The agent context + * @param kid The Veramo update or recovery Key ID (kid) + */ + constructor(private context: IContext, kid: string) { + this.kid = kid + } + + /** + * Sign the JWT header and payload using the Key ID (kid) provided during construction. + * + * @param header The JWT header (only 'alg' supported for now) + * @param payload The ION update delta payload + */ + async sign(header: any, payload: any): Promise { + if (!header) { + header = { + alg: 'ES256K', + } + } + const encodedHeader = base64url.encode(JSON.stringify(header)) + const encodedPayload = base64url.encode(JSON.stringify(payload)) + const toBeSigned = encodedHeader + '.' + encodedPayload + const message = u8a.fromString(toBeSigned) + const digest = crypto.createHash('sha256').update(message).digest('hex') + const sigObj = await this.context.agent.keyManagerSign({ + keyRef: this.kid, + algorithm: header.alg, + data: digest, + encoding: 'hex', + }) + const encodedSignature = sigObj // The keyManagerSign already performs base64Url encoding + return encodedHeader + '.' + encodedPayload + '.' + encodedSignature + } +} diff --git a/packages/did-provider-ion/src/json-canonicalizer.ts b/packages/did-provider-ion/src/json-canonicalizer.ts new file mode 100644 index 000000000..784a13cfa --- /dev/null +++ b/packages/did-provider-ion/src/json-canonicalizer.ts @@ -0,0 +1,36 @@ +import canonicalize from 'canonicalize' +/** + * Class containing reusable JSON canonicalization operations using JSON Canonicalization Scheme (JCS). + */ +export class JsonCanonicalizer { + /** + * Canonicalizes the given content as a string + * @param The content to canonicalize + * @return The canonicalized content + */ + static asString(content: unknown): string { + if (content == null) { + throw Error('Null content received in canonicalizer') + } + // We need to remove all properties with `undefined` as value so that JCS canonicalization will not produce invalid JSON. + const contentWithoutUndefinedProperties = JsonCanonicalizer.removeAllUndefinedProperties(content) + const canonicalizedString = canonicalize(contentWithoutUndefinedProperties) + if (!canonicalizedString) { + throw new Error('Could not canonicalize input') + } + return canonicalizedString + } + /** + * Removes all properties within the given object with `undefined` as value as that would mess up the validity + */ + private static removeAllUndefinedProperties(content: any): unknown { + for (const key in content) { + if (typeof content[key] === 'object') { + JsonCanonicalizer.removeAllUndefinedProperties(content[key]) + } else if (content[key] === undefined) { + delete content[key] + } + } + return content + } +} diff --git a/packages/did-provider-ion/src/types/@decentralized-identity/ion-tools/index.d.ts b/packages/did-provider-ion/src/types/@decentralized-identity/ion-tools/index.d.ts new file mode 100644 index 000000000..90f50ead0 --- /dev/null +++ b/packages/did-provider-ion/src/types/@decentralized-identity/ion-tools/index.d.ts @@ -0,0 +1 @@ +declare module '@decentralized-identity/ion-tools' diff --git a/packages/did-provider-ion/src/types/ion-provider-types.ts b/packages/did-provider-ion/src/types/ion-provider-types.ts new file mode 100644 index 000000000..81b44e190 --- /dev/null +++ b/packages/did-provider-ion/src/types/ion-provider-types.ts @@ -0,0 +1,66 @@ +import { IAgentContext, IKey, IKeyManager, IService, MinimalImportableKey } from '@veramo/core' +import { IonPublicKeyPurpose, IonPublicKeyModel, JwkEs256k } from '@decentralized-identity/ion-sdk' + +export type IContext = IAgentContext + +export interface VerificationMethod extends KeyOpts { + purposes: IonPublicKeyPurpose[] // In sidetree these are called purposes, but in DID-Core Verification Relationships +} + +export interface KeyOpts { + kid?: string // Key ID to assign in case we are importing a key + key?: MinimalImportableKey // Optional key to import. If not specified a key with random kid will be created + type?: KeyType // The key type. Defaults to Secp256k1 +} + +export interface ICreateIdentifierOpts { + verificationMethods?: VerificationMethod[] // The Verification method to add + recoveryKey?: KeyOpts // Recovery key options + updateKey?: KeyOpts // Update key options + services?: IService[] // Service(s) to add + actionTimestamp?: number // Unique number denoting the action. Used for ordering internally. Suggested to use current timestamp + anchor?: boolean // Whether the DID should be anchored on ION or not. Handy for testing or importing an ID +} + +export interface IAddKeyOpts extends IUpdateOpts { + purposes: IonPublicKeyPurpose[] // In sidetree these are called purposes, but in DID-Core Verification Relationships +} + +export interface IUpdateOpts { + actionTimestamp?: number // Unique number denoting the action. Used for ordering internally. Suggested to use current timestamp + anchor?: boolean // Whether the DID should be anchored on ION or not. Handy for testing or importing an ID +} + +export interface IonKeyMetadata { + purposes?: IonPublicKeyPurpose[] // The Verification Method Relationships, or purposes as they are called in ION/Sidetree + actionTimestamp: number // Unique number denoting the action. Used for ordering internally. Suggested to use current timestamp + relation: KeyIdentifierRelation // The type of key, which is either recovery, update or DID + commitment?: string // Commitment value in case this is an update or recovery key. Used to get latest update/recovery keys +} + +export enum KeyType { + Ed25519 = 'Ed25519', // EdDSA key type + Secp256k1 = 'Secp256k1', // EcDSA key type (not yet supported) +} + +export enum KeyIdentifierRelation { + RECOVERY = 'recovery', // A recovery key can be used to recover access to a DID, after loosing the update key(s) + UPDATE = 'update', // An update key is used to commit changes on ION. Please note that new update keys will be automatically created on every update + DID = 'did', // A key which ends up as a Verification Method in a DID document +} + +export enum IonDidForm { + LONG = 'long', // A long form ION DID, which contains the short form, but also a self-certifying part, which can be resolved before the anchor happened. This is handy during the initial anchoring (as that might take a long time) + SHORT = 'short', // The short form ION DID, which can only be used once anchored +} + +export interface IKeyRotation { + currentVeramoKey: IKey // Current update/recovery Veramo Key + currentIonKey: IonPublicKeyModel // Current update/recovery Key in ION form + currentJwk: JwkEs256k // Current update/recovery JWK of the key + nextVeramoKey: IKey // Next Veramo update/recovery key + nextIonKey: IonPublicKeyModel // Next update/recovery key in ION form + nextJwk: JwkEs256k // Next JWK of the update/recovery key +} + +export type IRequiredContext = IAgentContext diff --git a/packages/did-provider-ion/tsconfig.json b/packages/did-provider-ion/tsconfig.json new file mode 100644 index 000000000..c07878dd9 --- /dev/null +++ b/packages/did-provider-ion/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "declarationDir": "build" + }, + "references": [{ "path": "../core" }, { "path": "../did-manager" }, { "path": "../key-manager" }, { "path": "../kms-local" }] +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 82b34e0b1..9ca304380 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -13,6 +13,7 @@ { "path": "did-jwt" }, { "path": "did-manager" }, { "path": "did-provider-ethr" }, + { "path": "did-provider-ion" }, { "path": "did-provider-key" }, { "path": "did-provider-web" }, { "path": "did-resolver" }, diff --git a/yarn.lock b/yarn.lock index fafb5a3e7..7cf0fd96f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1523,6 +1523,18 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-1.0.0.tgz#91c560df2ed8d9700e4c7ed4ac21a3a322c9d975" integrity sha512-RkYG5KiGNX0fJ5YoI0f4Wfq2Yo74D25Hru4fxTOioYdQvHBxcrrtTTyT5Ozzh2ejcNrhFy7IEts2WyEY7yi5yw== +"@decentralized-identity/ion-sdk@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@decentralized-identity/ion-sdk/-/ion-sdk-0.5.0.tgz#624565876a8597e79c27012f2bc30cea7e9dc4e8" + integrity sha512-W6Cd3KXd8Mqv8dgFvUEt1Jj3jKUtursmOhr50o1nG3J0LbVw8inCrkrmd7R9C5zpBPP+1EwpUya+A9JU5nbp7w== + dependencies: + "@transmute/did-key-secp256k1" "^0.2.1-unstable.35" + "@waiting/base64" "4.2.9" + canonicalize "1.0.1" + multihashes "0.4.14" + randombytes "2.1.0" + uri-js "4.4.0" + "@did-core/data-model@^0.1.1-unstable.13": version "0.1.1-unstable.15" resolved "https://registry.yarnpkg.com/@did-core/data-model/-/data-model-0.1.1-unstable.15.tgz#51ef2e99adebdf2d8ba8c43ed537f33916d6b8a8" @@ -4077,6 +4089,24 @@ resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== +"@sphereon/ion-pow@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@sphereon/ion-pow/-/ion-pow-0.2.0.tgz#e275c00a04d699b786f02792364eb77bc65f7d0b" + integrity sha512-SpEG4mV5D+K/jrqGI9QSBPPKO5+Kpu6F3cINBKbWiz+ZI4boWwz9JAdNspD45YnnMqTbR14CDEGtHwOaHboJQg== + dependencies: + "@sphereon/isomorphic-argon2" "^1.0.0" + cross-fetch "^3.1.5" + debug "^4.3.4" + uint8arrays "^3.1.0" + +"@sphereon/isomorphic-argon2@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@sphereon/isomorphic-argon2/-/isomorphic-argon2-1.0.0.tgz#82e22a84c2aa389d2297416a6ba4512b27937201" + integrity sha512-k63y0CzQYRH/EEOLx/u9O6oDFzZ6aiPTraGtwm29Fi0k630oy8xnYxi6xCdObicn+LLO0tCxGMFRtH/cXJHJlA== + dependencies: + argon2-browser "^1.18.0" + uint8arrays "^3.1.0" + "@sqltools/formatter@^1.2.2": version "1.2.3" resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20" @@ -4414,6 +4444,16 @@ resolved "https://registry.yarnpkg.com/@transmute/did-context/-/did-context-0.6.1-unstable.37.tgz#12ad065e142bc688460090d0ce338948e513c262" integrity sha512-p/QnG3QKS4218hjIDgdvJOFATCXsAnZKgy4egqRrJLlo3Y6OaDBg7cA73dixOwUPoEKob0K6rLIGcsCI/L1acw== +"@transmute/did-key-common@^0.2.1-unstable.42": + version "0.2.1-unstable.42" + resolved "https://registry.yarnpkg.com/@transmute/did-key-common/-/did-key-common-0.2.1-unstable.42.tgz#494b266308b28a41a88cea7fe3697a5474c33bbb" + integrity sha512-mJ58IKEBxa6SorCrIBSPu0OcEj94Y5+0/qUKqbNTTqfCOsPi6E5BEzMIgpf3Unrb59u+u5JBL0T/Sy7coOSO1A== + dependencies: + base64url "^3.0.1" + borc "^2.1.2" + canonicalize "^1.0.3" + cbor "^5.1.0" + "@transmute/did-key-common@^0.3.0-unstable.8": version "0.3.0-unstable.8" resolved "https://registry.yarnpkg.com/@transmute/did-key-common/-/did-key-common-0.3.0-unstable.8.tgz#0c6be67d8a312b76c5377b9a08fec9bb7168b960" @@ -4433,6 +4473,18 @@ "@transmute/did-key-common" "^0.3.0-unstable.8" "@transmute/ed25519-key-pair" "^0.6.1-unstable.37" +"@transmute/did-key-secp256k1@0.2.1-unstable.42", "@transmute/did-key-secp256k1@^0.2.1-unstable.35": + version "0.2.1-unstable.42" + resolved "https://registry.yarnpkg.com/@transmute/did-key-secp256k1/-/did-key-secp256k1-0.2.1-unstable.42.tgz#619a2bcc10c53adb831930610f1c49f5ec771261" + integrity sha512-pLHsxVEeJYUz2jaUWznGJHRaRE+Fg3A4DiArWYpRSozYaSW5x2AhXELN+08qvU4E8FjiQmqInf8KqeS0hlUJoQ== + dependencies: + "@transmute/did-key-common" "^0.2.1-unstable.42" + "@trust/keyto" "^1.0.1" + base64url "^3.0.1" + bs58 "^4.0.1" + canonicalize "^1.0.1" + secp256k1 "^4.0.1" + "@transmute/did-key-secp256k1@^0.3.0-unstable.8": version "0.3.0-unstable.8" resolved "https://registry.yarnpkg.com/@transmute/did-key-secp256k1/-/did-key-secp256k1-0.3.0-unstable.8.tgz#ce936e960b580109d6a43be2f517c2dc793616af" @@ -5291,6 +5343,11 @@ crypto-ld "^7.0.0" json-stringify-deterministic "^1.0.7" +"@waiting/base64@4.2.9": + version "4.2.9" + resolved "https://registry.yarnpkg.com/@waiting/base64/-/base64-4.2.9.tgz#fa7e98d1a317355fed0cfb0ebdf94c361b9a3acf" + integrity sha512-yzt9ih63oePux/sQM8Df6DTv1IeXGByaHrFiJoPszk0LaJwIGJ3IKhKhT/O6YMo2RignlP2cfxoo1lGmKZPIgQ== + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -5767,6 +5824,11 @@ arg@^5.0.1: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== +argon2-browser@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/argon2-browser/-/argon2-browser-1.18.0.tgz#f35820211e0a431aed7f82b9348477234be69bec" + integrity sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw== + argparse@^1.0.7, argparse@~1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -6189,6 +6251,13 @@ base-64@^0.1.0: resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== +base-x@^3.0.2: + version "3.0.9" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" + integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== + dependencies: + safe-buffer "^5.0.1" + base64-js@*, base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -6246,6 +6315,11 @@ bignumber.js@^9.0.0: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== +bignumber.js@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62" + integrity sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A== + bin-links@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-3.0.0.tgz#8273063638919f6ba4fbe890de9438c1b3adf0b7" @@ -6364,6 +6438,19 @@ borc@3.0.0: json-text-sequence "~0.3.0" readable-stream "^3.6.0" +borc@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/borc/-/borc-2.1.2.tgz#6ce75e7da5ce711b963755117dd1b187f6f8cf19" + integrity sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w== + dependencies: + bignumber.js "^9.0.0" + buffer "^5.5.0" + commander "^2.15.0" + ieee754 "^1.1.13" + iso-url "~0.4.7" + json-text-sequence "~0.1.0" + readable-stream "^3.6.0" + bottleneck@^2.18.1: version "2.19.5" resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" @@ -6495,6 +6582,13 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" +bs58@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -6723,12 +6817,17 @@ caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001286: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz" integrity sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw== +canonicalize@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.1.tgz#657b4f3fa38a6ecb97a9e5b7b26d7a19cc6e0da9" + integrity sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg== + canonicalize@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.5.tgz#b43b390ce981d397908bb847c3a8d9614323a47b" integrity sha512-mAjKJPIyP0xqqv6IAkvso07StOmz6cmGtNDg3pXCSzXVZOqka7StIkAhJl/zHOi4M2CgpYfD6aeRWbnrmtvBEA== -canonicalize@^1.0.8: +canonicalize@^1.0.3, canonicalize@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.8.tgz#24d1f1a00ed202faafd9bf8e63352cd4450c6df1" integrity sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A== @@ -6756,6 +6855,14 @@ catering@^2.0.0, catering@^2.1.0: resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== +cbor@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-5.2.0.tgz#4cca67783ccd6de7b50ab4ed62636712f287a67c" + integrity sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A== + dependencies: + bignumber.js "^9.0.1" + nofilter "^1.0.4" + chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -7891,6 +7998,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +delimit-stream@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/delimit-stream/-/delimit-stream-0.1.0.tgz#9b8319477c0e5f8aeb3ce357ae305fc25ea1cd2b" + integrity sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -10780,6 +10892,11 @@ iso-url@^1.1.5: resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-1.2.1.tgz#db96a49d8d9a64a1c889fc07cc525d093afb1811" integrity sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng== +iso-url@~0.4.7: + version "0.4.7" + resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-0.4.7.tgz#de7e48120dae46921079fe78f325ac9e9217a385" + integrity sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog== + isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" @@ -11975,6 +12092,13 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json-text-sequence@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/json-text-sequence/-/json-text-sequence-0.1.1.tgz#a72f217dc4afc4629fff5feb304dc1bd51a2f3d2" + integrity sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w== + dependencies: + delimit-stream "0.1.0" + json-text-sequence@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/json-text-sequence/-/json-text-sequence-0.3.0.tgz#6603e0ee45da41f949669fd18744b97fb209e6ce" @@ -13146,6 +13270,14 @@ multiformats@^9.6.5: resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.5.tgz#f2d894a26664b454a90abf5a8911b7e39195db80" integrity sha512-vMwf/FUO+qAPvl3vlSZEgEVFY/AxeZq5yg761ScF3CZsXgmTi/HGkicUiNN0CI4PW8FiY2P0OLklOcmQjdQJhw== +multihashes@0.4.14: + version "0.4.14" + resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-0.4.14.tgz#774db9a161f81a8a27dc60788f91248e020f5244" + integrity sha512-V/g/EIN6nALXfS/xHUAgtfPP3mn3sPIF/i9beuGKf25QXS2QZYCpeVJbDPEannkz32B2fihzCe2D/KMrbcmefg== + dependencies: + bs58 "^4.0.1" + varint "^5.0.0" + multimatch@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" @@ -13333,6 +13465,11 @@ node-releases@^2.0.3: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== +nofilter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.4.tgz#78d6f4b6a613e7ced8b015cec534625f7667006e" + integrity sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA== + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -15327,7 +15464,7 @@ raf@^3.4.1: dependencies: performance-now "^2.1.0" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@2.1.0, randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -16097,6 +16234,15 @@ secp256k1@4.0.2, secp256k1@^4.0.2: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" +secp256k1@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -17595,6 +17741,11 @@ typeorm@^0.3.10: xml2js "^0.4.23" yargs "^17.3.1" +typescript@4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" + integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== + typescript@4.7.3: version "4.7.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" @@ -17617,6 +17768,13 @@ uint8arrays@^3.0.0: dependencies: multiformats "^9.4.2" +uint8arrays@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.0.tgz#8186b8eafce68f28bd29bd29d683a311778901e2" + integrity sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog== + dependencies: + multiformats "^9.4.2" + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -17714,6 +17872,13 @@ upath@^2.0.1: resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== +uri-js@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + dependencies: + punycode "^2.1.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -17843,6 +18008,11 @@ validator@^13.7.0: resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== +varint@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4" + integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow== + varint@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0"