From c5e24dae0cea0408305cfc5cc7ab7a7d4057f39f Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Tue, 5 Dec 2023 10:44:22 +0100 Subject: [PATCH 01/12] feat: basic identity class implementation --- packages/sdk-js/src/Identity.ts | 239 ++++++++++++++++++++++++++++++++ packages/sdk-js/src/index.ts | 2 + 2 files changed, 241 insertions(+) create mode 100644 packages/sdk-js/src/Identity.ts diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts new file mode 100644 index 000000000..cfd24b044 --- /dev/null +++ b/packages/sdk-js/src/Identity.ts @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2018-2023, 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 { u8aEq } from '@polkadot/util' +import type { Keypair } from '@polkadot/util-crypto/types' + +import { multibaseKeyToDidKey, resolve } from '@kiltprotocol/did' +import type { + Did, + DidDocument, + DidUrl, + KeyringPair, + SignerInterface, + UriFragment, +} from '@kiltprotocol/types' +import { SDKErrors, Signers } from '@kiltprotocol/utils' + +/** + * An Identity represents a DID and signing keys associated with it. + */ +export interface Identity { + did: Did + didDocument: DidDocument + signers: SignerInterface[] + /** + * Adds one or more signer interfaces to the `signers`. + * + * @param signers Signer Interface(s). + * @returns The (in-place) modified Identity object for chaining. + */ + addSigner: (...signers: SignerInterface[]) => Promise + /** + * Convenience function similar to {@link addSigner}, but creates signers for all matching known algorithms from a keypair. + * + * @param keypairs + * @returns The (in-place) modified Identity object for chaining. + */ + addKeypair: (...keypairs: Array) => Promise + /** + * Helps filtering and selecting appropriate signers related to the DID's verification methods. + * Only returns signers that match a currently active verification method. + * + * @param filterBy Additional selection criteria, including which VM the signer relates to, which algorithm it uses, or which relationship it has to the DID. + * @returns A (potentially empty) array of signers, wrapped in a Promise because {@link Signers.selectSigners} is async. + */ + getSigners: (filterBy: { + verificationMethod?: DidUrl | UriFragment + verificationRelationship?: string + algorithm?: string + }) => Promise + /** + * Same as {@link getSigners} but pre-selects a signer matching the criteria and throws if none are found. + * + * @param filterBy See {@link getSigners}. + * @returns A Promise of a signer, which rejects if none match the selection criteria. + */ + getSigner: (filterBy: { + verificationMethod?: DidUrl | UriFragment + verificationRelationship?: string + algorithm?: string + }) => Promise + /** + * Refreshes the didDocument and/or purges signers that are not linked to a VM currently referenced in the document. + * + * @param opts Controls whether you want to only reload the document, or only purge signers. Defaults to doing both. + * @returns The (in-place) modified Identity object for chaining. + */ + update: (opts: { + skipResolution?: boolean + purgeSigners?: boolean + }) => Promise +} + +async function loadDidDocument( + did: Did, + resolver: typeof resolve +): Promise { + const { didDocument } = await resolver(did) + if (!didDocument) { + throw new SDKErrors.DidNotFoundError(`failed to resolve ${did}`) + } + return didDocument +} + +class IdentityClass implements Identity { + public did: Did + public resolver: typeof resolve + public didDocument: DidDocument + private didSigners: SignerInterface[] + + constructor({ + did, + didDocument, + signers, + resolver = resolve, + }: { + did: Did + didDocument: DidDocument + signers?: SignerInterface[] + resolver?: typeof resolve + }) { + this.did = did + this.didDocument = didDocument + this.didSigners = signers ? [...signers] : [] + this.resolver = resolver + } + + get signers(): SignerInterface[] { + return [...this.didSigners] + } + + public async update({ + skipResolution = false, + skipPurgeSigners = false, + }: { + skipResolution?: boolean + skipPurgeSigners?: boolean + } = {}): Promise { + if (skipResolution !== true) { + this.didDocument = await loadDidDocument(this.did, this.resolver) + } + if (skipPurgeSigners !== true) { + this.didSigners = await this.getSigners() + } + return this + } + + public async addSigner( + ...signers: SignerInterface[] + ): Promise { + this.didSigners.push(...signers) + return this + } + + public async addKeypair( + ...keypairs: Array + ): Promise { + const didKeys = this.didDocument.verificationMethod?.map( + ({ publicKeyMultibase, id }) => ({ + ...multibaseKeyToDidKey(publicKeyMultibase), + id, + }) + ) + if (didKeys && didKeys.length !== 0) { + await Promise.all( + keypairs.map(async (keypair) => { + const thisType = 'type' in keypair ? keypair.type : undefined + const matchingKey = didKeys?.find(({ publicKey, keyType }) => { + if (thisType && thisType !== keyType) { + return false + } + return u8aEq(publicKey, keypair.publicKey) + }) + if (matchingKey) { + const id = matchingKey.id.startsWith('#') + ? this.did + matchingKey.id + : matchingKey.id + await this.addSigner( + ...(await Signers.getSignersForKeypair({ + keypair, + id, + type: matchingKey.keyType, + })) + ) + } + }) + ) + } + return this + } + + public async getSigners({ + verificationMethod, + verificationRelationship, + algorithm, + }: { + verificationMethod?: DidUrl | UriFragment + verificationRelationship?: string + algorithm?: string + } = {}): Promise { + const selectors = [ + Signers.select.byDid(this.didDocument, { verificationRelationship }), + ] + if (algorithm) { + selectors.push(Signers.select.byAlgorithm([algorithm])) + } + if (verificationMethod) { + selectors.push(Signers.select.bySignerId([verificationMethod])) + } + return Signers.selectSigners(this.didSigners) + } + + public async getSigner( + criteria: { + verificationMethod?: DidUrl | UriFragment + verificationRelationship?: string + algorithm?: string + } = {} + ): Promise { + const [signer] = await this.getSigners(criteria) + if (typeof signer === 'undefined') { + throw new SDKErrors.NoSuitableSignerError(undefined, { + signerRequirements: criteria, + availableSigners: this.didSigners, + }) + } + return signer + } +} + +export async function makeIdentity({ + did, + didDocument, + keypairs, + signers, + resolver = resolve, +}: { + did: Did + didDocument?: DidDocument + signers?: SignerInterface[] + keypairs?: Array + resolver?: typeof resolve +}): Promise { + const identity = new IdentityClass({ + did, + didDocument: didDocument ?? (await loadDidDocument(did, resolver)), + signers, + resolver, + }) + await identity.update({ skipResolution: true }) + if (keypairs && keypairs.length !== 0) { + await identity.addKeypair(...keypairs) + } + return identity +} diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index c1ef1bff2..7f2f05e23 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -19,6 +19,7 @@ import { Blockchain, } from '@kiltprotocol/chain-helpers' import { resolver as DidResolver } from '@kiltprotocol/did' +import { makeIdentity } from './Identity.js' const { signAndSubmitTx } = Blockchain // TODO: maybe we don't even need that if we have the identity class const { signerFromKeypair } = Signers @@ -34,4 +35,5 @@ export { signerFromKeypair, signAndSubmitTx, ConfigService, + makeIdentity, } From 344c12b2ce79606944f003590a4c244fecf91246 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Tue, 5 Dec 2023 10:46:48 +0100 Subject: [PATCH 02/12] feat: submission strategy & tests --- packages/sdk-js/src/Identity.ts | 22 +++++++ packages/sdk-js/src/index.ts | 3 +- tests/bundle/bundle-test.ts | 111 +++++++++++++++++--------------- 3 files changed, 84 insertions(+), 52 deletions(-) diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index cfd24b044..eaf332acf 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -5,6 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ +import type { GenericExtrinsic } from '@polkadot/types' import { u8aEq } from '@polkadot/util' import type { Keypair } from '@polkadot/util-crypto/types' @@ -14,10 +15,12 @@ import type { DidDocument, DidUrl, KeyringPair, + KiltAddress, SignerInterface, UriFragment, } from '@kiltprotocol/types' import { SDKErrors, Signers } from '@kiltprotocol/utils' +import type { TransactionResult } from '@kiltprotocol/credentials/src/V1/KiltAttestationProofV1' /** * An Identity represents a DID and signing keys associated with it. @@ -73,6 +76,25 @@ export interface Identity { skipResolution?: boolean purgeSigners?: boolean }) => Promise + /** + * The account that acts as the submitter account. + */ + submitterAccount?: KiltAddress + /** + * Uses a verification method related signer to DID-authorize a call to be executed on-chain with the identity's DID as the origin. + * + * @param tx The call to be authorized. + * @returns The authorized (signed) call. + */ + authorizeTx?: (tx: GenericExtrinsic) => Promise + /** + * Takes care of submitting the transaction to node in the network and listening for execution results. + * This can consist of signing the tx using the signer associated with submitterAccount and interacting directly with a node, or can use an external service for this. + * + * @param tx The extrinsic ready to be (signed and) submitted. + * @returns A promise resolving to an object indicating the block of inclusion, or rejecting if the transaction failed to be included or execute correctly. + */ + submitTx?: (tx: GenericExtrinsic) => Promise } async function loadDidDocument( diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index 7f2f05e23..c91e567b9 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -19,7 +19,7 @@ import { Blockchain, } from '@kiltprotocol/chain-helpers' import { resolver as DidResolver } from '@kiltprotocol/did' -import { makeIdentity } from './Identity.js' +import { makeIdentity, Identity } from './Identity.js' const { signAndSubmitTx } = Blockchain // TODO: maybe we don't even need that if we have the identity class const { signerFromKeypair } = Signers @@ -36,4 +36,5 @@ export { signAndSubmitTx, ConfigService, makeIdentity, + Identity, } diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 06e28b208..f87fab0d9 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,6 +7,7 @@ /// +import type { Identity } from '@kiltprotocol/sdk-js' import type { ApiPromise } from '@polkadot/api' import type { Did, @@ -27,6 +28,7 @@ const { DidResolver, signAndSubmitTx, signerFromKeypair, + makeIdentity, } = kilt async function authorizeTx( @@ -57,7 +59,7 @@ async function authorizeTx( return authorized } -async function createFullDid( +async function createFullDidIdentity( payer: SignerInterface<'Ed25519' | 'Sr25519', KiltAddress>, keypair: { publicKey: Uint8Array; secretKey: Uint8Array } ) { @@ -105,10 +107,17 @@ async function createFullDid( throw new Error(`failed to create did for account ${address}`) } + const identity = (await makeIdentity({ + did: didDocument.id, + didDocument, + keypairs: [keypair], + })) as Identity + return { didDocument, getSigners, address, + identity, } } @@ -136,36 +145,29 @@ async function runAll() { console.log('faucet signer created') - const { didDocument: alice, getSigners: aliceSign } = await createFullDid( - payerSigner, - { - publicKey: new Uint8Array([ - 136, 220, 52, 23, 213, 5, 142, 196, 180, 80, 62, 12, 18, 234, 26, 10, - 137, 190, 32, 15, 233, 137, 34, 66, 61, 67, 52, 1, 79, 166, 176, 238, - ]), - secretKey: new Uint8Array([ - 171, 248, 229, 189, 190, 48, 198, 86, 86, 192, 163, 203, 209, 129, 255, - 138, 86, 41, 74, 105, 223, 237, 210, 121, 130, 170, 206, 74, 118, 144, - 145, 21, - ]), - } - ) + const { identity: alice } = await createFullDidIdentity(payerSigner, { + publicKey: new Uint8Array([ + 136, 220, 52, 23, 213, 5, 142, 196, 180, 80, 62, 12, 18, 234, 26, 10, 137, + 190, 32, 15, 233, 137, 34, 66, 61, 67, 52, 1, 79, 166, 176, 238, + ]), + secretKey: new Uint8Array([ + 171, 248, 229, 189, 190, 48, 198, 86, 86, 192, 163, 203, 209, 129, 255, + 138, 86, 41, 74, 105, 223, 237, 210, 121, 130, 170, 206, 74, 118, 144, + 145, 21, + ]), + }) console.log('alice setup done') - const { didDocument: bob, getSigners: bobSign } = await createFullDid( - payerSigner, - { - publicKey: new Uint8Array([ - 209, 124, 45, 120, 35, 235, 242, 96, 253, 19, 143, 45, 126, 39, 209, 20, - 192, 20, 93, 150, 139, 95, 245, 0, 97, 37, 242, 65, 79, 173, 174, 105, - ]), - secretKey: new Uint8Array([ - 59, 123, 96, 175, 42, 188, 213, 123, 164, 1, 171, 57, 143, 132, 244, - 202, 84, 189, 107, 33, 64, 210, 80, 63, 188, 243, 40, 101, 53, 254, 63, - 241, - ]), - } - ) + const { identity: bob } = await createFullDidIdentity(payerSigner, { + publicKey: new Uint8Array([ + 209, 124, 45, 120, 35, 235, 242, 96, 253, 19, 143, 45, 126, 39, 209, 20, + 192, 20, 93, 150, 139, 95, 245, 0, 97, 37, 242, 65, 79, 173, 174, 105, + ]), + secretKey: new Uint8Array([ + 59, 123, 96, 175, 42, 188, 213, 123, 164, 1, 171, 57, 143, 132, 244, 202, + 84, 189, 107, 33, 64, 210, 80, 63, 188, 243, 40, 101, 53, 254, 63, 241, + ]), + }) console.log('bob setup done') @@ -190,11 +192,7 @@ async function runAll() { // Chain DID workflow -> creation & deletion console.log('DID workflow started') - const { - didDocument: fullDid, - getSigners, - address: didAddress, - } = await createFullDid(payerSigner, { + const keypair = { publicKey: new Uint8Array([ 157, 198, 166, 93, 125, 173, 238, 122, 17, 146, 49, 238, 62, 111, 140, 45, 26, 6, 94, 42, 60, 167, 79, 19, 142, 20, 212, 5, 130, 44, 214, 190, @@ -203,12 +201,20 @@ async function runAll() { 252, 195, 96, 143, 203, 194, 37, 74, 205, 243, 137, 71, 234, 82, 57, 46, 212, 14, 113, 177, 1, 241, 62, 118, 184, 230, 121, 219, 17, 45, 36, 143, ]), + } + + const { didDocument: fullDid, address: didAddress } = + await createFullDidIdentity(payerSigner, keypair) + + const identity = await makeIdentity({ + did: `did:kilt:${didAddress}` as Did, + keypairs: [keypair], }) if ( fullDid.authentication?.length === 1 && fullDid.assertionMethod?.length === 1 && - fullDid.id.endsWith(didAddress) + fullDid.id === identity.did ) { console.info('DID matches') } else { @@ -218,8 +224,11 @@ async function runAll() { const deleteTx = await authorizeTx( api, api.tx.did.delete(0), - fullDid.id, - getSigners(fullDid)[0], + identity.did, + await identity.getSigner({ + verificationRelationship: 'authentication', + algorithm: 'Ed25519', + }), payerSigner.id ) @@ -240,8 +249,11 @@ async function runAll() { const cTypeStoreTx = await authorizeTx( api, api.tx.ctype.add(DriversLicenseDef), - alice.id, - aliceSign(alice)[0], + alice.did, + await alice.getSigner({ + verificationRelationship: 'assertionMethod', + algorithm: 'Ed25519', + }), payerSigner.id ) @@ -267,8 +279,8 @@ async function runAll() { const credential = await Issuer.createCredential({ cType: DriversLicense, claims: content, - subject: bob.id, - issuer: alice.id, + subject: bob.did, + issuer: alice.did, }) console.info('Credential subject conforms to CType') @@ -276,16 +288,16 @@ async function runAll() { if ( credential.credentialSubject.name !== content.name || credential.credentialSubject.age !== content.age || - credential.credentialSubject.id !== bob.id + credential.credentialSubject.id !== bob.did ) { throw new Error('Claim content inside Credential mismatching') } - const issued = await Issuer.issue(credential, { - did: alice.id, - signers: [...(await aliceSign(alice)), payerSigner], - submitterAccount: payerSigner.id, - }) + // turn alice into a transaction submission enabled identity + alice.submitterAccount = payerSigner.id + await alice.addSigner(payerSigner) + + const issued = await Issuer.issue(credential, alice as any) console.info('Credential issued') const credentialResult = await Verifier.verifyCredential( @@ -312,10 +324,7 @@ async function runAll() { const presentation = await Holder.createPresentation( [derived], - { - did: bob.id, - signers: await bobSign(bob), - }, + bob, {}, { challenge, From 0d372e513be4d593b9ae5d3c08be359e03f5b3a3 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 6 Dec 2023 12:57:31 +0100 Subject: [PATCH 03/12] refactor: make methods synchronous where possible --- packages/sdk-js/src/Identity.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index eaf332acf..1820b0625 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -35,7 +35,7 @@ export interface Identity { * @param signers Signer Interface(s). * @returns The (in-place) modified Identity object for chaining. */ - addSigner: (...signers: SignerInterface[]) => Promise + addSigner: (...signers: SignerInterface[]) => Identity /** * Convenience function similar to {@link addSigner}, but creates signers for all matching known algorithms from a keypair. * @@ -54,18 +54,18 @@ export interface Identity { verificationMethod?: DidUrl | UriFragment verificationRelationship?: string algorithm?: string - }) => Promise + }) => SignerInterface[] /** * Same as {@link getSigners} but pre-selects a signer matching the criteria and throws if none are found. * * @param filterBy See {@link getSigners}. - * @returns A Promise of a signer, which rejects if none match the selection criteria. + * @returns A Promise of a signer, which throws if none match the selection criteria. */ getSigner: (filterBy: { verificationMethod?: DidUrl | UriFragment verificationRelationship?: string algorithm?: string - }) => Promise + }) => SignerInterface /** * Refreshes the didDocument and/or purges signers that are not linked to a VM currently referenced in the document. * @@ -146,14 +146,12 @@ class IdentityClass implements Identity { this.didDocument = await loadDidDocument(this.did, this.resolver) } if (skipPurgeSigners !== true) { - this.didSigners = await this.getSigners() + this.didSigners = this.getSigners() } return this } - public async addSigner( - ...signers: SignerInterface[] - ): Promise { + public addSigner(...signers: SignerInterface[]): IdentityClass { this.didSigners.push(...signers) return this } @@ -181,7 +179,7 @@ class IdentityClass implements Identity { const id = matchingKey.id.startsWith('#') ? this.did + matchingKey.id : matchingKey.id - await this.addSigner( + this.addSigner( ...(await Signers.getSignersForKeypair({ keypair, id, @@ -195,7 +193,7 @@ class IdentityClass implements Identity { return this } - public async getSigners({ + public getSigners({ verificationMethod, verificationRelationship, algorithm, @@ -203,7 +201,7 @@ class IdentityClass implements Identity { verificationMethod?: DidUrl | UriFragment verificationRelationship?: string algorithm?: string - } = {}): Promise { + } = {}): SignerInterface[] { const selectors = [ Signers.select.byDid(this.didDocument, { verificationRelationship }), ] @@ -216,14 +214,14 @@ class IdentityClass implements Identity { return Signers.selectSigners(this.didSigners) } - public async getSigner( + public getSigner( criteria: { verificationMethod?: DidUrl | UriFragment verificationRelationship?: string algorithm?: string } = {} - ): Promise { - const [signer] = await this.getSigners(criteria) + ): SignerInterface { + const [signer] = this.getSigners(criteria) if (typeof signer === 'undefined') { throw new SDKErrors.NoSuitableSignerError(undefined, { signerRequirements: criteria, From 9431d79689f67c5fa11fbebdf02b4e614b5b5f6e Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Fri, 8 Dec 2023 16:31:48 +0100 Subject: [PATCH 04/12] chore: replace did property with getter --- packages/sdk-js/src/Identity.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index 1820b0625..f1e9dac99 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -109,32 +109,31 @@ async function loadDidDocument( } class IdentityClass implements Identity { - public did: Did public resolver: typeof resolve public didDocument: DidDocument - private didSigners: SignerInterface[] + protected didSigners: SignerInterface[] + get signers(): SignerInterface[] { + return [...this.didSigners] + } + + public get did(): Did { + return this.didDocument.id + } constructor({ - did, didDocument, signers, resolver = resolve, }: { - did: Did didDocument: DidDocument signers?: SignerInterface[] resolver?: typeof resolve }) { - this.did = did this.didDocument = didDocument this.didSigners = signers ? [...signers] : [] this.resolver = resolver } - get signers(): SignerInterface[] { - return [...this.didSigners] - } - public async update({ skipResolution = false, skipPurgeSigners = false, @@ -239,15 +238,21 @@ export async function makeIdentity({ signers, resolver = resolve, }: { - did: Did + did?: Did didDocument?: DidDocument signers?: SignerInterface[] keypairs?: Array resolver?: typeof resolve }): Promise { + let didDoc = didDocument + if (!didDoc) { + if (!did) { + throw new Error('either `did` or `didDocument` is required') + } + didDoc = await loadDidDocument(did, resolver) + } const identity = new IdentityClass({ - did, - didDocument: didDocument ?? (await loadDidDocument(did, resolver)), + didDocument: didDoc, signers, resolver, }) From e09f33d0ab542e05effc08c1345eb6a987f6a134 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Fri, 8 Dec 2023 16:33:27 +0100 Subject: [PATCH 05/12] feat: submission strategy --- packages/credentials/src/issuer.ts | 2 + packages/sdk-js/package.json | 3 +- packages/sdk-js/src/Identity.ts | 93 +++++++++++++++++++++++++----- packages/sdk-js/src/index.ts | 5 +- tests/bundle/bundle-test.ts | 15 ++--- yarn.lock | 1 + 6 files changed, 94 insertions(+), 25 deletions(-) diff --git a/packages/credentials/src/issuer.ts b/packages/credentials/src/issuer.ts index b08e584e6..6c9699c7d 100644 --- a/packages/credentials/src/issuer.ts +++ b/packages/credentials/src/issuer.ts @@ -13,6 +13,8 @@ import type { UnsignedVc, VerifiableCredential } from './V1/types.js' import type { CTypeLoader } from './ctype/index.js' import type { IssuerOptions } from './interfaces.js' +export { IssuerOptions } + /** * Creates a new credential document as a basis for issuing a credential. * This document can be shown to users as a preview or be extended with additional properties before moving on to the second step of credential issuance: diff --git a/packages/sdk-js/package.json b/packages/sdk-js/package.json index 88c447401..9acaac6be 100644 --- a/packages/sdk-js/package.json +++ b/packages/sdk-js/package.json @@ -46,7 +46,8 @@ "@kiltprotocol/credentials": "workspace:*", "@kiltprotocol/did": "workspace:*", "@kiltprotocol/type-definitions": "^0.35.0", - "@kiltprotocol/utils": "workspace:*" + "@kiltprotocol/utils": "workspace:*", + "@polkadot/util": "^12.0.0" }, "peerDependencies": { "@kiltprotocol/augment-api": "^1.11210.0" diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index f1e9dac99..6cb9308b5 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -1,15 +1,17 @@ /** - * Copyright (c) 2018-2023, BOTLabs GmbH. + * 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 { GenericExtrinsic } from '@polkadot/types' import { u8aEq } from '@polkadot/util' import type { Keypair } from '@polkadot/util-crypto/types' -import { multibaseKeyToDidKey, resolve } from '@kiltprotocol/did' +import { Blockchain } from '@kiltprotocol/chain-helpers' +import { ConfigService } from '@kiltprotocol/config' +import type { Issuer } from '@kiltprotocol/credentials' +import { authorizeTx, multibaseKeyToDidKey, resolve } from '@kiltprotocol/did' import type { Did, DidDocument, @@ -20,7 +22,6 @@ import type { UriFragment, } from '@kiltprotocol/types' import { SDKErrors, Signers } from '@kiltprotocol/utils' -import type { TransactionResult } from '@kiltprotocol/credentials/src/V1/KiltAttestationProofV1' /** * An Identity represents a DID and signing keys associated with it. @@ -76,17 +77,13 @@ export interface Identity { skipResolution?: boolean purgeSigners?: boolean }) => Promise - /** - * The account that acts as the submitter account. - */ - submitterAccount?: KiltAddress /** * Uses a verification method related signer to DID-authorize a call to be executed on-chain with the identity's DID as the origin. * * @param tx The call to be authorized. * @returns The authorized (signed) call. */ - authorizeTx?: (tx: GenericExtrinsic) => Promise + authorizeTx?: Issuer.IssuerOptions['authorizeTx'] /** * Takes care of submitting the transaction to node in the network and listening for execution results. * This can consist of signing the tx using the signer associated with submitterAccount and interacting directly with a node, or can use an external service for this. @@ -94,7 +91,7 @@ export interface Identity { * @param tx The extrinsic ready to be (signed and) submitted. * @returns A promise resolving to an object indicating the block of inclusion, or rejecting if the transaction failed to be included or execute correctly. */ - submitTx?: (tx: GenericExtrinsic) => Promise + submitTx?: Issuer.IssuerOptions['submitTx'] } async function loadDidDocument( @@ -231,19 +228,49 @@ class IdentityClass implements Identity { } } -export async function makeIdentity({ +export type TransactionStrategy = ( + identity: IdentityClass & Identity +) => Promise + +export async function makeIdentity(args: { + did?: Did + didDocument?: DidDocument + signers?: SignerInterface[] + keypairs?: Array + resolver?: typeof resolve +}): Promise +export async function makeIdentity(args: { + did?: Did + didDocument?: DidDocument + signers?: SignerInterface[] + keypairs?: Array + resolver?: typeof resolve + transactionStrategy: TransactionStrategy +}): Promise +/** + * @param root0 + * @param root0.did + * @param root0.didDocument + * @param root0.signers + * @param root0.keypairs + * @param root0.resolver + * @param root0.transactionStrategy + */ +export async function makeIdentity({ did, didDocument, keypairs, signers, resolver = resolve, + transactionStrategy, }: { did?: Did didDocument?: DidDocument signers?: SignerInterface[] keypairs?: Array resolver?: typeof resolve -}): Promise { + transactionStrategy?: TransactionStrategy +}): Promise { let didDoc = didDocument if (!didDoc) { if (!did) { @@ -260,5 +287,45 @@ export async function makeIdentity({ if (keypairs && keypairs.length !== 0) { await identity.addKeypair(...keypairs) } - return identity + if (!transactionStrategy) { + return identity + } + return transactionStrategy(identity) +} + +export type IdentityWithSubmitter = IdentityClass & + Required> + +/** + * @param root0 + * @param root0.signer + */ +export function withSubmitterAccount({ + signer, +}: { + signer: Blockchain.TransactionSigner | KeyringPair +}): TransactionStrategy { + const submitterAddress = ( + 'address' in signer ? signer.address : signer.id + ) as KiltAddress + return async (identity) => { + /* eslint-disable-next-line no-param-reassign */ + identity.authorizeTx = (tx) => + authorizeTx(identity.didDocument, tx, identity.signers, submitterAddress) + /* eslint-disable-next-line no-param-reassign */ + identity.submitTx = async (tx) => { + const submittable = ConfigService.get('api').tx(tx) + const result = await Blockchain.signAndSubmitTx(submittable, signer, { + resolveOn: Blockchain.IS_FINALIZED, + }) + return { + status: 'Finalized', + includedAt: { + blockHash: result.status.asFinalized, + }, + events: result.events, + } + } + return identity as IdentityWithSubmitter + } } diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index c91e567b9..eae867530 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -19,7 +19,7 @@ import { Blockchain, } from '@kiltprotocol/chain-helpers' import { resolver as DidResolver } from '@kiltprotocol/did' -import { makeIdentity, Identity } from './Identity.js' +import { makeIdentity, Identity, withSubmitterAccount } from './Identity.js' const { signAndSubmitTx } = Blockchain // TODO: maybe we don't even need that if we have the identity class const { signerFromKeypair } = Signers @@ -35,6 +35,7 @@ export { signerFromKeypair, signAndSubmitTx, ConfigService, - makeIdentity, Identity, + makeIdentity, + withSubmitterAccount, } diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index f87fab0d9..3ac637c9c 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,7 +7,6 @@ /// -import type { Identity } from '@kiltprotocol/sdk-js' import type { ApiPromise } from '@polkadot/api' import type { Did, @@ -29,6 +28,7 @@ const { signAndSubmitTx, signerFromKeypair, makeIdentity, + withSubmitterAccount, } = kilt async function authorizeTx( @@ -107,11 +107,12 @@ async function createFullDidIdentity( throw new Error(`failed to create did for account ${address}`) } - const identity = (await makeIdentity({ + const identity = await makeIdentity({ did: didDocument.id, didDocument, keypairs: [keypair], - })) as Identity + transactionStrategy: withSubmitterAccount({ signer: payer }), + }) return { didDocument, @@ -225,7 +226,7 @@ async function runAll() { api, api.tx.did.delete(0), identity.did, - await identity.getSigner({ + identity.getSigner({ verificationRelationship: 'authentication', algorithm: 'Ed25519', }), @@ -250,7 +251,7 @@ async function runAll() { api, api.tx.ctype.add(DriversLicenseDef), alice.did, - await alice.getSigner({ + alice.getSigner({ verificationRelationship: 'assertionMethod', algorithm: 'Ed25519', }), @@ -293,10 +294,6 @@ async function runAll() { throw new Error('Claim content inside Credential mismatching') } - // turn alice into a transaction submission enabled identity - alice.submitterAccount = payerSigner.id - await alice.addSigner(payerSigner) - const issued = await Issuer.issue(credential, alice as any) console.info('Credential issued') diff --git a/yarn.lock b/yarn.lock index 502eb3af3..0a758d512 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2204,6 +2204,7 @@ __metadata: "@kiltprotocol/type-definitions": "npm:^0.35.0" "@kiltprotocol/utils": "workspace:*" "@polkadot/typegen": "npm:^10.7.3" + "@polkadot/util": "npm:^12.0.0" rimraf: "npm:^3.0.2" terser-webpack-plugin: "npm:^5.1.1" typescript: "npm:^4.8.3" From 16411fabf11322c4492315dfe662b122a10833fa Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Tue, 12 Dec 2023 17:42:53 +0100 Subject: [PATCH 06/12] feat: new identity creation --- packages/sdk-js/src/Identity.ts | 140 ++++++++++++++++++++++++++-- packages/sdk-js/src/index.ts | 8 +- tests/bundle/bundle-test.ts | 157 ++++++-------------------------- 3 files changed, 167 insertions(+), 138 deletions(-) diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index 6cb9308b5..a39ec5f0f 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -11,7 +11,16 @@ import type { Keypair } from '@polkadot/util-crypto/types' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' import type { Issuer } from '@kiltprotocol/credentials' -import { authorizeTx, multibaseKeyToDidKey, resolve } from '@kiltprotocol/did' +import { + NewDidVerificationKey, + authorizeTx, + createLightDidDocument, + didKeyToVerificationMethod, + getFullDid, + getStoreTx, + multibaseKeyToDidKey, + resolve, +} from '@kiltprotocol/did' import type { Did, DidDocument, @@ -21,7 +30,7 @@ import type { SignerInterface, UriFragment, } from '@kiltprotocol/types' -import { SDKErrors, Signers } from '@kiltprotocol/utils' +import { Crypto, SDKErrors, Signers } from '@kiltprotocol/utils' /** * An Identity represents a DID and signing keys associated with it. @@ -51,7 +60,7 @@ export interface Identity { * @param filterBy Additional selection criteria, including which VM the signer relates to, which algorithm it uses, or which relationship it has to the DID. * @returns A (potentially empty) array of signers, wrapped in a Promise because {@link Signers.selectSigners} is async. */ - getSigners: (filterBy: { + getSigners: (filterBy?: { verificationMethod?: DidUrl | UriFragment verificationRelationship?: string algorithm?: string @@ -62,7 +71,7 @@ export interface Identity { * @param filterBy See {@link getSigners}. * @returns A Promise of a signer, which throws if none match the selection criteria. */ - getSigner: (filterBy: { + getSigner: (filterBy?: { verificationMethod?: DidUrl | UriFragment verificationRelationship?: string algorithm?: string @@ -73,7 +82,7 @@ export interface Identity { * @param opts Controls whether you want to only reload the document, or only purge signers. Defaults to doing both. * @returns The (in-place) modified Identity object for chaining. */ - update: (opts: { + update: (opts?: { skipResolution?: boolean purgeSigners?: boolean }) => Promise @@ -92,6 +101,7 @@ export interface Identity { * @returns A promise resolving to an object indicating the block of inclusion, or rejecting if the transaction failed to be included or execute correctly. */ submitTx?: Issuer.IssuerOptions['submitTx'] + submitterAddress?: KiltAddress } async function loadDidDocument( @@ -293,8 +303,124 @@ export async function makeIdentity({ return transactionStrategy(identity) } +type TypedKeyPair = + | (Keypair & { type: KeyringPair['type'] | 'x25519' }) + | KeyringPair +// | SignerInterface + +function isTypedKeyPair( + input: unknown +): input is TypedKeyPair & NewDidVerificationKey { + return ( + typeof input === 'object' && + input !== null && + 'type' in input && + 'publicKey' in input && + ['ed25519', 'sr25519', 'ecdsa'].includes(input.type as string) && + ('secretKey' in input || 'sign' in input) + ) +} + +type TypedKeyPairs = { + authentication: [TypedKeyPair] + assertionMethod?: [TypedKeyPair] + delegationMethod?: [TypedKeyPair] + keyAgreement?: [TypedKeyPair] +} + +export async function newIdentity(args: { + keys: TypedKeyPair | TypedKeyPairs + resolver?: typeof resolve +}): Promise +export async function newIdentity< + T extends IdentityClass & + Required> +>(args: { + keys: TypedKeyPair | TypedKeyPairs + resolver?: typeof resolve + transactionStrategy: TransactionStrategy +}): Promise +export async function newIdentity< + T extends IdentityClass & + Required> +>({ + keys, + resolver = resolve, + transactionStrategy, +}: { + keys: TypedKeyPair | TypedKeyPairs + resolver?: typeof resolve + transactionStrategy?: TransactionStrategy +}): Promise { + let typedKeyPairs: TypedKeyPairs + const allKeypairs: TypedKeyPair[] = [] + if (isTypedKeyPair(keys)) { + allKeypairs.push(keys) + typedKeyPairs = { + authentication: [keys], + assertionMethod: [keys], + delegationMethod: [keys], + } + } else { + typedKeyPairs = keys as TypedKeyPairs + Object.entries(typedKeyPairs).forEach(([role, [key]]) => { + if ( + ['authentication', 'assertionMethod', 'delegationMethod'].includes( + role + ) && + isTypedKeyPair(key) + ) { + allKeypairs.push(key) + } + }) + } + + if (!transactionStrategy) { + const didDocument = createLightDidDocument(typedKeyPairs as any) + return makeIdentity({ didDocument, resolver, keypairs: allKeypairs }) + } + + const [authenticationPair] = typedKeyPairs.authentication + const authenticationAddress = Crypto.encodeAddress( + authenticationPair.publicKey, + 38 + ) + const did = getFullDid(authenticationAddress) + const preliminaryDid = await makeIdentity({ + didDocument: { + id: did, + verificationMethod: [ + didKeyToVerificationMethod(did, `#${authenticationAddress}`, { + keyType: authenticationPair.type as any, + publicKey: authenticationPair.publicKey, + }), + ], + authentication: [`#${authenticationAddress}`], + }, + resolver, + keypairs: [authenticationPair], + transactionStrategy, + }) + + const tx = await getStoreTx( + // @ts-ignore + typedKeyPairs, + preliminaryDid.submitterAddress ?? authenticationAddress, + preliminaryDid.signers.map((signer) => ({ + ...signer, + id: authenticationAddress, + })) + ) + + const result = await preliminaryDid.submitTx(tx) + if (result.status !== 'Finalized' && result.status !== 'InBlock') { + return Promise.reject(result) + } + return (await preliminaryDid.update()).addKeypair(...allKeypairs) +} + export type IdentityWithSubmitter = IdentityClass & - Required> + Required> /** * @param root0 @@ -309,6 +435,8 @@ export function withSubmitterAccount({ 'address' in signer ? signer.address : signer.id ) as KiltAddress return async (identity) => { + /* eslint-disable-next-line no-param-reassign */ + identity.submitterAddress = submitterAddress /* eslint-disable-next-line no-param-reassign */ identity.authorizeTx = (tx) => authorizeTx(identity.didDocument, tx, identity.signers, submitterAddress) diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index eae867530..102f03ceb 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -19,7 +19,12 @@ import { Blockchain, } from '@kiltprotocol/chain-helpers' import { resolver as DidResolver } from '@kiltprotocol/did' -import { makeIdentity, Identity, withSubmitterAccount } from './Identity.js' +import { + newIdentity, + makeIdentity, + Identity, + withSubmitterAccount, +} from './Identity.js' const { signAndSubmitTx } = Blockchain // TODO: maybe we don't even need that if we have the identity class const { signerFromKeypair } = Signers @@ -36,6 +41,7 @@ export { signAndSubmitTx, ConfigService, Identity, + newIdentity, makeIdentity, withSubmitterAccount, } diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 3ac637c9c..8ba74f497 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,117 +7,39 @@ /// -import type { ApiPromise } from '@polkadot/api' -import type { - Did, - DidDocument, - DidUrl, - KiltAddress, - SignerInterface, - SubmittableExtrinsic, -} from '@kiltprotocol/types' +import type { Did, KiltAddress, SignerInterface } from '@kiltprotocol/types' const { kilt } = window const { - ConfigService, Issuer, Verifier, Holder, DidResolver, signAndSubmitTx, signerFromKeypair, - makeIdentity, + newIdentity, withSubmitterAccount, } = kilt -async function authorizeTx( - api: ApiPromise, - call: SubmittableExtrinsic, - did: string, - signer: SignerInterface, - submitter: string, - nonce = 1 -) { - let authorized = api.tx.did.submitDidCall( - { - did: did.slice(9), - call, - blockNumber: await api.query.system.number(), - submitter, - txCounter: nonce, - }, - { ed25519: new Uint8Array(64) } - ) - - const signature = await signer.sign({ data: authorized.args[0].toU8a() }) - - authorized = api.tx.did.submitDidCall(authorized.args[0].toU8a(), { - ed25519: signature, - }) - - return authorized -} - async function createFullDidIdentity( payer: SignerInterface<'Ed25519' | 'Sr25519', KiltAddress>, - keypair: { publicKey: Uint8Array; secretKey: Uint8Array } -) { - const api = ConfigService.get('api') - - const signer: SignerInterface = await signerFromKeypair({ - keypair, - algorithm: 'Ed25519', - }) - const address = signer.id - const getSigners: ( - didDocument: DidDocument - ) => Array> = (didDocument) => { - return ( - didDocument.verificationMethod?.map< - Array> - >(({ id }) => [ - { - ...signer, - id: `${didDocument.id}${id}`, - }, - ]) ?? [] - ).flat() + keypair: { + publicKey: Uint8Array + secretKey: Uint8Array + type: 'ed25519' | 'sr25519' } - - let tx = api.tx.did.create( - { - did: address, - submitter: payer.id, - newAttestationKey: { ed25519: keypair.publicKey }, +) { + const identity = await newIdentity({ + keys: { + authentication: [keypair], + assertionMethod: [keypair], + delegationMethod: [keypair], }, - { ed25519: new Uint8Array(64) } - ) - - const signature = await signer.sign({ data: tx.args[0].toU8a() }) - tx = api.tx.did.create(tx.args[0].toU8a(), { ed25519: signature }) - - await signAndSubmitTx(tx, payer) - - const { didDocument } = await DidResolver.resolve( - `did:kilt:${address}` as Did, - {} - ) - if (!didDocument) { - throw new Error(`failed to create did for account ${address}`) - } - - const identity = await makeIdentity({ - did: didDocument.id, - didDocument, - keypairs: [keypair], transactionStrategy: withSubmitterAccount({ signer: payer }), }) return { - didDocument, - getSigners, - address, identity, } } @@ -129,6 +51,7 @@ async function runAll() { // Accounts console.log('Account setup started') const faucet = { + type: 'ed25519', publicKey: new Uint8Array([ 238, 93, 102, 137, 215, 142, 38, 187, 91, 53, 176, 68, 23, 64, 160, 101, 199, 189, 142, 253, 209, 193, 84, 34, 7, 92, 63, 43, 32, 33, 181, 210, @@ -147,6 +70,7 @@ async function runAll() { console.log('faucet signer created') const { identity: alice } = await createFullDidIdentity(payerSigner, { + type: 'ed25519', publicKey: new Uint8Array([ 136, 220, 52, 23, 213, 5, 142, 196, 180, 80, 62, 12, 18, 234, 26, 10, 137, 190, 32, 15, 233, 137, 34, 66, 61, 67, 52, 1, 79, 166, 176, 238, @@ -160,6 +84,7 @@ async function runAll() { console.log('alice setup done') const { identity: bob } = await createFullDidIdentity(payerSigner, { + type: 'ed25519', publicKey: new Uint8Array([ 209, 124, 45, 120, 35, 235, 242, 96, 253, 19, 143, 45, 126, 39, 209, 20, 192, 20, 93, 150, 139, 95, 245, 0, 97, 37, 242, 65, 79, 173, 174, 105, @@ -194,6 +119,7 @@ async function runAll() { // Chain DID workflow -> creation & deletion console.log('DID workflow started') const keypair = { + type: 'ed25519', publicKey: new Uint8Array([ 157, 198, 166, 93, 125, 173, 238, 122, 17, 146, 49, 238, 62, 111, 140, 45, 26, 6, 94, 42, 60, 167, 79, 19, 142, 20, 212, 5, 130, 44, 214, 190, @@ -202,40 +128,17 @@ async function runAll() { 252, 195, 96, 143, 203, 194, 37, 74, 205, 243, 137, 71, 234, 82, 57, 46, 212, 14, 113, 177, 1, 241, 62, 118, 184, 230, 121, 219, 17, 45, 36, 143, ]), - } - - const { didDocument: fullDid, address: didAddress } = - await createFullDidIdentity(payerSigner, keypair) + } as const - const identity = await makeIdentity({ - did: `did:kilt:${didAddress}` as Did, - keypairs: [keypair], + const identity = await newIdentity({ + keys: keypair, + transactionStrategy: withSubmitterAccount({ signer: payerSigner }), }) - if ( - fullDid.authentication?.length === 1 && - fullDid.assertionMethod?.length === 1 && - fullDid.id === identity.did - ) { - console.info('DID matches') - } else { - throw new Error('DIDs do not match') - } + const deleteTx = await identity.authorizeTx(api.tx.did.delete(0n)) + await identity.submitTx(deleteTx) - const deleteTx = await authorizeTx( - api, - api.tx.did.delete(0), - identity.did, - identity.getSigner({ - verificationRelationship: 'authentication', - algorithm: 'Ed25519', - }), - payerSigner.id - ) - - await signAndSubmitTx(deleteTx, payerSigner) - - const resolvedAgain = await DidResolver.resolve(fullDid.id, {}) + const resolvedAgain = await DidResolver.resolve(identity.did, {}) if (resolvedAgain.didDocumentMetadata.deactivated) { console.info('DID successfully deleted') } else { @@ -247,18 +150,10 @@ async function runAll() { const DriversLicenseDef = '{"$schema":"ipfs://bafybeiah66wbkhqbqn7idkostj2iqyan2tstc4tpqt65udlhimd7hcxjyq/","additionalProperties":false,"properties":{"age":{"type":"integer"},"name":{"type":"string"}},"title":"Drivers License","type":"object"}' - const cTypeStoreTx = await authorizeTx( - api, - api.tx.ctype.add(DriversLicenseDef), - alice.did, - alice.getSigner({ - verificationRelationship: 'assertionMethod', - algorithm: 'Ed25519', - }), - payerSigner.id + const cTypeStoreTx = await alice.authorizeTx( + api.tx.ctype.add(DriversLicenseDef) ) - - const result = await signAndSubmitTx(cTypeStoreTx, payerSigner) + const result = await signAndSubmitTx(api.tx(cTypeStoreTx), payerSigner) const ctypeHash = result.events ?.find((ev) => api.events.ctype.CTypeCreated.is(ev.event)) From 68a341e53439a676e0489dbbb924ca298737aec6 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Tue, 12 Dec 2023 18:49:07 +0100 Subject: [PATCH 07/12] fix: various bugs --- packages/sdk-js/src/Identity.ts | 8 ++++++-- packages/types/src/Address.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index a39ec5f0f..4c3518f21 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -152,7 +152,11 @@ class IdentityClass implements Identity { this.didDocument = await loadDidDocument(this.did, this.resolver) } if (skipPurgeSigners !== true) { - this.didSigners = this.getSigners() + try { + this.didSigners = this.getSigners() + } catch { + this.didSigners = [] + } } return this } @@ -217,7 +221,7 @@ class IdentityClass implements Identity { if (verificationMethod) { selectors.push(Signers.select.bySignerId([verificationMethod])) } - return Signers.selectSigners(this.didSigners) + return Signers.selectSigners(this.didSigners, ...selectors) } public getSigner( diff --git a/packages/types/src/Address.ts b/packages/types/src/Address.ts index 7bbd2b6b7..0c1c8152e 100644 --- a/packages/types/src/Address.ts +++ b/packages/types/src/Address.ts @@ -30,6 +30,6 @@ declare module '@polkadot/keyring' { ): string function encodeAddress( key: HexString | Uint8Array | string, - ss58Format?: 38 + ss58Format: 38 ): KiltAddress } From 3b4c453824acc8f217cbea7bb9d1055ea1efc90c Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Tue, 12 Dec 2023 18:54:41 +0100 Subject: [PATCH 08/12] chore: rename variable --- packages/sdk-js/src/Identity.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index 4c3518f21..b502cef63 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -389,8 +389,9 @@ export async function newIdentity< authenticationPair.publicKey, 38 ) + const did = getFullDid(authenticationAddress) - const preliminaryDid = await makeIdentity({ + const identity = await makeIdentity({ didDocument: { id: did, verificationMethod: [ @@ -409,18 +410,23 @@ export async function newIdentity< const tx = await getStoreTx( // @ts-ignore typedKeyPairs, - preliminaryDid.submitterAddress ?? authenticationAddress, - preliminaryDid.signers.map((signer) => ({ + identity.submitterAddress ?? authenticationAddress, + identity.signers.map((signer) => ({ ...signer, id: authenticationAddress, })) ) - const result = await preliminaryDid.submitTx(tx) + const result = await identity.submitTx(tx) if (result.status !== 'Finalized' && result.status !== 'InBlock') { return Promise.reject(result) } - return (await preliminaryDid.update()).addKeypair(...allKeypairs) + // update did document (preliminary signers will be purged) + await identity.update() + // re-add keys, now matched to actual VMs + await identity.addKeypair(...allKeypairs) + // return identity + return identity } export type IdentityWithSubmitter = IdentityClass & From 7c199bf6786ae10c1959d94e32e8e3a6d4489ac3 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 13 Dec 2023 18:20:48 +0100 Subject: [PATCH 09/12] feat: generateKeypair --- packages/sdk-js/src/index.ts | 5 +++-- packages/utils/src/Signers.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index 102f03ceb..79eaaf306 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -27,7 +27,7 @@ import { } from './Identity.js' const { signAndSubmitTx } = Blockchain // TODO: maybe we don't even need that if we have the identity class -const { signerFromKeypair } = Signers +const { signerFromKeypair, generateKeypair } = Signers export { init, @@ -37,8 +37,9 @@ export { Holder, Verifier, Issuer, - signerFromKeypair, signAndSubmitTx, + generateKeypair, + signerFromKeypair, ConfigService, Identity, newIdentity, diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index c09e95a30..a177bbf04 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -43,6 +43,7 @@ import type { } from '@kiltprotocol/types' import { DidError, NoSuitableSignerError } from './SDKErrors.js' +import { makeKeypairFromUri } from './Crypto.js' export const ALGORITHMS = Object.freeze({ ECRECOVER_SECP256K1_BLAKE2B: 'Ecrecover-Secp256k1-Blake2b' as const, // could also be called ES256K-R-Blake2b @@ -452,3 +453,34 @@ export function getPolkadotSigner( }, } } + +export function generateKeypair(args?: { + seed?: string + type?: T +}): Keypair & { type: T } +export function generateKeypair({ + seed = randomAsHex(32), + type = 'ed25519', +}: { + seed?: string + type?: string +} = {}): Keypair & { type: string } { + let typeForKeyring = type as KeyringPair['type'] + switch (type.toLowerCase()) { + case 'secpk256k1': + typeForKeyring = 'ecdsa' + break + case 'x25519': + typeForKeyring = 'ed25519' + break + default: + } + + const keyRingPair = makeKeypairFromUri( + seed.toLowerCase(), + typeForKeyring as any + ) + const secretKey = extractPk(keyRingPair) + const { publicKey } = keyRingPair + return { secretKey, publicKey, type } +} From e1c185c38d5c098b27a249df368753545b0f811d Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Wed, 13 Dec 2023 18:21:08 +0100 Subject: [PATCH 10/12] fix: bundle tests --- packages/sdk-js/src/Identity.ts | 2 +- packages/utils/src/Signers.ts | 1 + tests/bundle/bundle-test.ts | 127 ++++++++++++++------------------ tests/integration/utils.ts | 4 +- 4 files changed, 59 insertions(+), 75 deletions(-) diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index b502cef63..b536e95dd 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -320,7 +320,7 @@ function isTypedKeyPair( input !== null && 'type' in input && 'publicKey' in input && - ['ed25519', 'sr25519', 'ecdsa'].includes(input.type as string) && + ['ed25519', 'sr25519', 'ecdsa'].includes((input as any).type) && ('secretKey' in input || 'sign' in input) ) } diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index a177bbf04..57583e4ee 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -17,6 +17,7 @@ import { import { blake2AsU8a, encodeAddress, + randomAsHex, secp256k1Sign, } from '@polkadot/util-crypto' import type { Keypair } from '@polkadot/util-crypto/types' diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 8ba74f497..4c9b90e86 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,7 +7,11 @@ /// -import type { Did, KiltAddress, SignerInterface } from '@kiltprotocol/types' +import type { + KiltAddress, + KiltKeyringPair, + SignerInterface, +} from '@kiltprotocol/types' const { kilt } = window @@ -16,25 +20,27 @@ const { Verifier, Holder, DidResolver, - signAndSubmitTx, - signerFromKeypair, newIdentity, withSubmitterAccount, + signerFromKeypair, + generateKeypair, } = kilt async function createFullDidIdentity( - payer: SignerInterface<'Ed25519' | 'Sr25519', KiltAddress>, - keypair: { - publicKey: Uint8Array - secretKey: Uint8Array - type: 'ed25519' | 'sr25519' - } + payer: SignerInterface<'Ed25519', KiltAddress>, + seed: string, + signKeyType: KiltKeyringPair['type'] = 'sr25519' ) { + const keypair = generateKeypair({ seed, type: signKeyType }) + + const encryptionKey = generateKeypair({ seed, type: 'x25519' }) + const identity = await newIdentity({ keys: { authentication: [keypair], assertionMethod: [keypair], delegationMethod: [keypair], + keyAgreement: [encryptionKey], }, transactionStrategy: withSubmitterAccount({ signer: payer }), }) @@ -50,85 +56,62 @@ async function runAll() { // Accounts console.log('Account setup started') - const faucet = { - type: 'ed25519', - publicKey: new Uint8Array([ - 238, 93, 102, 137, 215, 142, 38, 187, 91, 53, 176, 68, 23, 64, 160, 101, - 199, 189, 142, 253, 209, 193, 84, 34, 7, 92, 63, 43, 32, 33, 181, 210, - ]), - secretKey: new Uint8Array([ - 205, 253, 96, 36, 210, 176, 235, 162, 125, 84, 204, 146, 164, 76, 217, - 166, 39, 198, 155, 45, 189, 161, 94, 215, 229, 128, 133, 66, 81, 25, 174, - 3, - ]), - } + const FaucetSeed = + 'receive clutch item involve chaos clutch furnace arrest claw isolate okay together' + const payerKp = generateKeypair({ seed: FaucetSeed, type: 'ed25519' }) const payerSigner = await signerFromKeypair<'Ed25519', KiltAddress>({ - keypair: faucet, + keypair: payerKp, algorithm: 'Ed25519', }) console.log('faucet signer created') - const { identity: alice } = await createFullDidIdentity(payerSigner, { - type: 'ed25519', - publicKey: new Uint8Array([ - 136, 220, 52, 23, 213, 5, 142, 196, 180, 80, 62, 12, 18, 234, 26, 10, 137, - 190, 32, 15, 233, 137, 34, 66, 61, 67, 52, 1, 79, 166, 176, 238, - ]), - secretKey: new Uint8Array([ - 171, 248, 229, 189, 190, 48, 198, 86, 86, 192, 163, 203, 209, 129, 255, - 138, 86, 41, 74, 105, 223, 237, 210, 121, 130, 170, 206, 74, 118, 144, - 145, 21, - ]), - }) + const { identity: alice } = await createFullDidIdentity( + payerSigner, + '//Alice', + 'ed25519' + ) console.log('alice setup done') - const { identity: bob } = await createFullDidIdentity(payerSigner, { - type: 'ed25519', - publicKey: new Uint8Array([ - 209, 124, 45, 120, 35, 235, 242, 96, 253, 19, 143, 45, 126, 39, 209, 20, - 192, 20, 93, 150, 139, 95, 245, 0, 97, 37, 242, 65, 79, 173, 174, 105, - ]), - secretKey: new Uint8Array([ - 59, 123, 96, 175, 42, 188, 213, 123, 164, 1, 171, 57, 143, 132, 244, 202, - 84, 189, 107, 33, 64, 210, 80, 63, 188, 243, 40, 101, 53, 254, 63, 241, - ]), - }) + const { identity: bob } = await createFullDidIdentity( + payerSigner, + '//Bob', + 'ed25519' + ) console.log('bob setup done') - // Light DID Account creation workflow - const authPublicKey = - '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - - // const encPublicKey = - // '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' - + const authPublicKey = new Uint8Array([ + 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, + 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, + 170, 170, + ]) + const encPublicKey = new Uint8Array([ + 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, + 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, + 187, 187, + ]) + const testDid = await newIdentity({ + keys: { + authentication: [ + { secretKey: authPublicKey, publicKey: authPublicKey, type: 'ed25519' }, + ], + keyAgreement: [ + { secretKey: encPublicKey, publicKey: encPublicKey, type: 'x25519' }, + ], + }, + }) const address = api.createType('Address', authPublicKey).toString() - const resolved = await DidResolver.resolve( - `did:kilt:light:01${address}:z1Ac9CMtYCTRWjetJfJqJoV7FcPDD9nHPHDHry7t3KZmvYe1HQP1tgnBuoG3enuGaowpF8V88sCxytDPDy6ZxhW` as Did, - {} - ) if ( - !resolved.didDocument || - resolved.didDocument?.keyAgreement?.length !== 1 + testDid.did !== + `did:kilt:light:01${address}:z15dZSRuzEZGdAF16HajRyxeLdQEn6KLQxWsfPQqjBBGhcHxU1zE5LRpVFfJmbCro7Qnr8qB7cYJpeqiU4XQoH51H35QMnZZnDV5ujsdEpDDj2oWQW5AUyQgXyMXHqPbdHwdZzQT93hGcubqNG7YJ4` ) { throw new Error('DID Test Unsuccessful') - } else console.info(`light DID successfully resolved`) + } else console.info(`light DID successfully created`) // Chain DID workflow -> creation & deletion console.log('DID workflow started') - const keypair = { - type: 'ed25519', - publicKey: new Uint8Array([ - 157, 198, 166, 93, 125, 173, 238, 122, 17, 146, 49, 238, 62, 111, 140, 45, - 26, 6, 94, 42, 60, 167, 79, 19, 142, 20, 212, 5, 130, 44, 214, 190, - ]), - secretKey: new Uint8Array([ - 252, 195, 96, 143, 203, 194, 37, 74, 205, 243, 137, 71, 234, 82, 57, 46, - 212, 14, 113, 177, 1, 241, 62, 118, 184, 230, 121, 219, 17, 45, 36, 143, - ]), - } as const + const keypair = generateKeypair({ seed: '//Foo', type: 'ed25519' }) const identity = await newIdentity({ keys: keypair, @@ -153,9 +136,9 @@ async function runAll() { const cTypeStoreTx = await alice.authorizeTx( api.tx.ctype.add(DriversLicenseDef) ) - const result = await signAndSubmitTx(api.tx(cTypeStoreTx), payerSigner) + const { events } = await alice.submitTx(cTypeStoreTx) - const ctypeHash = result.events + const ctypeHash = events ?.find((ev) => api.events.ctype.CTypeCreated.is(ev.event)) ?.event.data[1].toHex() diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts index 6a20a9ffc..8ddb630b0 100644 --- a/tests/integration/utils.ts +++ b/tests/integration/utils.ts @@ -25,7 +25,7 @@ import type { SubmittableExtrinsic, SubscriptionPromise, } from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' +import { Crypto, ss58Format } from '@kiltprotocol/utils' import { makeSigningKeyTool } from '../testUtils/TestUtils.js' @@ -110,7 +110,7 @@ export const devBob = Crypto.makeKeypairFromUri('//Bob') export const devCharlie = Crypto.makeKeypairFromUri('//Charlie') export function addressFromRandom(): KiltAddress { - return encodeAddress(randomAsU8a()) + return encodeAddress(randomAsU8a(), ss58Format) } export async function isCtypeOnChain(cType: ICType): Promise { From fcb022dc4892f5248937908a81f7cab1d5411ec2 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Tue, 19 Dec 2023 18:17:27 +0100 Subject: [PATCH 11/12] chore: some prettifications and QoL improvements --- packages/utils/src/Signers.ts | 5 +++-- tests/bundle/bundle-test.ts | 18 +++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index 57583e4ee..6c6f45af6 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -41,6 +41,7 @@ import type { DidUrl, KeyringPair, UriFragment, + KiltAddress, } from '@kiltprotocol/types' import { DidError, NoSuitableSignerError } from './SDKErrors.js' @@ -153,7 +154,7 @@ const signerFactory = { */ export async function signerFromKeypair< Alg extends KnownAlgorithms, - Id extends string + Id extends string = KiltAddress >({ id, keypair, @@ -226,7 +227,7 @@ function algsForKeyType(keyType: string): KnownAlgorithms[] { * @param input.type If `keypair` is not a {@link KeyringPair}, provide the key type here; otherwise, this is ignored. * @returns An array of signer interfaces based on the keypair and type. */ -export async function getSignersForKeypair({ +export async function getSignersForKeypair({ id, keypair, type = (keypair as KeyringPair).type, diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 4c9b90e86..66cc41235 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -29,7 +29,7 @@ const { async function createFullDidIdentity( payer: SignerInterface<'Ed25519', KiltAddress>, seed: string, - signKeyType: KiltKeyringPair['type'] = 'sr25519' + signKeyType: KiltKeyringPair['type'] = 'ed25519' ) { const keypair = generateKeypair({ seed, type: signKeyType }) @@ -59,7 +59,7 @@ async function runAll() { const FaucetSeed = 'receive clutch item involve chaos clutch furnace arrest claw isolate okay together' const payerKp = generateKeypair({ seed: FaucetSeed, type: 'ed25519' }) - const payerSigner = await signerFromKeypair<'Ed25519', KiltAddress>({ + const payerSigner = await signerFromKeypair({ keypair: payerKp, algorithm: 'Ed25519', }) @@ -81,16 +81,8 @@ async function runAll() { console.log('bob setup done') - const authPublicKey = new Uint8Array([ - 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, - 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, - 170, 170, - ]) - const encPublicKey = new Uint8Array([ - 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, - 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, - 187, 187, - ]) + const authPublicKey = new Uint8Array(32).fill(170) + const encPublicKey = new Uint8Array(32).fill(187) const testDid = await newIdentity({ keys: { authentication: [ @@ -172,7 +164,7 @@ async function runAll() { throw new Error('Claim content inside Credential mismatching') } - const issued = await Issuer.issue(credential, alice as any) + const issued = await Issuer.issue(credential, alice) console.info('Credential issued') const credentialResult = await Verifier.verifyCredential( From 4643d4ab189f5cafc3af9bbdb5282ed3117a37c8 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 25 Mar 2024 18:17:21 +0100 Subject: [PATCH 12/12] feat: decouple signersForDid --- packages/did/src/Did.signature.ts | 53 +++++++++++++++++++++++++++++-- packages/sdk-js/src/Identity.ts | 35 ++------------------ 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index ab9f730e7..4d1d43960 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -5,7 +5,8 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { isHex } from '@polkadot/util' +import { isHex, u8aEq } from '@polkadot/util' +import type { Keypair } from '@polkadot/util-crypto/types' import type { DereferenceDidUrl, @@ -14,9 +15,11 @@ import type { Did, DidUrl, SignatureVerificationRelationship, + KeyringPair, + SignerInterface, } from '@kiltprotocol/types' -import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { Crypto, SDKErrors, Signers } from '@kiltprotocol/utils' import { multibaseKeyToDidKey, parse, validateDid } from './Did.utils.js' import { dereference } from './DidResolver/DidResolver.js' @@ -182,3 +185,49 @@ export function signatureFromJson(input: DidSignature | LegacyDidSignature): { const signature = Crypto.coToUInt8(input.signature) return { signature, keyUri } } + +/** + * Creates signers for {@link Did} based on a {@link DidDocument} and one or more {@link Keypair} / {@link KeyringPair}. + * + * @param didDocument The Did's DidDocument. + * @param keypairs One or more signing key pairs. + * @returns An array of signers based on the key pair. + */ +export async function signersForDid( + didDocument: DidDocument, + ...keypairs: Array +): Promise>> { + const didKeys = didDocument.verificationMethod?.map( + ({ publicKeyMultibase, id }) => ({ + ...multibaseKeyToDidKey(publicKeyMultibase), + id, + }) + ) + if (didKeys && didKeys.length !== 0) { + return ( + await Promise.all( + keypairs.map(async (keypair) => { + const thisType = 'type' in keypair ? keypair.type : undefined + const matchingKey = didKeys?.find(({ publicKey, keyType }) => { + if (thisType && thisType !== keyType) { + return false + } + return u8aEq(publicKey, keypair.publicKey) + }) + if (matchingKey) { + const id: DidUrl = matchingKey.id.startsWith('#') + ? `${didDocument.id}${matchingKey.id}` + : (matchingKey.id as DidUrl) + return Signers.getSignersForKeypair({ + keypair, + id, + type: matchingKey.keyType, + }) + } + return [] + }) + ) + ).flat() + } + return [] +} diff --git a/packages/sdk-js/src/Identity.ts b/packages/sdk-js/src/Identity.ts index b536e95dd..2d63ec404 100644 --- a/packages/sdk-js/src/Identity.ts +++ b/packages/sdk-js/src/Identity.ts @@ -5,7 +5,6 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { u8aEq } from '@polkadot/util' import type { Keypair } from '@polkadot/util-crypto/types' import { Blockchain } from '@kiltprotocol/chain-helpers' @@ -18,8 +17,8 @@ import { didKeyToVerificationMethod, getFullDid, getStoreTx, - multibaseKeyToDidKey, resolve, + signersForDid, } from '@kiltprotocol/did' import type { Did, @@ -169,37 +168,7 @@ class IdentityClass implements Identity { public async addKeypair( ...keypairs: Array ): Promise { - const didKeys = this.didDocument.verificationMethod?.map( - ({ publicKeyMultibase, id }) => ({ - ...multibaseKeyToDidKey(publicKeyMultibase), - id, - }) - ) - if (didKeys && didKeys.length !== 0) { - await Promise.all( - keypairs.map(async (keypair) => { - const thisType = 'type' in keypair ? keypair.type : undefined - const matchingKey = didKeys?.find(({ publicKey, keyType }) => { - if (thisType && thisType !== keyType) { - return false - } - return u8aEq(publicKey, keypair.publicKey) - }) - if (matchingKey) { - const id = matchingKey.id.startsWith('#') - ? this.did + matchingKey.id - : matchingKey.id - this.addSigner( - ...(await Signers.getSignersForKeypair({ - keypair, - id, - type: matchingKey.keyType, - })) - ) - } - }) - ) - } + this.addSigner(...(await signersForDid(this.didDocument, ...keypairs))) return this }