diff --git a/__tests__/localAgent.test.ts b/__tests__/localAgent.test.ts index ac7018356..853534dd4 100644 --- a/__tests__/localAgent.test.ts +++ b/__tests__/localAgent.test.ts @@ -40,6 +40,7 @@ import { SelectiveDisclosure, } from '../packages/selective-disclosure/src' import { KeyManagementSystem, SecretBox } from '../packages/kms-local/src' +import { Web3KeyManagementSystem } from '../packages/kms-web3/src' import { DIDDiscovery, IDIDDiscovery } from '../packages/did-discovery/src' import { @@ -77,6 +78,9 @@ import messageHandler from './shared/messageHandler' import didDiscovery from './shared/didDiscovery' import dbInitOptions from './shared/dbInitOptions' import didCommWithEthrDidFlow from './shared/didCommWithEthrDidFlow' +import utils from './shared/utils' +import web3 from './shared/web3' + jest.setTimeout(60000) @@ -141,6 +145,9 @@ const setup = async (options?: IAgentOptions): Promise => { store: new KeyStore(dbConnection), kms: { local: new KeyManagementSystem(new PrivateKeyStore(dbConnection, new SecretBox(secretKey))), + web3: new Web3KeyManagementSystem({ + 'ganache': provider + }) }, }), new DIDManager({ @@ -261,5 +268,7 @@ describe('Local integration tests', () => { didCommPacking(testContext) didDiscovery(testContext) dbInitOptions(testContext) + utils(testContext) + web3(testContext) didCommWithEthrDidFlow(testContext) }) diff --git a/__tests__/localJsonStoreAgent.test.ts b/__tests__/localJsonStoreAgent.test.ts index ee4f1fdd3..a56081898 100644 --- a/__tests__/localJsonStoreAgent.test.ts +++ b/__tests__/localJsonStoreAgent.test.ts @@ -40,6 +40,7 @@ import { SelectiveDisclosure, } from '../packages/selective-disclosure/src' import { KeyManagementSystem, SecretBox } from '../packages/kms-local/src' +import { Web3KeyManagementSystem } from '../packages/kms-web3/src' import { DataStoreJson, DIDStoreJson, @@ -67,8 +68,10 @@ import keyManager from './shared/keyManager' import didManager from './shared/didManager' import didCommPacking from './shared/didCommPacking' import messageHandler from './shared/messageHandler' +import utils from './shared/utils' import { JsonFileStore } from './utils/json-file-store' + jest.setTimeout(60000) const infuraProjectId = '3586660d179141e3801c3895de1c2eba' @@ -120,6 +123,7 @@ const setup = async (options?: IAgentOptions): Promise => { store: new KeyStoreJson(jsonFileStore), kms: { local: new KeyManagementSystem(new PrivateKeyStoreJson(jsonFileStore, new SecretBox(secretKey))), + web3: new Web3KeyManagementSystem({}), }, }), new DIDManager({ @@ -218,4 +222,5 @@ describe('Local json-data-store integration tests', () => { didManager(testContext) messageHandler(testContext) didCommPacking(testContext) + utils(testContext) }) diff --git a/__tests__/localMemoryStoreAgent.test.ts b/__tests__/localMemoryStoreAgent.test.ts index 4310b1bdd..347a1b83c 100644 --- a/__tests__/localMemoryStoreAgent.test.ts +++ b/__tests__/localMemoryStoreAgent.test.ts @@ -39,6 +39,7 @@ import { SelectiveDisclosure, } from '../packages/selective-disclosure/src' import { KeyManagementSystem } from '../packages/kms-local/src' +import { Web3KeyManagementSystem } from '../packages/kms-web3/src' import { DataStore, DataStoreORM, Entities, migrations } from '../packages/data-store/src' import { FakeDidProvider, FakeDidResolver } from '../packages/test-utils/src' @@ -60,6 +61,7 @@ import keyManager from './shared/keyManager' import didManager from './shared/didManager' import didCommPacking from './shared/didCommPacking' import messageHandler from './shared/messageHandler' +import utils from './shared/utils' jest.setTimeout(60000) @@ -115,6 +117,7 @@ const setup = async (options?: IAgentOptions): Promise => { store: new MemoryKeyStore(), kms: { local: new KeyManagementSystem(new MemoryPrivateKeyStore()), + web3: new Web3KeyManagementSystem({}) }, }), new DIDManager({ @@ -214,4 +217,5 @@ describe('Local in-memory integration tests', () => { didManager(testContext) messageHandler(testContext) didCommPacking(testContext) + utils(testContext) }) diff --git a/__tests__/restAgent.test.ts b/__tests__/restAgent.test.ts index 6a852ebfc..cca6107f1 100644 --- a/__tests__/restAgent.test.ts +++ b/__tests__/restAgent.test.ts @@ -43,6 +43,7 @@ import { SelectiveDisclosure, } from '../packages/selective-disclosure/src' import { KeyManagementSystem, SecretBox } from '../packages/kms-local/src' +import { Web3KeyManagementSystem } from '../packages/kms-web3/src' import { DataStore, DataStoreORM, @@ -81,6 +82,7 @@ import didCommPacking from './shared/didCommPacking' import didWithFakeDidFlow from './shared/didCommWithFakeDidFlow' import messageHandler from './shared/messageHandler' import didDiscovery from './shared/didDiscovery' +import utils from './shared/utils' jest.setTimeout(60000) @@ -138,6 +140,7 @@ const setup = async (options?: IAgentOptions): Promise => { store: new KeyStore(dbConnection), kms: { local: new KeyManagementSystem(new PrivateKeyStore(dbConnection, new SecretBox(secretKey))), + web3: new Web3KeyManagementSystem({}) }, }), new DIDManager({ @@ -263,4 +266,5 @@ describe('REST integration tests', () => { didCommPacking(testContext) didWithFakeDidFlow(testContext) didDiscovery(testContext) + utils(testContext) }) diff --git a/__tests__/shared/keyManager.ts b/__tests__/shared/keyManager.ts index 1470a513b..899fede4b 100644 --- a/__tests__/shared/keyManager.ts +++ b/__tests__/shared/keyManager.ts @@ -23,7 +23,7 @@ export default (testContext: { it('should get a list of available key management systems', async () => { const keyManagementSystems = await agent.keyManagerGetKeyManagementSystems() - expect(keyManagementSystems).toEqual(['local']) + expect(keyManagementSystems).toEqual(['local', 'web3']) }) it('should create Secp256k1 key', async () => { diff --git a/__tests__/shared/utils.ts b/__tests__/shared/utils.ts new file mode 100644 index 000000000..56da45e7e --- /dev/null +++ b/__tests__/shared/utils.ts @@ -0,0 +1,61 @@ +import { IAgentOptions, IDIDManager, IResolver, MinimalImportableKey, TAgent } from '../../packages/core/src' +import { getChainIdForDidEthr, resolveDidOrThrow, mapIdentifierKeysToDoc } from '../../packages/utils/src' + +type ConfiguredAgent = TAgent + +export default (testContext: { + getAgent: (options?: IAgentOptions) => ConfiguredAgent + setup: (options?: IAgentOptions) => Promise + tearDown: () => Promise +}) => { + describe('utilities', () => { + let agent: ConfiguredAgent + + beforeAll(async () => { + await testContext.setup() + agent = testContext.getAgent() + return true + }) + afterAll(testContext.tearDown) + + it('should get chainId for ethr did', async () => { + const didUrl = 'did:ethr:rinkeby:0xb09b66026ba5909a7cfe99b76875431d2b8d5190' + const didDoc = await resolveDidOrThrow(didUrl, {agent}) + if (didDoc.verificationMethod) { + const chainId = getChainIdForDidEthr(didDoc.verificationMethod[0]) + expect(chainId).toEqual(4) + } + }) + + it('should map identifier keys to did doc', async () => { + const account = `0xb09b66026ba5909a7cfe99b76875431d2b8d5190` + const did = `did:ethr:0x4:${account}` + const controllerKeyId = `metamask-${account}` + await agent.didManagerImport({ + did, + provider: 'did:ethr:rinkeby', + controllerKeyId, + keys: [{ + kid: controllerKeyId, + type: 'Secp256k1', + kms: 'web3', + privateKeyHex: '', + publicKeyHex: '', + meta: { + account, + provider: 'metamask', + algorithms: [ + 'eth_signMessage', + 'eth_signTypedData', + ] + }, + } as MinimalImportableKey], + }) + + const identifier = await agent.didManagerGet({ did }) + const extendedKeys = await mapIdentifierKeysToDoc(identifier, 'verificationMethod', { agent }) + expect(extendedKeys[0].meta.verificationMethod?.blockchainAccountId?.toLocaleLowerCase()).toEqual(`eip155:4:${account}`) + + }) + }) +} \ No newline at end of file diff --git a/__tests__/shared/web3.ts b/__tests__/shared/web3.ts new file mode 100644 index 000000000..88aa10dce --- /dev/null +++ b/__tests__/shared/web3.ts @@ -0,0 +1,91 @@ +import { IAgentOptions, IDIDManager, IIdentifier, IKeyManager, IResolver, MinimalImportableKey, TAgent, VerifiableCredential } from '../../packages/core/src' + +type ConfiguredAgent = TAgent + +export default (testContext: { + getAgent: (options?: IAgentOptions) => ConfiguredAgent + setup: (options?: IAgentOptions) => Promise + tearDown: () => Promise +}) => { + describe('web3', () => { + let agent: ConfiguredAgent + let identifier: IIdentifier + let verifiableCredential: VerifiableCredential + + beforeAll(async () => { + await testContext.setup() + agent = testContext.getAgent() + return true + }) + afterAll(testContext.tearDown) + + it('should import ganache did', async () => { + const account = `0x7e5f4552091a69125d5dfcb7b8c2659029395bdf` + const did = `did:ethr:ganache:${account}` + const controllerKeyId = `ganache-${account}` + identifier = await agent.didManagerImport({ + did, + provider: 'did:ethr:ganache', + controllerKeyId, + keys: [{ + kid: controllerKeyId, + type: 'Secp256k1', + kms: 'web3', + privateKeyHex: '', + publicKeyHex: '', + meta: { + account, + provider: 'ganache', + algorithms: [ + 'eth_signMessage', + 'eth_signTypedData', + ] + }, + } as MinimalImportableKey], + }) + }) + + // getting error: The method personal_sign does not exist/is not available + // https://github.com/trufflesuite/ganache/issues/995 + it.skip('should sign a message', async () => { + if (identifier.controllerKeyId) { + const signature = await agent.keyManagerSign({ + data: 'Hello world', + keyRef: identifier.controllerKeyId, + algorithm: 'eth_signMessage' + }) + expect(signature).toBeTruthy() + } + }) + + it('should create verifiable credential with EthereumEip712Signature2021 proof type', async () => { + verifiableCredential = await agent.createVerifiableCredential({ + credential: { + issuer: { id: identifier.did }, + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://example.com/1/2/3'], + type: ['VerifiableCredential', 'Custom'], + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: 'did:web:example.com', + you: 'Rock', + } + }, + proofFormat: 'EthereumEip712Signature2021', + }) + + expect(verifiableCredential).toHaveProperty('proof.proofValue') + expect(verifiableCredential['@context']).toEqual([ + 'https://www.w3.org/2018/credentials/v1', + 'https://example.com/1/2/3', + ]) + expect(verifiableCredential['type']).toEqual(['VerifiableCredential', 'Custom']) + + const hash = await agent.dataStoreSaveVerifiableCredential({ verifiableCredential }) + expect(typeof hash).toEqual('string') + + const verifiableCredential2 = await agent.dataStoreGetVerifiableCredential({ hash }) + expect(verifiableCredential).toEqual(verifiableCredential2) + + }) + }) +} \ No newline at end of file diff --git a/__tests__/utils/ganache-provider.ts b/__tests__/utils/ganache-provider.ts index d340e47c6..4534d5935 100644 --- a/__tests__/utils/ganache-provider.ts +++ b/__tests__/utils/ganache-provider.ts @@ -9,7 +9,7 @@ import ganache from 'ganache' * * This provider can only be used in a single test suite, because of some concurrency issues with ganache. */ -export async function createGanacheProvider(): Promise<{ provider: JsonRpcProvider; registry: string }> { +export async function createGanacheProvider(): Promise<{ provider: Web3Provider; registry: string }> { const provider = new Web3Provider( ganache.provider({ logging: { quiet: true }, diff --git a/packages/credential-eip712/src/agent/CredentialEIP712.ts b/packages/credential-eip712/src/agent/CredentialEIP712.ts index da5ae6e29..a43c9343b 100644 --- a/packages/credential-eip712/src/agent/CredentialEIP712.ts +++ b/packages/credential-eip712/src/agent/CredentialEIP712.ts @@ -104,11 +104,11 @@ export class CredentialIssuerEIP712 implements IAgentPlugin { version: "1", }; - const allTypes = getEthTypesFromInputDoc(credential, "VerifiableCredential"); + const primaryType = "VerifiableCredential" + const allTypes = getEthTypesFromInputDoc(credential, primaryType); const types = {...allTypes} - delete(types.EIP712Domain) - const data = JSON.stringify({domain, types, message}) + const data = JSON.stringify({ domain, types, message, primaryType }) const signature = await context.agent.keyManagerSign({ keyRef, data, algorithm: 'eth_signTypedData' }) @@ -116,7 +116,7 @@ export class CredentialIssuerEIP712 implements IAgentPlugin { credential['proof']['eip712'] = { domain, messageSchema: allTypes, - primaryType: "VerifiableCredential", + primaryType, } return credential as VerifiableCredential; @@ -246,9 +246,9 @@ export class CredentialIssuerEIP712 implements IAgentPlugin { version: "1", }; - const allTypes = getEthTypesFromInputDoc(presentation, "VerifiablePresentation"); + const primaryType = 'VerifiablePresentation' + const allTypes = getEthTypesFromInputDoc(presentation, primaryType); const types = {...allTypes} - delete(types.EIP712Domain) const data = JSON.stringify({domain, types, message}) @@ -260,7 +260,7 @@ export class CredentialIssuerEIP712 implements IAgentPlugin { presentation.proof.eip712 = { domain, messageSchema: allTypes, - primaryType: "VerifiablePresentation", + primaryType, }; return presentation as VerifiablePresentation diff --git a/packages/did-provider-ethr/src/ethr-did-provider.ts b/packages/did-provider-ethr/src/ethr-did-provider.ts index 32dd833b5..152eafcbe 100644 --- a/packages/did-provider-ethr/src/ethr-did-provider.ts +++ b/packages/did-provider-ethr/src/ethr-did-provider.ts @@ -85,14 +85,25 @@ export class EthrDIDProvider extends AbstractIdentifierProvider { if (typeof controllerKey === 'undefined') { throw new Error('invalid_argument: identifier.controllerKeyId is not managed by this agent') } - return new EthrDID({ - identifier: identifier.did, - provider: this.web3Provider, - chainNameOrId: this.network, - rpcUrl: this.rpcUrl, - registry: this.registry, - txSigner: new KmsEthereumSigner(controllerKey, context, this.web3Provider), - }) + if (controllerKey.meta?.algorithms?.includes('eth_signTransaction')) { + return new EthrDID({ + identifier: identifier.did, + provider: this.web3Provider, + chainNameOrId: this.network, + rpcUrl: this.rpcUrl, + registry: this.registry, + txSigner: new KmsEthereumSigner(controllerKey, context, this.web3Provider), + }) + } else { + // Web3Provider should perform signing and sending transaction + return new EthrDID({ + identifier: identifier.did, + provider: this.web3Provider, + chainNameOrId: this.network, + rpcUrl: this.rpcUrl, + registry: this.registry, + }) + } } async addKey( diff --git a/packages/key-manager/package.json b/packages/key-manager/package.json index eaade7ec9..909889804 100644 --- a/packages/key-manager/package.json +++ b/packages/key-manager/package.json @@ -19,6 +19,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@ethersproject/abstract-signer": "^5.6.2", "typescript": "4.7.3" }, "files": [ diff --git a/packages/key-manager/src/index.ts b/packages/key-manager/src/index.ts index 451db007f..7da73e3ba 100644 --- a/packages/key-manager/src/index.ts +++ b/packages/key-manager/src/index.ts @@ -10,3 +10,4 @@ export { AbstractKeyStore } from './abstract-key-store' export { AbstractPrivateKeyStore, ImportablePrivateKey, ManagedPrivateKey } from './abstract-private-key-store' export { AbstractSecretBox } from './abstract-secret-box' export { MemoryKeyStore, MemoryPrivateKeyStore } from './memory-key-store' +export * from './types' \ No newline at end of file diff --git a/packages/key-manager/src/types.ts b/packages/key-manager/src/types.ts new file mode 100644 index 000000000..c28037c04 --- /dev/null +++ b/packages/key-manager/src/types.ts @@ -0,0 +1,8 @@ +import { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' + +export type Eip712Payload = { + domain: TypedDataDomain + types: Record + primaryType: string + message: Record +} \ No newline at end of file diff --git a/packages/kms-local/package.json b/packages/kms-local/package.json index 483c27d12..f37d73a27 100644 --- a/packages/kms-local/package.json +++ b/packages/kms-local/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@ethersproject/abstract-provider": "^5.6.1", - "@ethersproject/abstract-signer": "^5.6.2", "@ethersproject/bytes": "^5.6.1", "@ethersproject/random": "^5.6.1", "@ethersproject/signing-key": "^5.6.2", diff --git a/packages/kms-local/src/key-management-system.ts b/packages/kms-local/src/key-management-system.ts index df4445650..511031a5b 100644 --- a/packages/kms-local/src/key-management-system.ts +++ b/packages/kms-local/src/key-management-system.ts @@ -1,5 +1,5 @@ import { TKeyType, IKey, ManagedKeyInfo, MinimalImportableKey, RequireOnly } from '@veramo/core' -import { AbstractKeyManagementSystem, AbstractPrivateKeyStore } from '@veramo/key-manager' +import { AbstractKeyManagementSystem, AbstractPrivateKeyStore, Eip712Payload } from '@veramo/key-manager' import { ManagedPrivateKey } from '@veramo/key-manager' import { EdDSASigner, ES256KSigner } from 'did-jwt' @@ -14,7 +14,6 @@ import { generateKeyPairFromSeed as generateEncryptionKeyPairFromSeed, sharedKey, } from '@stablelib/x25519' -import { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' import { TransactionRequest } from '@ethersproject/abstract-provider' import { toUtf8String } from '@ethersproject/strings' import { parse } from '@ethersproject/transactions' @@ -198,6 +197,7 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { `invalid_arguments: Cannot sign typed data. 'domain', 'types', and 'message' must be provided`, ) } + delete(msgTypes.EIP712Domain) const wallet = new Wallet(privateKeyHex) const signature = await wallet._signTypedData(msgDomain, msgTypes, msg) @@ -312,10 +312,3 @@ export class KeyManagementSystem extends AbstractKeyManagementSystem { return key as ManagedKeyInfo } } - -type Eip712Payload = { - domain: TypedDataDomain - types: Record - primaryType: string - message: Record -} diff --git a/packages/kms-web3/LICENSE b/packages/kms-web3/LICENSE new file mode 100644 index 000000000..fd815d7f8 --- /dev/null +++ b/packages/kms-web3/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/kms-web3/README.md b/packages/kms-web3/README.md new file mode 100644 index 000000000..4287037d7 --- /dev/null +++ b/packages/kms-web3/README.md @@ -0,0 +1,8 @@ +# Veramo Web3 KMS + +A Veramo KMS implementation that provides secp256k1 crypto backed by web3 wallets. + +This module provides an implementation +of [`AbstractKeyManagementSystem`](../key-manager/src/abstract-key-management-system.ts#L6) that can be used by the +[`@veramo/key-manager`](../key-manager) plugin to provide Secp256k1 crypto functionality to a +Veramo agent. diff --git a/packages/kms-web3/package.json b/packages/kms-web3/package.json new file mode 100644 index 000000000..9aee45527 --- /dev/null +++ b/packages/kms-web3/package.json @@ -0,0 +1,41 @@ +{ + "name": "@veramo/kms-web3", + "description": "Veramo KMS implementation backed by web3 wallets", + "version": "3.1.4", + "main": "build/index.js", + "types": "build/index.d.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@ethersproject/providers": "^5.6.8", + "@ethersproject/strings": "5.6.1", + "@ethersproject/transactions": "5.6.2", + "@veramo/core": "^3.1.4", + "@veramo/key-manager": "^3.1.4", + "debug": "^4.3.3" + }, + "devDependencies": { + "@types/debug": "4.1.7", + "typescript": "4.7.3" + }, + "files": [ + "build/**/*", + "src/**/*", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:uport-project/veramo.git", + "author": "Simonas Karuzas ", + "contributors": [ + { + "name": "Mircea Nistor", + "email": "mircea.nistor@mesh.xyz" + } + ], + "license": "Apache-2.0", + "keywords": [] +} diff --git a/packages/kms-web3/src/index.ts b/packages/kms-web3/src/index.ts new file mode 100644 index 000000000..edffafb01 --- /dev/null +++ b/packages/kms-web3/src/index.ts @@ -0,0 +1,9 @@ +/** + * Provides a web3 wallet backed + * {@link @veramo/kms-web3#Web3KeyManagementSystem | key management system } + * for the {@link @veramo/key-manager#KeyManager} + * + * @packageDocumentation + */ +export { Web3KeyManagementSystem } from './web3-key-management-system' + diff --git a/packages/kms-web3/src/web3-key-management-system.ts b/packages/kms-web3/src/web3-key-management-system.ts new file mode 100644 index 000000000..1524f4219 --- /dev/null +++ b/packages/kms-web3/src/web3-key-management-system.ts @@ -0,0 +1,139 @@ +import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers' +import { + TKeyType, + IKey, + ManagedKeyInfo, + MinimalImportableKey, +} from '@veramo/core' +import { AbstractKeyManagementSystem, Eip712Payload } from '@veramo/key-manager' +import { toUtf8String } from '@ethersproject/strings' + +export class Web3KeyManagementSystem extends AbstractKeyManagementSystem { + /** + * + * @param providers - the key can be any unique name. Example { metamask: metamaskProvider, walletConnect: walletConnectProvider } + */ + constructor(private providers: Record) { + super() + } + + createKey({ type }: { type: TKeyType }): Promise { + throw Error('not_supported: Web3KeyManagementSystem cannot create new keys') + } + + async importKey( + args: Omit, + ): Promise { + // throw Error('Not implemented') + return args as any as ManagedKeyInfo + } + + async listKeys(): Promise { + throw Error('not_implemented: Web3KeyManagementSystem listKeys') + } + + async sharedSecret(args: { + myKeyRef: Pick + theirKey: Pick + }): Promise { + throw Error('not_implemented: Web3KeyManagementSystem sharedSecret') + } + + async deleteKey(args: { kid: string }) { + // this kms doesn't need to delete keys + return true + } + + // keyRef should be in this format '{providerName-account} + // example: 'metamask-0xf3beac30c498d9e26865f34fcaa57dbb935b0d74' + private getAccountAndSignerByKeyRef(keyRef: Pick): {account: string, signer: JsonRpcSigner } { + const [ providerName, account ] = keyRef.kid.split('-') + if (!this.providers[providerName]) { + throw Error(`not_available: provider ${providerName}`) + } + const signer = this.providers[providerName].getSigner(account) + return { account, signer } + } + + async sign({ + keyRef, + algorithm, + data, + }: { + keyRef: Pick + algorithm?: string + data: Uint8Array + }): Promise { + + if (algorithm) { + if (algorithm === 'eth_signMessage') { + return await this.eth_signMessage(keyRef, data) + } else if ( + ['eth_signTypedData', 'EthereumEip712Signature2021'].includes(algorithm) + ) { + return await this.eth_signTypedData(keyRef, data) + } + } + + throw Error(`not_supported: Cannot sign ${algorithm} `) + } + + /** + * @returns a `0x` prefixed hex string representing the signed EIP712 data + */ + private async eth_signTypedData(keyRef: Pick, data: Uint8Array) { + let msg, msgDomain, msgTypes, msgPrimaryType + const serializedData = toUtf8String(data) + try { + const jsonData = JSON.parse(serializedData) as Eip712Payload + if ( + typeof jsonData.domain === 'object' && + typeof jsonData.types === 'object' + ) { + const { domain, types, message, primaryType } = jsonData + msg = message + msgDomain = domain + msgTypes = types + msgPrimaryType = primaryType + } else { + // next check will throw since the data couldn't be parsed + } + } catch (e) { + // next check will throw since the data couldn't be parsed + } + if ( + typeof msgDomain !== 'object' || + typeof msgTypes !== 'object' || + typeof msg !== 'object' + ) { + throw Error( + `invalid_arguments: Cannot sign typed data. 'domain', 'types', and 'message' must be provided`, + ) + } + const { signer, account } = this.getAccountAndSignerByKeyRef(keyRef) + + const signature = await signer.provider.send('eth_signTypedData_v4', [ + account, + { + domain: msgDomain, + types: msgTypes, + primaryType: msgPrimaryType, + message: msg + } + ]) + // ._signTypedData(msgDomain, msgTypes, msg) + + return signature + } + + /** + * @returns a `0x` prefixed hex string representing the signed message + */ + private async eth_signMessage(keyRef: Pick, rawMessageBytes: Uint8Array) { + const { signer } = this.getAccountAndSignerByKeyRef(keyRef) + const signature = await signer.signMessage(rawMessageBytes) + // HEX encoded string, 0x prefixed + return signature + } + +} diff --git a/packages/kms-web3/tsconfig.json b/packages/kms-web3/tsconfig.json new file mode 100644 index 000000000..daae1afc0 --- /dev/null +++ b/packages/kms-web3/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "declarationDir": "build" + }, + "references": [{ "path": "../core" }, { "path": "../key-manager" }] +} diff --git a/packages/test-react-app/headless-tests/browserAgent.browser-test.ts b/packages/test-react-app/headless-tests/browserAgent.browser-test.ts index a9035e9bc..b0644a6dd 100644 --- a/packages/test-react-app/headless-tests/browserAgent.browser-test.ts +++ b/packages/test-react-app/headless-tests/browserAgent.browser-test.ts @@ -11,6 +11,7 @@ import saveClaims from '../../../__tests__/shared/saveClaims' import documentationExamples from '../../../__tests__/shared/documentationExamples' import didCommPacking from '../../../__tests__/shared/didCommPacking' import messageHandler from '../../../__tests__/shared/messageHandler' +import utils from '../../../__tests__/shared/utils' jest.setTimeout(3 * 60 * 1000) @@ -27,6 +28,7 @@ describe('Browser integration tests', () => { keyManager(testContext) didManager(testContext) messageHandler(testContext) + utils(testContext) didCommPacking(testContext) }) diff --git a/packages/test-react-app/src/veramo/setup.ts b/packages/test-react-app/src/veramo/setup.ts index 80b627b53..c80657977 100644 --- a/packages/test-react-app/src/veramo/setup.ts +++ b/packages/test-react-app/src/veramo/setup.ts @@ -30,6 +30,7 @@ import { getDidKeyResolver, KeyDIDProvider } from '@veramo/did-provider-key' import { DIDComm, DIDCommMessageHandler, IDIDComm } from '@veramo/did-comm' import { ISelectiveDisclosure, SdrMessageHandler, SelectiveDisclosure } from '@veramo/selective-disclosure' import { KeyManagementSystem, SecretBox } from '@veramo/kms-local' +import { Web3KeyManagementSystem } from '@veramo/kms-web3' import { EthrDIDProvider } from '@veramo/did-provider-ethr' import { WebDIDProvider } from '@veramo/did-provider-web' import { DataStoreJson, DIDStoreJson, KeyStoreJson, PrivateKeyStoreJson } from "@veramo/data-store-json"; @@ -71,6 +72,7 @@ export function getAgent(options?: IAgentOptions): TAgent { store: new KeyStoreJson(memoryJsonStore), kms: { local: new KeyManagementSystem(new PrivateKeyStoreJson(memoryJsonStore, new SecretBox(DB_SECRET_KEY))), + web3: new Web3KeyManagementSystem({}) }, }), new DIDManager({ diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 2d5fca6fc..82b34e0b1 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -18,6 +18,7 @@ { "path": "did-resolver" }, { "path": "key-manager" }, { "path": "kms-local" }, + { "path": "kms-web3" }, { "path": "message-handler" }, { "path": "remote-client" }, { "path": "remote-server" }, diff --git a/packages/utils/src/did-utils.ts b/packages/utils/src/did-utils.ts index b7d9e187a..8f4212864 100644 --- a/packages/utils/src/did-utils.ts +++ b/packages/utils/src/did-utils.ts @@ -56,10 +56,12 @@ export function compressIdentifierSecp256k1Keys(identifier: IIdentifier): IKey[] return identifier.keys .map((key) => { if (key.type === 'Secp256k1') { - const publicBytes = u8a.fromString(key.publicKeyHex, 'base16') - key.publicKeyHex = computePublicKey(publicBytes, true).substring(2) - key.meta = { ...key.meta } - key.meta.ethereumAddress = computeAddress('0x' + key.publicKeyHex) + if (key.publicKeyHex) { + const publicBytes = u8a.fromString(key.publicKeyHex, 'base16') + key.publicKeyHex = computePublicKey(publicBytes, true).substring(2) + key.meta = { ...key.meta } + key.meta.ethereumAddress = computeAddress('0x' + key.publicKeyHex) + } } return key }) @@ -86,6 +88,9 @@ function compareBlockchainAccountId( return false } let vmEthAddr = getEthereumAddress(verificationMethod) + if (localKey.meta?.account) { + return vmEthAddr === localKey.meta?.account + } const computedAddr = computeAddress('0x' + localKey.publicKeyHex).toLowerCase() return computedAddr === vmEthAddr } @@ -153,7 +158,6 @@ export async function mapIdentifierKeysToDoc( context: IAgentContext, ): Promise<_ExtendedIKey[]> { const didDocument = await resolveDidOrThrow(identifier.did, context) - // dereference all key agreement keys from DID document and normalize const documentKeys: _NormalizedVerificationMethod[] = await dereferenceDidKeys( didDocument, diff --git a/yarn.lock b/yarn.lock index 95c0233b8..d6cc8304b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1927,7 +1927,7 @@ elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/strings@^5.6.1": +"@ethersproject/strings@5.6.1", "@ethersproject/strings@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.1.tgz#dbc1b7f901db822b5cafd4ebf01ca93c373f8952" integrity sha512-2X1Lgk6Jyfg26MUnsHiT456U9ijxKUybz8IM1Vih+NJxYtXhmvKBcHOmvGqpFSVJ0nQ4ZCoIViR8XlRw1v/+Cw==