diff --git a/.changeset/hot-carrots-travel.md b/.changeset/hot-carrots-travel.md new file mode 100644 index 0000000..ce95878 --- /dev/null +++ b/.changeset/hot-carrots-travel.md @@ -0,0 +1,10 @@ +--- +'@zcloak/did-resolver': minor +'@zcloak/crypto': minor +'@zcloak/verify': minor +'@zcloak/ctype': minor +'@zcloak/did': minor +'@zcloak/vc': minor +--- + +Sining Data change, replace eip712 to eip191. diff --git a/package.json b/package.json index 9bad23f..cea377f 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "changeset": "zcloak-exec-changeset", "clean": "zcloak-dev-clean-build", "lint": "yarn lint:ts && yarn lint:dependencies", - "lint:ts": "zcloak-dev-run-lint", "lint:dependencies": "zcloak-dev-lint-dependencies --fix", + "lint:ts": "zcloak-dev-run-lint", "postinstall": "zcloak-dev-yarn-only", "test": "zcloak-dev-run-test --coverage --forceExit --runInBand --testPathIgnorePatterns e2e", "test:one": "zcloak-dev-run-test --runInBand", diff --git a/packages/crypto/src/eip191/eip191.ts b/packages/crypto/src/eip191/eip191.ts new file mode 100644 index 0000000..11141c8 --- /dev/null +++ b/packages/crypto/src/eip191/eip191.ts @@ -0,0 +1,25 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '../types'; + +import { stringToU8a, u8aConcat, u8aToU8a } from '@polkadot/util'; + +import { keccak256AsU8a } from '../keccak'; + +/** + * @name eip191HashMessage + * @description + * keccak256 hash `message` with eip191 method, returns `Uint8Array`. See https://eips.ethereum.org/EIPS/eip-191 + */ +export function eip191HashMessage(message: Uint8Array | HexString): Uint8Array { + const messageU8a = u8aToU8a(message); + + return keccak256AsU8a( + u8aConcat( + stringToU8a('\x19Ethereum Signed Message:\n'), + stringToU8a(String(messageU8a.length)), + messageU8a + ) + ); +} diff --git a/packages/crypto/src/eip191/index.ts b/packages/crypto/src/eip191/index.ts new file mode 100644 index 0000000..08dc4a5 --- /dev/null +++ b/packages/crypto/src/eip191/index.ts @@ -0,0 +1,4 @@ +// Copyright 2021-2023 zcloak authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from './eip191'; diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 46095eb..57ecbfa 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 './eip191'; export * from './eip712'; export * from './ethereum'; export * from './hdkey'; diff --git a/packages/ctype/src/publish.ts b/packages/ctype/src/publish.ts index fa9e53b..35a9bd1 100644 --- a/packages/ctype/src/publish.ts +++ b/packages/ctype/src/publish.ts @@ -4,15 +4,14 @@ import type { HexString } from '@zcloak/crypto/types'; import type { Did } from '@zcloak/did'; import type { DidUrl } from '@zcloak/did-resolver/types'; -import type { BaseCType, CType } from './types'; +import type { BaseCType, CType, CTypeVersion } from './types'; -import { u8aToHex } from '@polkadot/util'; +import { numberToU8a, stringToU8a, u8aConcat, u8aToHex } from '@polkadot/util'; 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, @@ -32,10 +31,17 @@ export function getCTypeHash( return u8aToHex(keccak256AsU8a(jsonCanonicalize(obj))); } +export function signedCTypeMessage(hash: HexString, version: CTypeVersion): Uint8Array { + return u8aConcat(stringToU8a('VersionedCtypeCreation'), numberToU8a(Number(version), 16), hash); +} + export async function getPublish(base: BaseCType, publisher: Did): Promise { const hash = getCTypeHash(base, publisher.id); - const message = getPublishCTypeTypedData(hash); + const version: CTypeVersion = '0'; + + const message = signedCTypeMessage(hash, version); + const { id, signature, @@ -48,6 +54,7 @@ export async function getPublish(base: BaseCType, publisher: Did): Promise { const document = this.getDocument(); - document.creationTime = Date.now(); - const proof: DidDocumentProof[] = document.proof ?? []; - const message = getPublishDocumentTypedData(document); + assert(document.version, 'Must have version field when publish'); + + const message = signedDidDocumentMessage(hashDidDocument(document), document.version); const { id, @@ -96,6 +97,7 @@ export abstract class DidChain extends DidKeyring { return { ...document, + creationTime: Date.now(), proof }; } diff --git a/packages/did/src/did/index.spec.ts b/packages/did/src/did/index.spec.ts index 0d7aef7..0ff2550 100644 --- a/packages/did/src/did/index.spec.ts +++ b/packages/did/src/did/index.spec.ts @@ -6,16 +6,14 @@ import { alice, bob, DOCUMENTS, testResolver } from 'test-support'; import { decodeMultibase, - eip712, + eip191HashMessage, ethereumEncode, initCrypto, - keccak256AsU8a, secp256k1Verify } from '@zcloak/crypto'; -import { TypedData } from '@zcloak/crypto/eip712/types'; import { Keyring } from '@zcloak/keyring'; -import { getPublishDocumentTypedData } from '../utils'; +import { hashDidDocument, signedDidDocumentMessage } from '../hasher'; import { createEcdsaFromMnemonic } from './helpers'; describe('Did', (): void => { @@ -85,87 +83,33 @@ describe('Did', (): void => { expect( secp256k1Verify( - keccak256AsU8a(message), + eip191HashMessage(message), signature1.signature, bob.get(bob.getKeyUrl('authentication')).publicKey ) ).toBe(true); expect( secp256k1Verify( - keccak256AsU8a(message), + eip191HashMessage(message), signature2.signature, bob.get(bob.getKeyUrl('assertionMethod')).publicKey ) ).toBe(true); expect( secp256k1Verify( - keccak256AsU8a(message), + eip191HashMessage(message), signature3.signature, bob.get(bob.getKeyUrl('capabilityDelegation')).publicKey ) ).toBe(true); expect( secp256k1Verify( - keccak256AsU8a(message), + eip191HashMessage(message), signature4.signature, bob.get(bob.getKeyUrl('capabilityInvocation')).publicKey ) ).toBe(true); }); - - it('sign a TypedData', async (): Promise => { - const typedData: TypedData = { - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' } - ], - Test: [{ name: 'test', type: 'bytes' }] - }, - primaryType: 'Test', - domain: { - name: 'Test', - version: '0' - }, - message: { - test: '0x1234' - } - }; - - const signature1 = await alice.signWithKey(typedData, 'authentication'); - const signature2 = await alice.signWithKey(typedData, 'assertionMethod'); - const signature3 = await alice.signWithKey(typedData, 'capabilityDelegation'); - const signature4 = await alice.signWithKey(typedData, 'capabilityInvocation'); - - expect( - secp256k1Verify( - eip712.getMessage(typedData, true), - signature1.signature, - alice.get(alice.getKeyUrl('authentication')).publicKey - ) - ).toBe(true); - expect( - secp256k1Verify( - eip712.getMessage(typedData, true), - signature2.signature, - alice.get(alice.getKeyUrl('assertionMethod')).publicKey - ) - ).toBe(true); - expect( - secp256k1Verify( - eip712.getMessage(typedData, true), - signature3.signature, - alice.get(alice.getKeyUrl('capabilityDelegation')).publicKey - ) - ).toBe(true); - expect( - secp256k1Verify( - eip712.getMessage(typedData, true), - signature4.signature, - alice.get(alice.getKeyUrl('capabilityInvocation')).publicKey - ) - ).toBe(true); - }); }); describe('encrypt and decrypt', (): void => { @@ -193,11 +137,13 @@ describe('Did', (): void => { it('getPublish verify', async (): Promise => { const document = await alice.getPublish(); - expect(document.proof[0].signatureType).toBe('EcdsaSecp256k1SignatureEip712'); + expect(document.proof[0].signatureType).toBe('EcdsaSecp256k1SignatureEip191'); expect( secp256k1Verify( - eip712.getMessage(getPublishDocumentTypedData(document), true), + eip191HashMessage( + signedDidDocumentMessage(hashDidDocument(document), document.version || '0') + ), decodeMultibase(document.proof[0].signature), alice.get(alice.getKeyUrl('capabilityInvocation')).publicKey ) diff --git a/packages/did/src/did/index.ts b/packages/did/src/did/index.ts index 7b17fd2..edbbb22 100644 --- a/packages/did/src/did/index.ts +++ b/packages/did/src/did/index.ts @@ -22,7 +22,7 @@ export class Did extends DidChain { const document = this.getDocument(); - return u8aEq(hashDidDocument(onChainDocument, false), hashDidDocument(document, false)); + return u8aEq(hashDidDocument(onChainDocument), hashDidDocument(document)); } public addService(service: Service): void { diff --git a/packages/did/src/did/keyring.ts b/packages/did/src/did/keyring.ts index d62180f..69b604e 100644 --- a/packages/did/src/did/keyring.ts +++ b/packages/did/src/did/keyring.ts @@ -7,10 +7,9 @@ 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, isHex, isU8a } from '@polkadot/util'; +import { assert } from '@polkadot/util'; -import { eip712, keccak256AsU8a } from '@zcloak/crypto'; -import { TypedData } from '@zcloak/crypto/eip712/types'; +import { eip191HashMessage } from '@zcloak/crypto'; import { defaultResolver } from '@zcloak/did-resolver/defaults'; import { isDidUrl } from '../utils'; @@ -82,7 +81,7 @@ export abstract class DidKeyring extends DidDetails implements IDidKeyring { } public async signWithKey( - message: Uint8Array | HexString | TypedData, + message: Uint8Array | HexString, keyOrDidUrl: DidUrl | Exclude ): Promise { const didUrl = isDidUrl(keyOrDidUrl) ? keyOrDidUrl : this.getKeyUrl(keyOrDidUrl); @@ -93,44 +92,19 @@ export abstract class DidKeyring extends DidDetails implements IDidKeyring { "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' - }; + if (type === 'EcdsaSecp256k1VerificationKey2019') { + message = eip191HashMessage(message); } - // 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' + type: + type === 'EcdsaSecp256k1VerificationKey2019' + ? 'EcdsaSecp256k1SignatureEip191' + : 'Ed25519Signature2018' }; } diff --git a/packages/did/src/hasher.spec.ts b/packages/did/src/hasher.spec.ts index 086877f..abb411a 100644 --- a/packages/did/src/hasher.spec.ts +++ b/packages/did/src/hasher.spec.ts @@ -62,7 +62,7 @@ const DOCUMENT_TWO: DidDocument = { capabilityInvocation: ['did:zk:0x11f8b77F34FCF14B7095BF5228Ac0606324E82D1#key-0'] }; -const HASH = '0x42e2b96a18575fcd2f694cd9a58b59f26683f768e82d0b7f6bf9ce9ea86c01e5'; +const HASH = '0x1086bc9b75ba37f82225b0fd4d118314f3b34f4d10681a36593509e79552a056'; describe('encode did document', (): void => { beforeAll(async () => { @@ -80,6 +80,6 @@ describe('encode did document', (): void => { }); it('without creationTime', (): void => { - expect(hashDidDocument(DOCUMENT_ONE, false)).toEqual(hashDidDocument(DOCUMENT_TWO, false)); + expect(hashDidDocument(DOCUMENT_ONE)).toEqual(hashDidDocument(DOCUMENT_TWO)); }); }); diff --git a/packages/did/src/hasher.ts b/packages/did/src/hasher.ts index 94bfcf9..5bb688e 100644 --- a/packages/did/src/hasher.ts +++ b/packages/did/src/hasher.ts @@ -1,24 +1,42 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { DidDocument } from '@zcloak/did-resolver/types'; +import type { HexString } from '@zcloak/crypto/types'; +import type { DidDocument, DidDocumentVersion } from '@zcloak/did-resolver/types'; -import { stringToU8a } from '@polkadot/util'; +import { numberToU8a, stringToU8a, u8aConcat } from '@polkadot/util'; -import { jsonCanonicalize, sha256AsU8a } from '@zcloak/crypto'; +import { jsonCanonicalize, keccak256AsU8a, 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]] */ -export function hashDidDocument(document: DidDocument, withCreationTime = true): Uint8Array { +export function hashDidDocument(document: DidDocument): Uint8Array { const obj = { ...document }; delete obj.proof; - if (!withCreationTime) delete obj.creationTime; + delete obj.creationTime; - return sha256AsU8a(stringToU8a(jsonCanonicalize(obj))); + let hash: Uint8Array; + + if (obj.version === '0') { + hash = keccak256AsU8a(stringToU8a(jsonCanonicalize(obj))); + } else { + hash = sha256AsU8a(stringToU8a(jsonCanonicalize(obj))); + } + + return hash; +} + +export function signedDidDocumentMessage( + hash: Uint8Array | HexString, + version: DidDocumentVersion +): Uint8Array { + return u8aConcat( + stringToU8a('VersionedDidDocument'), + numberToU8a(Number(version), 16), // default to set version `0` + hash + ); } diff --git a/packages/did/src/types.ts b/packages/did/src/types.ts index 93d7a5b..5895dc0 100644 --- a/packages/did/src/types.ts +++ b/packages/did/src/types.ts @@ -1,7 +1,6 @@ // 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 { DidResolver } from '@zcloak/did-resolver'; import type { @@ -52,7 +51,7 @@ export interface IDidDetails { export interface IDidKeyring { signWithKey( - message: Uint8Array | HexString | TypedData, + message: Uint8Array | HexString, keyOrDidUrl: DidUrl | Exclude ): Promise; /** diff --git a/packages/did/src/utils.ts b/packages/did/src/utils.ts index f354e33..a9283db 100644 --- a/packages/did/src/utils.ts +++ b/packages/did/src/utils.ts @@ -1,13 +1,9 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { TypedData } from '@zcloak/crypto/eip712/types'; -import type { DidDocument, DidUrl, VerificationMethodType } from '@zcloak/did-resolver/types'; +import type { 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'; /** @@ -52,99 +48,3 @@ 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/vc/src/credential/index.spec.ts b/packages/vc/src/credential/index.spec.ts index 057e03d..7b7b12c 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: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'assertionMethod' } ] @@ -169,7 +169,7 @@ describe('VerifiableCredential', (): void => { hasher: ['RescuePrime', 'Keccak256'], proof: [ { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'assertionMethod' } ] @@ -219,7 +219,7 @@ describe('VerifiableCredential', (): void => { hasher: ['RescuePrime', 'Keccak256'], proof: [ { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'assertionMethod' } ] diff --git a/packages/vc/src/credential/vc.ts b/packages/vc/src/credential/vc.ts index e9dc77a..d78ca74 100644 --- a/packages/vc/src/credential/vc.ts +++ b/packages/vc/src/credential/vc.ts @@ -15,13 +15,12 @@ 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 { getAttestationTypedData } from '../utils'; +import { signedVCMessage } from '../utils'; import { Raw } from './raw'; /** @@ -126,19 +125,8 @@ export class VerifiableCredentialBuilder { digestPayload, this.digestHashType ); - const { - id, - signature, - type: signType - } = await this._signDigest(issuer, digest, this.version); - - const proof: Proof = { - type: signType, - created: Date.now(), - verificationMethod: id, - proofPurpose: 'assertionMethod', - proofValue: base58Encode(signature) - }; + + const proof = await this._signDigest(issuer, digest, this.version); let vc: VerifiableCredential = { '@context': this['@context'], @@ -226,22 +214,28 @@ 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( + // sign digest by did, the signed message is `concat('CredentialVersionedDigest', version, digest)` + private async _signDigest( did: Did, digest: HexString, version: VerifiableCredentialVersion - ): Promise { - const { id, type } = did.get(did.getKeyUrl('assertionMethod')); + ): Promise { + let message: Uint8Array | HexString; - if (type === 'EcdsaSecp256k1VerificationKey2019') { - return did.signWithKey(getAttestationTypedData(digest, version), id); - } else if (type === 'Ed25519VerificationKey2020') { - return did.signWithKey(digest, id); + if (version === '1') { + message = signedVCMessage(digest, version); + } else { + message = digest; } - throw new Error(`Unable to sign with id: ${id}, because type is ${type}`); + const { id, signature, type: signType } = await did.signWithKey(message, 'assertionMethod'); + + return { + type: signType, + created: Date.now(), + verificationMethod: id, + proofPurpose: 'assertionMethod', + proofValue: base58Encode(signature) + }; } } diff --git a/packages/vc/src/defaults.ts b/packages/vc/src/defaults.ts index f7750c2..3a9ed6b 100644 --- a/packages/vc/src/defaults.ts +++ b/packages/vc/src/defaults.ts @@ -11,7 +11,7 @@ import type { export const DEFAULT_VC_VERSION: VerifiableCredentialVersion = '1'; -export const DEFAULT_VP_VERSION: VerifiablePresentationVersion = '0'; +export const DEFAULT_VP_VERSION: VerifiablePresentationVersion = '1'; export const DEFAULT_CONTEXT: string[] = ['https://www.w3.org/2018/credentials/v1']; @@ -40,6 +40,6 @@ export const ALL_VP_TYPES: VerifiablePresentationType[] = [ export const ALL_SIG_TYPES: SignatureType[] = [ 'EcdsaSecp256k1Signature2019', - 'EcdsaSecp256k1SignatureEip712', + 'EcdsaSecp256k1SignatureEip191', 'Ed25519Signature2018' ]; diff --git a/packages/vc/src/types.ts b/packages/vc/src/types.ts index 00fd591..5266485 100644 --- a/packages/vc/src/types.ts +++ b/packages/vc/src/types.ts @@ -28,7 +28,7 @@ export type VerifiablePresentationType = 'VP' | 'VP_Digest' | 'VP_SelectiveDiscl export type VerifiableCredentialVersion = '0' | '1'; -export type VerifiablePresentationVersion = '0'; +export type VerifiablePresentationVersion = '0' | '1'; export interface Proof { type: SignatureType; diff --git a/packages/vc/src/utils.ts b/packages/vc/src/utils.ts index b8b7a4f..1f87ec1 100644 --- a/packages/vc/src/utils.ts +++ b/packages/vc/src/utils.ts @@ -1,15 +1,17 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { HexString } from '@polkadot/util/types'; -import type { TypedData } from '@zcloak/crypto/eip712/types'; +import type { HexString } from '@zcloak/crypto/types'; import type { HashType, NativeType, NativeTypeWithOutNull, - VerifiableCredentialVersion + VerifiableCredentialVersion, + VerifiablePresentationVersion } from './types'; +import { numberToU8a, stringToU8a, u8aConcat } from '@polkadot/util'; + import { rlpEncode as rlpEncodeFn } from '@zcloak/crypto'; import { HASHER } from './hasher'; @@ -27,53 +29,26 @@ export function rlpEncode( } } -export function getAttestationTypedData( +export function signedVCMessage( digest: HexString, version: VerifiableCredentialVersion -): TypedData { - return { - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' } - ], - Attestation: [ - { name: 'digest', type: 'bytes' }, - { name: 'version', type: 'uint256' } - ] - }, - primaryType: 'Attestation', - domain: { - name: 'Attestation', - version: '0' - }, - message: { - digest, - version - } - }; +): Uint8Array { + return u8aConcat( + stringToU8a('CredentialVersionedDigest'), + numberToU8a(Number(version), 16), + 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 - } - }; +export function signedVPMessage( + hash: HexString, + version: VerifiablePresentationVersion, + challenge?: string +): Uint8Array { + return u8aConcat( + stringToU8a('VersionedCredPresentation'), + numberToU8a(Number(version), 16), + hash, + challenge || new Uint8Array() + ); } diff --git a/packages/vc/src/vp.spec.ts b/packages/vc/src/vp.spec.ts index 46a295e..d2c6754 100644 --- a/packages/vc/src/vp.spec.ts +++ b/packages/vc/src/vp.spec.ts @@ -13,7 +13,7 @@ import { Raw, VerifiableCredentialBuilder } from './credential'; import { DEFAULT_CONTEXT, DEFAULT_VP_HASH_TYPE, DEFAULT_VP_VERSION } from './defaults'; import { isPrivateVC } from './is'; import { calcRoothash } from './rootHash'; -import { hashDigests, VerifiablePresentationBuilder } from './vp'; +import { VerifiablePresentationBuilder, vpID } from './vp'; const CONTENTS1 = { name: 'zCloak', @@ -140,9 +140,9 @@ describe('VerifiablePresentation', (): void => { version: DEFAULT_VP_VERSION, type: ['VP'], verifiableCredential: [vc], - id: hashDigests([vc.digest], DEFAULT_VP_HASH_TYPE).hash, + id: vpID([vc.digest], vp.version, DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -176,9 +176,9 @@ describe('VerifiablePresentation', (): void => { credentialSubjectNonceMap: {} } ], - id: hashDigests([vc.digest], DEFAULT_VP_HASH_TYPE).hash, + id: vpID([vc.digest], vp.version, DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -216,9 +216,9 @@ describe('VerifiablePresentation', (): void => { } } ], - id: hashDigests([vc.digest], DEFAULT_VP_HASH_TYPE).hash, + id: vpID([vc.digest], vp.version, DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -250,9 +250,9 @@ describe('VerifiablePresentation', (): void => { version: DEFAULT_VP_VERSION, type: ['VP', 'VP'], verifiableCredential: [vc1, vc2], - id: hashDigests([vc1.digest, vc2.digest], DEFAULT_VP_HASH_TYPE).hash, + id: vpID([vc1.digest, vc2.digest], vp.version, DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -300,9 +300,9 @@ describe('VerifiablePresentation', (): void => { credentialSubjectNonceMap: {} } ], - id: hashDigests([vc1.digest, vc2.digest], DEFAULT_VP_HASH_TYPE).hash, + id: vpID([vc1.digest, vc2.digest], vp.version, DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -361,9 +361,9 @@ describe('VerifiablePresentation', (): void => { } } ], - id: hashDigests([vc1.digest, vc2.digest], DEFAULT_VP_HASH_TYPE).hash, + id: vpID([vc1.digest, vc2.digest], vp.version, DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] @@ -428,9 +428,9 @@ describe('VerifiablePresentation', (): void => { }, vc3 ], - id: hashDigests([vc1.digest, vc2.digest, vc3.digest], DEFAULT_VP_HASH_TYPE).hash, + id: vpID([vc1.digest, vc2.digest, vc3.digest], vp.version, DEFAULT_VP_HASH_TYPE).hash, proof: { - type: 'EcdsaSecp256k1SignatureEip712', + type: 'EcdsaSecp256k1SignatureEip191', proofPurpose: 'authentication' }, hasher: [DEFAULT_VP_HASH_TYPE] diff --git a/packages/vc/src/vp.ts b/packages/vc/src/vp.ts index c098db7..bd665e5 100644 --- a/packages/vc/src/vp.ts +++ b/packages/vc/src/vp.ts @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import type { HexString } from '@zcloak/crypto/types'; -import type { SignedData } from '@zcloak/did/types'; import type { HashType, + Proof, VerifiableCredential, VerifiablePresentation, - VerifiablePresentationType + VerifiablePresentationType, + VerifiablePresentationVersion } from './types'; import { assert, isHex, objectCopy, stringToU8a, u8aConcat, u8aToHex } from '@polkadot/util'; @@ -16,10 +17,11 @@ import { base58Encode } from '@zcloak/crypto'; import { Did } from '@zcloak/did'; import { isSameUri } from '@zcloak/did/utils'; -import { DEFAULT_CONTEXT, DEFAULT_VP_HASH_TYPE } from './defaults'; +import { DEFAULT_CONTEXT, DEFAULT_VP_HASH_TYPE, DEFAULT_VP_VERSION } from './defaults'; +import { HASHER } from './hasher'; import { isPublicVC, isVC } from './is'; import { calcRoothash } from './rootHash'; -import { getPresentationTypedData, rlpEncode } from './utils'; +import { rlpEncode, signedVPMessage } from './utils'; // @internal // transform private Verifiable Credential by [[VerifiablePresentationType]] @@ -65,11 +67,20 @@ function transformVC( return vc; } -export function hashDigests( +export function vpID( digests: HexString[], + version: VerifiablePresentationVersion, hashType: HashType = 'Keccak256' ): { hash: HexString; type: HashType } { - const hash = u8aToHex(u8aConcat(...digests)); + const content = u8aConcat(...digests); + + let hash: HexString; + + if (version === '0') { + hash = u8aToHex(content); + } else { + hash = u8aToHex(HASHER[hashType](content)); + } return { hash, type: hashType }; } @@ -94,9 +105,11 @@ export class VerifiablePresentationBuilder { #did: Did; // [[VerifiableCredential]] => [[VerifiablePresentation]] public vcMap: Map, VerifiablePresentationType> = new Map(); + public version: VerifiablePresentationVersion; - constructor(did: Did) { + constructor(did: Did, version = DEFAULT_VP_VERSION) { this.#did = did; + this.version = version; } /** @@ -137,40 +150,48 @@ export class VerifiablePresentationBuilder { vpTypes.push(vpType); } - const { hash, type: hashTypeOut } = hashDigests( + const { hash, type: hashTypeOut } = vpID( vcs.map(({ digest }) => digest), + this.version, hashType ); - const { id, signature, type: signType } = await this._sign(hash, challenge); + const proof = await this._sign(hash, challenge); return { '@context': DEFAULT_CONTEXT, - version: '0', + version: this.version, type: vpTypes, verifiableCredential: vcs, id: hash, - proof: { - type: signType, - created: Date.now(), - verificationMethod: id, - proofPurpose: 'authentication', - proofValue: base58Encode(signature), - challenge - }, + proof, hasher: [hashTypeOut] }; } - private _sign(hash: HexString, challenge?: string): Promise { - const { id, type } = this.#did.get(this.#did.getKeyUrl('assertionMethod')); + // sign digest by did, the signed message is `concat(VersionedCredPresentation, version, id, challenge)` + private async _sign(hash: HexString, challenge?: string): Promise { + let message: Uint8Array; - if (type === 'EcdsaSecp256k1VerificationKey2019') { - return this.#did.signWithKey(getPresentationTypedData(hash, challenge || ''), id); - } else if (type === 'Ed25519VerificationKey2020') { - return this.#did.signWithKey(u8aConcat(hash, stringToU8a(challenge)), id); + if (this.version === '0') { + message = u8aConcat(hash, stringToU8a(challenge)); + } else { + message = signedVPMessage(hash, this.version, challenge); } - throw new Error(`Unable to sign with id: ${id}, because type is ${type}`); + const { + id, + signature, + type: signType + } = await this.#did.signWithKey(message, 'authentication'); + + return { + type: signType, + created: Date.now(), + verificationMethod: id, + proofPurpose: 'authentication', + proofValue: base58Encode(signature), + challenge + }; } } diff --git a/packages/verify/src/ctypeVerify.ts b/packages/verify/src/ctypeVerify.ts index aa488f4..97e0fdf 100644 --- a/packages/verify/src/ctypeVerify.ts +++ b/packages/verify/src/ctypeVerify.ts @@ -5,9 +5,8 @@ import type { CType } from '@zcloak/ctype/types'; import type { DidResolver } from '@zcloak/did-resolver'; import type { DidDocument } from '@zcloak/did-resolver/types'; -import { decodeMultibase, eip712 } from '@zcloak/crypto'; -import { getCTypeHash } from '@zcloak/ctype/publish'; -import { getPublishCTypeTypedData } from '@zcloak/ctype/utils'; +import { decodeMultibase } from '@zcloak/crypto'; +import { getCTypeHash, signedCTypeMessage } from '@zcloak/ctype/publish'; import { didVerify } from './didVerify'; @@ -29,10 +28,7 @@ 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 message = ctype.version === '0' ? signedCTypeMessage(hash, ctype.version) : hash; const signatureType = ctype.signatureType || 'EcdsaSecp256k1Signature2019'; diff --git a/packages/verify/src/didVerify.ts b/packages/verify/src/didVerify.ts index 122611d..c36700d 100644 --- a/packages/verify/src/didVerify.ts +++ b/packages/verify/src/didVerify.ts @@ -4,9 +4,7 @@ import type { HexString } from '@zcloak/crypto/types'; import type { DidDocument, DidUrl, SignatureType } from '@zcloak/did-resolver/types'; -import { u8aToU8a } from '@polkadot/util'; - -import { ed25519Verify, keccak256AsU8a, secp256k1Verify } from '@zcloak/crypto'; +import { ed25519Verify, eip191HashMessage, keccak256AsU8a, secp256k1Verify } from '@zcloak/crypto'; import { helpers } from '@zcloak/did'; import { DidResolver } from '@zcloak/did-resolver'; import { defaultResolver } from '@zcloak/did-resolver/defaults'; @@ -16,7 +14,7 @@ const VERIFIERS: Record< (message: Uint8Array, signature: HexString | Uint8Array, publicKey: Uint8Array) => boolean > = { EcdsaSecp256k1Signature2019: secp256k1Verify, - EcdsaSecp256k1SignatureEip712: secp256k1Verify, + EcdsaSecp256k1SignatureEip191: secp256k1Verify, Ed25519Signature2018: ed25519Verify }; @@ -52,7 +50,7 @@ const VERIFIERS: Record< * ``` */ export async function didVerify( - message: HexString | Uint8Array | string, + message: HexString | Uint8Array, signature: HexString | Uint8Array, signatureType: SignatureType, didUrl: DidUrl, @@ -63,7 +61,9 @@ export async function didVerify( } const messageU8a: Uint8Array = - signatureType === 'EcdsaSecp256k1Signature2019' ? keccak256AsU8a(message) : u8aToU8a(message); + signatureType === 'EcdsaSecp256k1SignatureEip191' + ? eip191HashMessage(message) + : keccak256AsU8a(message); const document = resolverOrDidDocument instanceof DidResolver diff --git a/packages/verify/src/vcVerify.ts b/packages/verify/src/vcVerify.ts index ece8498..75a9c18 100644 --- a/packages/verify/src/vcVerify.ts +++ b/packages/verify/src/vcVerify.ts @@ -8,11 +8,10 @@ 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 { getAttestationTypedData, rlpEncode } from '@zcloak/vc/utils'; +import { rlpEncode, signedVCMessage } from '@zcloak/vc/utils'; import { digestVerify } from './digestVerify'; import { proofVerify } from './proofVerify'; @@ -44,10 +43,7 @@ async function verifyShared( hasher[1] ); - const message = - proof[0].type === 'EcdsaSecp256k1SignatureEip712' - ? eip712.getMessage(getAttestationTypedData(digest, version), true) - : digest; + const message = version === '1' ? signedVCMessage(digest, version) : digest; const proofValid = await (resolverOrDidDocument ? proofVerify(message, proof[0], resolverOrDidDocument) diff --git a/packages/verify/src/verifyDidDocumentProof.ts b/packages/verify/src/verifyDidDocumentProof.ts index 60ad72e..8be6409 100644 --- a/packages/verify/src/verifyDidDocumentProof.ts +++ b/packages/verify/src/verifyDidDocumentProof.ts @@ -1,9 +1,8 @@ // Copyright 2021-2023 zcloak authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { decodeMultibase, eip712 } from '@zcloak/crypto'; -import { hashDidDocument } from '@zcloak/did'; -import { getPublishDocumentTypedData } from '@zcloak/did/utils'; +import { decodeMultibase } from '@zcloak/crypto'; +import { hashDidDocument, signedDidDocumentMessage } from '@zcloak/did'; import { DidDocument } from '@zcloak/did-resolver/types'; import { didVerify } from './didVerify'; @@ -16,10 +15,9 @@ export async function verifyDidDocumentProof(document: DidDocument): Promise { assert(isVP(vp), 'input `vp` is not VerifiablePresentation object'); - const { hasher, id, proof, type, verifiableCredential } = vp; + const { hasher, id, proof, type, verifiableCredential, version } = vp; const idValid = idCheck( verifiableCredential.map(({ digest }) => digest), hasher[0], - id + id, + version ); // check vc is same did with proof's signer @@ -74,8 +80,8 @@ export async function vpVerify( } const message = - proof.type === 'EcdsaSecp256k1SignatureEip712' - ? eip712.getMessage(getPresentationTypedData(id, proof.challenge || ''), true) + version === '1' + ? signedVPMessage(id, version, proof.challenge) : u8aConcat(id, stringToU8a(proof.challenge)); const proofValid = await proofVerify(message, proof, resolverOrDidDocument);