From 872ed4500aeefb8d5d68cca7b94b2248092b23cb Mon Sep 17 00:00:00 2001 From: zzcwoshizz Date: Mon, 13 Feb 2023 22:18:05 +0800 Subject: [PATCH] Data singing (#45) - add [eip712](https://eips.ethereum.org/EIPS/eip-712) typed struct data hashing. - add signTypedData for DidKeyring. - vc, vp, ctype, did-document supports signTypedData. - verify functions support TypedData. --- .changeset/hip-buckets-battle.md | 17 ++ packages/crypto/package.json | 4 +- packages/crypto/src/eip712/eip712.spec.ts | 109 ++++++++++ packages/crypto/src/eip712/eip712.ts | 199 ++++++++++++++++++ packages/crypto/src/eip712/index.ts | 4 + packages/crypto/src/eip712/types.ts | 24 +++ packages/crypto/src/index.ts | 1 + packages/crypto/src/keccak/index.ts | 2 +- packages/crypto/src/secp256k1/recover.ts | 4 +- packages/crypto/src/secp256k1/sign.ts | 8 +- packages/crypto/src/secp256k1/verify.spec.ts | 3 +- packages/crypto/src/secp256k1/verify.ts | 11 +- packages/ctype/src/publish.ts | 11 +- packages/ctype/src/types.ts | 4 +- packages/ctype/src/utils.ts | 24 +++ packages/did-resolver/src/types.ts | 7 + packages/did/src/did/chain.ts | 15 +- packages/did/src/did/keyring.ts | 93 ++++++-- packages/did/src/hasher.ts | 2 + packages/did/src/types.ts | 21 +- packages/did/src/utils.ts | 104 ++++++++- packages/keyring/src/keyring.spec.ts | 12 +- packages/message/package.json | 3 +- packages/message/src/decrypt/index.ts | 18 -- packages/message/src/encrypt/index.ts | 4 - packages/message/src/types.ts | 3 - packages/vc/src/credential/index.spec.ts | 6 +- packages/vc/src/credential/vc.ts | 30 ++- packages/vc/src/defaults.ts | 3 +- packages/vc/src/digest.ts | 2 +- packages/vc/src/is.ts | 2 +- packages/vc/src/types.ts | 11 +- packages/vc/src/utils.ts | 61 ++++-- packages/vc/src/vp.spec.ts | 14 +- packages/vc/src/vp.ts | 23 +- packages/verify/src/ctypeVerify.ts | 14 +- packages/verify/src/didVerify.ts | 27 ++- packages/verify/src/proofVerify.ts | 10 +- packages/verify/src/vcVerify.ts | 12 +- packages/verify/src/verifyDidDocumentProof.ts | 18 +- packages/verify/src/vpVerify.ts | 11 +- yarn.lock | 64 +++++- 42 files changed, 847 insertions(+), 168 deletions(-) create mode 100644 .changeset/hip-buckets-battle.md create mode 100644 packages/crypto/src/eip712/eip712.spec.ts create mode 100644 packages/crypto/src/eip712/eip712.ts create mode 100644 packages/crypto/src/eip712/index.ts create mode 100644 packages/crypto/src/eip712/types.ts create mode 100644 packages/ctype/src/utils.ts diff --git a/.changeset/hip-buckets-battle.md b/.changeset/hip-buckets-battle.md new file mode 100644 index 0000000..f876663 --- /dev/null +++ b/.changeset/hip-buckets-battle.md @@ -0,0 +1,17 @@ +--- +'@zcloak/did-resolver': minor +'@zcloak/keyring': minor +'@zcloak/message': minor +'@zcloak/crypto': minor +'@zcloak/verify': minor +'@zcloak/ctype': minor +'@zcloak/did': minor +'@zcloak/vc': minor +--- + +Data signing. + +- add [eip712](https://eips.ethereum.org/EIPS/eip-712) typed struct data hashing. +- add signTypedData for DidKeyring. +- vc, vp, ctype, did-document supports signTypedData. +- verify functions support TypedData. diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 1bbb3e5..bffe8b0 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -28,9 +28,11 @@ "bip39": "^3.0.4", "canonicalize": "^1.0.8", "ed2curve": "^0.3.0", + "ethereumjs-abi": "^0.6.8", "tweetnacl": "^1.0.3" }, "devDependencies": { - "@types/ed2curve": "^0.2.2" + "@types/ed2curve": "^0.2.2", + "@types/ethereumjs-abi": "^0.6.3" } } diff --git a/packages/crypto/src/eip712/eip712.spec.ts b/packages/crypto/src/eip712/eip712.spec.ts new file mode 100644 index 0000000..a4b4171 --- /dev/null +++ b/packages/crypto/src/eip712/eip712.spec.ts @@ -0,0 +1,109 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Keypair } from '../types'; + +import { u8aToHex, u8aToNumber } from '@polkadot/util'; + +import { ethereumEncode } from '../ethereum'; +import { initCrypto } from '../initCrypto'; +import { keccak256AsU8a } from '../keccak'; +import { secp256k1PairFromSeed, secp256k1Sign } from '../secp256k1'; +import { encodeData, encodeType, getMessage, structHash, typeHash } from './eip712'; + +const typedData = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' } + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' } + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' } + ] + }, + primaryType: 'Mail', + domain: { + name: 'Ether Mail', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' + }, + message: { + from: { + name: 'Cow', + wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' + }, + to: { + name: 'Bob', + wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' + }, + contents: 'Hello, Bob!' + } +}; + +describe('EIP-712', (): void => { + let pair: Keypair; + + beforeAll(async (): Promise => { + await initCrypto(); + pair = secp256k1PairFromSeed(keccak256AsU8a('cow')); + }); + + it('eip712 encodeType', () => { + expect(encodeType(typedData, 'Mail')).toBe( + 'Mail(Person from,Person to,string contents)Person(string name,address wallet)' + ); + }); + + it('eip712 typeHash', () => { + expect(u8aToHex(typeHash(typedData, 'Mail'))).toBe( + '0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2' + ); + }); + + it('eip712 encodeData', () => { + expect(u8aToHex(encodeData(typedData, typedData.primaryType, typedData.message))).toBe( + '0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8' + ); + }); + + it('eip712 structHash', () => { + expect(u8aToHex(structHash(typedData, typedData.primaryType, typedData.message))).toBe( + '0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e' + ); + expect(u8aToHex(structHash(typedData, 'EIP712Domain', typedData.domain))).toBe( + '0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f' + ); + }); + + it('eip712 getMessage', () => { + expect(u8aToHex(getMessage(typedData, true))).toBe( + '0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2' + ); + }); + + it('eip712 getMessage signature', () => { + const message = getMessage(typedData, true); + + expect(ethereumEncode(pair.publicKey)).toBe( + ethereumEncode('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826') + ); + const signature = secp256k1Sign(message, pair); + + const v = signature.slice(-1); + const r = signature.slice(0, 32); + const s = signature.slice(32, 64); + + expect(u8aToNumber(v)).toBe(1); + expect(u8aToHex(r)).toBe('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d'); + expect(u8aToHex(s)).toBe('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562'); + }); +}); diff --git a/packages/crypto/src/eip712/eip712.ts b/packages/crypto/src/eip712/eip712.ts new file mode 100644 index 0000000..3bd790d --- /dev/null +++ b/packages/crypto/src/eip712/eip712.ts @@ -0,0 +1,199 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Reference +// https://eips.ethereum.org/EIPS/eip-712 +// https://eips.ethereum.org/assets/eip-712/Example.js + +import type { TypedData } from './types'; + +import { hexToU8a, isU8a, u8aConcat, u8aToBuffer, u8aToU8a } from '@polkadot/util'; +import abi from 'ethereumjs-abi'; + +import { keccak256AsU8a } from '../keccak'; + +const EIP_191_PREFIX = hexToU8a('0x1901'); + +export const ARRAY_REGEX = /^(.*)\[([0-9]*?)]$/; + +function encode(types: string[], values: any[]) { + return abi.rawEncode( + types, + // ethereumjs-abi not allowd uin8array + values.map((value) => (isU8a(value) ? u8aToBuffer(value) : value)) + ); +} + +/** + * @name getDependencies + * @description + * Get the dependencies of a struct type. If a struct has the same dependency multiple times, it's only included once + * in the resulting array. + */ +export function getDependencies( + typedData: TypedData, + type: string, + dependencies: string[] = [] +): string[] { + if (dependencies.includes(type)) { + return dependencies; + } + + if (!typedData.types[type]) { + return dependencies; + } + + return [ + type, + ...typedData.types[type].reduce( + (previous, type) => [ + ...previous, + ...getDependencies(typedData, type.type, previous).filter( + (dependency) => !previous.includes(dependency) + ) + ], + [] + ) + ]; +} + +/** + * @name encodeType + * @description + * Encode a type to a string. All dependant types are alphabetically sorted. + */ +export function encodeType(typedData: TypedData, type: string): string { + const [primary, ...dependencies] = getDependencies(typedData, type); + const types = [primary, ...dependencies.sort()]; + + return types + .map((dependency) => { + return `${dependency}(${typedData.types[dependency].map( + (type) => `${type.type} ${type.name}` + )})`; + }) + .join(''); +} + +/** + * @name typeHash + * @description + * Get a type string as hash. + */ +export function typeHash(typedData: TypedData, type: string): Uint8Array { + return keccak256AsU8a(encodeType(typedData, type)); +} + +/** + * @name encodeValue + * @description + * Encodes a single value to an ABI serialisable string, number or Buffer. Returns the data as tuple, which consists of + * an array of ABI compatible types, and an array of corresponding values. + */ +function encodeValue( + typedData: TypedData, + type: string, + data: unknown +): [string, string | Uint8Array | number] { + const match = type.match(ARRAY_REGEX); + + // Checks for array types + if (match) { + const arrayType = match[1]; + const length = Number(match[2]) || undefined; + + if (!Array.isArray(data)) { + throw new Error('Cannot encode data: value is not of array type'); + } + + if (length && data.length !== length) { + throw new Error(`Cannot encode data: expected length of ${length}, but got ${data.length}`); + } + + const encodedData = data.map((item) => encodeValue(typedData, arrayType, item)); + const types = encodedData.map((item) => item[0]); + const values = encodedData.map((item) => item[1]); + + return ['bytes32', keccak256AsU8a(encode(types, values))]; + } + + if (typedData.types[type]) { + return ['bytes32', structHash(typedData, type, data as Record)]; + } + + // Strings and arbitrary byte arrays are hashed to bytes32 + if (type === 'string') { + return ['bytes32', keccak256AsU8a(data as string)]; + } + + if (type === 'bytes') { + return ['bytes32', keccak256AsU8a(u8aToU8a(data as string))]; + } + + return [type, data as string]; +} + +/** + * @name encodeData + * @description + * Encode the data to an ABI encoded Buffer. The data should be a key -> value object with all the required values. All + * dependant types are automatically encoded. + */ +export function encodeData( + typedData: TypedData, + type: string, + data: Record +): Uint8Array { + const [types, values] = typedData.types[type].reduce<[string[], unknown[]]>( + ([types, values], field) => { + if (data[field.name] === undefined || data[field.name] === null) { + throw new Error(`Cannot encode data: missing data for '${field.name}'`); + } + + const value = data[field.name]; + const [type, encodedValue] = encodeValue(typedData, field.type, value); + + return [ + [...types, type], + [...values, encodedValue] + ]; + }, + [['bytes32'], [typeHash(typedData, type)]] + ); + + return encode(types, values); +} + +/** + * @name structHash + * @description + * Get encoded data as a hash. The data should be a key -> value object with all the required values. All dependant + * types are automatically encoded. + */ +export function structHash( + typedData: TypedData, + type: string, + data: Record +): Uint8Array { + return keccak256AsU8a(encodeData(typedData, type, data)); +} + +/** + * @name getMessage + * @description + * Get the EIP-191 encoded message to sign, from the typedData object. If `hash` is enabled, the message will be hashed + * with Keccak256. + */ +export function getMessage(typedData: TypedData, hash?: boolean): Uint8Array { + const message = u8aConcat( + EIP_191_PREFIX, + structHash(typedData, 'EIP712Domain', typedData.domain), + structHash(typedData, typedData.primaryType, typedData.message) + ); + + if (hash) { + return keccak256AsU8a(message); + } + + return message; +} diff --git a/packages/crypto/src/eip712/index.ts b/packages/crypto/src/eip712/index.ts new file mode 100644 index 0000000..880e015 --- /dev/null +++ b/packages/crypto/src/eip712/index.ts @@ -0,0 +1,4 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export * as eip712 from './eip712'; diff --git a/packages/crypto/src/eip712/types.ts b/packages/crypto/src/eip712/types.ts new file mode 100644 index 0000000..3d86382 --- /dev/null +++ b/packages/crypto/src/eip712/types.ts @@ -0,0 +1,24 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export interface DataTypeProperty { + name: string; + type: string; +} + +export interface DataTypes { + [additionalProperties: string]: DataTypeProperty[]; +} + +export interface TypedData { + types: DataTypes; + primaryType: string; + domain: { + name?: string; + version?: string; + chainId?: number; + verifyingContract?: string; + salt?: string; + }; + message: Record; +} diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 8eaf8ff..f9c0a43 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -5,6 +5,7 @@ export * from './blake2'; export * from './blake3'; export * from './blake3-2to1'; export * from './ed25519'; +export * from './eip712'; export * from './ethereum'; export * from './hmac'; export * from './json'; diff --git a/packages/crypto/src/keccak/index.ts b/packages/crypto/src/keccak/index.ts index e70e9cc..3750e07 100644 --- a/packages/crypto/src/keccak/index.ts +++ b/packages/crypto/src/keccak/index.ts @@ -1,4 +1,4 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -export { keccak256AsU8a, keccak512AsU8a } from './asU8a'; +export { keccak256AsHex, keccak512AsHex, keccak256AsU8a, keccak512AsU8a } from './asU8a'; diff --git a/packages/crypto/src/secp256k1/recover.ts b/packages/crypto/src/secp256k1/recover.ts index ecdf95b..bab6620 100644 --- a/packages/crypto/src/secp256k1/recover.ts +++ b/packages/crypto/src/secp256k1/recover.ts @@ -6,8 +6,6 @@ import type { HexString } from '@polkadot/util/types'; import { recoverPublicKey, Signature } from '@noble/secp256k1'; import { u8aToU8a } from '@polkadot/util'; -import { secp256k1Expand } from './expand'; - /** * @name secp256k1Recover * @description Recovers a publicKey from the supplied signature @@ -25,5 +23,5 @@ export function secp256k1Recover( throw new Error('Unable to recover publicKey from signature'); } - return secp256k1Expand(publicKey); + return publicKey; } diff --git a/packages/crypto/src/secp256k1/sign.ts b/packages/crypto/src/secp256k1/sign.ts index 0e25411..12d2fbe 100644 --- a/packages/crypto/src/secp256k1/sign.ts +++ b/packages/crypto/src/secp256k1/sign.ts @@ -7,7 +7,6 @@ import { Signature, signSync } from '@noble/secp256k1'; import { bnToU8a, u8aConcat } from '@polkadot/util'; import { BN_BE_256_OPTS } from '../bn'; -import { keccak256AsU8a } from '../keccak'; /** * @name secp256k1Sign @@ -21,9 +20,10 @@ export function secp256k1Sign( throw new Error('Expected valid secp256k1 secretKey, 32-bytes'); } - const data = keccak256AsU8a(message); - - const [sigBytes, recoveryParam] = signSync(data, secretKey, { canonical: true, recovered: true }); + const [sigBytes, recoveryParam] = signSync(message, secretKey, { + canonical: true, + recovered: true + }); const { r, s } = Signature.fromHex(sigBytes); return u8aConcat( diff --git a/packages/crypto/src/secp256k1/verify.spec.ts b/packages/crypto/src/secp256k1/verify.spec.ts index 79a0382..092586d 100644 --- a/packages/crypto/src/secp256k1/verify.spec.ts +++ b/packages/crypto/src/secp256k1/verify.spec.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { initCrypto } from '../initCrypto'; +import { keccak256AsU8a } from '../keccak'; import { secp256k1Verify } from '.'; const message = @@ -15,7 +16,7 @@ describe('secp256k1Verify', (): void => { it('validates known ETH against address', (): void => { expect( secp256k1Verify( - `\x19Ethereum Signed Message:\n${message.length.toString()}${message}`, + keccak256AsU8a(`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`), '0x55bd020bdbbdc02de34e915effc9b18a99002f4c29f64e22e8dcbb69e722ea6c28e1bb53b9484063fbbfd205e49dcc1f620929f520c9c4c3695150f05a28f52a01', '0x002309df96687e44280bb72c3818358faeeb699c' ) diff --git a/packages/crypto/src/secp256k1/verify.ts b/packages/crypto/src/secp256k1/verify.ts index a683f93..0c1dac1 100644 --- a/packages/crypto/src/secp256k1/verify.ts +++ b/packages/crypto/src/secp256k1/verify.ts @@ -3,10 +3,9 @@ import type { HexString } from '@polkadot/util/types'; -import { u8aEq, u8aToU8a } from '@polkadot/util'; +import { u8aToU8a } from '@polkadot/util'; import { ethereumEncode } from '../ethereum'; -import { keccak256AsU8a } from '../keccak'; import { secp256k1Recover } from './recover'; /** @@ -24,10 +23,10 @@ export function secp256k1Verify( throw new Error(`Expected signature with 65 bytes, ${sig.length} found instead`); } - const publicKey = secp256k1Recover(keccak256AsU8a(msgHash), sig, sig[64]); - const signerAddr = keccak256AsU8a(publicKey); - const inputAddr = u8aToU8a(ethereumEncode(publicKeyOrAddress)); + const publicKey = secp256k1Recover(msgHash, sig, sig[64]); + const signerAddr = ethereumEncode(publicKey); + const inputAddr = ethereumEncode(publicKeyOrAddress); // for Ethereum (keccak) the last 20 bytes is the address - return u8aEq(publicKey, inputAddr) || u8aEq(signerAddr.slice(-20), inputAddr.slice(-20)); + return signerAddr === inputAddr; } diff --git a/packages/ctype/src/publish.ts b/packages/ctype/src/publish.ts index e367dfc..fa9e53b 100644 --- a/packages/ctype/src/publish.ts +++ b/packages/ctype/src/publish.ts @@ -12,6 +12,7 @@ import { base58Encode, jsonCanonicalize, keccak256AsU8a } from '@zcloak/crypto'; import { parseDid } from '@zcloak/did-resolver/parseDid'; import { DEFAULT_CTYPE_SCHEMA } from './defaults'; +import { getPublishCTypeTypedData } from './utils'; export function getCTypeHash( base: BaseCType, @@ -34,13 +35,19 @@ export function getCTypeHash( export async function getPublish(base: BaseCType, publisher: Did): Promise { const hash = getCTypeHash(base, publisher.id); - const { id, signature } = await publisher.signWithKey(hash, 'authentication'); + const message = getPublishCTypeTypedData(hash); + const { + id, + signature, + type: signatureType + } = await publisher.signWithKey(message, 'authentication'); return { $id: hash, $schema: DEFAULT_CTYPE_SCHEMA, ...base, publisher: id, - signature: base58Encode(signature) + signature: base58Encode(signature), + signatureType }; } diff --git a/packages/ctype/src/types.ts b/packages/ctype/src/types.ts index 1319617..691b29d 100644 --- a/packages/ctype/src/types.ts +++ b/packages/ctype/src/types.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { HexString } from '@zcloak/crypto/types'; -import type { DidUrl } from '@zcloak/did-resolver/types'; +import type { DidUrl, SignatureType } from '@zcloak/did-resolver/types'; export type CTypeVersion = '1'; @@ -75,4 +75,6 @@ export interface CType extends BaseCType { $schema: string; publisher: DidUrl; signature: string; + // since `@zcloak/ctype@1.0.0` + signatureType?: SignatureType; } diff --git a/packages/ctype/src/utils.ts b/packages/ctype/src/utils.ts new file mode 100644 index 0000000..3b54d75 --- /dev/null +++ b/packages/ctype/src/utils.ts @@ -0,0 +1,24 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { TypedData } from '@zcloak/crypto/eip712/types'; + +export function getPublishCTypeTypedData(hash: string): TypedData { + return { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' } + ], + PublishCType: [{ name: 'hash', type: 'bytes' }] + }, + primaryType: 'PublishCType', + domain: { + name: 'PublishCType', + version: '0' + }, + message: { + hash + } + }; +} diff --git a/packages/did-resolver/src/types.ts b/packages/did-resolver/src/types.ts index 1c9368a..01a6132 100644 --- a/packages/did-resolver/src/types.ts +++ b/packages/did-resolver/src/types.ts @@ -20,6 +20,11 @@ export type VerificationMethodType = | 'EcdsaSecp256k1VerificationKey2019' | 'Ed25519VerificationKey2020'; +export type SignatureType = + | 'EcdsaSecp256k1Signature2019' + | 'EcdsaSecp256k1SignatureEip712' + | 'Ed25519Signature2018'; + export interface VerificationMethod { id: DidUrl; controller: DidUrl[]; @@ -36,6 +41,8 @@ export interface Service { export interface DidDocumentProof { signature: string; type: string; + // since `@zcloak/did-resolver@1.0.0` + signatureType?: SignatureType; id: DidUrl; } diff --git a/packages/did/src/did/chain.ts b/packages/did/src/did/chain.ts index 5332543..57556a0 100644 --- a/packages/did/src/did/chain.ts +++ b/packages/did/src/did/chain.ts @@ -13,7 +13,7 @@ import { assert } from '@polkadot/util'; import { base58Encode } from '@zcloak/crypto'; -import { hashDidDocument } from '../hasher'; +import { getPublishDocumentTypedData } from '../utils'; import { DidKeyring } from './keyring'; export abstract class DidChain extends DidKeyring { @@ -84,12 +84,15 @@ export abstract class DidChain extends DidKeyring { const proof: DidDocumentProof[] = document.proof ?? []; - const { id, signature } = await this.signWithKey( - hashDidDocument(document), - 'capabilityInvocation' - ); + const message = getPublishDocumentTypedData(document); - proof.push({ id, signature: base58Encode(signature), type: 'creation' }); + const { + id, + signature, + type: signatureType + } = await this.signWithKey(message, 'capabilityInvocation'); + + proof.push({ id, signature: base58Encode(signature), type: 'creation', signatureType }); return { ...document, diff --git a/packages/did/src/did/keyring.ts b/packages/did/src/did/keyring.ts index fae639c..d62180f 100644 --- a/packages/did/src/did/keyring.ts +++ b/packages/did/src/did/keyring.ts @@ -7,11 +7,13 @@ import type { DidUrl } from '@zcloak/did-resolver/types'; import type { KeyringInstance, KeyringPair } from '@zcloak/keyring/types'; import type { DidKeys, EncryptedData, IDidKeyring, SignedData } from '../types'; -import { assert } from '@polkadot/util'; +import { assert, isHex, isU8a } from '@polkadot/util'; +import { eip712, keccak256AsU8a } from '@zcloak/crypto'; +import { TypedData } from '@zcloak/crypto/eip712/types'; import { defaultResolver } from '@zcloak/did-resolver/defaults'; -import { typeTransform } from '../utils'; +import { isDidUrl } from '../utils'; import { DidDetails } from './details'; import { fromDid } from './helpers'; @@ -28,17 +30,14 @@ export abstract class DidKeyring extends DidDetails implements IDidKeyring { this.#keyring = keyring; } - public sign(message: Uint8Array | HexString, id: DidUrl): Promise { - const { id: _id, publicKey } = this.get(id); - const pair = this._getPair(publicKey); - - const signature = pair.sign(message); + /** + * DEPRECATED + * @since `@zcloak/did@1.0.0` + */ + public sign(): Promise { + console.warn('sign method deprecated in 1.0.0'); - return Promise.resolve({ - signature, - type: typeTransform(pair.type), - id: _id - }); + throw new Error('sign method deprecated in 1.0.0'); } public async encrypt( @@ -82,12 +81,72 @@ export abstract class DidKeyring extends DidDetails implements IDidKeyring { return decrypted; } - public signWithKey( - message: Uint8Array | HexString, - key: Exclude + public async signWithKey( + message: Uint8Array | HexString | TypedData, + keyOrDidUrl: DidUrl | Exclude ): Promise { - const didUrl = this.getKeyUrl(key); + const didUrl = isDidUrl(keyOrDidUrl) ? keyOrDidUrl : this.getKeyUrl(keyOrDidUrl); + const { type } = this.get(didUrl); + + assert( + type !== 'X25519KeyAgreementKey2019', + "sign method only call with key type: 'EcdsaSecp256k1VerificationKey2019', 'Ed25519VerificationKey2020'" + ); + + if (isU8a(message) || isHex(message)) { + if (type === 'EcdsaSecp256k1VerificationKey2019') { + console.warn( + `Using ${type} to sign signature is not a safe way, and it will be deprecat in a future version` + ); + message = keccak256AsU8a(message); + } + + const { id, signature } = await this._sign(message, didUrl); + + return { + id, + signature, + type: + type === 'EcdsaSecp256k1VerificationKey2019' + ? 'EcdsaSecp256k1Signature2019' + : 'Ed25519Signature2018' + }; + } + + // sign data use eip-712 when the key type is `EcdsaSecp256k1VerificationKey2019` + assert( + type === 'EcdsaSecp256k1VerificationKey2019', + `this method call only [EcdsaSecp256k1VerificationKey2019] with message: ${message}` + ); + + return this.signTypedData(message, didUrl); + } + + public async signTypedData(typedData: TypedData, didUrl: DidUrl): Promise { + const message = eip712.getMessage(typedData, true); + + const { id, signature } = await this._sign(message, didUrl); + + return { + id, + signature, + type: 'EcdsaSecp256k1SignatureEip712' + }; + } + + private _sign( + message: Uint8Array | HexString, + id: DidUrl + ): Promise<{ signature: Uint8Array; id: DidUrl }> { + const { id: _id, publicKey } = this.get(id); + + const pair = this._getPair(publicKey); + + const signature = pair.sign(message); - return this.sign(message, didUrl); + return Promise.resolve({ + signature, + id: _id + }); } } diff --git a/packages/did/src/hasher.ts b/packages/did/src/hasher.ts index 3976ca4..94bfcf9 100644 --- a/packages/did/src/hasher.ts +++ b/packages/did/src/hasher.ts @@ -8,6 +8,8 @@ import { stringToU8a } from '@polkadot/util'; import { jsonCanonicalize, sha256AsU8a } from '@zcloak/crypto'; /** + * since `@zcloak/did@1.0.0`, this function is not used when publish, please use [[getPublishDocumentTypedData]]. + * * serialize did document as sha256, used to sign it, do not encode proof, because the signature will push to. * @param document an object of [[DidDocument]] * @returns [[Uint8Array]] diff --git a/packages/did/src/types.ts b/packages/did/src/types.ts index b4ccb87..93d7a5b 100644 --- a/packages/did/src/types.ts +++ b/packages/did/src/types.ts @@ -1,10 +1,15 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { TypedData } from '@zcloak/crypto/eip712/types'; import type { HexString } from '@zcloak/crypto/types'; -import type { DidUrl, Service, VerificationMethodType } from '@zcloak/did-resolver/types'; - -import { DidResolver } from '@zcloak/did-resolver'; +import type { DidResolver } from '@zcloak/did-resolver'; +import type { + DidUrl, + Service, + SignatureType, + VerificationMethodType +} from '@zcloak/did-resolver/types'; export type DidKeys = | 'authentication' @@ -15,7 +20,7 @@ export type DidKeys = export type SignedData = { id: DidUrl; - type: VerificationMethodType; + type: SignatureType; signature: Uint8Array; }; @@ -47,9 +52,13 @@ export interface IDidDetails { export interface IDidKeyring { signWithKey( - message: Uint8Array | HexString, - key: Exclude + message: Uint8Array | HexString | TypedData, + keyOrDidUrl: DidUrl | Exclude ): Promise; + /** + * DEPRECATED + * @since `@zcloak/did@1.0.0` + */ sign(message: Uint8Array | HexString, id: DidUrl): Promise; encrypt( message: HexString | Uint8Array, diff --git a/packages/did/src/utils.ts b/packages/did/src/utils.ts index 7892fdd..f354e33 100644 --- a/packages/did/src/utils.ts +++ b/packages/did/src/utils.ts @@ -1,10 +1,14 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DidUrl, VerificationMethodType } from '@zcloak/did-resolver/types'; +import type { TypedData } from '@zcloak/crypto/eip712/types'; +import type { DidDocument, DidUrl, VerificationMethodType } from '@zcloak/did-resolver/types'; +import type { KeypairType } from '@zcloak/keyring/types'; +import { assert, isNumber } from '@polkadot/util'; + +import { decodeMultibase } from '@zcloak/crypto'; import { parseDid } from '@zcloak/did-resolver/parseDid'; -import { KeypairType } from '@zcloak/keyring/types'; /** * Compare whether two didUrls have the same Uri @@ -48,3 +52,99 @@ export function typeTransform(type: KeypairType): VerificationMethodType { throw new Error(`Can not transform type: ${type}`); } } + +export function getPublishDocumentTypedData(document: DidDocument): TypedData { + const message = { + id: document.id, + controller: document.controller, + verificationMethod: + document.verificationMethod?.map((method) => ({ + id: method.id, + controller: method.controller, + type: method.type, + publicKey: decodeMultibase(method.publicKeyMultibase) + })) ?? [], + authentication: + document.authentication?.map((didUrl) => { + const index = document.verificationMethod?.findIndex((method) => method.id === didUrl); + + assert(isNumber(index), `Can't find authentication verificationMethod with key: ${didUrl}`); + + return index; + }) ?? [], + assertionMethod: + document.assertionMethod?.map((didUrl) => { + const index = document.verificationMethod?.findIndex((method) => method.id === didUrl); + + assert( + isNumber(index), + `Can't find assertionMethod verificationMethod with key: ${didUrl}` + ); + + return index; + }) ?? [], + keyAgreement: + document.keyAgreement?.map((didUrl) => { + const index = document.verificationMethod?.findIndex((method) => method.id === didUrl); + + assert(isNumber(index), `Can't find keyAgreement verificationMethod with key: ${didUrl}`); + + return index; + }) ?? [], + capabilityInvocation: + document.capabilityInvocation?.map((didUrl) => { + const index = document.verificationMethod?.findIndex((method) => method.id === didUrl); + + assert( + isNumber(index), + `Can't find capabilityInvocation verificationMethod with key: ${didUrl}` + ); + + return index; + }) ?? [], + capabilityDelegation: + document.capabilityDelegation?.map((didUrl) => { + const index = document.verificationMethod?.findIndex((method) => method.id === didUrl); + + assert( + isNumber(index), + `Can't find capabilityDelegation verificationMethod with key: ${didUrl}` + ); + + return index; + }) ?? [], + creationTime: document.creationTime || Date.now() + }; + + return { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' } + ], + VerificationMethod: [ + { name: 'id', type: 'string' }, + { name: 'controller', type: 'string[]' }, + { name: 'type', type: 'string' }, + { name: 'publicKey', type: 'bytes' } + ], + PublishDocument: [ + { name: 'id', type: 'string' }, + { name: 'controller', type: 'string[]' }, + { name: 'verificationMethod', type: 'VerificationMethod[]' }, + { name: 'authentication', type: 'uint256[]' }, + { name: 'assertionMethod', type: 'uint256[]' }, + { name: 'keyAgreement', type: 'uint256[]' }, + { name: 'capabilityInvocation', type: 'uint256[]' }, + { name: 'capabilityDelegation', type: 'uint256[]' }, + { name: 'creationTime', type: 'uint256' } + ] + }, + primaryType: 'PublishDocument', + domain: { + name: 'DidDocument', + version: '0' + }, + message + }; +} diff --git a/packages/keyring/src/keyring.spec.ts b/packages/keyring/src/keyring.spec.ts index cb52028..c2a7cd3 100644 --- a/packages/keyring/src/keyring.spec.ts +++ b/packages/keyring/src/keyring.spec.ts @@ -4,7 +4,13 @@ import { stringToU8a, u8aToHex } from '@polkadot/util'; import { computeAddress, getAddress } from 'ethers'; -import { ed25519Verify, initCrypto, randomAsU8a, secp256k1Verify } from '@zcloak/crypto'; +import { + ed25519Verify, + initCrypto, + keccak256AsU8a, + randomAsU8a, + secp256k1Verify +} from '@zcloak/crypto'; import { Keyring } from './keyring'; @@ -49,7 +55,9 @@ describe('Keyring', (): void => { expect(secp256k1Verify(MESSAGE, signature, pair.publicKey)).toBe(true); expect(secp256k1Verify(MESSAGE, signature, randomAsU8a(32))).toBe(false); - expect(secp256k1Verify(new Uint8Array(), signature, pair.publicKey)).toBe(false); + expect(secp256k1Verify(keccak256AsU8a(new Uint8Array()), signature, pair.publicKey)).toBe( + false + ); }); it('encodes a pair toJSON (and decodes)', (): void => { diff --git a/packages/message/package.json b/packages/message/package.json index b7172d7..a4e842d 100644 --- a/packages/message/package.json +++ b/packages/message/package.json @@ -24,8 +24,7 @@ "@zcloak/crypto": "workspace:^", "@zcloak/did": "workspace:^", "@zcloak/did-resolver": "workspace:^", - "@zcloak/vc": "workspace:^", - "@zcloak/verify": "workspace:^" + "@zcloak/vc": "workspace:^" }, "devDependencies": { "@zcloak/ctype": "workspace:^" diff --git a/packages/message/src/decrypt/index.ts b/packages/message/src/decrypt/index.ts index d987f3f..6e272a8 100644 --- a/packages/message/src/decrypt/index.ts +++ b/packages/message/src/decrypt/index.ts @@ -11,7 +11,6 @@ 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/is'; -import { didVerify } from '@zcloak/verify'; import { SUPPORT_MESSAGE_TYPES } from '../defaults'; @@ -135,22 +134,6 @@ export function verifyMessageEnvelope(message: BaseMessag } } -export async function verifyMessageSignature( - message: BaseMessage, - resolver: DidResolver -): Promise { - if (message.version !== '1') { - assert(message.signer, 'No signer find'); - assert(message.signature, 'No signature find'); - assert(isSameUri(message.sender, message.signer), 'Expect signer is the sender'); - - assert( - await didVerify(message.id, decodeMultibase(message.signature), message.signer, resolver), - 'Signature verify failed' - ); - } -} - /** * @name decryptMessage * @summary Decrypted the data to Message @@ -163,7 +146,6 @@ export async function decryptMessage( resolver: DidResolver = defaultResolver ): Promise> { verifyMessageEnvelope(message); - await verifyMessageSignature(message, resolver); const decrypted = await did.decrypt( decodeMultibase(message.encryptedMsg), diff --git a/packages/message/src/encrypt/index.ts b/packages/message/src/encrypt/index.ts index 1c8699d..afadd2d 100644 --- a/packages/message/src/encrypt/index.ts +++ b/packages/message/src/encrypt/index.ts @@ -68,8 +68,6 @@ export async function encryptMessage( const ctype = getCtype(type, data); - const signData = await sender.signWithKey(id, 'authentication'); - return { id, reply, @@ -79,8 +77,6 @@ export async function encryptMessage( sender: encrypted.senderUrl, receiver: encrypted.receiverUrl, ctype, - signer: signData.id, - signature: base58Encode(signData.signature), encryptedMsg: base58Encode(encrypted.data) }; } diff --git a/packages/message/src/types.ts b/packages/message/src/types.ts index e42eede..9686b07 100644 --- a/packages/message/src/types.ts +++ b/packages/message/src/types.ts @@ -52,9 +52,6 @@ export interface BaseMessage { sender: DidUrl; receiver: DidUrl; ctype?: HexString; - // since version 2 - signer?: DidUrl; - signature?: string; } export interface Message extends BaseMessage { diff --git a/packages/vc/src/credential/index.spec.ts b/packages/vc/src/credential/index.spec.ts index 22d087c..6bc0808 100644 --- a/packages/vc/src/credential/index.spec.ts +++ b/packages/vc/src/credential/index.spec.ts @@ -123,7 +123,7 @@ describe('VerifiableCredential', (): void => { hasher: ['RescuePrime', 'Keccak256'], proof: [ { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'assertionMethod' } ] @@ -169,7 +169,7 @@ describe('VerifiableCredential', (): void => { hasher: ['RescuePrime', 'Keccak256'], proof: [ { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'assertionMethod' } ] @@ -222,7 +222,7 @@ describe('VerifiableCredential', (): void => { hasher: ['RescuePrime', 'Keccak256'], proof: [ { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'assertionMethod' } ] diff --git a/packages/vc/src/credential/vc.ts b/packages/vc/src/credential/vc.ts index aba8cae..89f2c33 100644 --- a/packages/vc/src/credential/vc.ts +++ b/packages/vc/src/credential/vc.ts @@ -1,6 +1,7 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { HexString } from '@polkadot/util/types'; import type { HashType, Proof, @@ -14,12 +15,13 @@ import { assert } from '@polkadot/util'; import { base58Encode } from '@zcloak/crypto'; import { CType } from '@zcloak/ctype/types'; import { Did } from '@zcloak/did'; +import { SignedData } from '@zcloak/did/types'; import { DEFAULT_CONTEXT, DEFAULT_VC_VERSION } from '../defaults'; import { calcDigest, DigestPayload } from '../digest'; import { isRawCredential } from '../is'; import { calcRoothash, RootHashResult } from '../rootHash'; -import { keyTypeToSignatureType } from '../utils'; +import { getAttestationTypedData } from '../utils'; import { Raw } from './raw'; /** @@ -110,22 +112,19 @@ export class VerifiableCredentialBuilder { rootHash: rootHashResult.rootHash, expirationDate: this.expirationDate || undefined, holder: this.raw.owner, - ctype: this.raw.ctype.$id + ctype: this.raw.ctype.$id, + issuanceDate: this.issuanceDate }; - if (this.version) { - (digestPayload as DigestPayload<'1'>).issuanceDate = this.issuanceDate; - } - const { digest, type: digestHashType } = calcDigest( this.version, digestPayload, this.digestHashType ); - const { id, signature, type: keyType } = await issuer.signWithKey(digest, 'assertionMethod'); + const { id, signature, type: signType } = await this._signDigest(issuer, digest); const proof: Proof = { - type: keyTypeToSignatureType(keyType), + type: signType, created: Date.now(), verificationMethod: id, proofPurpose: 'assertionMethod', @@ -217,4 +216,19 @@ export class VerifiableCredentialBuilder { return this; } + + // sign digest by did, if the key type is `Ed25519VerificationKey2020`, it will sign `digest`, + // if the key type is `EcdsaSecp256k1VerificationKey2019`, it will sign `getAttestationTypedData`. + // otherwise, it will throw Error + private _signDigest(did: Did, digest: HexString): Promise { + const { id, type } = did.get(did.getKeyUrl('assertionMethod')); + + if (type === 'EcdsaSecp256k1VerificationKey2019') { + return did.signWithKey(getAttestationTypedData(digest), id); + } else if (type === 'Ed25519VerificationKey2020') { + return did.signWithKey(digest, id); + } + + throw new Error(`Unable to sign with id: ${id}, because type is ${type}`); + } } diff --git a/packages/vc/src/defaults.ts b/packages/vc/src/defaults.ts index 53f9e86..f7750c2 100644 --- a/packages/vc/src/defaults.ts +++ b/packages/vc/src/defaults.ts @@ -1,9 +1,9 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { SignatureType } from '@zcloak/did-resolver/types'; import type { HashType, - SignatureType, VerifiableCredentialVersion, VerifiablePresentationType, VerifiablePresentationVersion @@ -40,5 +40,6 @@ export const ALL_VP_TYPES: VerifiablePresentationType[] = [ export const ALL_SIG_TYPES: SignatureType[] = [ 'EcdsaSecp256k1Signature2019', + 'EcdsaSecp256k1SignatureEip712', 'Ed25519Signature2018' ]; diff --git a/packages/vc/src/digest.ts b/packages/vc/src/digest.ts index 7d538b0..ff26f0c 100644 --- a/packages/vc/src/digest.ts +++ b/packages/vc/src/digest.ts @@ -33,7 +33,7 @@ export interface DigestPayloadV0 { export interface DigestPayloadV1 extends DigestPayloadV0 { /** - * @since `v1` + * @since `@zcloak/vc@1.0.0` and `VerifiableCredential.version is 1` * issuance date */ issuanceDate: number; diff --git a/packages/vc/src/is.ts b/packages/vc/src/is.ts index 696fcd5..caf2211 100644 --- a/packages/vc/src/is.ts +++ b/packages/vc/src/is.ts @@ -1,11 +1,11 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { SignatureType } from '@zcloak/did-resolver/types'; import type { HashType, Proof, RawCredential, - SignatureType, VerifiableCredential, VerifiablePresentation, VerifiablePresentationType diff --git a/packages/vc/src/types.ts b/packages/vc/src/types.ts index f6da968..00fd591 100644 --- a/packages/vc/src/types.ts +++ b/packages/vc/src/types.ts @@ -2,9 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import type { HexString } from '@zcloak/crypto/types'; -import type { DidUrl } from '@zcloak/did-resolver/types'; - -import { DidKeys } from '@zcloak/did/types'; +import type { DidKeys } from '@zcloak/did/types'; +import type { DidUrl, SignatureType } from '@zcloak/did-resolver/types'; export type NativeType = string | number | boolean | null | undefined; @@ -25,10 +24,6 @@ export type HashType = | 'Sha256' | 'Sha512'; -export type SignatureType = 'EcdsaSecp256k1Signature2019' | 'Ed25519Signature2018'; - -export type ProofType = SignatureType; - export type VerifiablePresentationType = 'VP' | 'VP_Digest' | 'VP_SelectiveDisclosure'; export type VerifiableCredentialVersion = '0' | '1'; @@ -36,7 +31,7 @@ export type VerifiableCredentialVersion = '0' | '1'; export type VerifiablePresentationVersion = '0'; export interface Proof { - type: ProofType; + type: SignatureType; created: number; verificationMethod: DidUrl; proofPurpose: DidKeys; diff --git a/packages/vc/src/utils.ts b/packages/vc/src/utils.ts index c8af251..8d9cdd7 100644 --- a/packages/vc/src/utils.ts +++ b/packages/vc/src/utils.ts @@ -1,25 +1,14 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { VerificationMethodType } from '@zcloak/did-resolver/types'; -import type { HashType, NativeType, NativeTypeWithOutNull, SignatureType } from './types'; +import type { HexString } from '@polkadot/util/types'; +import type { TypedData } from '@zcloak/crypto/eip712/types'; +import type { HashType, NativeType, NativeTypeWithOutNull } from './types'; import { rlpEncode as rlpEncodeFn } from '@zcloak/crypto'; import { HASHER } from './hasher'; -export function keyTypeToSignatureType(type: VerificationMethodType): SignatureType { - switch (type) { - case 'EcdsaSecp256k1VerificationKey2019': - return 'EcdsaSecp256k1Signature2019'; - case 'Ed25519VerificationKey2020': - return 'Ed25519Signature2018'; - - default: - throw new Error(`Can not transform type: ${type}`); - } -} - export function rlpEncode( input: NativeType | NativeTypeWithOutNull[], hashType: HashType @@ -32,3 +21,47 @@ export function rlpEncode( return HASHER[hashType](result); } } + +export function getAttestationTypedData(digest: HexString): TypedData { + return { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' } + ], + Attestation: [{ name: 'digest', type: 'bytes' }] + }, + primaryType: 'Attestation', + domain: { + name: 'Attestation', + version: '0' + }, + message: { + digest + } + }; +} + +export function getPresentationTypedData(hash: HexString, challenge: string): TypedData { + return { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' } + ], + Presentation: [ + { name: 'hash', type: 'bytes' }, + { name: 'challenge', type: 'string' } + ] + }, + primaryType: 'Presentation', + domain: { + name: 'Presentation', + version: '0' + }, + message: { + hash, + challenge + } + }; +} diff --git a/packages/vc/src/vp.spec.ts b/packages/vc/src/vp.spec.ts index 360bbec..46a295e 100644 --- a/packages/vc/src/vp.spec.ts +++ b/packages/vc/src/vp.spec.ts @@ -142,7 +142,7 @@ describe('VerifiablePresentation', (): void => { verifiableCredential: [vc], id: hashDigests([vc.digest], DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -178,7 +178,7 @@ describe('VerifiablePresentation', (): void => { ], id: hashDigests([vc.digest], DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -218,7 +218,7 @@ describe('VerifiablePresentation', (): void => { ], id: hashDigests([vc.digest], DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -252,7 +252,7 @@ describe('VerifiablePresentation', (): void => { verifiableCredential: [vc1, vc2], id: hashDigests([vc1.digest, vc2.digest], DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -302,7 +302,7 @@ describe('VerifiablePresentation', (): void => { ], id: hashDigests([vc1.digest, vc2.digest], DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -363,7 +363,7 @@ describe('VerifiablePresentation', (): void => { ], id: hashDigests([vc1.digest, vc2.digest], DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -430,7 +430,7 @@ describe('VerifiablePresentation', (): void => { ], id: hashDigests([vc1.digest, vc2.digest, vc3.digest], DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1Signature2019', + type: 'EcdsaSecp256k1SignatureEip712', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] diff --git a/packages/vc/src/vp.ts b/packages/vc/src/vp.ts index 54c33c2..c098db7 100644 --- a/packages/vc/src/vp.ts +++ b/packages/vc/src/vp.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { HexString } from '@zcloak/crypto/types'; +import type { SignedData } from '@zcloak/did/types'; import type { HashType, VerifiableCredential, @@ -18,7 +19,7 @@ import { isSameUri } from '@zcloak/did/utils'; import { DEFAULT_CONTEXT, DEFAULT_VP_HASH_TYPE } from './defaults'; import { isPublicVC, isVC } from './is'; import { calcRoothash } from './rootHash'; -import { keyTypeToSignatureType, rlpEncode } from './utils'; +import { getPresentationTypedData, rlpEncode } from './utils'; // @internal // transform private Verifiable Credential by [[VerifiablePresentationType]] @@ -141,11 +142,7 @@ export class VerifiablePresentationBuilder { hashType ); - const { - id, - signature, - type: signType - } = await this.#did.signWithKey(u8aConcat(hash, stringToU8a(challenge)), 'authentication'); + const { id, signature, type: signType } = await this._sign(hash, challenge); return { '@context': DEFAULT_CONTEXT, @@ -154,7 +151,7 @@ export class VerifiablePresentationBuilder { verifiableCredential: vcs, id: hash, proof: { - type: keyTypeToSignatureType(signType), + type: signType, created: Date.now(), verificationMethod: id, proofPurpose: 'authentication', @@ -164,4 +161,16 @@ export class VerifiablePresentationBuilder { hasher: [hashTypeOut] }; } + + private _sign(hash: HexString, challenge?: string): Promise { + const { id, type } = this.#did.get(this.#did.getKeyUrl('assertionMethod')); + + if (type === 'EcdsaSecp256k1VerificationKey2019') { + return this.#did.signWithKey(getPresentationTypedData(hash, challenge || ''), id); + } else if (type === 'Ed25519VerificationKey2020') { + return this.#did.signWithKey(u8aConcat(hash, stringToU8a(challenge)), id); + } + + throw new Error(`Unable to sign with id: ${id}, because type is ${type}`); + } } diff --git a/packages/verify/src/ctypeVerify.ts b/packages/verify/src/ctypeVerify.ts index 8125a05..aa488f4 100644 --- a/packages/verify/src/ctypeVerify.ts +++ b/packages/verify/src/ctypeVerify.ts @@ -5,8 +5,9 @@ import type { CType } from '@zcloak/ctype/types'; import type { DidResolver } from '@zcloak/did-resolver'; import type { DidDocument } from '@zcloak/did-resolver/types'; -import { decodeMultibase } from '@zcloak/crypto'; +import { decodeMultibase, eip712 } from '@zcloak/crypto'; import { getCTypeHash } from '@zcloak/ctype/publish'; +import { getPublishCTypeTypedData } from '@zcloak/ctype/utils'; import { didVerify } from './didVerify'; @@ -28,7 +29,14 @@ import { didVerify } from './didVerify'; export function ctypeVerify(ctype: CType, document?: DidDocument | DidResolver): Promise { const hash = getCTypeHash(ctype, ctype.publisher, ctype.$schema); + const message = + ctype.signatureType === 'EcdsaSecp256k1SignatureEip712' + ? eip712.getMessage(getPublishCTypeTypedData(hash), true) + : hash; + + const signatureType = ctype.signatureType || 'EcdsaSecp256k1Signature2019'; + return document - ? didVerify(hash, decodeMultibase(ctype.signature), ctype.publisher, document) - : didVerify(hash, decodeMultibase(ctype.signature), ctype.publisher); + ? didVerify(message, decodeMultibase(ctype.signature), signatureType, ctype.publisher, document) + : didVerify(message, decodeMultibase(ctype.signature), signatureType, ctype.publisher); } diff --git a/packages/verify/src/didVerify.ts b/packages/verify/src/didVerify.ts index 95227fe..122611d 100644 --- a/packages/verify/src/didVerify.ts +++ b/packages/verify/src/didVerify.ts @@ -2,22 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 import type { HexString } from '@zcloak/crypto/types'; -import type { DidDocument, DidUrl, VerificationMethodType } from '@zcloak/did-resolver/types'; +import type { DidDocument, DidUrl, SignatureType } from '@zcloak/did-resolver/types'; import { u8aToU8a } from '@polkadot/util'; -import { ed25519Verify, secp256k1Verify } from '@zcloak/crypto'; +import { ed25519Verify, keccak256AsU8a, secp256k1Verify } from '@zcloak/crypto'; import { helpers } from '@zcloak/did'; import { DidResolver } from '@zcloak/did-resolver'; import { defaultResolver } from '@zcloak/did-resolver/defaults'; const VERIFIERS: Record< - VerificationMethodType, + SignatureType, (message: Uint8Array, signature: HexString | Uint8Array, publicKey: Uint8Array) => boolean > = { - EcdsaSecp256k1VerificationKey2019: secp256k1Verify, - Ed25519VerificationKey2020: ed25519Verify, - X25519KeyAgreementKey2019: () => false + EcdsaSecp256k1Signature2019: secp256k1Verify, + EcdsaSecp256k1SignatureEip712: secp256k1Verify, + Ed25519Signature2018: ed25519Verify }; /** @@ -54,6 +54,7 @@ const VERIFIERS: Record< export async function didVerify( message: HexString | Uint8Array | string, signature: HexString | Uint8Array, + signatureType: SignatureType, didUrl: DidUrl, resolverOrDidDocument?: DidDocument | DidResolver ): Promise { @@ -61,7 +62,8 @@ export async function didVerify( resolverOrDidDocument = defaultResolver; } - const messageU8a = u8aToU8a(message); + const messageU8a: Uint8Array = + signatureType === 'EcdsaSecp256k1Signature2019' ? keccak256AsU8a(message) : u8aToU8a(message); const document = resolverOrDidDocument instanceof DidResolver @@ -70,13 +72,8 @@ export async function didVerify( const did = helpers.fromDidDocument(document); - for (const [, { publicKey, type }] of did.keyRelationship) { - const isTrue = VERIFIERS[type](messageU8a, signature, publicKey); + const { publicKey } = did.get(didUrl); + const isTrue = VERIFIERS[signatureType](messageU8a, signature, publicKey); - if (isTrue) { - return isTrue; - } - } - - return false; + return isTrue; } diff --git a/packages/verify/src/proofVerify.ts b/packages/verify/src/proofVerify.ts index e7c6149..fe65338 100644 --- a/packages/verify/src/proofVerify.ts +++ b/packages/verify/src/proofVerify.ts @@ -5,8 +5,6 @@ import type { HexString } from '@zcloak/crypto/types'; import type { DidDocument } from '@zcloak/did-resolver/types'; import type { Proof } from '@zcloak/vc/types'; -import { stringToU8a, u8aConcat } from '@polkadot/util'; - import { decodeMultibase } from '@zcloak/crypto'; import { DidResolver } from '@zcloak/did-resolver'; @@ -25,15 +23,13 @@ export async function proofVerify( proof: Proof, resolverOrDidDocument?: DidDocument | DidResolver ): Promise { - const { challenge, proofValue, verificationMethod } = proof; + const { proofValue, verificationMethod } = proof; const signature = decodeMultibase(proofValue); - message = u8aConcat(message, stringToU8a(challenge)); - if (!resolverOrDidDocument) { - return didVerify(message, signature, verificationMethod); + return didVerify(message, signature, proof.type, verificationMethod); } else { - return didVerify(message, signature, verificationMethod, resolverOrDidDocument); + return didVerify(message, signature, proof.type, verificationMethod, resolverOrDidDocument); } } diff --git a/packages/verify/src/vcVerify.ts b/packages/verify/src/vcVerify.ts index a063926..82c5174 100644 --- a/packages/verify/src/vcVerify.ts +++ b/packages/verify/src/vcVerify.ts @@ -8,10 +8,11 @@ import type { VerifiableCredential } from '@zcloak/vc/types'; import { assert, bufferToU8a, isHex, u8aConcat, u8aToHex } from '@polkadot/util'; +import { eip712 } from '@zcloak/crypto'; import { calcRoothash, makeMerkleTree } from '@zcloak/vc'; import { HASHER } from '@zcloak/vc/hasher'; import { isPublicVC, isVC } from '@zcloak/vc/is'; -import { rlpEncode } from '@zcloak/vc/utils'; +import { getAttestationTypedData, rlpEncode } from '@zcloak/vc/utils'; import { digestVerify } from './digestVerify'; import { proofVerify } from './proofVerify'; @@ -43,9 +44,14 @@ async function verifyShared( hasher[1] ); + const message = + proof[0].type === 'EcdsaSecp256k1SignatureEip712' + ? eip712.getMessage(getAttestationTypedData(digest), true) + : digest; + const proofValid = await (resolverOrDidDocument - ? proofVerify(digest, proof[0], resolverOrDidDocument) - : proofVerify(digest, proof[0])); + ? proofVerify(message, proof[0], resolverOrDidDocument) + : proofVerify(message, proof[0])); return digestValid && proofValid; } diff --git a/packages/verify/src/verifyDidDocumentProof.ts b/packages/verify/src/verifyDidDocumentProof.ts index 95a17b0..60ad72e 100644 --- a/packages/verify/src/verifyDidDocumentProof.ts +++ b/packages/verify/src/verifyDidDocumentProof.ts @@ -1,8 +1,9 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { decodeMultibase } from '@zcloak/crypto'; +import { decodeMultibase, eip712 } from '@zcloak/crypto'; import { hashDidDocument } from '@zcloak/did'; +import { getPublishDocumentTypedData } from '@zcloak/did/utils'; import { DidDocument } from '@zcloak/did-resolver/types'; import { didVerify } from './didVerify'; @@ -12,12 +13,21 @@ export async function verifyDidDocumentProof(document: DidDocument): Promise VERIFIERS[t](verifiableCredential[i], resolverOrDidDocument)) diff --git a/yarn.lock b/yarn.lock index 75bc5f2..f016590 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2725,6 +2725,15 @@ __metadata: languageName: node linkType: hard +"@types/bn.js@npm:^4.11.3": + version: 4.11.6 + resolution: "@types/bn.js@npm:4.11.6" + dependencies: + "@types/node": "*" + checksum: 7f66f2c7b7b9303b3205a57184261974b114495736b77853af5b18d857c0b33e82ce7146911e86e87a87837de8acae28986716fd381ac7c301fd6e8d8b6c811f + languageName: node + linkType: hard + "@types/bn.js@npm:^5.1.0, @types/bn.js@npm:^5.1.1": version: 5.1.1 resolution: "@types/bn.js@npm:5.1.1" @@ -2815,6 +2824,15 @@ __metadata: languageName: node linkType: hard +"@types/ethereumjs-abi@npm:^0.6.3": + version: 0.6.3 + resolution: "@types/ethereumjs-abi@npm:0.6.3" + dependencies: + "@types/node": "*" + checksum: 4a89d30e78f590f750acf344b8f193fe82642320445bbc5eccb17156fa8637c99822fa45e3b7a2c43d108c4346cec07dac71638191db76417181ca58df0d62cf + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.18": version: 4.17.31 resolution: "@types/express-serve-static-core@npm:4.17.31" @@ -3475,10 +3493,12 @@ __metadata: "@polkadot/util": ^10.3.1 "@scure/base": ^1.1.1 "@types/ed2curve": ^0.2.2 + "@types/ethereumjs-abi": ^0.6.3 "@zcloak/wasm-bridge": "workspace:^" bip39: ^3.0.4 canonicalize: ^1.0.8 ed2curve: ^0.3.0 + ethereumjs-abi: ^0.6.8 tweetnacl: ^1.0.3 languageName: unknown linkType: soft @@ -3645,7 +3665,6 @@ __metadata: "@zcloak/did": "workspace:^" "@zcloak/did-resolver": "workspace:^" "@zcloak/vc": "workspace:^" - "@zcloak/verify": "workspace:^" languageName: unknown linkType: soft @@ -3662,7 +3681,7 @@ __metadata: languageName: unknown linkType: soft -"@zcloak/verify@workspace:^, @zcloak/verify@workspace:packages/verify": +"@zcloak/verify@workspace:packages/verify": version: 0.0.0-use.local resolution: "@zcloak/verify@workspace:packages/verify" dependencies: @@ -4488,7 +4507,7 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^4.11.9": +"bn.js@npm:^4.11.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9": version: 4.12.0 resolution: "bn.js@npm:4.12.0" checksum: 39afb4f15f4ea537b55eaf1446c896af28ac948fdcf47171961475724d1bb65118cca49fa6e3d67706e4790955ec0e74de584e45c8f1ef89f46c812bee5b5a12 @@ -6267,7 +6286,7 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.4": +"elliptic@npm:^6.5.2, elliptic@npm:^6.5.4": version: 6.5.4 resolution: "elliptic@npm:6.5.4" dependencies: @@ -6992,6 +7011,31 @@ __metadata: languageName: node linkType: hard +"ethereumjs-abi@npm:^0.6.8": + version: 0.6.8 + resolution: "ethereumjs-abi@npm:0.6.8" + dependencies: + bn.js: ^4.11.8 + ethereumjs-util: ^6.0.0 + checksum: cede2a8ae7c7e04eeaec079c2f925601a25b2ef75cf9230e7c5da63b4ea27883b35447365a47e35c1e831af520973a2252af89022c292c18a09a4607821a366b + languageName: node + linkType: hard + +"ethereumjs-util@npm:^6.0.0": + version: 6.2.1 + resolution: "ethereumjs-util@npm:6.2.1" + dependencies: + "@types/bn.js": ^4.11.3 + bn.js: ^4.11.0 + create-hash: ^1.1.2 + elliptic: ^6.5.2 + ethereum-cryptography: ^0.1.3 + ethjs-util: 0.1.6 + rlp: ^2.2.3 + checksum: e3cb4a2c034a2529281fdfc21a2126fe032fdc3038863f5720352daa65ddcc50fc8c67dbedf381a882dc3802e05d979287126d7ecf781504bde1fd8218693bde + languageName: node + linkType: hard + "ethereumjs-util@npm:^7.1.0": version: 7.1.5 resolution: "ethereumjs-util@npm:7.1.5" @@ -7029,6 +7073,16 @@ __metadata: languageName: node linkType: hard +"ethjs-util@npm:0.1.6": + version: 0.1.6 + resolution: "ethjs-util@npm:0.1.6" + dependencies: + is-hex-prefixed: 1.0.0 + strip-hex-prefix: 1.0.0 + checksum: 1f42959e78ec6f49889c49c8a98639e06f52a15966387dd39faf2930db48663d026efb7db2702dcffe7f2a99c4a0144b7ce784efdbf733f4077aae95de76d65f + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.0": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -12489,7 +12543,7 @@ __metadata: languageName: node linkType: hard -"rlp@npm:^2.2.4": +"rlp@npm:^2.2.3, rlp@npm:^2.2.4": version: 2.2.7 resolution: "rlp@npm:2.2.7" dependencies: