diff --git a/jest.config.cjs b/jest.config.cjs index 4065637..b1d84cb 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -10,6 +10,7 @@ module.exports = Object.assign({}, config, { '@zcloak/did-resolver(.*)$': '/packages/did-resolver/src/$1', '@zcloak/did(.*)$': '/packages/did/src/$1', '@zcloak/keyring(.*)$': '/packages/keyring/src/$1', + '@zcloak/message(.*)$': '/packages/message/src/$1', '@zcloak/vc(.*)$': '/packages/vc/src/$1', '@zcloak/verify(.*)$': '/packages/verify/src/$1', '@zcloak/wasm(.*)$': '/packages/wasm/src/$1' diff --git a/package.json b/package.json index 1c0c761..e0bd589 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "devDependencies": { "@types/jest": "^27.4.0", - "@zcloak/dev": "^0.6.2" + "@zcloak/dev": "^0.6.3" }, "resolutions": { "typescript": "^4.8.4" diff --git a/packages/ctype/src/publish.spec.ts b/packages/ctype/src/publish.spec.ts index 1892147..b574067 100644 --- a/packages/ctype/src/publish.spec.ts +++ b/packages/ctype/src/publish.spec.ts @@ -1,7 +1,6 @@ // Copyright 2021-2022 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DidUrl } from '@zcloak/did-resolver/types'; import type { BaseCType } from './types'; import { generateMnemonic, initCrypto } from '@zcloak/crypto'; @@ -39,7 +38,7 @@ describe('publish ctype', (): void => { }; expect(getCTypeHash(base, publisher.id)).toEqual( - getCTypeHash(base, publisher.getKeyUrl('authentication') as DidUrl) + getCTypeHash(base, publisher.getKeyUrl('authentication')) ); }); diff --git a/packages/ctype/src/publish.ts b/packages/ctype/src/publish.ts index 9b98dff..0d274a7 100644 --- a/packages/ctype/src/publish.ts +++ b/packages/ctype/src/publish.ts @@ -34,7 +34,7 @@ export function getCTypeHash( export function getPublish(base: BaseCType, publisher: Did): CType { const hash = getCTypeHash(base, publisher.id); - const { id, signature } = publisher.signWithKey('authentication', hash); + const { id, signature } = publisher.signWithKey(hash, 'authentication'); return { $id: hash, diff --git a/packages/did-resolver/src/MockDidResolver.ts b/packages/did-resolver/src/MockDidResolver.ts new file mode 100644 index 0000000..28f029d --- /dev/null +++ b/packages/did-resolver/src/MockDidResolver.ts @@ -0,0 +1,27 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DidDocument, DidUrl } from './types'; + +import { DidResolver } from './DidResolver'; +import { DidNotFoundError } from './errors'; + +export class MockDidResolver extends DidResolver { + #map: Map = new Map(); + + public override resolve(didUrl: string): Promise { + const { did } = this.parseDid(didUrl); + + const document = this.#map.get(did); + + if (!document) { + throw new DidNotFoundError(); + } + + return Promise.resolve(document); + } + + public addDocument(document: DidDocument): void { + this.#map.set(document.id, document); + } +} diff --git a/packages/did-resolver/src/index.ts b/packages/did-resolver/src/index.ts index bcbc325..e4df3b5 100644 --- a/packages/did-resolver/src/index.ts +++ b/packages/did-resolver/src/index.ts @@ -3,3 +3,5 @@ export * from './DidResolver'; export * from './ArweaveDidResolver'; + +export * from './MockDidResolver'; diff --git a/packages/did/src/did/chain.ts b/packages/did/src/did/chain.ts index f0c0106..d6849bc 100644 --- a/packages/did/src/did/chain.ts +++ b/packages/did/src/did/chain.ts @@ -20,7 +20,7 @@ export abstract class DidChain extends DidDetails { const proof: DidDocumentProof[] = document.proof ?? []; - const { id, signature } = this.signWithKey('capabilityInvocation', hashDidDocument(document)); + const { id, signature } = this.signWithKey(hashDidDocument(document), 'capabilityInvocation'); proof.push({ id, signature: base58Encode(signature), type: 'creation' }); diff --git a/packages/did/src/did/details.ts b/packages/did/src/did/details.ts index b82a98f..87b1d98 100644 --- a/packages/did/src/did/details.ts +++ b/packages/did/src/did/details.ts @@ -10,13 +10,15 @@ import type { VerificationMethodType } from '@zcloak/did-resolver/types'; import type { KeypairType, KeyringPair } from '@zcloak/keyring/types'; -import type { IDidDetails, KeyRelationship } from '../types'; -import type { DidKeys, SignedData } from './types'; +import type { DidKeys, EncryptedData, IDidDetails, KeyRelationship, SignedData } from '../types'; import { assert } from '@polkadot/util'; import { base58Encode } from '@zcloak/crypto'; +import { DidResolver } from '@zcloak/did-resolver'; +import { defaultResolver } from '@zcloak/did-resolver/defaults'; +import { fromDid } from './helpers'; import { DidKeyring } from './keyring'; export function typeTransform(type: KeypairType): VerificationMethodType { @@ -67,37 +69,85 @@ export abstract class DidDetails extends DidKeyring implements IDidDetails { this.service = service; } - public getKeyUrl(key: DidKeys): DidUrl | undefined { - return Array.from(this[key] ?? [])[0]; + public getKeyUrl(key: DidKeys): DidUrl { + const didUrl = Array.from(this[key] ?? [])[0]; + + assert(didUrl, `Not find verification method with the key: ${key}`); + + return didUrl; } public get(id: DidUrl): KeyRelationship { const method = this.keyRelationship.get(id); - assert(method, `Not find verficationMethod with id ${id}`); + assert(method, `Not find verficationMethod with id: ${id}`); return method; } - public signWithKey(key: DidKeys, message: Uint8Array | HexString): SignedData { + public override signWithKey( + message: Uint8Array | HexString, + key: Exclude + ): SignedData { const didUrl = this.getKeyUrl(key); - assert(didUrl, `can not find verification method with the key: ${key}`); - - return this.signWithId(didUrl, message); + return this.sign(message, didUrl); } - public signWithId(id: DidUrl, message: Uint8Array | HexString): SignedData { - const { publicKey } = this.get(id); - const signature = this.sign(publicKey, message); + public override sign(message: Uint8Array | HexString, id: DidUrl): SignedData { + const { id: _id, publicKey } = this.get(id); + const pair = this.getPair(publicKey); + + const signature = pair.sign(message); return { signature, type: typeTransform(this.getPair(publicKey).type), - id + id: _id + }; + } + + public override async encrypt( + message: Uint8Array | HexString, + receiverUrlIn: DidUrl, + senderUrl: DidUrl = this.getKeyUrl('keyAgreement'), + resolver: DidResolver = defaultResolver + ): Promise { + const { id, publicKey } = this.get(senderUrl); + const pair = this.getPair(publicKey); + + const receiver = await fromDid(receiverUrlIn, undefined, resolver); + + const { id: receiverUrl, publicKey: receiverPublicKey } = receiver.get(receiverUrlIn); + + const encrypted = pair.encrypt(message, receiverPublicKey); + + return { + senderUrl: id, + receiverUrl, + type: 'X25519KeyAgreementKey2019', + data: encrypted }; } + public override async decrypt( + encryptedMessageWithNonce: Uint8Array | HexString, + senderUrlIn: DidUrl, + receiverUrl: DidUrl, + resolver: DidResolver = defaultResolver + ): Promise { + const { publicKey } = this.get(receiverUrl); + const pair = this.getPair(publicKey); + + const sender = await fromDid(senderUrlIn, undefined, resolver); + + const { publicKey: senderPublicKey } = sender.get(senderUrlIn); + + const decrypted = pair.decrypt(encryptedMessageWithNonce, senderPublicKey); + + return decrypted; + } + public getDocument(): DidDocument { assert(this.controller.size > 0, 'Must has one controller'); diff --git a/packages/did/src/did/helpers.ts b/packages/did/src/did/helpers.ts index bc42d40..baa7fab 100644 --- a/packages/did/src/did/helpers.ts +++ b/packages/did/src/did/helpers.ts @@ -68,7 +68,7 @@ export function parseDidDocument(document: DidDocument): IDidDetails { */ export async function fromDid( did: DidUrl, - keyring: KeyringInstance = new Keyring(), + keyring?: KeyringInstance, resolver: DidResolver = defaultResolver ): Promise { const document = await resolver.resolve(did); @@ -94,7 +94,7 @@ export async function fromDid( */ export function fromDidDocument( document: DidDocument, - keyring: KeyringInstance = new Keyring(), + keyring?: KeyringInstance, resolver: DidResolver = defaultResolver ): Did { const details = parseDidDocument(document); diff --git a/packages/did/src/did/index.spec.ts b/packages/did/src/did/index.spec.ts index f170216..3da7f42 100644 --- a/packages/did/src/did/index.spec.ts +++ b/packages/did/src/did/index.spec.ts @@ -1,7 +1,10 @@ // Copyright 2021-2022 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { ethereumEncode } from '@zcloak/crypto'; +import { stringToU8a } from '@polkadot/util'; + +import { ethereumEncode, generateMnemonic } from '@zcloak/crypto'; +import { MockDidResolver } from '@zcloak/did-resolver'; import { DidDocument } from '@zcloak/did-resolver/types'; import { Keyring } from '@zcloak/keyring'; @@ -33,7 +36,13 @@ const DOCUMENT: DidDocument = { service: [] }; +const resolver = new MockDidResolver(); + describe('Did', (): void => { + beforeAll(() => { + resolver.addDocument(DOCUMENT); + }); + describe('create', (): void => { let keyring: Keyring; @@ -59,6 +68,8 @@ describe('Did', (): void => { 'health correct setup usage father decorate curious copper sorry recycle skin equal'; const did = createEcdsaFromMnemonic(mnemonic, keyring); + resolver.addDocument(did.getDocument()); + expect(did.get([...(did.authentication ?? [])][0]).publicKey).toEqual(key0); expect(did.get([...(did.keyAgreement ?? [])][0]).publicKey).toEqual(key1); expect([...did.controller][0]).toEqual(`did:zk:${ethereumEncode(controllerKey)}`); @@ -84,4 +95,31 @@ describe('Did', (): void => { expect(document.service).toEqual(DOCUMENT.service); }); }); + + describe('encrypt and decrypt', (): void => { + it('encrypt and decrypt', async (): Promise => { + const sender = createEcdsaFromMnemonic(generateMnemonic(12)); + const receiver = createEcdsaFromMnemonic(generateMnemonic(12)); + + resolver.addDocument(sender.getDocument()); + resolver.addDocument(receiver.getDocument()); + + const message = stringToU8a('abcd'); + + const { + data: encrypted, + receiverUrl, + senderUrl + } = await sender.encrypt( + message, + receiver.getKeyUrl('keyAgreement'), + sender.getKeyUrl('keyAgreement'), + resolver + ); + + const decrypted = await receiver.decrypt(encrypted, senderUrl, receiverUrl, resolver); + + expect(decrypted).toEqual(message); + }); + }); }); diff --git a/packages/did/src/did/keyring.ts b/packages/did/src/did/keyring.ts index c275cdd..0929b13 100644 --- a/packages/did/src/did/keyring.ts +++ b/packages/did/src/did/keyring.ts @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import type { HexString } from '@zcloak/crypto/types'; +import type { DidResolver } from '@zcloak/did-resolver'; +import type { DidUrl } from '@zcloak/did-resolver/types'; import type { KeyringInstance } from '@zcloak/keyring/types'; -import type { IDidKeyring } from '../types'; +import type { DidKeys, EncryptedData, IDidKeyring, SignedData } from '../types'; import { assert } from '@polkadot/util'; @@ -20,30 +22,24 @@ export abstract class DidKeyring implements IDidKeyring { return this.#keyring.getPair(publicKey); } - public sign(publicKey: Uint8Array, message: Uint8Array | HexString): Uint8Array { - const pair = this.getPair(publicKey); + public abstract signWithKey( + message: Uint8Array | HexString, + key: Exclude + ): SignedData; - return pair.sign(message); - } + public abstract sign(message: Uint8Array | HexString, id: DidUrl): SignedData; - public encrypt( - publicKey: Uint8Array, + public abstract encrypt( message: Uint8Array | HexString, - recipientPublicKey: Uint8Array | HexString, - nonce?: Uint8Array | HexString | undefined - ): Uint8Array { - const pair = this.getPair(publicKey); - - return pair.encrypt(message, recipientPublicKey, nonce); - } + receiverUrl: DidUrl, + senderUrl?: DidUrl, + resolver?: DidResolver + ): Promise; - public decrypt( - publicKey: Uint8Array, + public abstract decrypt( encryptedMessageWithNonce: Uint8Array | HexString, - senderPublicKey: Uint8Array | HexString - ): Uint8Array { - const pair = this.getPair(publicKey); - - return pair.decrypt(encryptedMessageWithNonce, senderPublicKey); - } + senderUrl: DidUrl, + receiverUrl: DidUrl, + resolver?: DidResolver + ): Promise; } diff --git a/packages/did/src/did/types.ts b/packages/did/src/did/types.ts index c1b937f..e7c5f09 100644 --- a/packages/did/src/did/types.ts +++ b/packages/did/src/did/types.ts @@ -1,9 +1,6 @@ // Copyright 2021-2022 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DidUrl, VerificationMethodType } from '@zcloak/did-resolver/types'; - -// @internal generate keys export type KeyGen = { // the identifier publicKey identifier: Uint8Array; @@ -13,16 +10,3 @@ export type KeyGen = { */ keys: [Uint8Array, Uint8Array]; }; - -export type SignedData = { - id: DidUrl; - type: VerificationMethodType; - signature: Uint8Array; -}; - -export type DidKeys = - | 'authentication' - | 'assertionMethod' - | 'keyAgreement' - | 'capabilityInvocation' - | 'capabilityDelegation'; diff --git a/packages/did/src/types.ts b/packages/did/src/types.ts index b225cfe..9474357 100644 --- a/packages/did/src/types.ts +++ b/packages/did/src/types.ts @@ -2,9 +2,31 @@ // SPDX-License-Identifier: Apache-2.0 import type { HexString } from '@zcloak/crypto/types'; -import type { DidUrl, Service } from '@zcloak/did-resolver/types'; +import type { DidUrl, Service, VerificationMethodType } from '@zcloak/did-resolver/types'; import type { KeyringPair } from '@zcloak/keyring/types'; +import { DidResolver } from '@zcloak/did-resolver'; + +export type DidKeys = + | 'authentication' + | 'keyAgreement' + | 'assertionMethod' + | 'capabilityInvocation' + | 'capabilityDelegation'; + +export type SignedData = { + id: DidUrl; + type: VerificationMethodType; + signature: Uint8Array; +}; + +export type EncryptedData = { + senderUrl: DidUrl; + receiverUrl: DidUrl; + type: VerificationMethodType; + data: Uint8Array; +}; + export interface KeyRelationship { id: DidUrl; controller: DidUrl[]; @@ -25,16 +47,18 @@ export interface IDidDetails { export interface IDidKeyring { getPair(publicKey: Uint8Array): KeyringPair; - sign(publicKey: Uint8Array, message: HexString | Uint8Array): Uint8Array; + signWithKey(message: Uint8Array | HexString, key: Exclude): SignedData; + sign(message: Uint8Array | HexString, id: DidUrl): SignedData; encrypt( - publicKey: Uint8Array, message: HexString | Uint8Array, - recipientPublicKey: HexString | Uint8Array, - nonce?: HexString | Uint8Array - ): Uint8Array; + receiverUrl: DidUrl, + senderUrl?: DidUrl, + resolver?: DidResolver + ): Promise; decrypt( - publicKey: Uint8Array, encryptedMessageWithNonce: HexString | Uint8Array, - senderPublicKey: HexString | Uint8Array - ): Uint8Array; + senderUrl: DidUrl, + receiverUrl: DidUrl, + resolver?: DidResolver + ): Promise; } diff --git a/packages/message/LICENSE b/packages/message/LICENSE new file mode 100644 index 0000000..0d381b2 --- /dev/null +++ b/packages/message/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/message/README.md b/packages/message/README.md new file mode 100644 index 0000000..98dd784 --- /dev/null +++ b/packages/message/README.md @@ -0,0 +1 @@ +# @zcloak/message diff --git a/packages/message/package.json b/packages/message/package.json new file mode 100644 index 0000000..909148d --- /dev/null +++ b/packages/message/package.json @@ -0,0 +1,29 @@ +{ + "author": "zCloak", + "bugs": "https://github.com/zCloak-Network/zkid-sdk/issues", + "contributors": [], + "description": "zCloak message package", + "homepage": "https://github.com/zCloak-Network/zkid-sdk/tree/master/packages/message#readme", + "license": "Apache-2.0", + "maintainers": [], + "name": "@zcloak/message", + "repository": { + "directory": "packages/message", + "type": "git", + "url": "https://github.com/zCloak-Network/zkid-sdk.git" + }, + "sideEffects": false, + "type": "module", + "version": "0.0.1-12", + "main": "index.js", + "dependencies": { + "@polkadot/util": "^10.1.12", + "@zcloak/crypto": "0.0.1-12", + "@zcloak/did": "0.0.1-12", + "@zcloak/did-resolver": "0.0.1-12", + "@zcloak/vc": "0.0.1-12" + }, + "devDependencies": { + "@zcloak/ctype": "0.0.1-12" + } +} diff --git a/packages/message/src/decrypt/index.ts b/packages/message/src/decrypt/index.ts new file mode 100644 index 0000000..8485486 --- /dev/null +++ b/packages/message/src/decrypt/index.ts @@ -0,0 +1,150 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { IDidKeyring } from '@zcloak/did/types'; +import type { DidResolver } from '@zcloak/did-resolver'; +import type { BaseMessage, DecryptedMessage, Message, MessageData, MessageType } from '../types'; + +import { assert, isHex, isNumber, u8aToString } from '@polkadot/util'; + +import { decodeMultibase } from '@zcloak/crypto'; +import { isDidUrl, isSameUri } from '@zcloak/did/utils'; +import { defaultResolver } from '@zcloak/did-resolver/defaults'; +import { isRawCredential, isVC, isVP } from '@zcloak/vc/utils'; + +import { SUPPORT_MESSAGE_TYPES } from '../defaults'; + +export function verifyMessageBody(message: DecryptedMessage): void { + const { data, msgType, sender } = message; + + switch (msgType) { + case 'Request_Attestation': + assert( + isRawCredential(data), + `Expected message data with msgType:${msgType} is RawCredential object` + ); + assert(isSameUri(sender, data.holder), 'Message sender is not the holder of RawCredential'); + + break; + + case 'Response_Approve_Attestation': + assert( + isVC(data), + `Expected message data with msgType:${msgType} is VerifiableCredential object` + ); + assert( + isSameUri(sender, data.issuer), + 'Message sender is not the issuer of VerifiableCredential' + ); + + break; + + case 'Response_Reject_Attestation': + assert( + isDidUrl((data as any).holder) && isHex((data as any).ctype), + `Expected message data with msgType:${msgType} has required keys holder(DidUrl) and ctype(HexString)` + ); + + break; + + case 'Reqeust_VP': + break; + + case 'Response_Accept_VP': + assert( + isVP(data), + `Expected message data with msgType:${msgType} is VerifiablePresentation object` + ); + assert( + isSameUri(sender, data.proof.verificationMethod), + 'Message sender is not the signer of VerifiablePresentation' + ); + + break; + + case 'Response_Reject_VP': + break; + + case 'Send_VP': + assert( + isVP(data), + `Expected message data with msgType:${msgType} is VerifiablePresentation object` + ); + assert( + isSameUri(sender, data.proof.verificationMethod), + 'Message sender is not the signer of VerifiablePresentation' + ); + + break; + + case 'Send_issuedVC': + assert( + isVC(data), + `Expected message data with msgType:${msgType} is VerifiableCredential object` + ); + assert( + isSameUri(sender, data.issuer), + 'Message sender is not the issuer of VerifiableCredential' + ); + + break; + + default: + throw new Error('Unsupport msgType $:msgType}'); + } +} + +export function verifyMessageEnvelope(message: BaseMessage): void { + const { createTime, ctype, id, msgType, receiver, reply, sender } = message; + + assert(isHex(id), 'Expected id is hex string'); + + assert(isNumber(createTime), 'Expected createTime is number'); + + assert(isHex(ctype), 'Expected ctype is hex string'); + + assert(isHex(ctype), 'Expected ctype is hex string'); + + assert(SUPPORT_MESSAGE_TYPES.includes(msgType), `Unsupported msgType:${msgType}`); + + assert(isDidUrl(receiver), 'Expected receiver is DidUrl'); + + assert(isDidUrl(sender), 'Expected sender is DidUrl'); + + if (reply && !isHex(reply)) { + throw new Error('Expected reply is hex string'); + } +} + +export async function decryptMessage( + message: Message, + did: IDidKeyring, + resolver: DidResolver = defaultResolver +): Promise> { + verifyMessageEnvelope(message); + + const decrypted = await did.decrypt( + decodeMultibase(message.encryptedMsg), + message.sender, + message.receiver, + resolver + ); + + const data: MessageData[T] = JSON.parse(u8aToString(decrypted)); + + const decryptedMessage: DecryptedMessage = { + id: message.id, + reply: message.reply, + createTime: message.createTime, + version: message.version, + msgType: message.msgType, + sender: message.sender, + receiver: message.receiver, + ctype: message.ctype, + data + }; + + verifyMessageBody(decryptedMessage); + + return decryptedMessage; +} diff --git a/packages/message/src/defaults.ts b/packages/message/src/defaults.ts new file mode 100644 index 0000000..dd8249f --- /dev/null +++ b/packages/message/src/defaults.ts @@ -0,0 +1,17 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { MessageType, MessageVersion } from './types'; + +export const DEFAULT_MESSAGE_VERSION: MessageVersion = '1'; + +export const SUPPORT_MESSAGE_TYPES: MessageType[] = [ + 'Request_Attestation', + 'Response_Approve_Attestation', + 'Response_Reject_Attestation', + 'Reqeust_VP', + 'Response_Accept_VP', + 'Response_Reject_VP', + 'Send_VP', + 'Send_issuedVC' +]; diff --git a/packages/message/src/encrypt/index.ts b/packages/message/src/encrypt/index.ts new file mode 100644 index 0000000..63c37da --- /dev/null +++ b/packages/message/src/encrypt/index.ts @@ -0,0 +1,77 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '@zcloak/crypto/types'; +import type { IDidKeyring } from '@zcloak/did/types'; +import type { DidResolver } from '@zcloak/did-resolver'; +import type { DidUrl } from '@zcloak/did-resolver/types'; +import type { Message, MessageData, MessageType } from '../types'; + +import { stringToU8a } from '@polkadot/util'; + +import { base58Encode, randomAsHex } from '@zcloak/crypto'; +import { defaultResolver } from '@zcloak/did-resolver/defaults'; + +import { DEFAULT_MESSAGE_VERSION } from '../defaults'; + +function getCtype(type: T, data: MessageData[T]): HexString | undefined { + switch (type) { + case 'Request_Attestation': + return (data as MessageData['Request_Attestation']).ctype; + + case 'Response_Approve_Attestation': + return (data as MessageData['Response_Approve_Attestation']).ctype; + + case 'Response_Reject_Attestation': + return (data as MessageData['Response_Reject_Attestation']).ctype; + + case 'Reqeust_VP': + return (data as MessageData['Reqeust_VP']).ctypes?.[0]; + + case 'Response_Accept_VP': + return (data as MessageData['Request_Attestation']).ctype; + + case 'Response_Reject_VP': + return (data as MessageData['Response_Reject_VP']).ctypes?.[0]; + + case 'Send_VP': + return (data as MessageData['Send_VP']).verifiableCredential?.[0]?.ctype; + + case 'Send_issuedVC': + return (data as MessageData['Send_issuedVC']).ctype; + + default: + return undefined; + } +} + +export async function encryptMessage( + type: T, + data: MessageData[T], + sender: IDidKeyring, + receiverUrl: DidUrl, + reply?: string, + resolver: DidResolver = defaultResolver +): Promise> { + const id = randomAsHex(32); + const createTime = Date.now(); + const version = DEFAULT_MESSAGE_VERSION; + + const message = stringToU8a(JSON.stringify(data)); + + const encrypted = await sender.encrypt(message, receiverUrl, undefined, resolver); + + const ctype = getCtype(type, data); + + return { + id, + reply, + createTime, + version, + msgType: type, + sender: encrypted.senderUrl, + receiver: encrypted.receiverUrl, + ctype, + encryptedMsg: base58Encode(encrypted.data) + }; +} diff --git a/packages/message/src/index.ts b/packages/message/src/index.ts new file mode 100644 index 0000000..a1159d9 --- /dev/null +++ b/packages/message/src/index.ts @@ -0,0 +1,4 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export {}; diff --git a/packages/message/src/message.spec.ts b/packages/message/src/message.spec.ts new file mode 100644 index 0000000..0a7559e --- /dev/null +++ b/packages/message/src/message.spec.ts @@ -0,0 +1,120 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CType } from '@zcloak/ctype/types'; +import type { RawCredential } from '@zcloak/vc/types'; + +import { generateMnemonic, initCrypto } from '@zcloak/crypto'; +import { getPublish } from '@zcloak/ctype'; +import { Did, helpers } from '@zcloak/did'; +import { MockDidResolver } from '@zcloak/did-resolver'; +import { Raw, VerifiableCredentialBuilder } from '@zcloak/vc'; + +import { decryptMessage } from './decrypt'; +import { encryptMessage } from './encrypt'; + +describe('message encrypt and decrypt', (): void => { + const resolver = new MockDidResolver(); + let holder: Did; + let issuer: Did; + let rawCredential: RawCredential; + + let ctype: CType; + + beforeAll(async () => { + await initCrypto(); + holder = helpers.createEcdsaFromMnemonic(generateMnemonic(12)); + issuer = helpers.createEcdsaFromMnemonic(generateMnemonic(12)); + resolver.addDocument(holder.getDocument()); + resolver.addDocument(issuer.getDocument()); + ctype = getPublish( + { + title: 'Test', + description: 'Test', + type: 'object', + properties: { + name: { + type: 'string' + }, + age: { + type: 'integer' + }, + no: { + type: 'string' + } + }, + required: ['name', 'age'] + }, + issuer + ); + const raw = new Raw({ + contents: { + name: 'zCloak', + age: 1, + no: '1234' + }, + owner: holder.id, + ctype, + hashType: 'Rescue' + }); + + raw.calcRootHash(); + + rawCredential = raw.toRawCredential('Keccak256'); + }); + + describe('Send Attestation message types', () => { + it('Send Request_Attestation message', async () => { + const message = await encryptMessage( + 'Request_Attestation', + rawCredential, + holder, + issuer.getKeyUrl('keyAgreement'), + undefined, + resolver + ); + const decrypted = await decryptMessage(message, issuer, resolver); + + expect(decrypted.data).toEqual(rawCredential); + }); + + it('Send Response_Reject_Attestation message', async () => { + const message = await encryptMessage( + 'Response_Reject_Attestation', + { + reason: 'No reason', + ctype: ctype.$id, + holder: holder.id + }, + issuer, + holder.getKeyUrl('keyAgreement'), + undefined, + resolver + ); + const decrypted = await decryptMessage(message, holder, resolver); + + expect(decrypted.data).toEqual({ + reason: 'No reason', + ctype: ctype.$id, + holder: holder.id + }); + }); + + it('Send Response_Approve_Attestation message', async () => { + const vc = VerifiableCredentialBuilder.fromRawCredential(rawCredential, ctype) + .setExpirationDate(null) + .build(issuer); + const message = await encryptMessage( + 'Response_Approve_Attestation', + vc, + issuer, + holder.getKeyUrl('keyAgreement'), + undefined, + resolver + ); + const decrypted = await decryptMessage(message, holder, resolver); + + expect(decrypted.data).toEqual(vc); + }); + }); +}); diff --git a/packages/message/src/types.ts b/packages/message/src/types.ts new file mode 100644 index 0000000..9b203b0 --- /dev/null +++ b/packages/message/src/types.ts @@ -0,0 +1,60 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '@zcloak/crypto/types'; +import type { DidUrl } from '@zcloak/did-resolver/types'; + +import { RawCredential, VerifiableCredential, VerifiablePresentation } from '@zcloak/vc/types'; + +export type MessageVersion = '1'; + +export type Reject = { + reason: string; +} & T; + +export type RequestVP = { + issuers?: DidUrl[]; + ctypes?: HexString[]; +}; + +export type RejectAttestation = Reject<{ + ctype: HexString; + holder: DidUrl; +}>; + +export type RejectVP = Reject<{ + issuers?: DidUrl[]; + ctypes?: HexString[]; +}>; + +export type MessageData = { + Request_Attestation: RawCredential; + Response_Approve_Attestation: VerifiableCredential; + Response_Reject_Attestation: RejectAttestation; + Reqeust_VP: RequestVP; + Response_Accept_VP: VerifiablePresentation; + Response_Reject_VP: RejectVP; + Send_VP: VerifiablePresentation; + Send_issuedVC: VerifiableCredential; +}; + +export type MessageType = keyof MessageData; + +export interface BaseMessage { + id: string; + reply?: string; + createTime: number; + version: MessageVersion; + msgType: T; + sender: DidUrl; + receiver: DidUrl; + ctype?: HexString; +} + +export interface Message extends BaseMessage { + encryptedMsg: string; +} + +export interface DecryptedMessage extends BaseMessage { + data: MessageData[T]; +} diff --git a/packages/message/tsconfig.build.json b/packages/message/tsconfig.build.json new file mode 100644 index 0000000..fdf6a90 --- /dev/null +++ b/packages/message/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "..", + "outDir": "./build", + "rootDir": "./src" + }, + "exclude": [ + "**/*.spec.ts", + "**/test/**/*" + ], + "references": [ + { "path": "../crypto/tsconfig.build.json" }, + { "path": "../ctype/tsconfig.build.json" }, + { "path": "../did/tsconfig.build.json" }, + { "path": "../did-resolver/tsconfig.build.json" }, + { "path": "../vc/tsconfig.build.json" } + ] +} diff --git a/packages/vc/src/credential/raw.ts b/packages/vc/src/credential/raw.ts index 2165697..b06f934 100644 --- a/packages/vc/src/credential/raw.ts +++ b/packages/vc/src/credential/raw.ts @@ -13,6 +13,7 @@ import { validateSubject } from '@zcloak/ctype'; import { isDidUrl } from '@zcloak/did/utils'; import { calcRoothash, RootHashResult } from '../rootHash'; +import { isRawCredential } from '../utils'; /** * [[IRaw]] implements @@ -39,6 +40,7 @@ export class Raw implements IRaw { public nonceMap?: Record; public static fromRawCredential(rawCredential: RawCredential, ctype: CType): Raw { + assert(isRawCredential(rawCredential), 'input is not a RawCredential object'); assert(!isHex(rawCredential.credentialSubject), 'credentialSubject can not be hex string'); assert(ctype.$id === rawCredential.ctype, '`ctype` is not the raw credential ctype'); diff --git a/packages/vc/src/credential/vc.ts b/packages/vc/src/credential/vc.ts index 884b876..3c16373 100644 --- a/packages/vc/src/credential/vc.ts +++ b/packages/vc/src/credential/vc.ts @@ -17,7 +17,7 @@ import { Did } from '@zcloak/did'; import { DEFAULT_CONTEXT, DEFAULT_VC_VERSION } from '../defaults'; import { calcDigest } from '../digest'; -import { keyTypeToSignatureType } from '../utils'; +import { isRawCredential, keyTypeToSignatureType } from '../utils'; import { Raw } from './raw'; /** @@ -66,6 +66,7 @@ export class VerifiableCredentialBuilder { rawCredential: RawCredential, ctype: CType ): VerifiableCredentialBuilder { + assert(isRawCredential(rawCredential), 'input is not a RawCredential object'); assert(ctype.$id === rawCredential.ctype, '`ctype` is not the raw credential ctype'); const raw = Raw.fromRawCredential(rawCredential, ctype); @@ -105,7 +106,7 @@ export class VerifiableCredentialBuilder { this.digestHashType ); - const { id, signature, type: keyType } = issuer.signWithKey('assertionMethod', digest); + const { id, signature, type: keyType } = issuer.signWithKey(digest, 'assertionMethod'); const proof: Proof = { type: keyTypeToSignatureType(keyType), diff --git a/packages/vc/src/defaults.ts b/packages/vc/src/defaults.ts index 05457bb..34031c8 100644 --- a/packages/vc/src/defaults.ts +++ b/packages/vc/src/defaults.ts @@ -1,7 +1,13 @@ // Copyright 2021-2022 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { HashType, VerifiableCredentialVersion, VerifiablePresentationVersion } from './types'; +import type { + HashType, + SignatureType, + VerifiableCredentialVersion, + VerifiablePresentationType, + VerifiablePresentationVersion +} from './types'; export const DEFAULT_VC_VERSION: VerifiableCredentialVersion = '0'; @@ -14,3 +20,24 @@ export const DEFAULT_DIGEST_HASH_TYPE: HashType = 'Keccak256'; export const DEFAULT_ROOT_HASH_TYPE: HashType = 'Rescue'; export const DEFAULT_VP_HASH_TYPE: HashType = 'Keccak256'; + +export const ALL_HASH_TYPES: HashType[] = [ + 'Rescue', + 'Blake3', + 'Blake2', + 'Keccak256', + 'Keccak512', + 'Sha256', + 'Sha512' +]; + +export const ALL_VP_TYPES: VerifiablePresentationType[] = [ + 'VP', + 'VP_Digest', + 'VP_SelectiveDisclosure' +]; + +export const ALL_SIG_TYPES: SignatureType[] = [ + 'EcdsaSecp256k1Signature2019', + 'Ed25519Signature2018' +]; diff --git a/packages/vc/src/is.spec.ts b/packages/vc/src/is.spec.ts new file mode 100644 index 0000000..ae8051d --- /dev/null +++ b/packages/vc/src/is.spec.ts @@ -0,0 +1,200 @@ +// Copyright 2021-2022 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { VerifiableCredential } from './types'; + +import { isProof, isVC, isVP } from './utils'; + +const fullVC: VerifiableCredential = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + version: '0', + ctype: '0xc79824e312467b9d38f5448aef37791ac9d45e5c66267eb19f327005a45fb3d4', + issuanceDate: 1668362860149, + credentialSubject: { + name: 'zCloak', + age: 1, + birthday: 1668362860149, + links: ['https://zcloak.network', 'https://zkid.app'] + }, + credentialSubjectNonceMap: { + '0x88af5a7ba28c1de54ebd589dea81d30caa3f467646f6d714c0d2604599d63e1e': + '0xb5c9ca01565bb3b0a81b846a0cd32095e48e61fa13ae0bf5ef68666d2b39fb52', + '0x6bc2ebdc9345c21f804ef735209993cb8e5b3755af0650c41aa164a9faf674bd': + '0x70c2201247382a5896533e4df4b62e90ef1c56def63ad927e81266b6d5365817', + '0x95287a551283a931614166c5698c28e437f1e0c734b2a31d652aca0d4d8a7bc1': + '0x1aec028b67166967b9f8341376671f6d197ba9f7e34b07df904b5947dfd21b1a', + '0x116ad0bc048d66633b9f5be46d715f32978a31dd83935a13f1a859b82099f9d4': + '0xb06bd71a7e17fb019d2eed7f3f0c81f8c16f7098215250bd8f2f2832b2ad53ce' + }, + credentialSubjectHashes: [ + '0x7c50940e02707b8ec244379dbdb5ca9de0dc725ce71a9325dd4a81d7f3cc0e0b', + '0x928901c5e6ce37f97184299089c446dd5fa2817bf1cbd7237cb4449ead9986dd', + '0x35dc57dfbb72ce0ff49c2dff190ad49156bd37b97a84c81791518522cf311caf', + '0xcbdfd21263dcc1739b42aedf703fbb3038bb06b4afd8e3dd8872c05d7f663e9e' + ], + issuer: 'did:zk:0xf02AC70b695b3211813a207d937719D22BeC04a7', + holder: 'did:zk:0xC68B9B2250Cbb10e08CABCaDEF2383e76b4e4b59', + hasher: ['Rescue', 'Keccak256'], + digest: '0x01f28d1bc9860880ab068bf10d2fb389f2080b6ba9a37bea693051027ef9525f', + proof: [ + { + type: 'EcdsaSecp256k1Signature2019', + created: 1668362860153, + verificationMethod: 'did:zk:0xf02AC70b695b3211813a207d937719D22BeC04a7#key-0', + proofPurpose: 'assertionMethod', + proofValue: + 'z27SZMWaWxSrVC3GCB4YNMy63TkdFjUUeJ2aKsQGorK9Btt4v4yDU3HRZySGUL9rCP2r3DBa3nDNQyxW2L1pyYy13W' + } + ] +}; + +const vcWithExpiration: VerifiableCredential = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + version: '0', + ctype: '0xa69ef32aabc84331421bc553ec5b85b6bee54789985b66c679ad68db5d69bb6a', + issuanceDate: 1668362768974, + credentialSubject: { + name: 'zCloak', + age: 1, + birthday: 1668362768974, + links: ['https://zcloak.network', 'https://zkid.app'] + }, + credentialSubjectNonceMap: { + '0x88af5a7ba28c1de54ebd589dea81d30caa3f467646f6d714c0d2604599d63e1e': + '0xbc578bedb386eb9967ebc757f1b41623232d392f3ffb77bec5f5c20d90932111', + '0x6bc2ebdc9345c21f804ef735209993cb8e5b3755af0650c41aa164a9faf674bd': + '0x3c79951e8af409db8e716c4ae9ef2de24fb1d53b4e3a3eff15c1a8a20066f1e2', + '0x788b6723ee6570f774188c4e7765c425a82e83439b690e009fc8cacc06790e05': + '0xb2fd6f3580b2283a7d7cf9277b4adef087beff23815d783bded8cf9276eab14c', + '0x116ad0bc048d66633b9f5be46d715f32978a31dd83935a13f1a859b82099f9d4': + '0x8c584d0a3e05740257dbf4a5d008283e6438f291382f706d24ab7f23a6d73504' + }, + credentialSubjectHashes: [ + '0x3599f44bce81f54bb5fdfcf8292bf36ce8e94e002fe5a2d7323129fe50b7879a', + '0xcb680c9cb52bae0abb440861dc9ab9687fc966dba9b02f5f37c612f9b99113b9', + '0x12a2896d4d2541bc1ed3d3aa9a4f223d9f2a77a6a62bd7c409d0d2fc590f84e8', + '0x52db0b0477d1f654349df3e35bb6bd92f8f5cb05f3cedf27ae3bc4063ad7e6fd' + ], + issuer: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb', + holder: 'did:zk:0xbBf6b56C5606Ce2D24dec5BbbD11442c6E03ca3F', + hasher: ['Rescue', 'Keccak256'], + digest: '0x523fa24c9e82018b19ae004ebb5b07a66b762bf05ddc4a35177f56635d35eb44', + proof: [ + { + type: 'EcdsaSecp256k1Signature2019', + created: 1668362768979, + verificationMethod: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb#key-0', + proofPurpose: 'assertionMethod', + proofValue: + 'zx4yCEGm6tbZ7bDVLkZor5UrmSDjbLdL1YCVbYfteA4bwi5ZsdpW5CZHtFgvMiukWSpfQpXyjvrfaa9wZ7k2k1tHd' + } + ], + expirationDate: 1668362768974 +}; + +describe('test is functions', (): void => { + it('is proof object', (): void => { + expect( + isProof({ + type: 'EcdsaSecp256k1Signature2019', + created: 1668362768979, + verificationMethod: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb#key-0', + proofPurpose: 'assertionMethod', + proofValue: + 'zx4yCEGm6tbZ7bDVLkZor5UrmSDjbLdL1YCVbYfteA4bwi5ZsdpW5CZHtFgvMiukWSpfQpXyjvrfaa9wZ7k2k1tHd' + }) + ).toBe(true); + expect( + isProof({ + type: 'ecdsa', + created: 1668362768979, + verificationMethod: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb#key-0', + proofPurpose: 'assertionMethod', + proofValue: + 'zx4yCEGm6tbZ7bDVLkZor5UrmSDjbLdL1YCVbYfteA4bwi5ZsdpW5CZHtFgvMiukWSpfQpXyjvrfaa9wZ7k2k1tHd' + }) + ).toBe(false); + expect( + isProof({ + type: 'EcdsaSecp256k1Signature2019', + created: 1668362768979, + verificationMethod: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb#key-0', + proofPurpose: 'assertionMethod', + proofValue: 'proofValue' + }) + ).toBe(false); + }); + + it('is vc object', (): void => { + expect(isVC(fullVC)).toBe(true); + expect(isVC(vcWithExpiration)).toBe(true); + expect( + isVC({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + version: '0', + ctype: '0xa69ef32aabc84331421bc553ec5b85b6bee54789985b66c679ad68db5d69bb6a', + issuanceDate: 1668362768974, + credentialSubject: { + name: 'zCloak', + age: 1, + birthday: 1668362768974, + links: ['https://zcloak.network', 'https://zkid.app'] + }, + issuer: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb', + holder: 'did:zk', + hasher: ['Rescue', 'Keccak256'], + digest: '0x523fa24c9e82018b19ae004ebb5b07a66b762bf05ddc4a35177f56635d35eb44', + proof: [ + { + type: 'EcdsaSecp256k1Signature2019', + created: 1668362768979, + verificationMethod: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb#key-0', + proofPurpose: 'assertionMethod', + proofValue: + 'zx4yCEGm6tbZ7bDVLkZor5UrmSDjbLdL1YCVbYfteA4bwi5ZsdpW5CZHtFgvMiukWSpfQpXyjvrfaa9wZ7k2k1tHd' + } + ], + expirationDate: 1668362768974 + }) + ).toBe(false); + }); + + it('is vp object', (): void => { + expect( + isVP({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + version: '0', + type: ['VP'], + verifiableCredential: [fullVC], + id: '0xc79824e312467b9d38f5448aef37791ac9d45e5c66267eb19f327005a45fb3d4', + proof: { + type: 'EcdsaSecp256k1Signature2019', + created: 1668362768979, + verificationMethod: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb#key-0', + proofPurpose: 'assertionMethod', + proofValue: + 'zx4yCEGm6tbZ7bDVLkZor5UrmSDjbLdL1YCVbYfteA4bwi5ZsdpW5CZHtFgvMiukWSpfQpXyjvrfaa9wZ7k2k1tHd' + }, + hasher: ['Keccak256'] + }) + ).toBe(true); + expect( + isVP({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + version: '0', + type: ['VP', 'VP'], + verifiableCredential: [fullVC], + id: '0xc79824e312467b9d38f5448aef37791ac9d45e5c66267eb19f327005a45fb3d4', + proof: { + type: 'EcdsaSecp256k1Signature2019', + created: 1668362768979, + verificationMethod: 'did:zk:0x565ee4a279Ad611010DF17082220987CcaD381fb#key-0', + proofPurpose: 'assertionMethod', + proofValue: + 'zx4yCEGm6tbZ7bDVLkZor5UrmSDjbLdL1YCVbYfteA4bwi5ZsdpW5CZHtFgvMiukWSpfQpXyjvrfaa9wZ7k2k1tHd' + }, + hasher: ['Keccak256'] + }) + ).toBe(false); + }); +}); diff --git a/packages/vc/src/types.ts b/packages/vc/src/types.ts index a9f287a..e732a4b 100644 --- a/packages/vc/src/types.ts +++ b/packages/vc/src/types.ts @@ -4,6 +4,8 @@ import type { HexString } from '@zcloak/crypto/types'; import type { DidUrl } from '@zcloak/did-resolver/types'; +import { DidKeys } from '@zcloak/did/types'; + export type NativeType = string | number | boolean | null | undefined; export type NativeTypeWithOutNull = Omit; @@ -28,13 +30,6 @@ export type ProofType = SignatureType; export type VerifiablePresentationType = 'VP' | 'VP_Digest' | 'VP_SelectiveDisclosure'; -export type ProofPurpose = - | 'authentication' - | 'assertionMethod' - | 'keyAgreement' - | 'capabilityInvocation' - | 'capabilityDelegation'; - export type VerifiableCredentialVersion = '0'; export type VerifiablePresentationVersion = '0'; @@ -43,7 +38,7 @@ export interface Proof { type: ProofType; created: number; verificationMethod: DidUrl; - proofPurpose: ProofPurpose; + proofPurpose: DidKeys; proofValue: string; challenge?: string; } diff --git a/packages/vc/src/utils.ts b/packages/vc/src/utils.ts index c0a19fd..d2170f8 100644 --- a/packages/vc/src/utils.ts +++ b/packages/vc/src/utils.ts @@ -2,10 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 import type { VerificationMethodType } from '@zcloak/did-resolver/types'; -import type { HashType, NativeType, NativeTypeWithOutNull, SignatureType } from './types'; +import type { + HashType, + NativeType, + NativeTypeWithOutNull, + Proof, + RawCredential, + SignatureType, + VerifiableCredential, + VerifiablePresentation, + VerifiablePresentationType +} from './types'; import { encode, Input } from '@ethereumjs/rlp'; +import { + isArray, + isHex, + isJsonObject, + isNull, + isNumber, + isString, + isUndefined +} from '@polkadot/util'; +import { isBase32, isBase58, isBase64 } from '@zcloak/crypto'; +import { isDidUrl } from '@zcloak/did/utils'; + +import { ALL_HASH_TYPES, ALL_SIG_TYPES, ALL_VP_TYPES } from './defaults'; import { HASHER } from './hasher'; export function keyTypeToSignatureType(type: VerificationMethodType): SignatureType { @@ -39,3 +62,71 @@ export function rlpEncode( return HASHER[hashType](result); } } + +export function isHashType(input: unknown): input is HashType { + return ALL_HASH_TYPES.includes(input as any); +} + +export function isVpType(input: unknown): input is VerifiablePresentationType { + return ALL_VP_TYPES.includes(input as any); +} + +export function isSignatureType(input: unknown): input is SignatureType { + return ALL_SIG_TYPES.includes(input as any); +} + +export function isProof(input: unknown): input is Proof { + return ( + isJsonObject(input) && + isSignatureType(input.type) && + isNumber(input.created) && + isDidUrl(input.verificationMethod) && + isString(input.proofPurpose) && + (isBase58(input.proofValue) || isBase64(input.proofValue) || isBase32(input.proofValue)) + ); +} + +export function isRawCredential(input: unknown): input is RawCredential { + return ( + isJsonObject(input) && + isHex(input.ctype) && + (isHex(input.credentialSubject) || isJsonObject(input.credentialSubject)) && + isArray(input.credentialSubjectHashes) && + isJsonObject(input.credentialSubjectNonceMap) && + isDidUrl(input.holder) && + isHashType(input.hasher?.[0]) && + isHashType(input.hasher?.[1]) + ); +} + +export function isVC(input: unknown): input is VerifiableCredential { + return ( + isJsonObject(input) && + isArray(input['@context']) && + isString(input.version) && + isNumber(input.issuanceDate) && + (isUndefined(input.expirationDate) || + isNull(input.expirationDate) || + isNumber(input.expirationDate)) && + isDidUrl(input.issuer) && + isHex(input.digest) && + isArray(input.proof) && + !input.proof.map((p) => isProof(p)).includes(false) && + isRawCredential(input) + ); +} + +export function isVP(input: unknown): input is VerifiablePresentation { + return ( + isJsonObject(input) && + isArray(input['@context']) && + isArray(input.type) && + isArray(input.verifiableCredential) && + input.type.length === input.verifiableCredential.length && + !input.type.map((type) => isVpType(type)).includes(false) && + !input.verifiableCredential.map((vc) => isVC(vc)).includes(false) && + isHex(input.id) && + isHashType(input.hasher?.[0]) && + isProof(input.proof) + ); +} diff --git a/packages/vc/src/vp.ts b/packages/vc/src/vp.ts index 2cb78f2..c7bc93b 100644 --- a/packages/vc/src/vp.ts +++ b/packages/vc/src/vp.ts @@ -17,7 +17,7 @@ import { isSameUri } from '@zcloak/did/utils'; import { DEFAULT_CONTEXT, DEFAULT_VP_HASH_TYPE } from './defaults'; import { calcRoothash } from './rootHash'; -import { keyTypeToSignatureType, rlpEncode } from './utils'; +import { isVC, keyTypeToSignatureType, rlpEncode } from './utils'; // @internal // transform Verifiable Credential by [[VerifiablePresentationType]] @@ -112,6 +112,7 @@ export class VerifiablePresentationBuilder { isSameUri(this.#did.id, vc.holder), `the did "${this.#did.id}" is not the holder of "${vc.digest}" VC` ); + assert(isVC(vc), 'input `vc` is not a VerifiableCredential object'); this.vcMap.set(transformVC(vc, vpType, selectedAttributes), vpType); @@ -143,7 +144,7 @@ export class VerifiablePresentationBuilder { id, signature, type: signType - } = this.#did.signWithKey('authentication', u8aConcat(hash, stringToU8a(challenge))); + } = this.#did.signWithKey(u8aConcat(hash, stringToU8a(challenge)), 'authentication'); return { '@context': DEFAULT_CONTEXT, diff --git a/packages/verify/src/vcVerify.ts b/packages/verify/src/vcVerify.ts index dc24271..36e21e9 100644 --- a/packages/verify/src/vcVerify.ts +++ b/packages/verify/src/vcVerify.ts @@ -9,7 +9,7 @@ import { assert, bufferToU8a, isHex, u8aConcat, u8aToHex } from '@polkadot/util' import { makeMerkleTree } from '@zcloak/vc'; import { HASHER } from '@zcloak/vc/hasher'; -import { rlpEncode } from '@zcloak/vc/utils'; +import { isVC, rlpEncode } from '@zcloak/vc/utils'; import { digestVerify } from './digestVerify'; import { proofVerify } from './proofVerify'; @@ -48,6 +48,8 @@ export async function vcVerify( vc: VerifiableCredential, didDocument?: DidDocument ): Promise { + assert(isVC(vc), 'input `vc` is not a VerifiableCredential'); + const { credentialSubject, credentialSubjectHashes, credentialSubjectNonceMap, hasher } = vc; assert(!isHex(credentialSubject), 'subject must be an object'); @@ -69,6 +71,8 @@ export async function vcVerifyDigest( vc: VerifiableCredential, didDocument?: DidDocument ): Promise { + assert(isVC(vc), 'input `vc` is not a VerifiableCredential'); + const { credentialSubject } = vc; assert(isHex(credentialSubject), 'subject must be an hash value'); diff --git a/packages/verify/src/vpVerify.ts b/packages/verify/src/vpVerify.ts index 8a30a62..0971f9e 100644 --- a/packages/verify/src/vpVerify.ts +++ b/packages/verify/src/vpVerify.ts @@ -9,8 +9,11 @@ import type { VerifiablePresentationType } from '@zcloak/vc/types'; +import { assert } from '@polkadot/util'; + import { isSameUri } from '@zcloak/did/utils'; import { hashDigests } from '@zcloak/vc'; +import { isVP } from '@zcloak/vc/utils'; import { proofVerify } from './proofVerify'; import { vcVerify, vcVerifyDigest } from './vcVerify'; @@ -38,6 +41,8 @@ const VERIFIERS: Record< * */ export async function vpVerify(vp: VerifiablePresentation): Promise { + assert(isVP(vp), 'input `vp` is not VerifiablePresentation object'); + const { hasher, id, proof, type, verifiableCredential } = vp; const idValid = idCheck( verifiableCredential.map(({ digest }) => digest), diff --git a/tsconfig.base.json b/tsconfig.base.json index 8d865c0..5b4896f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,6 +13,8 @@ "@zcloak/did-resolver/*": ["did-resolver/src/*"], "@zcloak/keyring": ["keyring/src"], "@zcloak/keyring/*": ["keyring/src/*"], + "@zcloak/message": ["message/src"], + "@zcloak/message/*": ["message/src/*"], "@zcloak/vc": ["vc/src"], "@zcloak/vc/*": ["vc/src/*"], "@zcloak/verify": ["verify/src"], diff --git a/tsconfig.build.json b/tsconfig.build.json index bbe4dbe..ec00f60 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -9,6 +9,7 @@ { "path": "./packages/did/tsconfig.build.json" }, { "path": "./packages/did-resolver/tsconfig.build.json" }, { "path": "./packages/keyring/tsconfig.build.json" }, + { "path": "./packages/message/tsconfig.build.json" }, { "path": "./packages/vc/tsconfig.build.json" }, { "path": "./packages/verify/tsconfig.build.json" }, { "path": "./packages/wasm/tsconfig.build.json" } diff --git a/yarn.lock b/yarn.lock index 0ab5432..abc54e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3565,9 +3565,9 @@ __metadata: languageName: unknown linkType: soft -"@zcloak/dev@npm:^0.6.2": - version: 0.6.2 - resolution: "@zcloak/dev@npm:0.6.2" +"@zcloak/dev@npm:^0.6.3": + version: 0.6.3 + resolution: "@zcloak/dev@npm:0.6.3" dependencies: "@babel/cli": ^7.19.3 "@babel/core": ^7.20.2 @@ -3595,7 +3595,7 @@ __metadata: "@rushstack/eslint-patch": ^1.2.0 "@typescript-eslint/eslint-plugin": ^5.42.1 "@typescript-eslint/parser": ^5.42.1 - "@zcloak/lint": 0.6.2 + "@zcloak/lint": 0.6.3 babel-jest: ^29.3.1 babel-plugin-module-extension-resolver: ^1.0.0-rc.2 babel-plugin-module-resolver: ^4.1.0 @@ -3661,7 +3661,7 @@ __metadata: zcloak-exec-rollup: scripts/zcloak-exec-rollup.mjs zcloak-exec-tsc: scripts/zcloak-exec-tsc.mjs zcloak-exec-webpack: scripts/zcloak-exec-webpack.mjs - checksum: e366543f0dd27d714cff9e72dba1904107da8b3318e4ea6a926e28cc5d6070f4e5bf6a9b617efe62e34a0f8523feed4f364e33e51ffa5b6db50e5eab8ee74024 + checksum: 88a3e233d6e1c1bf0655e8498059ec4187451b3c10e9915bb166e9c0057331a7f594ac0a5df25b71baeae9fbb136658b7a8b31b3dc028c766f9bbb4775f7f9de languageName: node linkType: hard @@ -3694,18 +3694,31 @@ __metadata: languageName: unknown linkType: soft -"@zcloak/lint@npm:0.6.2": - version: 0.6.2 - resolution: "@zcloak/lint@npm:0.6.2" +"@zcloak/lint@npm:0.6.3": + version: 0.6.3 + resolution: "@zcloak/lint@npm:0.6.3" dependencies: chalk: ^5.1.2 glob: ^8.0.3 parse-imports-ts: ^1.0.2 throat: ^6.0.1 - checksum: 36844d32f3f68c0b027d91789a2e12dfc798a5033467a9e468ef9d97525e22399143d0094925eb71f5edaf33326a3e8779129988b695fe0998c76f24ff3bf6e6 + checksum: 3701f6c8373ba7fa0c7fd4f1b04a201746cb1269aad91e101302d95a072b2dc45e27181e066ebf6d27608f7192c1dfab1129516d2d57689a7a7b70d5fdfbc352 languageName: node linkType: hard +"@zcloak/message@workspace:packages/message": + version: 0.0.0-use.local + resolution: "@zcloak/message@workspace:packages/message" + dependencies: + "@polkadot/util": ^10.1.12 + "@zcloak/crypto": 0.0.1-12 + "@zcloak/ctype": 0.0.1-12 + "@zcloak/did": 0.0.1-12 + "@zcloak/did-resolver": 0.0.1-12 + "@zcloak/vc": 0.0.1-12 + languageName: unknown + linkType: soft + "@zcloak/vc@0.0.1-12, @zcloak/vc@workspace:packages/vc": version: 0.0.0-use.local resolution: "@zcloak/vc@workspace:packages/vc" @@ -11231,7 +11244,7 @@ __metadata: resolution: "root-workspace-0b6124@workspace:." dependencies: "@types/jest": ^27.4.0 - "@zcloak/dev": ^0.6.2 + "@zcloak/dev": ^0.6.3 bin: "build:wasm": scripts/build-wasm.js languageName: unknown