diff --git a/package-lock.json b/package-lock.json index 6cd7fe3f..cadd95a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,13 @@ "cosmjs-types": "^0.5.0", "did-jwt": "^6.2.0", "multiformats": "^9.7.0", - "uint8arrays": "^3.0.0" + "uint8arrays": "^3.0.0", + "uuid": "^8.3.2" }, "devDependencies": { "@types/jest": "^28.1.4", "@types/node": "^18.0.3", + "@types/uuid": "^8.3.4", "jest": "^28.1.2", "ts-jest": "^28.0.6", "ts-node": "^10.8.2", @@ -1632,6 +1634,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.10", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz", @@ -4359,6 +4367,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -5929,6 +5945,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "@types/yargs": { "version": "17.0.10", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz", @@ -7940,6 +7962,11 @@ "picocolors": "^1.0.0" } }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 3bebcf36..ec757d38 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@types/jest": "^28.1.4", "@types/node": "^18.0.3", + "@types/uuid": "^8.3.4", "jest": "^28.1.2", "ts-jest": "^28.0.6", "ts-node": "^10.8.2", @@ -45,6 +46,7 @@ "cosmjs-types": "^0.5.0", "did-jwt": "^6.2.0", "multiformats": "^9.7.0", - "uint8arrays": "^3.0.0" + "uint8arrays": "^3.0.0", + "uuid": "^8.3.2" } } diff --git a/src/modules/did.ts b/src/modules/did.ts index 19c7ac4c..b5985420 100644 --- a/src/modules/did.ts +++ b/src/modules/did.ts @@ -5,7 +5,6 @@ import { CheqdSigningStargateClient } from "../signer" import { DidStdFee, IContext, ISignInputs } from "../types" import { MsgCreateDid, MsgCreateDidPayload } from "@cheqd/ts-proto/cheqd/v1/tx" import { MsgCreateDidEncodeObject, typeUrlMsgCreateDid } from "../registry" -import { VerificationMethod } from "@cheqd/ts-proto/cheqd/v1/did" export class DIDModule extends AbstractCheqdSDKModule { constructor(signer: CheqdSigningStargateClient){ @@ -24,9 +23,6 @@ export class DIDModule extends AbstractCheqdSDKModule { const payload = MsgCreateDidPayload.fromPartial(didPayload) const signatures = await this._signer.signDidTx(signInputs, payload) - console.warn(payload) - console.warn(signatures) - const value: MsgCreateDid = { payload, signatures diff --git a/src/signer.ts b/src/signer.ts index 68c3cba3..4a9cc545 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -86,31 +86,6 @@ export class CheqdSigningStargateClient extends SigningStargateClient { } */ } - async checkDidSigners(verificationMethods: VerificationMethod[] = []): Promise { - if (verificationMethods.length === 0) { - throw new Error('No verification methods provided') - } - - verificationMethods.forEach((verificationMethod) => { - if (!(Object.values(VerificationMethods) as string[]).includes(verificationMethod.type)) { - throw new Error(`Unsupported verification method type: ${verificationMethod.type}`) - } - if (!this.didSigners[verificationMethod.type]) { - this.didSigners[verificationMethod.type] = EdDSASigner - } - }) - - return this.didSigners - } - - async getDidSigner(verificationMethodId: string, verificationMethods: Partial[]): Promise<(secretKey: Uint8Array) => Signer> { - const verificationMethod = verificationMethods.find(method => method.id === verificationMethodId)?.type - if (!verificationMethod) { - throw new Error(`Verification method for ${verificationMethodId} not found`) - } - return this.didSigners[verificationMethod] ?? EdDSASigner - } - async signAndBroadcast( signerAddress: string, messages: readonly EncodeObject[], @@ -196,6 +171,32 @@ export class CheqdSigningStargateClient extends SigningStargateClient { }) } + async checkDidSigners(verificationMethods: Partial[] = []): Promise { + if (verificationMethods.length === 0) { + throw new Error('No verification methods provided') + } + + verificationMethods.forEach((verificationMethod) => { + if (!(Object.values(VerificationMethods) as string[]).includes(verificationMethod.type ?? '')) { + throw new Error(`Unsupported verification method type: ${verificationMethod.type}`) + } + if (!this.didSigners[verificationMethod.type ?? '']) { + this.didSigners[verificationMethod.type ?? ''] = EdDSASigner + } + }) + + return this.didSigners + } + + async getDidSigner(verificationMethodId: string, verificationMethods: Partial[]): Promise<(secretKey: Uint8Array) => Signer> { + await this.checkDidSigners(verificationMethods) + const verificationMethod = verificationMethods.find(method => method.id === verificationMethodId)?.type + if (!verificationMethod) { + throw new Error(`Verification method for ${verificationMethodId} not found`) + } + return this.didSigners[verificationMethod]! + } + async signDidTx(signInputs: ISignInputs[], payload: MsgCreateDidPayload): Promise { await this.checkDidSigners(payload?.verificationMethod) diff --git a/src/types.ts b/src/types.ts index d3d8d726..483af068 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,10 +18,15 @@ export interface IContext { } export enum VerificationMethods { - Multibase58 = 'Ed25519VerificationKey2020', + Base58 = 'Ed25519VerificationKey2020', JWK = 'JsonWebKey2020', } +export enum MethodSpecificIdAlgo { + Base58 = 'base58btc', + Uuid = 'uuid', +} + export type TSignerAlgo = { [key in VerificationMethods as string]?: (secretKey: Uint8Array) => Signer } @@ -41,6 +46,19 @@ export interface IKeyValuePair { value: any } +export type TVerificationKeyPrefix = string + +export type TVerificationKey = `${K}-${N}` + +export interface IVerificationKeys { + readonly methodSpecificId: TMethodSpecificId + readonly didUrl: `did:cheqd:${CheqdNetwork}:${IVerificationKeys['methodSpecificId']}` extends string ? string : never + readonly keyId: `${IVerificationKeys['didUrl']}#${TVerificationKey}` + readonly publicKey: string +} + +export type TMethodSpecificId = string + export interface DidStdFee { readonly amount: readonly Coin[] readonly gas: string diff --git a/tests/modules/did.test.ts b/tests/modules/did.test.ts index c16d178b..cebd9b38 100644 --- a/tests/modules/did.test.ts +++ b/tests/modules/did.test.ts @@ -3,8 +3,8 @@ import { DeliverTxResponse } from "@cosmjs/stargate" import { fromString, toString } from 'uint8arrays' import { DIDModule } from "../../src" import { CheqdSigningStargateClient } from "../../src/signer" -import { CheqdNetwork, DidStdFee, ISignInputs, VerificationMethods } from "../../src/types" -import { createDidPayload, createKeyPairBase64, exampleCheqdNetwork, faucet } from "../testutils.test" +import { CheqdNetwork, DidStdFee, ISignInputs, MethodSpecificIdAlgo, VerificationMethods } from "../../src/types" +import { createDidPayload, createDidVerificationMethod, createKeyPairBase64, createVerificationKeys, exampleCheqdNetwork, faucet } from "../testutils.test" describe('DIDModule', () => { @@ -18,14 +18,15 @@ describe('DIDModule', () => { }) describe('createDidTx', () => { + jest.setTimeout(20000) it('should create a new DID', async () => { - jest.setTimeout(10000) - const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, {prefix: faucet.prefix}) const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet) const didModule = new DIDModule(signer) const keyPair = createKeyPairBase64() - const didPayload = createDidPayload(keyPair, VerificationMethods.JWK, CheqdNetwork.Testnet) + const verificationKeys = createVerificationKeys(keyPair, MethodSpecificIdAlgo.Base58, 'key-1', 16) + const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58], [verificationKeys]) + const didPayload = createDidPayload(verificationMethods, [verificationKeys]) const signInputs: ISignInputs[] = [ { verificationMethodId: didPayload.verificationMethod[0].id, @@ -49,9 +50,17 @@ describe('DIDModule', () => { fee ) - console.warn(didTx) + console.warn(`Using payload: ${JSON.stringify(didPayload)}`) + console.warn(`DID Tx: ${JSON.stringify(didTx)}`) expect(didTx.code).toBe(0) }) }) + + describe('updateDidTx', () => { + jest.setTimeout(20000) + it('should update a DID', async () => { + + }) + }) }) \ No newline at end of file diff --git a/tests/signer.test.ts b/tests/signer.test.ts index b053c0a2..4b11ff02 100644 --- a/tests/signer.test.ts +++ b/tests/signer.test.ts @@ -4,15 +4,32 @@ import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing" import { base64ToBytes, EdDSASigner } from "did-jwt" import { typeUrlMsgCreateDid } from "../src/registry" import { CheqdSigningStargateClient } from "../src/signer" -import { ISignInputs, VerificationMethods } from "../src/types" +import { ISignInputs, MethodSpecificIdAlgo, VerificationMethods } from "../src/types" import { fromString, toString } from 'uint8arrays' -import { createDidPayload, createKeyPairBase64, exampleCheqdNetwork, faucet } from "./testutils.test" +import { createDidPayload, createDidVerificationMethod, createKeyPairBase64, createVerificationKeys, exampleCheqdNetwork, faucet } from "./testutils.test" import { verify } from "@stablelib/ed25519" const nonExistingDid = "did:cHeQd:fantasticnet:123" const nonExistingKeyId = 'did:cHeQd:fantasticnet:123#key-678' const nonExistingPublicKeyMultibase = '1234567890' const nonExistingVerificationMethod = 'ExtraTerrestrialVerificationKey2045' +const nonExistingVerificationDidDocument = { + "authentication": [ + "did:cheqd:testnet:z6Jn6NmYkaCepQe2#key-1" + ], + "controller": [ + "did:cheqd:testnet:z6Jn6NmYkaCepQe2" + ], + "id": "did:cheqd:testnet:z6Jn6NmYkaCepQe2", + "verificationMethod": [ + { + "controller": "did:cheqd:testnet:z6Jn6NmYkaCepQe2", + "id": "did:cheqd:testnet:z6Jn6NmYkaCepQe2#key-1", + "publicKeyMultibase": "z6Jn6NmYkaCepQe29vgCZQhFfRkN3YpEPiu14F8HbbmqW", + "type": nonExistingVerificationMethod + } + ] +} describe('CheqdSigningStargateClient', () => { describe('constructor', () => { @@ -35,25 +52,29 @@ describe('CheqdSigningStargateClient', () => { it('can get a signer for a did', async () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic) const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet) - const didPayload = createDidPayload(createKeyPairBase64(), VerificationMethods.Multibase58) + const keyPair = createKeyPairBase64() + const verificationKeys = createVerificationKeys(keyPair, MethodSpecificIdAlgo.Base58, 'key-1', 16) + const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58], [verificationKeys]) + const didPayload = createDidPayload(verificationMethods, [verificationKeys]) const didSigner = await signer.getDidSigner(didPayload.verificationMethod[0].id, didPayload.verificationMethod) expect(didSigner).toBe(EdDSASigner) }) - it('should not return a did payload for a non-supported verification method', async () => { + it('should throw for a non-supported verification method', async () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic) const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet) - //@ts-ignore - const didPayload = createDidPayload(createKeyPairBase64(), 'ExtraTerrestrialVerificationKey2045') - expect(didPayload).toBe(undefined) + await expect(signer.getDidSigner(nonExistingVerificationDidDocument.verificationMethod[0].id, nonExistingVerificationDidDocument.verificationMethod)).rejects.toThrow() }) it('should throw for non-matching verification method id', async () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic) const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet) - const didPayload = createDidPayload(createKeyPairBase64(), VerificationMethods.JWK) + const keyPair = createKeyPairBase64() + const verificationKeys = createVerificationKeys(keyPair, MethodSpecificIdAlgo.Base58, 'key-1', 16) + const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58], [verificationKeys]) + const didPayload = createDidPayload(verificationMethods, [verificationKeys]) await expect(signer.getDidSigner(nonExistingKeyId, didPayload.verificationMethod)).rejects.toThrow() }) }) @@ -62,23 +83,29 @@ describe('CheqdSigningStargateClient', () => { it('it should instantiate a signer for a did', async () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic) const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet) - const didPayload = createDidPayload(createKeyPairBase64(), VerificationMethods.Multibase58) + const keyPair = createKeyPairBase64() + const verificationKeys = createVerificationKeys(keyPair, MethodSpecificIdAlgo.Base58, 'key-1', 16) + const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58], [verificationKeys]) + const didPayload = createDidPayload(verificationMethods, [verificationKeys]) const didSigners = await signer.checkDidSigners(didPayload.verificationMethod) - expect(didSigners[VerificationMethods.Multibase58]).toBe(EdDSASigner) + expect(didSigners[VerificationMethods.Base58]).toBe(EdDSASigner) }) it('should instantiate multiple signers for a did with multiple verification methods', async () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic) const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet) - const didPayload = createDidPayload(createKeyPairBase64(), VerificationMethods.Multibase58) - - didPayload.verificationMethod.push(createDidPayload(createKeyPairBase64(), VerificationMethods.JWK).verificationMethod[0]) + const keyPair1 = createKeyPairBase64() + const keyPair2 = createKeyPairBase64() + const verificationKeys1 = createVerificationKeys(keyPair1, MethodSpecificIdAlgo.Base58, 'key-1', 16) + const verificationKeys2 = createVerificationKeys(keyPair2, MethodSpecificIdAlgo.Base58, 'key-2', 16) + const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58, VerificationMethods.JWK], [verificationKeys1, verificationKeys2]) + const didPayload = createDidPayload(verificationMethods, [verificationKeys1, verificationKeys2]) const didSigners = await signer.checkDidSigners(didPayload.verificationMethod) - expect(didSigners[VerificationMethods.Multibase58]).toBe(EdDSASigner) + expect(didSigners[VerificationMethods.Base58]).toBe(EdDSASigner) expect(didSigners[VerificationMethods.JWK]).toBe(EdDSASigner) }) @@ -101,7 +128,9 @@ describe('CheqdSigningStargateClient', () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic) const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet) const keyPair = createKeyPairBase64() - const didPayload = createDidPayload(keyPair, VerificationMethods.Multibase58) + const verificationKeys = createVerificationKeys(keyPair, MethodSpecificIdAlgo.Base58, 'key-1', 16) + const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58], [verificationKeys]) + const didPayload = createDidPayload(verificationMethods, [verificationKeys]) const signInputs: ISignInputs[] = [ { verificationMethodId: didPayload.verificationMethod[0].id, diff --git a/tests/testutils.test.ts b/tests/testutils.test.ts index 4abfee96..22812b08 100644 --- a/tests/testutils.test.ts +++ b/tests/testutils.test.ts @@ -1,10 +1,12 @@ import { MsgCreateDidPayload } from "@cheqd/ts-proto/cheqd/v1/tx" -import { CheqdNetwork, IKeyPair, IKeyValuePair, TSignerAlgo, VerificationMethods } from "../src/types" +import { CheqdNetwork, IKeyPair, IKeyValuePair, IVerificationKeys, MethodSpecificIdAlgo, TMethodSpecificId, TSignerAlgo, TVerificationKey, TVerificationKeyPrefix, VerificationMethods } from "../src/types" import { bases } from 'multiformats/basics' import { base64ToBytes } from "did-jwt" import { fromString, toString } from 'uint8arrays' import { generateKeyPair, KeyPair } from '@stablelib/ed25519' import { GasPrice } from "@cosmjs/stargate" +import uuid from 'uuid' +import { VerificationMethod } from "@cheqd/ts-proto/cheqd/v1/did" export const faucet = { prefix: 'cheqd', @@ -31,49 +33,78 @@ export function createKeyPairBase64(): IKeyPair { } } -export function createDidPayload(keyPair: IKeyPair, verificationMethodType: VerificationMethods, network: CheqdNetwork = CheqdNetwork.Testnet): MsgCreateDidPayload { - const methodSpecificId = bases['base58btc'].encode(base64ToBytes(keyPair.publicKey)) - const did = `did:cheqd:${network}:${methodSpecificId.substring(0, 16)}` - const keyId = `${did}#key-1` - - switch (verificationMethodType) { - case VerificationMethods.Multibase58: - return MsgCreateDidPayload.fromPartial({ - id: did, - controller: [did], - verificationMethod: [ - { - id: keyId, - type: VerificationMethods.Multibase58, - controller: did, - publicKeyMultibase: methodSpecificId - } - ], - authentication: [keyId] - }) - - case VerificationMethods.JWK: - const jwk = { - crv: 'Ed25519', - kty: 'OKP', - x: toString( fromString( keyPair.publicKey, 'base64pad' ), 'base64url' ) +export function createVerificationKeys(keyPair: IKeyPair, algo: MethodSpecificIdAlgo, key: TVerificationKey, length: number = 32, network: CheqdNetwork = CheqdNetwork.Testnet): IVerificationKeys { + let methodSpecificId: TMethodSpecificId + let didUrl: IVerificationKeys['didUrl'] + switch (algo) { + case MethodSpecificIdAlgo.Base58: + methodSpecificId = bases['base58btc'].encode(base64ToBytes(keyPair.publicKey)) + didUrl = `did:cheqd:${network}:${methodSpecificId.substring(0, length)}` + return { + methodSpecificId, + didUrl, + keyId: `${didUrl}#${key}`, + publicKey: keyPair.publicKey, + } + case MethodSpecificIdAlgo.Uuid: + methodSpecificId = bases['base58btc'].encode(base64ToBytes(keyPair.publicKey)) + didUrl = `did:cheqd:${network}:${uuid.v4()}` + return { + methodSpecificId, + didUrl, + keyId: `${didUrl}#${key}`, + publicKey: keyPair.publicKey, } - return MsgCreateDidPayload.fromPartial({ - id: did, - controller: [did], - verificationMethod: [ - { - id: keyId, - type: VerificationMethods.JWK, - controller: did, - publicKeyJwk: parseToKeyValuePair(jwk), - } - ], - authentication: [keyId] - }) } } +export function createDidVerificationMethod(verificationMethodTypes: VerificationMethods[], verificationKeys: IVerificationKeys[]): VerificationMethod[] { + return verificationMethodTypes.map((type, _) => { + switch (type) { + case VerificationMethods.Base58: + return { + id: verificationKeys[_].keyId, + type: type, + controller: verificationKeys[_].didUrl, + publicKeyMultibase: verificationKeys[_].methodSpecificId, + publicKeyJwk: [] + } + + case VerificationMethods.JWK: + return { + id: verificationKeys[_].keyId, + type: type, + controller: verificationKeys[_].didUrl, + publicKeyJwk: parseToKeyValuePair( + { + crv: 'Ed25519', + kty: 'OKP', + x: toString( fromString( verificationKeys[_].publicKey, 'base64pad' ), 'base64url' ) + } + ), + publicKeyMultibase: '' + } + } + }) ?? [] +} + +export function createDidPayload(verificationMethods: VerificationMethod[], verificationKeys: IVerificationKeys[]): MsgCreateDidPayload { + if (!verificationMethods || verificationMethods.length === 0) + throw new Error('No verification methods provided') + if (!verificationKeys || verificationKeys.length === 0) + throw new Error('No verification keys provided') + + const did = verificationKeys[0].didUrl + return MsgCreateDidPayload.fromPartial( + { + id: did, + controller: verificationKeys.map(key => key.didUrl), + verificationMethod: verificationMethods, + authentication: verificationKeys.map(key => key.keyId) + } + ) +} + export function parseToKeyValuePair(object: { [key: string]: any }): IKeyValuePair[] { return Object.entries(object).map(([key, value]) => ({ key, value })) } \ No newline at end of file