diff --git a/packages/sdk-js/src/DidHelpers/index.ts b/packages/sdk-js/src/DidHelpers/index.ts index 5d978add1..d10bf9c2a 100644 --- a/packages/sdk-js/src/DidHelpers/index.ts +++ b/packages/sdk-js/src/DidHelpers/index.ts @@ -13,8 +13,11 @@ import type { SharedArguments } from './interfaces.js' export { createDid } from './createDid.js' export { addService, removeService } from './service.js' -export { setVerificationMethod } from './setVerificationMethod.js' export { transact } from './transact.js' +export { + removeVerificationMethod, + setVerificationMethod, +} from './verificationMethod.js' export { claimWeb3Name, releaseWeb3Name } from './w3names.js' /** diff --git a/packages/sdk-js/src/DidHelpers/interfaces.ts b/packages/sdk-js/src/DidHelpers/interfaces.ts index 42d7cbcb9..43cf3fb78 100644 --- a/packages/sdk-js/src/DidHelpers/interfaces.ts +++ b/packages/sdk-js/src/DidHelpers/interfaces.ts @@ -114,7 +114,12 @@ export type SharedArguments = { submitter: KeyringPair | Blockchain.TransactionSigner } +type PublicKeyAndType = { + publicKey: Uint8Array + type: KeyringPair['type'] | 'x25519' +} + export type AcceptedPublicKeyEncodings = | KeyMultibaseEncoded | { publicKeyMultibase: KeyMultibaseEncoded } - | Pick // interface allows KeyringPair too + | PublicKeyAndType // interface allows KeyringPair too diff --git a/packages/sdk-js/src/DidHelpers/service.ts b/packages/sdk-js/src/DidHelpers/service.ts index 50beabab1..b2aee90dd 100644 --- a/packages/sdk-js/src/DidHelpers/service.ts +++ b/packages/sdk-js/src/DidHelpers/service.ts @@ -6,16 +6,17 @@ */ import { serviceToChain, urlFragmentToChain } from '@kiltprotocol/did' -import { DidUrl, Service, UriFragment } from '@kiltprotocol/types' +import type { DidUrl, Service, UriFragment } from '@kiltprotocol/types' import { SharedArguments, TransactionHandlers } from './interfaces.js' import { transact } from './transact.js' /** * Adds a service to the DID Document. * + * @param options Any {@link SharedArguments} and additional parameters. * @param options.service The service entry to add to the document. * If the service id is relative (begins with #) it is automatically expanded with the DID taken from didDocument.id. - * @param options + * @returns A set of {@link TransactionHandlers}. */ export function addService( options: SharedArguments & { @@ -35,9 +36,10 @@ export function addService( /** * Removes a service from the DID Document. * + * @param options Any {@link SharedArguments} and additional parameters. * @param options.id The id of the service to remove from the document. * If the service id is relative (begins with #) it is automatically expanded with the DID taken from didDocument.id. - * @param options + * @returns A set of {@link TransactionHandlers}. */ export function removeService( options: SharedArguments & { diff --git a/packages/sdk-js/src/DidHelpers/verificationMethod.spec.ts b/packages/sdk-js/src/DidHelpers/verificationMethod.spec.ts new file mode 100644 index 000000000..5c5e2c130 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/verificationMethod.spec.ts @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidDocument, KiltKeyringPair } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, +} from '../../../../tests/testUtils/index.js' +import { ConfigService } from '../index.js' +import { + removeVerificationMethod, + setVerificationMethod, +} from './verificationMethod.js' +import { transact } from './transact.js' + +jest.mock('./transact.js') + +const mockedTransact = jest.mocked(transact) +const mockedApi = ApiMocks.createAugmentedApi() + +let didDocument: DidDocument +let keypair: KiltKeyringPair +beforeAll(async () => { + ConfigService.set({ api: mockedApi }) + + keypair = Crypto.makeKeypairFromUri('//Alice') + const { id, verificationMethod, authentication } = + await createLocalDemoFullDidFromKeypair(keypair, { + verificationRelationships: new Set(['assertionMethod']), + }) + didDocument = { + id, + authentication, + assertionMethod: authentication, + verificationMethod: verificationMethod?.filter( + (vm) => vm.id === authentication![0] + ), + } +}) + +describe('signing keys', () => { + it('creates a set VM tx', async () => { + setVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + publicKey: keypair, + relationship: 'assertionMethod', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + call: expect.any(Object), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect(mockedTransact.mock.lastCall?.[0].call.toHuman()).toMatchObject({ + method: { + section: 'did', + method: 'setAttestationKey', + args: { new_key: { Ed25519: Crypto.u8aToHex(keypair.publicKey) } }, + }, + }) + }) + + it('creates a remove VM tx', async () => { + didDocument.assertionMethod = didDocument.authentication + removeVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + verificationMethodId: didDocument.assertionMethod![0], + relationship: 'assertionMethod', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + call: expect.any(Object), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect(mockedTransact.mock.lastCall?.[0].call.toHuman()).toMatchObject({ + method: { + section: 'did', + method: 'removeAttestationKey', + }, + }) + }) +}) + +describe('key agreement keys', () => { + it('creates a set VM tx for the first key agreement', async () => { + setVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + publicKey: { publicKey: keypair.publicKey, type: 'x25519' } as any, + relationship: 'keyAgreement', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + call: expect.any(Object), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect(mockedTransact.mock.lastCall?.[0].call.toHuman()).toMatchObject({ + method: { + section: 'utility', + method: 'batchAll', + args: { + calls: [ + { + section: 'did', + method: 'addKeyAgreementKey', + args: { new_key: { X25519: Crypto.u8aToHex(keypair.publicKey) } }, + }, + ], + }, + }, + }) + }) + + it('creates a set VM tx for the second key agreement', async () => { + didDocument.keyAgreement = [ + `${didDocument.id}#${Crypto.hashStr('keyAgreement1')}`, + ] + setVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + publicKey: { publicKey: keypair.publicKey, type: 'x25519' } as any, + relationship: 'keyAgreement', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + call: expect.any(Object), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect(mockedTransact.mock.lastCall?.[0].call.toHuman()).toMatchObject({ + method: { + section: 'utility', + method: 'batchAll', + args: { + calls: [ + { + section: 'did', + method: 'removeKeyAgreementKey', + args: { key_id: expect.stringContaining('0x') }, + }, + { + section: 'did', + method: 'addKeyAgreementKey', + args: { new_key: { X25519: Crypto.u8aToHex(keypair.publicKey) } }, + }, + ], + }, + }, + }) + }) + + it('creates a remove VM tx', async () => { + removeVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + verificationMethodId: didDocument.keyAgreement![0], + relationship: 'keyAgreement', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + call: expect.any(Object), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect(mockedTransact.mock.lastCall?.[0].call.toHuman()).toMatchObject({ + method: { + section: 'did', + method: 'removeKeyAgreementKey', + }, + }) + }) +}) diff --git a/packages/sdk-js/src/DidHelpers/setVerificationMethod.ts b/packages/sdk-js/src/DidHelpers/verificationMethod.ts similarity index 51% rename from packages/sdk-js/src/DidHelpers/setVerificationMethod.ts rename to packages/sdk-js/src/DidHelpers/verificationMethod.ts index bd5d05706..21ec5c3d0 100644 --- a/packages/sdk-js/src/DidHelpers/setVerificationMethod.ts +++ b/packages/sdk-js/src/DidHelpers/verificationMethod.ts @@ -12,6 +12,7 @@ import { urlFragmentToChain, } from '@kiltprotocol/did' import type { + DidUrl, SubmittableExtrinsic, VerificationRelationship, } from '@kiltprotocol/types' @@ -27,9 +28,11 @@ import { transact } from './transact.js' /** * Replaces all existing verification methods for the selected `relationship` with `publicKey`. * + * @param options Any {@link SharedArguments} and additional parameters. * @param options.publicKey The public key to be used for this verification method. * @param options.relationship The relationship for which this verification method shall be useable. - * @param options + * + * @returns A set of {@link TransactionHandlers}. */ export function setVerificationMethod( options: SharedArguments & { @@ -41,7 +44,6 @@ export function setVerificationMethod( let didKeyUpdateTx if (options.relationship === 'keyAgreement') { - // TODO: check if types of keys are valid? const didEncryptionKey: NewDidEncryptionKey = { publicKey: pk.publicKey, type: pk.keyType as any, @@ -91,3 +93,79 @@ export function setVerificationMethod( expectedEvents: [{ section: 'did', method: 'DidUpdated' }], }) } + +/** + * Removes the verification method for the selected `verificationMethodId` and `relationship`. + * + * Note: authentication verification method can not be removed. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.relationship The relationship for which this verification method shall be removed. + * @param options.verificationMethodId The id of the verification method that should be removed. + * + * @returns A set of {@link TransactionHandlers}. + */ +export function removeVerificationMethod( + options: SharedArguments & { + verificationMethodId: DidUrl + relationship: Omit + } +): TransactionHandlers { + let didKeyUpdateTx + switch (options.relationship) { + case 'authentication': { + throw new Error('authentication verification methods can not be removed') + } + case 'capabilityDelegation': { + if ( + options.didDocument.capabilityDelegation?.includes( + options.verificationMethodId + ) + ) { + didKeyUpdateTx = options.api.tx.did.removeDelegationKey() + } else { + throw new Error( + 'the specified capabilityDelegation method does not exist in the DID Document' + ) + } + break + } + case 'keyAgreement': { + if ( + options.didDocument.keyAgreement?.includes(options.verificationMethodId) + ) { + didKeyUpdateTx = options.api.tx.did.removeKeyAgreementKey( + urlFragmentToChain(options.verificationMethodId) + ) + } else { + throw new Error( + 'the specified keyAgreement key does not exist in the DID Document' + ) + } + break + } + case 'assertionMethod': { + if ( + options.didDocument.assertionMethod?.includes( + options.verificationMethodId + ) + ) { + didKeyUpdateTx = options.api.tx.did.removeAttestationKey() + } else { + throw new Error( + 'the specified assertionMethod does not exist in the DID Document' + ) + } + break + } + default: { + throw new Error('the specified method relationship is not supported') + } + } + + return transact({ + ...options, + call: didKeyUpdateTx, + expectedEvents: [{ section: 'did', method: 'DidUpdated' }], + }) +} diff --git a/tests/integration/didHelpers.spec.ts b/tests/integration/didHelpers.spec.ts index b5c2a5436..8bf87f091 100644 --- a/tests/integration/didHelpers.spec.ts +++ b/tests/integration/didHelpers.spec.ts @@ -166,6 +166,140 @@ describe('services', () => { }, 30_000) }) +describe('verification methods', () => { + let keypair: KeyringPair + let didDocument: DidDocument + beforeAll(async () => { + keypair = Crypto.makeKeypairFromUri('//Vms') + const result = await DidHelpers.createDid({ + api, + signers: [keypair], + submitter: paymentAccount, + fromPublicKey: keypair, + }).submit() + didDocument = result.asConfirmed.didDocument + }) + + it('sets an assertion method', async () => { + expect(didDocument).not.toHaveProperty('assertionMethod') + const result = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: keypair, + relationship: 'assertionMethod', + }).submit() + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).toHaveProperty( + 'assertionMethod', + didDocument.authentication + ) + + const result2 = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: { publicKey: new Uint8Array(32).fill(1), type: 'ed25519' }, + relationship: 'assertionMethod', + }).submit() + + expect(result2.status).toStrictEqual('confirmed') + didDocument = result2.asConfirmed.didDocument + expect(didDocument.assertionMethod).toHaveLength(1) + expect(didDocument.assertionMethod![0]).not.toEqual( + didDocument.authentication![0] + ) + }, 60_000) + + it('sets a key agreement method', async () => { + expect(didDocument).not.toHaveProperty('keyAgreement') + const result = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: { publicKey: new Uint8Array(32).fill(0), type: 'x25519' }, + relationship: 'keyAgreement', + }).submit() + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).toHaveProperty('keyAgreement', expect.any(Array)) + expect(didDocument.keyAgreement).toHaveLength(1) + + const [oldKey] = didDocument.keyAgreement! + + const result2 = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: { publicKey: new Uint8Array(32).fill(1), type: 'x25519' }, + relationship: 'keyAgreement', + }).submit() + + expect(result2.status).toStrictEqual('confirmed') + didDocument = result2.asConfirmed.didDocument + expect(didDocument.keyAgreement).toHaveLength(1) + expect(didDocument.keyAgreement![0]).not.toEqual(oldKey) + }, 60_000) + + it('removes an assertion method', async () => { + expect(didDocument.assertionMethod).toHaveLength(1) + + const result = await DidHelpers.removeVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + verificationMethodId: didDocument.assertionMethod![0], + relationship: 'assertionMethod', + }).submit() + + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).not.toHaveProperty('assertionMethod') + }, 30_000) + + it('removes a key agreement method', async () => { + expect(didDocument.keyAgreement).toHaveLength(1) + + const result = await DidHelpers.removeVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + verificationMethodId: didDocument.keyAgreement![0], + relationship: 'keyAgreement', + }).submit() + + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).not.toHaveProperty('keyAgreement') + }, 30_000) + + it('fails to remove authentication method', async () => { + await expect( + Promise.resolve() + .then(() => + DidHelpers.removeVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + verificationMethodId: didDocument.authentication![0], + relationship: 'authentication', + }).submit() + ) + .then(({ asConfirmed }) => asConfirmed) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"authentication verification methods can not be removed"` + ) + }, 30_000) +}) + afterAll(async () => { await disconnect() })