Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add batch sign and batch encrypt #95

Merged
merged 2 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
};
});
}
}