Skip to content

Commit

Permalink
Merge pull request #95 from zCloak-Network/batch-sign
Browse files Browse the repository at this point in the history
feat: add batch sign and batch encrypt
  • Loading branch information
whgreate committed Oct 17, 2023
2 parents eb3e8d6 + da9ff25 commit e7b9858
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 11 deletions.
9 changes: 9 additions & 0 deletions .changeset/smooth-otters-sin.md
Original file line number Diff line number Diff line change
@@ -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
31 changes: 30 additions & 1 deletion login/did/src/LoginDid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<DidKeys, 'keyAgreement'>
): Promise<SignedData[]> {
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<EncryptedData[]> {
const encrypts = await this.provider.batchEncrypt(params);

return encrypts.map(({ data, receiverUrl, senderUrl, type }) => {
return {
data: hexToU8a(data),
receiverUrl,
senderUrl,
type
};
});
}
}
8 changes: 8 additions & 0 deletions login/providers/src/base/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,12 @@ export class BaseProvider extends Events<ProviderEvents> {
): Promise<any> {
return this.request('send_tx', { tx, providerUrl, keyOrDidUrl });
}

public async batchSign(params: RpcRequest<'batch_sign'>): Promise<RpcResponse<'batch_sign'>> {
return this.request('batch_sign', params);
}

public async batchEncrypt(params: RpcRequest<'batch_encrypt'>): Promise<RpcResponse<'batch_encrypt'>> {
return this.request('batch_encrypt', params);
}
}
10 changes: 7 additions & 3 deletions login/rpc-defines/src/defineZk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -78,12 +78,14 @@ export type ZkpGenRequest = {
publicInput?: string;
};

export type SendTx = {
export type SendTxParams = {
tx: UnsignedTransaction;
providerUrl: string;
keyOrDidUrl: DidUrl | Exclude<DidKeys, 'keyAgreement'>;
};

export type BatchSignParams = { keyId?: DidUrl | Exclude<DidKeys, 'keyAgreement'>; payload: HexString[] };

declare module '@zcloak/login-rpc/rpcs' {
interface Rpcs {
wallet_requestAuth: [undefined, boolean];
Expand All @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions protocol/did/src/did/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,26 @@ export abstract class DidKeyring extends DidDetails implements IDidKeyring {
id: _id
});
}

public batchSignWithKey(
messages: (Uint8Array | HexString)[] | Uint8Array[] | HexString[],
keyOrDidUrl: DidUrl | Exclude<DidKeys, 'keyAgreement'> = 'controller'
): Promise<SignedData[]> {
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<EncryptedData[]> {
const encrypts = params.map(({ message, receiver }) => this.encrypt(message, receiver, senderUrl, resolver));

return Promise.all(encrypts);
}
}
53 changes: 53 additions & 0 deletions protocol/vc/src/credential/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,57 @@ describe('VerifiableCredential', (): void => {
});
});
});

describe('Batch PrivateVerifiableCredential', () => {
it('build batch VC from Raw instance', async (): Promise<void> => {
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'
}
]
});
});
});
});
128 changes: 121 additions & 7 deletions protocol/vc/src/credential/vc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<boolean> = {
'@context': this['@context'],
Expand Down Expand Up @@ -162,7 +162,7 @@ export class VerifiableCredentialBuilder {
}

/**
* set arrtibute `@context`
* set attribute `@context`
*/
public setContext(context: string[]): this {
this['@context'] = context;
Expand All @@ -171,7 +171,7 @@ export class VerifiableCredentialBuilder {
}

/**
* set arrtibute `version`
* set attribute `version`
*/
public setVersion(version: VerifiableCredentialVersion): this {
this.version = version;
Expand All @@ -180,7 +180,7 @@ export class VerifiableCredentialBuilder {
}

/**
* set arrtibute `issuanceDate`
* set attribute `issuanceDate`
*/
public setIssuanceDate(timestamp: number): this {
this.issuanceDate = timestamp;
Expand All @@ -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;
Expand All @@ -198,7 +198,7 @@ export class VerifiableCredentialBuilder {
}

/**
* set arrtibute `raw`
* set attribute `raw`
* @param rawIn instance of [[Raw]]
*/
public setRaw(rawIn: Raw): this {
Expand Down Expand Up @@ -242,4 +242,118 @@ export class VerifiableCredentialBuilder {
proofValue: base58Encode(signature)
};
}

/**
*
* build batch VerifiableCredential<boolean>
* @static
* @param {VerifiableCredentialBuilder[]} builders
* @param {Did} issuer
* @return {*} {Promise<VerifiableCredential<boolean>[]>}
* @memberof VerifiableCredentialBuilder
*/
public static async batchBuild(
builders: VerifiableCredentialBuilder[],
issuer: Did
): Promise<VerifiableCredential<boolean>[]> {
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<VerifiableCredentialVersion> = {
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<boolean> = {
'@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<Proof[]> {
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)
};
});
}
}

0 comments on commit e7b9858

Please sign in to comment.