diff --git a/.changeset/smooth-otters-sin.md b/.changeset/smooth-otters-sin.md new file mode 100644 index 0000000..60e8abb --- /dev/null +++ b/.changeset/smooth-otters-sin.md @@ -0,0 +1,9 @@ +--- +"@zcloak/login-rpc-defines": patch +"@zcloak/login-providers": patch +"@zcloak/did": patch +"@zcloak/vc": patch +"@zcloak/login-did": patch +--- + +add batch sign and batch encrypt diff --git a/login/did/src/LoginDid.ts b/login/did/src/LoginDid.ts index 318fc29..12c6143 100644 --- a/login/did/src/LoginDid.ts +++ b/login/did/src/LoginDid.ts @@ -6,7 +6,7 @@ import type { DidUrl } from '@zcloak/did-resolver/types'; import type { BaseProvider } from '@zcloak/login-providers/base/Provider'; import { UnsignedTransaction } from '@ethersproject/transactions'; -import { hexToU8a } from '@polkadot/util'; +import { hexToU8a, isU8a, u8aToHex } from '@polkadot/util'; import { Did } from '@zcloak/did'; import { parseDidDocument } from '@zcloak/did/did/helpers'; @@ -100,4 +100,33 @@ export class LoginDid extends Did implements IDidKeyring { return result; } + + public override async batchSignWithKey( + messages: (Uint8Array | `0x${string}`)[] | Uint8Array[] | `0x${string}`[], + keyOrDidUrl: DidUrl | Exclude + ): Promise { + const payload: HexString[] = messages.map((message) => (isU8a(message) ? u8aToHex(message) : message)); + + const result = this.provider.batchSign({ keyId: keyOrDidUrl, payload }); + + return result; + } + + public override async batchEncrypt( + params: { + receiver: DidUrl; + message: HexString; + }[] + ): Promise { + const encrypts = await this.provider.batchEncrypt(params); + + return encrypts.map(({ data, receiverUrl, senderUrl, type }) => { + return { + data: hexToU8a(data), + receiverUrl, + senderUrl, + type + }; + }); + } } diff --git a/login/providers/src/base/Provider.ts b/login/providers/src/base/Provider.ts index d17c460..b4c9549 100644 --- a/login/providers/src/base/Provider.ts +++ b/login/providers/src/base/Provider.ts @@ -161,4 +161,12 @@ export class BaseProvider extends Events { ): Promise { return this.request('send_tx', { tx, providerUrl, keyOrDidUrl }); } + + public async batchSign(params: RpcRequest<'batch_sign'>): Promise> { + return this.request('batch_sign', params); + } + + public async batchEncrypt(params: RpcRequest<'batch_encrypt'>): Promise> { + return this.request('batch_encrypt', params); + } } diff --git a/login/rpc-defines/src/defineZk.ts b/login/rpc-defines/src/defineZk.ts index 86bf7f6..58c3fc5 100644 --- a/login/rpc-defines/src/defineZk.ts +++ b/login/rpc-defines/src/defineZk.ts @@ -3,7 +3,7 @@ import '@zcloak/login-rpc/rpcs'; -import type { DidKeys } from '@zcloak/did/types'; +import type { DidKeys, SignedData } from '@zcloak/did/types'; import type { DidDocument, DidUrl, SignatureType, VerificationMethodType } from '@zcloak/did-resolver/types'; import type { VerifiablePresentation } from '@zcloak/vc/types'; @@ -78,12 +78,14 @@ export type ZkpGenRequest = { publicInput?: string; }; -export type SendTx = { +export type SendTxParams = { tx: UnsignedTransaction; providerUrl: string; keyOrDidUrl: DidUrl | Exclude; }; +export type BatchSignParams = { keyId?: DidUrl | Exclude; payload: HexString[] }; + declare module '@zcloak/login-rpc/rpcs' { interface Rpcs { wallet_requestAuth: [undefined, boolean]; @@ -98,7 +100,9 @@ declare module '@zcloak/login-rpc/rpcs' { did_encrypt: [DidEncryptParams, DidEncrypted]; did_decrypt: [DidDecryptParams, HexString]; proof_generate: [ZkpGenRequest, ZkpGenResponse]; - send_tx: [SendTx, any]; + send_tx: [SendTxParams, any]; + batch_sign: [BatchSignParams, SignedData[]]; + batch_encrypt: [DidEncryptParams[], DidEncrypted[]]; } interface RpcEvents { diff --git a/protocol/did/src/did/keyring.ts b/protocol/did/src/did/keyring.ts index 2ed18a2..4af30d3 100644 --- a/protocol/did/src/did/keyring.ts +++ b/protocol/did/src/did/keyring.ts @@ -161,4 +161,26 @@ export abstract class DidKeyring extends DidDetails implements IDidKeyring { id: _id }); } + + public batchSignWithKey( + messages: (Uint8Array | HexString)[] | Uint8Array[] | HexString[], + keyOrDidUrl: DidUrl | Exclude = 'controller' + ): Promise { + const pendingMessages = messages.map((message) => this.signWithKey(message, keyOrDidUrl)); + + return Promise.all(pendingMessages); + } + + public batchEncrypt( + params: { + receiver: DidUrl; + message: HexString; + }[], + senderUrl: DidUrl = this.getKeyUrl('keyAgreement'), + resolver: DidResolver = defaultResolver + ): Promise { + const encrypts = params.map(({ message, receiver }) => this.encrypt(message, receiver, senderUrl, resolver)); + + return Promise.all(encrypts); + } } diff --git a/protocol/vc/src/credential/index.spec.ts b/protocol/vc/src/credential/index.spec.ts index 2ea9a0b..f4dc18f 100644 --- a/protocol/vc/src/credential/index.spec.ts +++ b/protocol/vc/src/credential/index.spec.ts @@ -314,4 +314,57 @@ describe('VerifiableCredential', (): void => { }); }); }); + + describe('Batch PrivateVerifiableCredential', () => { + it('build batch VC from Raw instance', async (): Promise => { + const raw = new Raw({ + contents: CONTENTS, + owner: holder.id, + ctype, + hashType: 'RescuePrime' + }); + + const vcBuilder = new VerifiableCredentialBuilder(raw); + + const now = Date.now(); + + vcBuilder + .setContext(DEFAULT_CONTEXT) + .setVersion(DEFAULT_VC_VERSION) + .setIssuanceDate(now) + .setDigestHashType('Keccak256') + .setExpirationDate(null); + + expect(vcBuilder).toMatchObject({ + raw, + '@context': DEFAULT_CONTEXT, + issuanceDate: now, + digestHashType: 'Keccak256' + }); + + const vcs = await VerifiableCredentialBuilder.batchBuild([vcBuilder], issuer); + const vc = vcs[0]; + + expect(Array.isArray(vcs)).toBe(true); + + expect(isPrivateVC(vc)).toBe(true); + + expect(vc).toMatchObject({ + '@context': DEFAULT_CONTEXT, + version: DEFAULT_VC_VERSION, + ctype: ctype.$id, + issuanceDate: now, + credentialSubject: CONTENTS, + issuer: [issuer.id], + holder: holder.id, + hasher: ['RescuePrime', 'Keccak256'], + proof: [ + { + type: 'EcdsaSecp256k1SignatureEip191', + proofPurpose: 'assertionMethod' + } + ] + }); + }); + }); }); diff --git a/protocol/vc/src/credential/vc.ts b/protocol/vc/src/credential/vc.ts index 5bab232..4dda0e0 100644 --- a/protocol/vc/src/credential/vc.ts +++ b/protocol/vc/src/credential/vc.ts @@ -44,7 +44,7 @@ import { Raw } from './raw'; * * * const builder = VerifiableCredentialBuilder.fromRaw(raw) - * .setExpirationDate(null); // if you don't want the vc to expirate, set it to `null` + * .setExpirationDate(null); // if you don't want the vc to expiate, set it to `null` * * const issuer: Did = helpers.createEcdsaFromMnemonic('pass your mnemonic') * const vc: VerifiableCredential = builder.build(issuer) @@ -128,7 +128,7 @@ export class VerifiableCredentialBuilder { const proof = await VerifiableCredentialBuilder._signDigest(issuer, digest, this.version); - // NOTE: at this moment, the first proof is fullfiled, this maybe not enough because of multiple issuers + // NOTE: at this moment, the first proof is fulfilled, this maybe not enough because of multiple issuers // Use addIssuerProof() to add more proofs let vc: VerifiableCredential = { '@context': this['@context'], @@ -162,7 +162,7 @@ export class VerifiableCredentialBuilder { } /** - * set arrtibute `@context` + * set attribute `@context` */ public setContext(context: string[]): this { this['@context'] = context; @@ -171,7 +171,7 @@ export class VerifiableCredentialBuilder { } /** - * set arrtibute `version` + * set attribute `version` */ public setVersion(version: VerifiableCredentialVersion): this { this.version = version; @@ -180,7 +180,7 @@ export class VerifiableCredentialBuilder { } /** - * set arrtibute `issuanceDate` + * set attribute `issuanceDate` */ public setIssuanceDate(timestamp: number): this { this.issuanceDate = timestamp; @@ -189,7 +189,7 @@ export class VerifiableCredentialBuilder { } /** - * set arrtibute `expirationDate`, if you want to set the expiration date, pass `null` to this method. + * set attribute `expirationDate`, if you want to set the expiration date, pass `null` to this method. */ public setExpirationDate(timestamp: number | null): this { this.expirationDate = timestamp; @@ -198,7 +198,7 @@ export class VerifiableCredentialBuilder { } /** - * set arrtibute `raw` + * set attribute `raw` * @param rawIn instance of [[Raw]] */ public setRaw(rawIn: Raw): this { @@ -242,4 +242,118 @@ export class VerifiableCredentialBuilder { proofValue: base58Encode(signature) }; } + + /** + * + * build batch VerifiableCredential + * @static + * @param {VerifiableCredentialBuilder[]} builders + * @param {Did} issuer + * @return {*} {Promise[]>} + * @memberof VerifiableCredentialBuilder + */ + public static async batchBuild( + builders: VerifiableCredentialBuilder[], + issuer: Did + ): Promise[]> { + const digests: HexString[] = []; + const versions: VerifiableCredentialVersion[] = []; + const digestHashTypes: HashType[] = []; + const rootHashResults: RootHashResult[] = []; + + for (const builder of builders) { + const raw = builder.raw; + + assert(raw.checkSubject(), `Subject check failed when use ctype ${raw.ctype}`); + assert(builder.version, 'Unknown vc version.'); + + const rootHashResult: RootHashResult = calcRoothash(raw.contents, raw.hashType, builder.version, {}); + + const digestPayload: DigestPayload = { + rootHash: rootHashResult.rootHash, + expirationDate: builder.expirationDate || undefined, + holder: raw.owner, + ctype: raw.ctype.$id, + issuanceDate: builder.issuanceDate + }; + + const { digest, type: digestHashType } = calcDigest(builder.version, digestPayload, builder.digestHashType); + + rootHashResults.push(rootHashResult); + versions.push(builder.version); + digestHashTypes.push(digestHashType); + digests.push(digest); + } + + const proofs = await VerifiableCredentialBuilder._batchSignDigest(issuer, digests, versions); + + return proofs.map((proof, index) => { + const builder = builders[index]; + const rootHashResult = rootHashResults[index]; + + if ( + builder['@context'] && + builder.version && + builder.issuanceDate && + builder.digestHashType && + builder.expirationDate !== undefined + ) { + const vc: VerifiableCredential = { + '@context': builder['@context'], + version: builder.version, + ctype: builder.raw.ctype.$id, + issuanceDate: builder.issuanceDate, + credentialSubject: builder.raw.contents, + issuer: [issuer.id], + holder: builder.raw.owner, + hasher: [rootHashResult.type, digestHashTypes[index]], + digest: digests[index], + proof: [proof], + credentialSubjectHashes: rootHashResult.hashes, + credentialSubjectNonceMap: rootHashResult.nonceMap + }; + + if (builder.expirationDate) { + vc.expirationDate = builder.expirationDate; + } + + return vc; + } + + throw new Error('Can not to build batch VerifiableCredentials'); + }); + } + + public static async _batchSignDigest( + did: Did, + digests: HexString[], + versions: VerifiableCredentialVersion[] + ): Promise { + const messages = digests.map((digest, index) => { + const version = versions[index]; + + if (version === '1') { + return signedVCMessage(digest, version); + } else if (version === '0' || version === '2') { + return digest; + } else { + const check: never = version; + + throw new Error(`VC Version invalid, the wrong VC Version is ${check}`); + } + }); + const signDidUrl: DidUrl = did.getKeyUrl('assertionMethod'); + + const signedData = await did.batchSignWithKey(messages, signDidUrl); + + return signedData.map(({ id, signature, type: signType }) => { + return { + type: signType, + created: Date.now(), + verificationMethod: id, + proofPurpose: 'assertionMethod', + proofValue: base58Encode(signature) + }; + }); + } }