diff --git a/.changeset/bright-gifts-flash.md b/.changeset/bright-gifts-flash.md new file mode 100644 index 0000000..c20a652 --- /dev/null +++ b/.changeset/bright-gifts-flash.md @@ -0,0 +1,10 @@ +--- +'@zcloak/verify': minor +'@zcloak/vc': minor +--- + +upgrade vc version to v1 + +1. add issuance date param whencalc digest. +2. When there are multiple versions of vc, there are different build logics. The difference between v1 and v2 is that the digest is constructed differently +3. vcVerify function can pass `version`(VerifiableCredentialVersion) for verification. diff --git a/packages/vc/src/credential/vc.ts b/packages/vc/src/credential/vc.ts index 2a15dd4..aba8cae 100644 --- a/packages/vc/src/credential/vc.ts +++ b/packages/vc/src/credential/vc.ts @@ -16,7 +16,7 @@ import { CType } from '@zcloak/ctype/types'; import { Did } from '@zcloak/did'; import { DEFAULT_CONTEXT, DEFAULT_VC_VERSION } from '../defaults'; -import { calcDigest } from '../digest'; +import { calcDigest, DigestPayload } from '../digest'; import { isRawCredential } from '../is'; import { calcRoothash, RootHashResult } from '../rootHash'; import { keyTypeToSignatureType } from '../utils'; @@ -106,16 +106,22 @@ export class VerifiableCredentialBuilder { rootHashResult = calcRoothash(this.raw.contents, this.raw.hashType, {}); } + const digestPayload: DigestPayload = { + rootHash: rootHashResult.rootHash, + expirationDate: this.expirationDate || undefined, + holder: this.raw.owner, + ctype: this.raw.ctype.$id + }; + + if (this.version) { + (digestPayload as DigestPayload<'1'>).issuanceDate = this.issuanceDate; + } + const { digest, type: digestHashType } = calcDigest( - { - rootHash: rootHashResult.rootHash, - expirationDate: this.expirationDate || undefined, - holder: this.raw.owner, - ctype: this.raw.ctype.$id - }, + this.version, + digestPayload, this.digestHashType ); - const { id, signature, type: keyType } = await issuer.signWithKey(digest, 'assertionMethod'); const proof: Proof = { diff --git a/packages/vc/src/defaults.ts b/packages/vc/src/defaults.ts index b117f3c..53f9e86 100644 --- a/packages/vc/src/defaults.ts +++ b/packages/vc/src/defaults.ts @@ -9,7 +9,7 @@ import type { VerifiablePresentationVersion } from './types'; -export const DEFAULT_VC_VERSION: VerifiableCredentialVersion = '0'; +export const DEFAULT_VC_VERSION: VerifiableCredentialVersion = '1'; export const DEFAULT_VP_VERSION: VerifiablePresentationVersion = '0'; diff --git a/packages/vc/src/digest.spec.ts b/packages/vc/src/digest.spec.ts index b2f9f76..b272366 100644 --- a/packages/vc/src/digest.spec.ts +++ b/packages/vc/src/digest.spec.ts @@ -3,7 +3,7 @@ import { initCrypto } from '@zcloak/crypto'; -import { calcDigest, DigestPayload } from './digest'; +import { calcDigest, DigestPayloadV0 } from './digest'; import { calcRoothash } from './rootHash'; describe('digest', (): void => { @@ -13,27 +13,27 @@ describe('digest', (): void => { describe('digest use keccak256', (): void => { it('digest without expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload)).toEqual({ + expect(calcDigest('0', payload)).toEqual({ type: 'Keccak256', digest: '0x358c172298da91c7736df58b30ddc87fcec1ff13f85bcfd60f0ef4d54a12c419' }); }); it('digest include expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', expirationDate: 1668167309925, ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload)).toEqual({ + expect(calcDigest('0', payload)).toEqual({ type: 'Keccak256', digest: '0xafe8c82e9581af9917f20d72d997ece3ed6edad6fc25498c5a2f61324be79a2a' }); @@ -58,7 +58,7 @@ describe('digest', (): void => { }); expect( - calcDigest({ + calcDigest('0', { rootHash, holder: 'did:zk:0x4042F3631656227d92452C9561889677c48f2C4c', ctype: '0x7d31ec6cceb9d313ab35dafa9058c88e758f30887362c2fab3c845902f9ccb31' @@ -72,27 +72,27 @@ describe('digest', (): void => { describe('digest use Blake3', (): void => { it('digest without expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload, 'Blake3')).toEqual({ + expect(calcDigest('0', payload, 'Blake3')).toEqual({ type: 'Blake3', digest: '0x95a9fe24ced5800caf0b31558700ed96d8c896cc9ecc877adc5059577f98c4fd' }); }); it('digest include expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', expirationDate: 1668167309925, ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload, 'Blake3')).toEqual({ + expect(calcDigest('0', payload, 'Blake3')).toEqual({ type: 'Blake3', digest: '0xd6648d4aa8054975e0e7bc1b012d9dd95c052269c32a082eaeda25c021e18765' }); @@ -101,27 +101,27 @@ describe('digest', (): void => { describe('digest use Blake2', (): void => { it('digest without expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload, 'Blake2')).toEqual({ + expect(calcDigest('0', payload, 'Blake2')).toEqual({ type: 'Blake2', digest: '0x895723d7b51f60fb3a4d7c408600cdbdfd077899a23f3009a8968348b4cf49cc' }); }); it('digest include expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', expirationDate: 1668167309925, ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload, 'Blake2')).toEqual({ + expect(calcDigest('0', payload, 'Blake2')).toEqual({ type: 'Blake2', digest: '0xbd54e9609a47ccef94111f870b4fd32c9388cbe5105a57e8f67e84c9d3edd335' }); @@ -130,27 +130,27 @@ describe('digest', (): void => { describe('digest use Sha256', (): void => { it('digest without expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload, 'Sha256')).toEqual({ + expect(calcDigest('0', payload, 'Sha256')).toEqual({ type: 'Sha256', digest: '0x8f35a8411fdb42cdca44ac8666b370e0de1233f5a56267183e939e9659a4ba80' }); }); it('digest include expirationDate', (): void => { - const payload: DigestPayload = { + const payload: DigestPayloadV0 = { rootHash: '0x0a86d6642395770ac481f8a215b08a881e9e0229de2686d498893dec0572649a', holder: 'did:zk:0x082d674c00e27fBaAAE123a85f5024A1DD702e51', expirationDate: 1668167309925, ctype: '0xd50f5298fda74ff0b46be740e602fa5ce0bc2a48fc5ddfbbae3c0678f59b5b97' }; - expect(calcDigest(payload, 'Sha256')).toEqual({ + expect(calcDigest('0', payload, 'Sha256')).toEqual({ type: 'Sha256', digest: '0x5b6d9f4d1f188494b1a32ba5995d797ba573efe40291774e92dd77cc3bc7dfa6' }); diff --git a/packages/vc/src/digest.ts b/packages/vc/src/digest.ts index 9d98e0b..7d538b0 100644 --- a/packages/vc/src/digest.ts +++ b/packages/vc/src/digest.ts @@ -3,7 +3,7 @@ import type { HexString } from '@zcloak/crypto/types'; import type { DidUrl } from '@zcloak/did-resolver/types'; -import type { HashType } from './types'; +import type { HashType, VerifiableCredentialVersion } from './types'; import { numberToU8a, stringToU8a, u8aConcat, u8aToHex } from '@polkadot/util'; @@ -12,7 +12,7 @@ import { HASHER } from './hasher'; export type DigestResult = { digest: HexString; type: HashType }; -export type DigestPayload = { +export interface DigestPayloadV0 { /** * rootHash of credential subject */ @@ -29,31 +29,56 @@ export type DigestPayload = { * ctype hash */ ctype: HexString; -}; +} + +export interface DigestPayloadV1 extends DigestPayloadV0 { + /** + * @since `v1` + * issuance date + */ + issuanceDate: number; +} + +export type DigestPayload = Version extends '0' + ? DigestPayloadV0 + : DigestPayloadV1; /** * @name calcDigest - * @summary calc credential digest + * @summary calc credential digest with version * @description - * 1. it will encode by ctype, expirationDate, rootHash, holder + * 1. it will encode by ctype, expirationDate, rootHash, holder, ?issuanceDate * 2. generate hash value use provide [[hashType]] * @example * ```typescript * import { calcDigest } from '@zcloak/vc'; * - * calcDigest({ rootHash: '0x...', holder: 'did:zk:...', ctype: '0x...' }); // { digest: '0x...', type: 'Keccak256' } + * calcDigest('1', { rootHash: '0x...', holder: 'did:zk:...', ctype: '0x...' }); // { digest: '0x...', type: 'Keccak256' } * ``` */ -export function calcDigest( - payload: DigestPayload, +export function calcDigest( + version: Version, + payload: DigestPayload, hashType: HashType = DEFAULT_DIGEST_HASH_TYPE -): DigestResult { - const encoded = u8aConcat( - payload.rootHash, - stringToU8a(payload.holder), - numberToU8a(payload.expirationDate), - payload.ctype - ); +) { + let encoded: Uint8Array; + + if (version === '0') { + encoded = u8aConcat( + payload.rootHash, + stringToU8a(payload.holder), + numberToU8a(payload.expirationDate || 0), + payload.ctype + ); + } else { + encoded = u8aConcat( + payload.rootHash, + stringToU8a(payload.holder), + numberToU8a((payload as DigestPayload<'1'>).issuanceDate), + numberToU8a(payload.expirationDate || 0), + payload.ctype + ); + } return { type: hashType, diff --git a/packages/vc/src/types.ts b/packages/vc/src/types.ts index 2130d87..f6da968 100644 --- a/packages/vc/src/types.ts +++ b/packages/vc/src/types.ts @@ -31,7 +31,7 @@ export type ProofType = SignatureType; export type VerifiablePresentationType = 'VP' | 'VP_Digest' | 'VP_SelectiveDisclosure'; -export type VerifiableCredentialVersion = '0'; +export type VerifiableCredentialVersion = '0' | '1'; export type VerifiablePresentationVersion = '0'; diff --git a/packages/verify/src/digestVerify.spec.ts b/packages/verify/src/digestVerify.spec.ts index 6a5031e..09144b7 100644 --- a/packages/verify/src/digestVerify.spec.ts +++ b/packages/verify/src/digestVerify.spec.ts @@ -20,6 +20,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, @@ -36,6 +37,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, @@ -55,6 +57,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, @@ -71,6 +74,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, @@ -90,6 +94,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, @@ -106,6 +111,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, @@ -125,6 +131,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, @@ -141,6 +148,7 @@ describe('verify digest', (): void => { expect( digestVerify( + '0', expectedDigest, { rootHash, diff --git a/packages/verify/src/digestVerify.ts b/packages/verify/src/digestVerify.ts index 7a71a15..3c07c3e 100644 --- a/packages/verify/src/digestVerify.ts +++ b/packages/verify/src/digestVerify.ts @@ -4,8 +4,10 @@ import type { HexString } from '@zcloak/crypto/types'; import type { DigestPayload } from '@zcloak/vc'; +import { u8aEq } from '@polkadot/util'; + import { calcDigest } from '@zcloak/vc'; -import { HashType } from '@zcloak/vc/types'; +import { HashType, VerifiableCredentialVersion } from '@zcloak/vc/types'; /** * @name verifyDigest @@ -27,12 +29,13 @@ import { HashType } from '@zcloak/vc/types'; * digestVerify(expectedDigest, payload, 'Keccak256'); // true * ``` */ -export function digestVerify( +export function digestVerify( + version: Version, digestIn: HexString, - payload: DigestPayload, + payload: DigestPayload, hashType?: HashType ): boolean { - const { digest } = calcDigest(payload, hashType); + const digest: HexString = calcDigest(version, payload, hashType).digest; - return digestIn === digest; + return u8aEq(digest, digestIn); } diff --git a/packages/verify/src/vcVerify.ts b/packages/verify/src/vcVerify.ts index b56eef0..a063926 100644 --- a/packages/verify/src/vcVerify.ts +++ b/packages/verify/src/vcVerify.ts @@ -24,19 +24,21 @@ async function verifyShared( ): Promise { assert(isVC(vc), 'input `vc` is not a VerifiableCredential'); - const { ctype, digest, expirationDate, hasher, holder, proof } = vc; + const { ctype, digest, expirationDate, hasher, holder, issuanceDate, proof, version } = vc; if (expirationDate && expirationDate < Date.now()) { return false; } const digestValid = digestVerify( + version, digest, { rootHash, holder, expirationDate, - ctype + ctype, + issuanceDate: version === '0' ? undefined : issuanceDate }, hasher[1] );