From 85a2e0e521496a8257e55dc3cec3d546dbdc6d41 Mon Sep 17 00:00:00 2001 From: Kyle Huang Junyuan Date: Fri, 8 Nov 2024 17:15:44 +0800 Subject: [PATCH 1/6] chore: update v3 deprecation message --- src/3.0/digest.ts | 2 +- src/3.0/obfuscate.ts | 2 +- src/3.0/salt.ts | 8 ++++---- src/3.0/sign.ts | 2 +- src/3.0/traverseAndFlatten.ts | 2 +- src/3.0/types.ts | 24 ++++++++++++------------ src/3.0/verify.ts | 2 +- src/3.0/wrap.ts | 4 ++-- src/index.ts | 4 ++-- src/shared/@types/wrap.ts | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/3.0/digest.ts b/src/3.0/digest.ts index 11020d2f..809bb866 100644 --- a/src/3.0/digest.ts +++ b/src/3.0/digest.ts @@ -4,7 +4,7 @@ import { Salt } from "./types"; import { OpenAttestationDocument } from "../__generated__/schema.3.0"; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const digestCredential = (document: OpenAttestationDocument, salts: Salt[], obfuscatedData: string[]) => { // Prepare array of hashes from visible data diff --git a/src/3.0/obfuscate.ts b/src/3.0/obfuscate.ts index cdcf36b3..1f7ef020 100644 --- a/src/3.0/obfuscate.ts +++ b/src/3.0/obfuscate.ts @@ -38,7 +38,7 @@ const obfuscate = (_data: WrappedDocument, fields: stri }; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const obfuscateVerifiableCredential = ( document: WrappedDocument, diff --git a/src/3.0/salt.ts b/src/3.0/salt.ts index 4b17eac3..1527fbdf 100644 --- a/src/3.0/salt.ts +++ b/src/3.0/salt.ts @@ -22,12 +22,12 @@ const illegalCharactersCheck = (data: Record) => { // Using 32 bytes of entropy as compared to 16 bytes in uuid // Using hex encoding as compared to base64 for constant string length /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const secureRandomString = () => randomBytes(ENTROPY_IN_BYTES).toString("hex"); /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const salt = (data: any): Salt[] => { // Check for illegal characters e.g. '.', '[' or ']' @@ -36,12 +36,12 @@ export const salt = (data: any): Salt[] => { }; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const encodeSalt = (salts: Salt[]): string => Base64.encode(JSON.stringify(salts)); /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const decodeSalt = (salts: string): Salt[] => { const decoded: Salt[] = JSON.parse(Base64.decode(salts)); diff --git a/src/3.0/sign.ts b/src/3.0/sign.ts index 2656cd47..608e5c8d 100644 --- a/src/3.0/sign.ts +++ b/src/3.0/sign.ts @@ -10,7 +10,7 @@ import { isSignedWrappedV3Document } from "../shared/utils"; import { Signer } from "@ethersproject/abstract-signer"; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const signDocument = async ( document: SignedWrappedDocument | WrappedDocument, diff --git a/src/3.0/traverseAndFlatten.ts b/src/3.0/traverseAndFlatten.ts index 3b8bae97..ff8c0170 100644 --- a/src/3.0/traverseAndFlatten.ts +++ b/src/3.0/traverseAndFlatten.ts @@ -8,7 +8,7 @@ interface Options { } /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export function traverseAndFlatten(data: any[], options: Options): T[]; export function traverseAndFlatten(data: string | number | boolean | null, options: Options): T; diff --git a/src/3.0/types.ts b/src/3.0/types.ts index 884df64a..9dcfb5db 100644 --- a/src/3.0/types.ts +++ b/src/3.0/types.ts @@ -4,25 +4,25 @@ import { OpenAttestationHexString, ProofPurpose, SchemaId, SignatureAlgorithm } import { Array as RunTypesArray, Record as RunTypesRecord, Static, String } from "runtypes"; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export interface Salt { value: string; path: string; } /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const ObfuscationMetadata = RunTypesRecord({ obfuscated: RunTypesArray(OpenAttestationHexString), }); /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export type ObfuscationMetadata = Static; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const VerifiableCredentialWrappedProof = RunTypesRecord({ type: SignatureAlgorithm, @@ -34,11 +34,11 @@ export const VerifiableCredentialWrappedProof = RunTypesRecord({ proofPurpose: ProofPurpose, }); /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export type VerifiableCredentialWrappedProof = Static; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const VerifiableCredentialWrappedProofStrict = VerifiableCredentialWrappedProof.And( RunTypesRecord({ @@ -48,12 +48,12 @@ export const VerifiableCredentialWrappedProofStrict = VerifiableCredentialWrappe }) ); /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export type VerifiableCredentialWrappedProofStrict = Static; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const VerifiableCredentialSignedProof = VerifiableCredentialWrappedProof.And( RunTypesRecord({ @@ -62,14 +62,14 @@ export const VerifiableCredentialSignedProof = VerifiableCredentialWrappedProof. }) ); /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export type VerifiableCredentialSignedProof = Static; // TODO rename to something else that is not proof to allow for did-signed documents // Also it makes sense to use `proof` to denote a document that has been issued /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export type WrappedDocument = T & { version: SchemaId.v3; @@ -78,7 +78,7 @@ export type WrappedDocument = WrappedDocument & { @@ -86,6 +86,6 @@ export type SignedWrappedDocument(document: T): document is WrappedDocument => { if (!document.proof) { diff --git a/src/3.0/wrap.ts b/src/3.0/wrap.ts index 288e5fe4..921b859a 100644 --- a/src/3.0/wrap.ts +++ b/src/3.0/wrap.ts @@ -13,7 +13,7 @@ import { getSchema } from "../shared/ajv"; const getExternalSchema = (schema?: string) => (schema ? { schema } : {}); /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const wrapDocument = async ( credential: T, @@ -71,7 +71,7 @@ export const wrapDocument = async ( }; /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export const wrapDocuments = async ( documents: T[], diff --git a/src/index.ts b/src/index.ts index 54375b69..905be848 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,7 +47,7 @@ export function wrapDocuments( } /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export function __unsafe__use__it__at__your__own__risks__wrapDocument( data: T, @@ -57,7 +57,7 @@ export function __unsafe__use__it__at__your__own__risks__wrapDocument( dataArray: T[], diff --git a/src/shared/@types/wrap.ts b/src/shared/@types/wrap.ts index 42dc609f..6f70aa48 100644 --- a/src/shared/@types/wrap.ts +++ b/src/shared/@types/wrap.ts @@ -11,7 +11,7 @@ export interface WrapDocumentOptionV2 { } /** - * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/alpha) + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) */ export interface WrapDocumentOptionV3 { externalSchemaId?: string; From 8741efef1f8a6e17f65306ead88e56047a5c6d30 Mon Sep 17 00:00:00 2001 From: Kyle Huang Junyuan Date: Wed, 13 Nov 2024 16:56:13 +0800 Subject: [PATCH 2/6] chore: use correct terminologies like digest and vc --- src/4.0/__tests__/digest.test.ts | 279 ++++++++---------- src/4.0/__tests__/documentBuilder.test.ts | 16 +- src/4.0/__tests__/e2e.test.ts | 34 +-- src/4.0/__tests__/guard.test.ts | 18 +- src/4.0/__tests__/hash.test.ts | 175 +++++++++++ src/4.0/__tests__/obfuscate.test.ts | 57 ++-- src/4.0/__tests__/salt.test.ts | 2 +- src/4.0/__tests__/sign.test.ts | 16 +- .../{verify.test.ts => validate.test.ts} | 32 +- src/4.0/__tests__/wrap.test.ts | 132 --------- src/4.0/digest.ts | 210 +++++++++---- src/4.0/documentBuilder.ts | 14 +- src/4.0/exports/digest.ts | 1 + src/4.0/exports/index.ts | 4 +- src/4.0/exports/obfuscate.ts | 2 +- src/4.0/exports/sign.ts | 2 +- src/4.0/exports/validate.ts | 1 + src/4.0/exports/verify.ts | 1 - src/4.0/exports/wrap.ts | 1 - src/4.0/hash.ts | 74 +++++ src/4.0/obfuscate.ts | 10 +- src/4.0/sign.ts | 2 +- src/4.0/{verify.ts => validate.ts} | 6 +- src/4.0/wrap.ts | 160 ---------- src/index.ts | 7 +- 25 files changed, 622 insertions(+), 634 deletions(-) create mode 100644 src/4.0/__tests__/hash.test.ts rename src/4.0/__tests__/{verify.test.ts => validate.test.ts} (93%) delete mode 100644 src/4.0/__tests__/wrap.test.ts create mode 100644 src/4.0/exports/digest.ts create mode 100644 src/4.0/exports/validate.ts delete mode 100644 src/4.0/exports/verify.ts delete mode 100644 src/4.0/exports/wrap.ts create mode 100644 src/4.0/hash.ts rename src/4.0/{verify.ts => validate.ts} (76%) delete mode 100644 src/4.0/wrap.ts diff --git a/src/4.0/__tests__/digest.test.ts b/src/4.0/__tests__/digest.test.ts index de911b3c..4d351327 100644 --- a/src/4.0/__tests__/digest.test.ts +++ b/src/4.0/__tests__/digest.test.ts @@ -1,175 +1,132 @@ -import { digestCredential } from "../digest"; -import { decodeSalt } from "../salt"; -import { SIGNED_WRAPPED_DOCUMENT_DID as ROOT_CREDENTIAL } from "../fixtures"; -import { V4SignedWrappedDocument } from "../types"; -import { obfuscateVerifiableCredential } from "../obfuscate"; +import { V4OpenAttestationDocument, V4WrappedDocument, W3cVerifiableCredential } from "../types"; +import { digestVC } from "../digest"; -// All obfuscated documents are generated from the ROOT_CREDENTIAL -const ROOT_CREDENTIAL_TARGET_HASH = ROOT_CREDENTIAL.proof.targetHash; - -describe("V4 digestCredential", () => { - test("given that obfuscated documents are generated from the ROOT_CREDENTIAL, ROOT_CREDENTIAL_TARGET_HASH should match snapshot", () => { - expect(ROOT_CREDENTIAL_TARGET_HASH).toMatchInlineSnapshot( - `"0b1f90bc8e87cfce8ec49cea60d406291ad130ddedc26e866a8c4f2152747abc"` - ); +describe("V4.0 digest", () => { + test("given a valid v4 document, should wrap correctly", async () => { + const wrapped = await digestVC({ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://schemata.openattestation.com/com/openattestation/4.0/context.json", + ], + type: ["VerifiableCredential", "OpenAttestationCredential"], + credentialSubject: { + id: "0x1234567890123456789012345678901234567890", + name: "John Doe", + country: "SG", + }, + issuer: { + id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + type: "OpenAttestationIssuer", + name: "Government Technology Agency of Singapore (GovTech)", + identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, + }, + }); + const parsedResults = V4WrappedDocument.safeParse(wrapped); + if (!parsedResults.success) { + throw new Error("Parsing failed"); + } + const { proof } = parsedResults.data; + expect(proof.merkleRoot.length).toBe(64); + expect(proof.privacy.obfuscated).toEqual([]); + expect(proof.proofPurpose).toBe("assertionMethod"); + expect(proof.proofs).toEqual([]); + expect(proof.salts.length).toBeGreaterThan(0); + expect(proof.targetHash.length).toBe(64); + expect(proof.type).toBe("OpenAttestationHashProof2018"); }); - test("given a document with ALL FIELDS VISIBLE, should digest and match the root credential's target hash", () => { - expect(ROOT_CREDENTIAL.credentialSubject).toMatchInlineSnapshot(` - { - "id": "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42", - "licenses": [ - { - "class": "3", - "description": "Motor cars with unladen weight <= 3000kg", - "effectiveDate": "2013-05-16T00:00:00+08:00", - }, - { - "class": "3A", - "description": "Motor cars with unladen weight <= 3000kg", - "effectiveDate": "2013-05-16T00:00:00+08:00", - }, - ], - "name": "John Doe", - "type": [ - "DriversLicense", - ], - } - `); - expect(ROOT_CREDENTIAL.proof.privacy.obfuscated).toMatchInlineSnapshot(`[]`); - - const digest = digestCredential(ROOT_CREDENTIAL, decodeSalt(ROOT_CREDENTIAL.proof.salts), []); - expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); - }); + test("given a document with explicit v4 contexts, but does not conform to the V4 document schema, should throw", async () => { + await expect( + digestVC({ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://schemata.openattestation.com/com/openattestation/4.0/context.json", + ], - test("given a document with ONE element obfuscated, should digest and match the root credential's target hash", () => { - const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateVerifiableCredential(ROOT_CREDENTIAL, "credentialSubject.id"); - expect(OBFUSCATED_WRAPPED_DOCUMENT.credentialSubject).toMatchInlineSnapshot(` - { - "licenses": [ - { - "class": "3", - "description": "Motor cars with unladen weight <= 3000kg", - "effectiveDate": "2013-05-16T00:00:00+08:00", - }, - { - "class": "3A", - "description": "Motor cars with unladen weight <= 3000kg", - "effectiveDate": "2013-05-16T00:00:00+08:00", - }, - ], - "name": "John Doe", - "type": [ - "DriversLicense", - ], + type: ["VerifiableCredential", "OpenAttestationCredential"], + credentialSubject: { + id: "0x1234567890123456789012345678901234567890", + name: "John Doe", + country: "SG", + }, + issuer: { + id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + name: "Government Technology Agency of Singapore (GovTech)", + identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, + } as V4OpenAttestationDocument["issuer"], + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Input document does not conform to Open Attestation v4.0 Data Model: + { + "_errors": [], + "issuer": { + "_errors": [], + "type": { + "_errors": [ + "Invalid literal value, expected \\"OpenAttestationIssuer\\"" + ] + } } - `); - expect(OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated).toMatchInlineSnapshot(` - [ - "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", - ] + }" `); - - const digest = digestCredential( - OBFUSCATED_WRAPPED_DOCUMENT, - decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), - OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated - ); - expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); - expect(digest).toBe(OBFUSCATED_WRAPPED_DOCUMENT.proof.targetHash); }); - test("given a document with THREE elements obfuscated, should digest and match the root credential's target hash", () => { - const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateVerifiableCredential(ROOT_CREDENTIAL, [ - "credentialSubject.id", - "credentialSubject.name", - "credentialSubject.licenses[0].description", - ]); - expect(OBFUSCATED_WRAPPED_DOCUMENT.credentialSubject).toMatchInlineSnapshot(` - { - "licenses": [ - { - "class": "3", - "effectiveDate": "2013-05-16T00:00:00+08:00", - }, - { - "class": "3A", - "description": "Motor cars with unladen weight <= 3000kg", - "effectiveDate": "2013-05-16T00:00:00+08:00", - }, - ], - "type": [ - "DriversLicense", - ], - } - `); - expect(OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated).toMatchInlineSnapshot(` - [ - "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", - "f49443c7e5fcb9f20dad4463a5e0b2cb3e341c430d4792cb87cb11bce0efd9b0", - "7f2ecdae29b49b3a971d5acdfbbf9225a193e735ce41b89b0d84cca801794fc9", - ] - `); + test("given a valid v4 document but has an extra field, should throw", async () => { + await expect( + digestVC({ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://schemata.openattestation.com/com/openattestation/4.0/context.json", + ], - const digest = digestCredential( - OBFUSCATED_WRAPPED_DOCUMENT, - decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), - OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated - ); - expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); - expect(digest).toBe(OBFUSCATED_WRAPPED_DOCUMENT.proof.targetHash); + type: ["VerifiableCredential", "OpenAttestationCredential"], + credentialSubject: { + id: "0x1234567890123456789012345678901234567890", + name: "John Doe", + country: "SG", + }, + issuer: { + id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + type: "OpenAttestationIssuer", + name: "Government Technology Agency of Singapore (GovTech)", + extraField: "extra", + identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, + }, + // this should not exist + extraField: "extra", + } as V4OpenAttestationDocument) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Input document does not conform to Open Attestation v4.0 Data Model: + { + "_errors": [ + "Unrecognized key(s) in object: 'extraField'" + ] + }" + `); }); - test("given a document with NO VISIBLE FIELDS, should digest and match the root credential's target hash", () => { - // this has to be manually generated, since obfuscateVerifiableCredential does not allow obfuscating fields that - // result in a non compliant V4 OA document - const OBFUSCATED_WRAPPED_DOCUMENT = { - // no visible fields - proof: { - type: "OpenAttestationHashProof2018", - proofPurpose: "assertionMethod", - targetHash: "0b1f90bc8e87cfce8ec49cea60d406291ad130ddedc26e866a8c4f2152747abc", - proofs: [], - merkleRoot: "0b1f90bc8e87cfce8ec49cea60d406291ad130ddedc26e866a8c4f2152747abc", - salts: "W10=", - privacy: { - obfuscated: [ - "fb3e116ab528a97d055822754f9ccd1ca5d2962a74d533cc34f066e65a93c76f", - "fe5c8db00ea1f1b4cfcbc29d00810cd6e18f715b98d3660090ee30cf88b4375c", - "27c33bf2f9e5ba4d94c017569174f1432f8887994bfaa70a50c0cf42e62e9f3e", - "5094d0467785684f843648d3edbd1e370df296327796a13b18112e0941bbf14e", - "a4723abfc6809faa72d62d44bb9a11d35e93a780c7a5cb69cdd3693c45960367", - "62858cb5907188767134ec958c6cdfd17e44e52f1511e56b06670fe1b0588160", - "f0250ff7053e849fda119078d5d5dd6689eb7751a74cab71aa11f92941d22aa9", - "d1741f3c9b8bde24eea271870f8200c6c627a94739051d7b7a480e0aaff60bc0", - "6da741164cefb41160b23388b3ee9b0944fab0bedd70b63e20cee0af3fabe565", - "780e835a67653d28f0582d8fb3a1980709b178841fe4d1f6019be0f49db41ac3", - "5c91f334f63f258e4ba299da14880019711538169512e5c6449fbfca7edd7110", - "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", - "ab0957fe8747ac06749268e6398bd4cf67a8a22bf0e67eaacc030bcb5f11e3ed", - "f49443c7e5fcb9f20dad4463a5e0b2cb3e341c430d4792cb87cb11bce0efd9b0", - "0df8aa79b275612b491103b10804276364da6dc49f398faa7be2190de1d60cd2", - "7f2ecdae29b49b3a971d5acdfbbf9225a193e735ce41b89b0d84cca801794fc9", - "0eccbf844ac0b68bdd5de85894dce6ecb429f36f4e21630ff70d487a92b2e75f", - "135c5417e9baec64bbe977f9244496aae4a452bf58177b4fd9064c8afdfe483a", - "b8e8cc46e99c58420e5819ed9f80b90489b2db72f6eb94dc84d1f6a15a331030", - "b5554487209f1b99fc73190a8f32e3b2087a6e310f3d05f7c8f7c1f488565b0c", - "c38928d0bad7d71f6e2a7aa33b4983afbeaa9e3c990de6137385b30fc6d5a9ac", - "856d307b40543221d78ba858c6438f4f3e773ab2a81f3140bdff8bc21e30b0d5", - "2be8c866f23b27108c9f2d9acfc21bfef5f61124a2272eb3cee1e94cd79c68c0", - ], - }, - key: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90#controller", - signature: - "0x949b76d8df493a56c1cf21303a74d6a54904461c1c10f4619b43ad7d339c64467c61eb4c0873f279cd21d5bdd044d3af5318f14d63f57acbd4cde30f271f3eb71c", + test("given a generic w3c vc, should wrap with context and type corrected", async () => { + const genericW3cVc: W3cVerifiableCredential = { + "@context": ["https://www.w3.org/ns/credentials/v2"], + type: ["VerifiableCredential"], + credentialSubject: { + id: "0x1234567890123456789012345678901234567890", + name: "John Doe", + country: "SG", }, - } as unknown as V4SignedWrappedDocument; - - const digest = digestCredential( - OBFUSCATED_WRAPPED_DOCUMENT, - decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), - OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated - ); - expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); + issuer: { + id: "https://example.com/issuer/123", + }, + }; + const wrapped = await digestVC(genericW3cVc as unknown as V4OpenAttestationDocument); + const parsedResults = V4WrappedDocument.pick({ "@context": true, type: true }).passthrough().safeParse(wrapped); + expect(parsedResults.success).toBe(true); + expect(wrapped.proof.merkleRoot.length).toBe(64); + expect(wrapped.proof.privacy.obfuscated).toEqual([]); + expect(wrapped.proof.proofPurpose).toBe("assertionMethod"); + expect(wrapped.proof.proofs).toEqual([]); + expect(wrapped.proof.salts.length).toBeGreaterThan(0); + expect(wrapped.proof.targetHash.length).toBe(64); + expect(wrapped.proof.type).toBe("OpenAttestationHashProof2018"); }); }); diff --git a/src/4.0/__tests__/documentBuilder.test.ts b/src/4.0/__tests__/documentBuilder.test.ts index a218db75..6d64bf2d 100644 --- a/src/4.0/__tests__/documentBuilder.test.ts +++ b/src/4.0/__tests__/documentBuilder.test.ts @@ -1,9 +1,9 @@ -import { verify } from "../verify"; +import { validateDigest } from "../validate"; import { DocumentBuilder, DocumentBuilderErrors } from "../documentBuilder"; -import { isSignedWrappedDocument, isWrappedDocument, signDocument } from "../exports"; +import { isSignedWrappedDocument, isWrappedDocument, signVC } from "../exports"; import { SAMPLE_SIGNING_KEYS } from "../fixtures"; -describe(`DocumentBuilder`, () => { +describe(`V4.0 DocumentBuilder`, () => { describe("given a single document", () => { const document = new DocumentBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma" }) .embeddedRenderer({ @@ -53,7 +53,7 @@ describe(`DocumentBuilder`, () => { } `); expect(isSignedWrappedDocument(signed)).toBe(true); - expect(verify(signed)).toBe(true); + expect(validateDigest(signed)).toBe(true); }); test("given wrap is called, return a wrapped document", async () => { @@ -139,7 +139,7 @@ describe(`DocumentBuilder`, () => { `); expect(signed[0].credentialStatus).toBeUndefined(); expect(isSignedWrappedDocument(signed[0])).toBe(true); - expect(verify(signed[0])).toBe(true); + expect(validateDigest(signed[0])).toBe(true); expect(signed[1].issuer).toMatchInlineSnapshot(` { @@ -168,7 +168,7 @@ describe(`DocumentBuilder`, () => { `); expect(signed[1].credentialStatus).toBeUndefined(); expect(isSignedWrappedDocument(signed[1])).toBe(true); - expect(verify(signed[1])).toBe(true); + expect(validateDigest(signed[1])).toBe(true); }); test("given wrap is called, return a list of wrapped document", async () => { @@ -402,10 +402,10 @@ describe(`DocumentBuilder`, () => { }) .justWrapWithoutSigning(); - const signed = await signDocument(wrapped, "Secp256k1VerificationKey2018", SAMPLE_SIGNING_KEYS); + const signed = await signVC(wrapped, "Secp256k1VerificationKey2018", SAMPLE_SIGNING_KEYS); expect(isSignedWrappedDocument(signed)).toBe(true); - expect(verify(signed)).toBe(true); + expect(validateDigest(signed)).toBe(true); }); test("given re-setting of values, should throw", async () => { diff --git a/src/4.0/__tests__/e2e.test.ts b/src/4.0/__tests__/e2e.test.ts index e2a2ec88..38aa97bf 100644 --- a/src/4.0/__tests__/e2e.test.ts +++ b/src/4.0/__tests__/e2e.test.ts @@ -2,7 +2,7 @@ import { obfuscate, validateSchema, verifySignature } from "../.."; import { cloneDeep, omit } from "lodash"; import { RAW_DOCUMENT_DID, SIGNED_WRAPPED_DOCUMENT_DID, WRAPPED_DOCUMENT_DID } from "../fixtures"; import { V4OpenAttestationDocument } from "../types"; -import { wrapDocument, wrapDocuments } from "../wrap"; +import { digestVC, digestVCs } from "../digest"; const DOCUMENT_ONE = { ...RAW_DOCUMENT_DID, @@ -40,13 +40,13 @@ const DOCUMENT_FOUR = { }; const DATUM = [DOCUMENT_ONE, DOCUMENT_TWO, DOCUMENT_THREE, DOCUMENT_FOUR] satisfies V4OpenAttestationDocument[]; -describe("V4 E2E Test Scenarios", () => { +describe("V4.0 E2E Test Scenarios", () => { describe("Issuing a single document", () => { test("fails for missing data", async () => { const missingData = { ...omit(cloneDeep(DOCUMENT_ONE), "issuer"), }; - await expect(wrapDocument(missingData as unknown as V4OpenAttestationDocument)).rejects + await expect(digestVC(missingData as unknown as V4OpenAttestationDocument)).rejects .toThrowErrorMatchingInlineSnapshot(` "Input document does not conform to Open Attestation v4.0 Data Model: { @@ -61,7 +61,7 @@ describe("V4 E2E Test Scenarios", () => { }); test("creates a wrapped document", async () => { - const wrappedDocument = await wrapDocument(RAW_DOCUMENT_DID); + const wrappedDocument = await digestVC(RAW_DOCUMENT_DID); expect(wrappedDocument["@context"]).toEqual([ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -75,25 +75,25 @@ describe("V4 E2E Test Scenarios", () => { }); test("checks that document is wrapped correctly", async () => { - const wrappedDocument = await wrapDocument(DOCUMENT_ONE); + const wrappedDocument = await digestVC(DOCUMENT_ONE); const verified = verifySignature(wrappedDocument); expect(verified).toBe(true); }); test("checks that document conforms to the schema", async () => { - const wrappedDocument = await wrapDocument(DOCUMENT_ONE); + const wrappedDocument = await digestVC(DOCUMENT_ONE); expect(validateSchema(wrappedDocument)).toBe(true); }); test("does not allow for the same merkle root to be generated", async () => { // This test takes some time to run, so we set the timeout to 14s - const wrappedDocument = await wrapDocument(DOCUMENT_ONE); - const newDocument = await wrapDocument(DOCUMENT_ONE); + const wrappedDocument = await digestVC(DOCUMENT_ONE); + const newDocument = await digestVC(DOCUMENT_ONE); expect(wrappedDocument.proof.merkleRoot).not.toBe(newDocument.proof.merkleRoot); }, 14000); test("obfuscate data correctly", async () => { - const newDocument = await wrapDocument(DOCUMENT_THREE); + const newDocument = await digestVC(DOCUMENT_THREE); expect(newDocument.credentialSubject.key2).toBeDefined(); const obfuscatedDocument = obfuscate(newDocument, ["credentialSubject.key2"]); expect(verifySignature(obfuscatedDocument)).toBe(true); @@ -102,7 +102,7 @@ describe("V4 E2E Test Scenarios", () => { }); test("obfuscate data transistively", async () => { - const newDocument = await wrapDocument(DOCUMENT_THREE); + const newDocument = await digestVC(DOCUMENT_THREE); const intermediateDocument = obfuscate(newDocument, ["credentialSubject.key2"]); const obfuscatedDocument = obfuscate(intermediateDocument, ["credentialSubject.key3"]); expect(obfuscate(newDocument, ["credentialSubject.key2", "credentialSubject.key3"])).toEqual(obfuscatedDocument); @@ -116,13 +116,13 @@ describe("V4 E2E Test Scenarios", () => { laurent: "task force, assemble!!", } as unknown as V4OpenAttestationDocument, ]; - await expect(wrapDocuments(malformedDatum)).rejects.toThrow( + await expect(digestVCs(malformedDatum)).rejects.toThrow( "Input document does not conform to Verifiable Credentials" ); }); test("creates a batch of documents if all are in the right format", async () => { - const wrappedDocuments = await wrapDocuments(DATUM); + const wrappedDocuments = await digestVCs(DATUM); wrappedDocuments.forEach((doc, i: number) => { expect(doc.type).toEqual(["VerifiableCredential", "OpenAttestationCredential"]); expect(doc.proof.type).toBe("OpenAttestationHashProof2018"); @@ -135,13 +135,13 @@ describe("V4 E2E Test Scenarios", () => { }); test("checks that documents are wrapped correctly", async () => { - const wrappedDocuments = await wrapDocuments(DATUM); + const wrappedDocuments = await digestVCs(DATUM); const verified = wrappedDocuments.reduce((prev, curr) => verifySignature(curr) && prev, true); expect(verified).toBe(true); }); test("checks that documents conforms to the schema", async () => { - const wrappedDocuments = await wrapDocuments(DATUM); + const wrappedDocuments = await digestVCs(DATUM); const validatedSchema = wrappedDocuments.reduce( (prev: boolean, curr: any) => validateSchema(curr) && prev, true @@ -150,8 +150,8 @@ describe("V4 E2E Test Scenarios", () => { }); test("does not allow for same merkle root to be generated", async () => { - const wrappedDocuments = await wrapDocuments(DATUM); - const newWrappedDocuments = await wrapDocuments(DATUM); + const wrappedDocuments = await digestVCs(DATUM); + const newWrappedDocuments = await digestVCs(DATUM); expect(wrappedDocuments[0].proof.merkleRoot).not.toBe(newWrappedDocuments[0].proof.merkleRoot); }); }); @@ -210,7 +210,7 @@ describe("V4 E2E Test Scenarios", () => { key4: "خحثىشففثسفشفهخى", }, }; - const wrapped = await wrapDocument(document); + const wrapped = await digestVC(document); expect(wrapped.proof.merkleRoot).toBeTruthy(); expect(wrapped.credentialSubject.key1).toBe(document.credentialSubject.key1); expect(wrapped.credentialSubject.key2).toBe(document.credentialSubject.key2); diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index f7dcee28..701763ce 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -1,13 +1,13 @@ import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign"; import { RAW_DOCUMENT_DID } from "../fixtures"; -import { signDocument } from "../sign"; +import { signVC } from "../sign"; import { W3cVerifiableCredential, V4OpenAttestationDocument, V4WrappedDocument, V4SignedWrappedDocument, } from "../types"; -import { wrapDocument } from "../wrap"; +import { digestVC } from "../digest"; const RAW_DOCUMENT = { ...RAW_DOCUMENT_DID, @@ -27,15 +27,11 @@ describe("V4.0 guard", () => { let WRAPPED_DOCUMENT: V4WrappedDocument; let SIGNED_WRAPPED_DOCUMENT: V4SignedWrappedDocument; beforeAll(async () => { - WRAPPED_DOCUMENT = await wrapDocument(RAW_DOCUMENT); - SIGNED_WRAPPED_DOCUMENT = await signDocument( - WRAPPED_DOCUMENT, - SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, - { - public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", - private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", - } - ); + WRAPPED_DOCUMENT = await digestVC(RAW_DOCUMENT); + SIGNED_WRAPPED_DOCUMENT = await signVC(WRAPPED_DOCUMENT, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { + public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", + }); }); describe("given a raw document", () => { diff --git a/src/4.0/__tests__/hash.test.ts b/src/4.0/__tests__/hash.test.ts new file mode 100644 index 00000000..78dfc2dc --- /dev/null +++ b/src/4.0/__tests__/hash.test.ts @@ -0,0 +1,175 @@ +import { genTargetHash } from "../hash"; +import { decodeSalt } from "../salt"; +import { SIGNED_WRAPPED_DOCUMENT_DID as ROOT_CREDENTIAL } from "../fixtures"; +import { V4SignedWrappedDocument } from "../types"; +import { obfuscateVC } from "../obfuscate"; + +// All obfuscated documents are generated from the ROOT_CREDENTIAL +const ROOT_CREDENTIAL_TARGET_HASH = ROOT_CREDENTIAL.proof.targetHash; + +describe("V4.0 hash", () => { + test("given that obfuscated documents are generated from the ROOT_CREDENTIAL, ROOT_CREDENTIAL_TARGET_HASH should match snapshot", () => { + expect(ROOT_CREDENTIAL_TARGET_HASH).toMatchInlineSnapshot( + `"0b1f90bc8e87cfce8ec49cea60d406291ad130ddedc26e866a8c4f2152747abc"` + ); + }); + + test("given a document with ALL FIELDS VISIBLE, should digest and match the root credential's target hash", () => { + expect(ROOT_CREDENTIAL.credentialSubject).toMatchInlineSnapshot(` + { + "id": "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42", + "licenses": [ + { + "class": "3", + "description": "Motor cars with unladen weight <= 3000kg", + "effectiveDate": "2013-05-16T00:00:00+08:00", + }, + { + "class": "3A", + "description": "Motor cars with unladen weight <= 3000kg", + "effectiveDate": "2013-05-16T00:00:00+08:00", + }, + ], + "name": "John Doe", + "type": [ + "DriversLicense", + ], + } + `); + expect(ROOT_CREDENTIAL.proof.privacy.obfuscated).toMatchInlineSnapshot(`[]`); + + const digest = genTargetHash(ROOT_CREDENTIAL, decodeSalt(ROOT_CREDENTIAL.proof.salts), []); + expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); + }); + + test("given a document with ONE element obfuscated, should digest and match the root credential's target hash", () => { + const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateVC(ROOT_CREDENTIAL, "credentialSubject.id"); + expect(OBFUSCATED_WRAPPED_DOCUMENT.credentialSubject).toMatchInlineSnapshot(` + { + "licenses": [ + { + "class": "3", + "description": "Motor cars with unladen weight <= 3000kg", + "effectiveDate": "2013-05-16T00:00:00+08:00", + }, + { + "class": "3A", + "description": "Motor cars with unladen weight <= 3000kg", + "effectiveDate": "2013-05-16T00:00:00+08:00", + }, + ], + "name": "John Doe", + "type": [ + "DriversLicense", + ], + } + `); + expect(OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated).toMatchInlineSnapshot(` + [ + "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", + ] + `); + + const digest = genTargetHash( + OBFUSCATED_WRAPPED_DOCUMENT, + decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), + OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated + ); + expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); + expect(digest).toBe(OBFUSCATED_WRAPPED_DOCUMENT.proof.targetHash); + }); + + test("given a document with THREE elements obfuscated, should digest and match the root credential's target hash", () => { + const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateVC(ROOT_CREDENTIAL, [ + "credentialSubject.id", + "credentialSubject.name", + "credentialSubject.licenses[0].description", + ]); + expect(OBFUSCATED_WRAPPED_DOCUMENT.credentialSubject).toMatchInlineSnapshot(` + { + "licenses": [ + { + "class": "3", + "effectiveDate": "2013-05-16T00:00:00+08:00", + }, + { + "class": "3A", + "description": "Motor cars with unladen weight <= 3000kg", + "effectiveDate": "2013-05-16T00:00:00+08:00", + }, + ], + "type": [ + "DriversLicense", + ], + } + `); + expect(OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated).toMatchInlineSnapshot(` + [ + "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", + "f49443c7e5fcb9f20dad4463a5e0b2cb3e341c430d4792cb87cb11bce0efd9b0", + "7f2ecdae29b49b3a971d5acdfbbf9225a193e735ce41b89b0d84cca801794fc9", + ] + `); + + const digest = genTargetHash( + OBFUSCATED_WRAPPED_DOCUMENT, + decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), + OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated + ); + expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); + expect(digest).toBe(OBFUSCATED_WRAPPED_DOCUMENT.proof.targetHash); + }); + + test("given a document with NO VISIBLE FIELDS, should digest and match the root credential's target hash", () => { + // this has to be manually generated, since obfuscateVerifiableCredential does not allow obfuscating fields that + // result in a non compliant V4 OA document + const OBFUSCATED_WRAPPED_DOCUMENT = { + // no visible fields + proof: { + type: "OpenAttestationHashProof2018", + proofPurpose: "assertionMethod", + targetHash: "0b1f90bc8e87cfce8ec49cea60d406291ad130ddedc26e866a8c4f2152747abc", + proofs: [], + merkleRoot: "0b1f90bc8e87cfce8ec49cea60d406291ad130ddedc26e866a8c4f2152747abc", + salts: "W10=", + privacy: { + obfuscated: [ + "fb3e116ab528a97d055822754f9ccd1ca5d2962a74d533cc34f066e65a93c76f", + "fe5c8db00ea1f1b4cfcbc29d00810cd6e18f715b98d3660090ee30cf88b4375c", + "27c33bf2f9e5ba4d94c017569174f1432f8887994bfaa70a50c0cf42e62e9f3e", + "5094d0467785684f843648d3edbd1e370df296327796a13b18112e0941bbf14e", + "a4723abfc6809faa72d62d44bb9a11d35e93a780c7a5cb69cdd3693c45960367", + "62858cb5907188767134ec958c6cdfd17e44e52f1511e56b06670fe1b0588160", + "f0250ff7053e849fda119078d5d5dd6689eb7751a74cab71aa11f92941d22aa9", + "d1741f3c9b8bde24eea271870f8200c6c627a94739051d7b7a480e0aaff60bc0", + "6da741164cefb41160b23388b3ee9b0944fab0bedd70b63e20cee0af3fabe565", + "780e835a67653d28f0582d8fb3a1980709b178841fe4d1f6019be0f49db41ac3", + "5c91f334f63f258e4ba299da14880019711538169512e5c6449fbfca7edd7110", + "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", + "ab0957fe8747ac06749268e6398bd4cf67a8a22bf0e67eaacc030bcb5f11e3ed", + "f49443c7e5fcb9f20dad4463a5e0b2cb3e341c430d4792cb87cb11bce0efd9b0", + "0df8aa79b275612b491103b10804276364da6dc49f398faa7be2190de1d60cd2", + "7f2ecdae29b49b3a971d5acdfbbf9225a193e735ce41b89b0d84cca801794fc9", + "0eccbf844ac0b68bdd5de85894dce6ecb429f36f4e21630ff70d487a92b2e75f", + "135c5417e9baec64bbe977f9244496aae4a452bf58177b4fd9064c8afdfe483a", + "b8e8cc46e99c58420e5819ed9f80b90489b2db72f6eb94dc84d1f6a15a331030", + "b5554487209f1b99fc73190a8f32e3b2087a6e310f3d05f7c8f7c1f488565b0c", + "c38928d0bad7d71f6e2a7aa33b4983afbeaa9e3c990de6137385b30fc6d5a9ac", + "856d307b40543221d78ba858c6438f4f3e773ab2a81f3140bdff8bc21e30b0d5", + "2be8c866f23b27108c9f2d9acfc21bfef5f61124a2272eb3cee1e94cd79c68c0", + ], + }, + key: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90#controller", + signature: + "0x949b76d8df493a56c1cf21303a74d6a54904461c1c10f4619b43ad7d339c64467c61eb4c0873f279cd21d5bdd044d3af5318f14d63f57acbd4cde30f271f3eb71c", + }, + } as unknown as V4SignedWrappedDocument; + + const digest = genTargetHash( + OBFUSCATED_WRAPPED_DOCUMENT, + decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), + OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated + ); + expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); + }); +}); diff --git a/src/4.0/__tests__/obfuscate.test.ts b/src/4.0/__tests__/obfuscate.test.ts index c6b27005..6f481ecf 100644 --- a/src/4.0/__tests__/obfuscate.test.ts +++ b/src/4.0/__tests__/obfuscate.test.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { obfuscateVerifiableCredential } from "../obfuscate"; +import { obfuscateVC } from "../obfuscate"; import { get } from "lodash"; import { decodeSalt } from "../salt"; -import { wrapDocument } from "../wrap"; +import { digestVC } from "../digest"; import { Salt, V4OpenAttestationDocument, V4WrappedDocument } from "../types"; import { verifySignature } from "../../"; import { RAW_DOCUMENT_DID, SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED, WRAPPED_DOCUMENT_DID } from "../fixtures"; -import { hashLeafNode } from "../digest"; +import { hashLeafNode } from "../hash"; import { getObfuscatedData, isObfuscated } from "../../shared/utils"; const makeV4RawDocument = >(props: T) => @@ -43,14 +43,14 @@ const expectRemovedFieldsWithoutArrayNotation = ( expect(obfuscatedDocument).not.toHaveProperty(field); }; -describe("privacy", () => { - describe("obfuscateDocument", () => { +describe("V4.0 obfuscate", () => { + describe("obfuscateVC", () => { test("removes one field from the root object", async () => { const PATH_TO_REMOVE = "name"; - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { id: "S1234567A", name: "John Doe" } }) ); - const obfuscatedDocument = obfuscateVerifiableCredential(wrappedDocument, PATH_TO_REMOVE); + const obfuscatedDocument = obfuscateVC(wrappedDocument, PATH_TO_REMOVE); const verified = verifySignature(obfuscatedDocument); expect(verified).toBe(true); @@ -60,17 +60,17 @@ describe("privacy", () => { test("removes paths that result in an invalid wrapped document, should throw", async () => { const PATHS_TO_REMOVE = ["credentialSubject", "renderMethod.0.id", "name"]; - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { id: "S1234567A", name: "John Doe" } }) ); - expect(() => obfuscateVerifiableCredential(wrappedDocument, PATHS_TO_REMOVE)).toThrowError( + expect(() => obfuscateVC(wrappedDocument, PATHS_TO_REMOVE)).toThrowError( `"credentialSubject", "renderMethod.0.id"` ); }); test("removes one key of an object from an array", async () => { const PATH_TO_REMOVE = "credentialSubject.arrayOfObject[0].foo" as const; - const newDocument = await wrapDocument( + const newDocument = await digestVC( makeV4RawDocument({ credentialSubject: { id: "did:example:ebfeb1f712ebc6f1c276e12ec21", @@ -83,7 +83,7 @@ describe("privacy", () => { }, }) ); - const obfuscatedDocument = await obfuscateVerifiableCredential(newDocument, PATH_TO_REMOVE); + const obfuscatedDocument = await obfuscateVC(newDocument, PATH_TO_REMOVE); const verified = verifySignature(obfuscatedDocument); expect(verified).toBe(true); @@ -103,7 +103,7 @@ describe("privacy", () => { test("given an object is to be removed, should remove the object itself, as well as add each of its key's hash into privacy.obfuscated", async () => { const PATH_TO_REMOVE = "credentialSubject.hee"; - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { hee: { foo: "bar", doo: "foo" }, @@ -111,7 +111,7 @@ describe("privacy", () => { }, }) ); - const obfuscatedDocument = obfuscateVerifiableCredential(wrappedDocument, PATH_TO_REMOVE); + const obfuscatedDocument = obfuscateVC(wrappedDocument, PATH_TO_REMOVE); const verified = verifySignature(obfuscatedDocument); expect(verified).toBe(true); @@ -135,7 +135,7 @@ describe("privacy", () => { test("given an entire array of objects to remove, should remove the array itself, then for every item, add each of its key's hash into privacy.obfuscated", async () => { const PATH_TO_REMOVE = "credentialSubject.attachments"; - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { arrayOfObject: [ @@ -157,7 +157,7 @@ describe("privacy", () => { }, }) ); - const obfuscatedDocument = await obfuscateVerifiableCredential(wrappedDocument, PATH_TO_REMOVE); + const obfuscatedDocument = await obfuscateVC(wrappedDocument, PATH_TO_REMOVE); const verified = verifySignature(obfuscatedDocument); expect(verified).toBe(true); @@ -186,7 +186,7 @@ describe("privacy", () => { test("given multiple fields to be removed, should remove fields and add their hash into privacy.obfuscated", async () => { const PATHS_TO_REMOVE = ["credentialSubject.key1", "credentialSubject.key2"]; - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { key1: "value1", @@ -195,7 +195,7 @@ describe("privacy", () => { }, }) ); - const obfuscatedDocument = await obfuscateVerifiableCredential(wrappedDocument, PATHS_TO_REMOVE); + const obfuscatedDocument = await obfuscateVC(wrappedDocument, PATHS_TO_REMOVE); const verified = verifySignature(obfuscatedDocument); expect(verified).toBe(true); @@ -206,7 +206,7 @@ describe("privacy", () => { }); test("given a path to remove an entire item from an array, should throw", async () => { - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { arrayOfObject: [ @@ -235,15 +235,12 @@ describe("privacy", () => { ); expect(() => - obfuscateVerifiableCredential(wrappedDocument, [ - "credentialSubject.attachments[0]", - "credentialSubject.attachments[2]", - ]) + obfuscateVC(wrappedDocument, ["credentialSubject.attachments[0]", "credentialSubject.attachments[2]"]) ).toThrow(); }); test("given a path to remove all elements in an object, should throw", async () => { - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { arrayOfObject: [ @@ -258,22 +255,20 @@ describe("privacy", () => { ); expect(() => - obfuscateVerifiableCredential(wrappedDocument, [ + obfuscateVC(wrappedDocument, [ "credentialSubject.arrayOfObject[0].foo", "credentialSubject.arrayOfObject[0].doo", ]) ).toThrowErrorMatchingInlineSnapshot( `"Obfuscation of "credentialSubject.arrayOfObject[0].doo" has resulted in an empty {}, this is currently not supported. Alternatively, if the object is not part of an array, you may choose to obfuscate the parent of "credentialSubject.arrayOfObject[0].doo"."` ); - expect(() => - obfuscateVerifiableCredential(wrappedDocument, ["credentialSubject.object.foo"]) - ).toThrowErrorMatchingInlineSnapshot( + expect(() => obfuscateVC(wrappedDocument, ["credentialSubject.object.foo"])).toThrowErrorMatchingInlineSnapshot( `"Obfuscation of "credentialSubject.object.foo" has resulted in an empty {}, this is currently not supported. Alternatively, if the object is not part of an array, you may choose to obfuscate the parent of "credentialSubject.object.foo"."` ); }); test("is transitive", async () => { - const wrappedDocument = await wrapDocument( + const wrappedDocument = await digestVC( makeV4RawDocument({ credentialSubject: { key1: "value1", @@ -282,9 +277,9 @@ describe("privacy", () => { }, }) ); - const intermediateDoc = obfuscateVerifiableCredential(wrappedDocument, "key1"); - const finalDoc1 = obfuscateVerifiableCredential(intermediateDoc, "key2"); - const finalDoc2 = obfuscateVerifiableCredential(wrappedDocument, ["key1", "key2"]); + const intermediateDoc = obfuscateVC(wrappedDocument, "key1"); + const finalDoc1 = obfuscateVC(intermediateDoc, "key2"); + const finalDoc2 = obfuscateVC(wrappedDocument, ["key1", "key2"]); expect(finalDoc1).toEqual(finalDoc2); expect(intermediateDoc).not.toHaveProperty("key1"); diff --git a/src/4.0/__tests__/salt.test.ts b/src/4.0/__tests__/salt.test.ts index a512668f..afe20bdc 100644 --- a/src/4.0/__tests__/salt.test.ts +++ b/src/4.0/__tests__/salt.test.ts @@ -1,7 +1,7 @@ import { salt, decodeSalt } from "../salt"; import { Base64 } from "js-base64"; -describe("V4.0 digest", () => { +describe("V4.0 salt", () => { describe("salt", () => { test("handles shadowed keys correctly (type 1: root, dot notation)", () => { const document = { diff --git a/src/4.0/__tests__/sign.test.ts b/src/4.0/__tests__/sign.test.ts index cd6ed2f7..58cb1a01 100644 --- a/src/4.0/__tests__/sign.test.ts +++ b/src/4.0/__tests__/sign.test.ts @@ -2,11 +2,11 @@ import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign"; import { Wallet } from "@ethersproject/wallet"; import { WRAPPED_DOCUMENT_DID } from "../fixtures"; import { V4SignedWrappedDocument } from "../types"; -import { signDocument } from "../sign"; +import { signVC } from "../sign"; -describe("V4 sign", () => { +describe("V4.0 sign", () => { it("should sign a document", async () => { - const signedWrappedDocument = await signDocument( + const signedWrappedDocument = await signVC( WRAPPED_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { @@ -29,7 +29,7 @@ describe("V4 sign", () => { const wallet = Wallet.fromMnemonic( "tourist quality multiply denial diary height funny calm disease buddy speed gold" ); - const signedWrappedDocument = await signDocument( + const signedWrappedDocument = await signVC( WRAPPED_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, wallet @@ -47,7 +47,7 @@ describe("V4 sign", () => { }); it("should a signed document to be resigned", async () => { - const signedDocument = await signDocument( + const signedDocument = await signVC( WRAPPED_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { @@ -56,7 +56,7 @@ describe("V4 sign", () => { } ); - const resignedDocument = await signDocument( + const resignedDocument = await signVC( WRAPPED_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { @@ -70,13 +70,13 @@ describe("V4 sign", () => { it("should throw error if a key or signer is invalid", async () => { await expect( - signDocument(WRAPPED_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, {} as any) + signVC(WRAPPED_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, {} as any) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Either a keypair or ethers.js Signer must be provided"`); }); it("should throw error if proof is malformed", async () => { await expect( - signDocument( + signVC( { ...WRAPPED_DOCUMENT_DID, proof: { ...WRAPPED_DOCUMENT_DID.proof, merkleRoot: undefined as unknown as string }, diff --git a/src/4.0/__tests__/verify.test.ts b/src/4.0/__tests__/validate.test.ts similarity index 93% rename from src/4.0/__tests__/verify.test.ts rename to src/4.0/__tests__/validate.test.ts index edd587fb..77b57adb 100644 --- a/src/4.0/__tests__/verify.test.ts +++ b/src/4.0/__tests__/validate.test.ts @@ -1,7 +1,7 @@ import { cloneDeep } from "lodash"; import { BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID, SIGNED_WRAPPED_DOCUMENT_DID } from "../fixtures"; import { V4SignedWrappedDocument } from "../types"; -import { verify } from "../verify"; +import { validateDigest } from "../validate"; const TEST_DOCUMENTS = { "Documents without proofs mean these documents are wrapped individually (i.e. targetHash == merkleRoot)": @@ -10,11 +10,11 @@ const TEST_DOCUMENTS = { BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID[0], } as const; -describe("V4 verify", () => { +describe("V4.0 validate", () => { Object.entries(TEST_DOCUMENTS).forEach(([description, document]) => { describe(`${description}`, () => { test("given a document wiht unaltered data, should return true", () => { - expect(verify(document)).toBe(true); + expect(validateDigest(document)).toBe(true); }); describe("tempering", () => { @@ -22,7 +22,7 @@ describe("V4 verify", () => { const newName = "Fake Name"; expect(document.issuer.name).not.toBe(newName); expect( - verify({ + validateDigest({ ...document, issuer: { ...document.issuer, @@ -36,7 +36,7 @@ describe("V4 verify", () => { const { name, ...issuerWithoutName } = document.issuer; expect( - verify({ + validateDigest({ ...document, issuer: { ...issuerWithoutName, @@ -57,7 +57,7 @@ describe("V4 verify", () => { expect(modifiedCredentialSubject.licenses[2].description).toBeDefined(); expect( - verify({ + validateDigest({ ...document, credentialSubject: modifiedCredentialSubject, }) @@ -71,7 +71,7 @@ describe("V4 verify", () => { expect(modifiedCredentialSubject.licenses[0].description).toBeUndefined(); expect( - verify({ + validateDigest({ ...document, credentialSubject: modifiedCredentialSubject, }) @@ -81,7 +81,7 @@ describe("V4 verify", () => { describe("given insertion of an empty object, should return false", () => { test("given insertion into an object", () => { expect( - verify({ + validateDigest({ ...document, credentialSubject: { ...document.credentialSubject, @@ -98,7 +98,7 @@ describe("V4 verify", () => { expect(modifiedCredentialSubject.licenses[2]).toEqual({}); expect( - verify({ + validateDigest({ ...document, credentialSubject: modifiedCredentialSubject, }) @@ -109,7 +109,7 @@ describe("V4 verify", () => { describe("given insertion of an empty array, should return false", () => { test("given insertion into an object", () => { expect( - verify({ + validateDigest({ ...document, credentialSubject: { ...document.credentialSubject, @@ -126,7 +126,7 @@ describe("V4 verify", () => { expect(modifiedCredentialSubject.licenses[2]).toEqual([]); expect( - verify({ + validateDigest({ ...document, credentialSubject: modifiedCredentialSubject, }) @@ -136,7 +136,7 @@ describe("V4 verify", () => { test("given insertion of a null value into an object, should return false", () => { expect( - verify({ + validateDigest({ ...document, credentialSubject: { ...document.credentialSubject, @@ -151,7 +151,7 @@ describe("V4 verify", () => { expect(modifiedCredentialSubject.licenses[2]).toEqual({}); expect( - verify({ + validateDigest({ ...document, credentialSubject: modifiedCredentialSubject, }) @@ -165,7 +165,7 @@ describe("V4 verify", () => { expect(modifiedCredentialSubject.licenses[2]).toBe(null); expect( - verify({ + validateDigest({ ...document, credentialSubject: modifiedCredentialSubject, }) @@ -179,7 +179,7 @@ describe("V4 verify", () => { expect(typeof modifiedCredentialSubject.licenses[0].class).toBe("number"); expect( - verify({ + validateDigest({ ...document, credentialSubject: modifiedCredentialSubject, }) @@ -196,7 +196,7 @@ describe("V4 verify", () => { expect(modifiedCredentialSubject.id).toBeUndefined(); expect( - verify({ + validateDigest({ ...document, id, credentialSubject: modifiedCredentialSubject, diff --git a/src/4.0/__tests__/wrap.test.ts b/src/4.0/__tests__/wrap.test.ts deleted file mode 100644 index fc8a5377..00000000 --- a/src/4.0/__tests__/wrap.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { V4OpenAttestationDocument, V4WrappedDocument, W3cVerifiableCredential } from "../types"; -import { wrapDocument } from "../wrap"; - -describe("V4.0 wrap document", () => { - test("given a valid v4 document, should wrap correctly", async () => { - const wrapped = await wrapDocument({ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://schemata.openattestation.com/com/openattestation/4.0/context.json", - ], - type: ["VerifiableCredential", "OpenAttestationCredential"], - credentialSubject: { - id: "0x1234567890123456789012345678901234567890", - name: "John Doe", - country: "SG", - }, - issuer: { - id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", - type: "OpenAttestationIssuer", - name: "Government Technology Agency of Singapore (GovTech)", - identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, - }, - }); - const parsedResults = V4WrappedDocument.safeParse(wrapped); - if (!parsedResults.success) { - throw new Error("Parsing failed"); - } - const { proof } = parsedResults.data; - expect(proof.merkleRoot.length).toBe(64); - expect(proof.privacy.obfuscated).toEqual([]); - expect(proof.proofPurpose).toBe("assertionMethod"); - expect(proof.proofs).toEqual([]); - expect(proof.salts.length).toBeGreaterThan(0); - expect(proof.targetHash.length).toBe(64); - expect(proof.type).toBe("OpenAttestationHashProof2018"); - }); - - test("given a document with explicit v4 contexts, but does not conform to the V4 document schema, should throw", async () => { - await expect( - wrapDocument({ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://schemata.openattestation.com/com/openattestation/4.0/context.json", - ], - - type: ["VerifiableCredential", "OpenAttestationCredential"], - credentialSubject: { - id: "0x1234567890123456789012345678901234567890", - name: "John Doe", - country: "SG", - }, - issuer: { - id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", - name: "Government Technology Agency of Singapore (GovTech)", - identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, - } as V4OpenAttestationDocument["issuer"], - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Input document does not conform to Open Attestation v4.0 Data Model: - { - "_errors": [], - "issuer": { - "_errors": [], - "type": { - "_errors": [ - "Invalid literal value, expected \\"OpenAttestationIssuer\\"" - ] - } - } - }" - `); - }); - - test("given a valid v4 document but has an extra field, should throw", async () => { - await expect( - wrapDocument({ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://schemata.openattestation.com/com/openattestation/4.0/context.json", - ], - - type: ["VerifiableCredential", "OpenAttestationCredential"], - credentialSubject: { - id: "0x1234567890123456789012345678901234567890", - name: "John Doe", - country: "SG", - }, - issuer: { - id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", - type: "OpenAttestationIssuer", - name: "Government Technology Agency of Singapore (GovTech)", - extraField: "extra", - identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, - }, - // this should not exist - extraField: "extra", - } as V4OpenAttestationDocument) - ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Input document does not conform to Open Attestation v4.0 Data Model: - { - "_errors": [ - "Unrecognized key(s) in object: 'extraField'" - ] - }" - `); - }); - - test("given a generic w3c vc, should wrap with context and type corrected", async () => { - const genericW3cVc: W3cVerifiableCredential = { - "@context": ["https://www.w3.org/ns/credentials/v2"], - type: ["VerifiableCredential"], - credentialSubject: { - id: "0x1234567890123456789012345678901234567890", - name: "John Doe", - country: "SG", - }, - issuer: { - id: "https://example.com/issuer/123", - }, - }; - const wrapped = await wrapDocument(genericW3cVc as unknown as V4OpenAttestationDocument); - const parsedResults = V4WrappedDocument.pick({ "@context": true, type: true }).passthrough().safeParse(wrapped); - expect(parsedResults.success).toBe(true); - expect(wrapped.proof.merkleRoot.length).toBe(64); - expect(wrapped.proof.privacy.obfuscated).toEqual([]); - expect(wrapped.proof.proofPurpose).toBe("assertionMethod"); - expect(wrapped.proof.proofs).toEqual([]); - expect(wrapped.proof.salts.length).toBeGreaterThan(0); - expect(wrapped.proof.targetHash.length).toBe(64); - expect(wrapped.proof.type).toBe("OpenAttestationHashProof2018"); - }); -}); diff --git a/src/4.0/digest.ts b/src/4.0/digest.ts index c2c85cb7..2b0a411d 100644 --- a/src/4.0/digest.ts +++ b/src/4.0/digest.ts @@ -1,74 +1,160 @@ -import { sortBy } from "lodash"; -import { keccak256 } from "js-sha3"; -import { W3cVerifiableCredential, Salt } from "./types"; -import { LeafValue, traverseAndFlatten } from "./traverseAndFlatten"; import { hashToBuffer } from "../shared/utils/hashing"; +import { MerkleTree } from "../shared/merkle"; +import { ContextUrl, ContextType, UnableToInterpretContextError, interpretContexts } from "./context"; +import { NoExtraProperties, V4OpenAttestationDocument, V4WrappedDocument, W3cVerifiableCredential } from "./types"; +import { genTargetHash } from "../4.0/hash"; +import { encodeSalt, salt } from "./salt"; +import { ZodError } from "zod"; -export const digestCredential = (document: W3cVerifiableCredential, salts: Salt[], obfuscatedData: string[]) => { - // find all leaf nodes in the document and hash them - // proof is not part of the digest - const { proof: _, ...documentWithoutProof } = document; - const saltsMap = new Map(salts.map((salt) => [salt.path, salt.value])); - const isEmptyDocument = Object.keys(documentWithoutProof).length === 0; - const hashedLeafNodes = - // skip if document without proof is empty as it will treat the empty document as a leaf node - isEmptyDocument - ? [] - : traverseAndFlatten(documentWithoutProof, ({ value, path }) => { - const salt = saltsMap.get(path); - if (!salt) throw new SaltNotFoundError(path); - return hashLeafNode({ path, salt, value }); - }); - - // combine both array and sort them to ensure determinism - const combinedHashes = obfuscatedData.concat(hashedLeafNodes); - const sortedHashes = sortBy(combinedHashes); - - // finally, return the digest of the entire set of data - return keccak256(JSON.stringify(sortedHashes)); -}; +export const digestVC = async ( + // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict + vc: NoExtraProperties +): Promise> => { + /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ + const oav4context = await V4OpenAttestationDocument.pick({ "@context": true }).passthrough().safeParseAsync(vc); // Superficial check on user intention + let validatedRawDocument: W3cVerifiableCredential | undefined; + if (oav4context.success) { + const oav4 = await V4OpenAttestationDocument.safeParseAsync(vc); + if (!oav4.success) { + throw new DataModelValidationError("Open Attestation v4.0", oav4.error); + } + validatedRawDocument = oav4.data; + } + + /* 1b. only if OA VC validation fail do we continue with W3C VC data model validation */ + if (!validatedRawDocument) { + const w3cVc = await W3cVerifiableCredential.safeParseAsync(vc); + if (!w3cVc.success) { + throw new DataModelValidationError("Verifiable Credentials v2.0", w3cVc.error); + } + validatedRawDocument = w3cVc.data; + } + + /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ + await interpretContexts(validatedRawDocument); + + /* 3. Context validation */ + // Ensure that required contexts are present and in the correct order + // type: [Base, OA, ...] + const REQUIRED_CONTEXTS = [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4] as const; + const contexts = new Set(REQUIRED_CONTEXTS); + if (typeof validatedRawDocument["@context"] === "string") { + contexts.add(validatedRawDocument["@context"]); + } else if (isStringArray(validatedRawDocument["@context"])) { + validatedRawDocument["@context"].forEach((context) => contexts.add(context)); + } + REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); + const finalContexts: V4OpenAttestationDocument["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; + + /* 4. Type validation */ + // Ensure that required types are present and in the correct order + // type: ["VerifiableCredential", "OpenAttestationCredential", ...] + const REQUIRED_TYPES = [ContextType.BaseContext, ContextType.OAV4Context] as const; + const types = new Set([ContextType.BaseContext, ContextType.OAV4Context]); + if (typeof validatedRawDocument["type"] === "string") { + types.add(validatedRawDocument["type"]); + } else if (isStringArray(validatedRawDocument["type"])) { + types.forEach((type) => types.add(type)); + } + REQUIRED_TYPES.forEach((t) => types.delete(t)); + const finalTypes: V4OpenAttestationDocument["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; + + const documentReadyForWrapping = { + ...validatedRawDocument, + ...extractAndAssertAsV4OpenAttestationDocumentProps(validatedRawDocument, [ + "issuer", + "credentialStatus", + "credentialSubject", + ]), + "@context": finalContexts, + type: finalTypes, + } satisfies W3cVerifiableCredential; -type HashParams = { - salt: string; - value: LeafValue; - path: string; + /* 5. OA wrapping */ + const salts = salt(documentReadyForWrapping); + const digest = genTargetHash(documentReadyForWrapping, salts, []); + + const batchBuffers = [digest].map(hashToBuffer); + + const merkleTree = new MerkleTree(batchBuffers); + const merkleRoot = merkleTree.getRoot().toString("hex"); + const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer) => buffer.toString("hex")); + const verifiableCredential: V4WrappedDocument = { + ...documentReadyForWrapping, + proof: { + type: "OpenAttestationHashProof2018", + proofPurpose: "assertionMethod", + targetHash: digest, + proofs: merkleProof, + merkleRoot, + salts: encodeSalt(salts), + privacy: { + obfuscated: [], + }, + }, + }; + + return verifiableCredential as V4WrappedDocument; }; -type HashOptions = { - toHexString: true; + +export const digestVCs = async ( + // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict + vcs: NoExtraProperties[] +): Promise[]> => { + // create individual verifiable credential + const verifiableCredentials = await Promise.all(vcs.map((vc) => digestVC(vc))); + + // get all the target hashes to compute the merkle tree and the merkle root + const merkleTree = new MerkleTree( + verifiableCredentials.map((verifiableCredential) => verifiableCredential.proof.targetHash).map(hashToBuffer) + ); + const merkleRoot = merkleTree.getRoot().toString("hex"); + + // for each document, update the merkle root and add the proofs needed + return verifiableCredentials.map((verifiableCredential) => { + const digest = verifiableCredential.proof.targetHash; + const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer) => buffer.toString("hex")); + + return { + ...verifiableCredential, + proof: { + ...verifiableCredential.proof, + proofs: merkleProof, + merkleRoot, + }, + }; + }); }; -export function hashLeafNode({ path, salt, value }: HashParams, options?: HashOptions) { - const type = deriveType(value); - const hash = keccak256(JSON.stringify({ [path]: `${salt}:${type}:${value}` })); - return !options?.toHexString ? hash : hashToBuffer(hash).toString("hex"); + +/** Extract a set of properties from w3cVerifiableCredential but only include the ones + * that are defined in the original document. For example, if we extract + * "a" and "b" from { b: "something" } we should only get { b: "something" } NOT + * { a: undefined, b: "something" }. We also assert that the extracted properties + * are of V4OpenAttestationDocument type. + **/ +function extractAndAssertAsV4OpenAttestationDocumentProps( + original: W3cVerifiableCredential, + keys: K[] +) { + const temp: Record = {}; + Object.entries(original).forEach(([k, v]) => { + if (keys.includes(k as K)) temp[k] = v; + }); + return temp as { [key in K]: V4OpenAttestationDocument[key] }; } -export function deriveType(value: unknown): "string" | "number" | "boolean" | "null" | "object" | "array" { - if (Array.isArray(value)) { - return "array"; - } else if (value === null) { - return "null"; - } else { - switch (typeof value) { - case "string": - return "string"; - case "number": - return "number"; - case "object": - return "object"; - case "boolean": - return "boolean"; - default: - throw new Error(`Unsupported type ${typeof value}`); - } +class DataModelValidationError extends Error { + constructor(dataModel: "Open Attestation v4.0" | "Verifiable Credentials v2.0", public error: ZodError) { + super(`Input document does not conform to ${dataModel} Data Model: \n ${JSON.stringify(error.format(), null, 2)}`); + Object.setPrototypeOf(this, DataModelValidationError.prototype); } } -export class SaltNotFoundError extends Error { - constructor(public path: string) { - super(`Salt not found for ${path}`); +export const wrapDocumentErrors = { + DataModelValidationError, + UnableToInterpretContextError, +}; - // we shd consider changing the compilation target to >= es6 - // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript - Object.setPrototypeOf(this, SaltNotFoundError.prototype); - } +function isStringArray(input: unknown): input is string[] { + return Array.isArray(input) && input.every((i) => typeof i === "string"); } diff --git a/src/4.0/documentBuilder.ts b/src/4.0/documentBuilder.ts index f11f942f..b3d5e4a1 100644 --- a/src/4.0/documentBuilder.ts +++ b/src/4.0/documentBuilder.ts @@ -1,5 +1,5 @@ -import { wrapDocument, wrapDocuments, wrapDocumentErrors } from "./wrap"; -import { signDocument, signDocumentErrors } from "./sign"; +import { digestVC, digestVCs, wrapDocumentErrors } from "./digest"; +import { signVC, signDocumentErrors } from "./sign"; import { Override, DecentralisedEmbeddedRenderer, @@ -142,14 +142,14 @@ export class DocumentBuilder { } satisfies V4OpenAttestationDocument) ); - return wrapDocuments(toWrap) as unknown as WrappedReturn; + return digestVCs(toWrap) as unknown as WrappedReturn; } // this should never happen if (!data) throw new Error("CredentialSubject is required"); const { name, credentialSubject } = data; - return wrapDocument({ + return digestVC({ "@context": [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4], type: [ContextType.BaseContext, ContextType.OAV4Context], issuer, @@ -160,15 +160,15 @@ export class DocumentBuilder { }) as unknown as WrappedReturn; }; - private sign = async (props: { signer: Parameters[2] }): Promise> => { + private sign = async (props: { signer: Parameters[2] }): Promise> => { const wrapped = await this.wrap(); if (Array.isArray(wrapped)) { - return Promise.all(wrapped.map((d) => signDocument(d, "Secp256k1VerificationKey2018", props.signer))) as Promise< + return Promise.all(wrapped.map((d) => signVC(d, "Secp256k1VerificationKey2018", props.signer))) as Promise< SignedReturn >; } - return signDocument(wrapped, "Secp256k1VerificationKey2018", props.signer) as Promise>; + return signVC(wrapped, "Secp256k1VerificationKey2018", props.signer) as Promise>; }; // add issuance methods here diff --git a/src/4.0/exports/digest.ts b/src/4.0/exports/digest.ts new file mode 100644 index 00000000..e8b38265 --- /dev/null +++ b/src/4.0/exports/digest.ts @@ -0,0 +1 @@ +export { digestVC, digestVCs, wrapDocumentErrors } from "../digest"; diff --git a/src/4.0/exports/index.ts b/src/4.0/exports/index.ts index 40d37afe..c6ea6fdc 100644 --- a/src/4.0/exports/index.ts +++ b/src/4.0/exports/index.ts @@ -1,7 +1,7 @@ -export * from "./wrap"; +export * from "./digest"; export * from "./sign"; export * from "./obfuscate"; -export * from "./verify"; +export * from "./validate"; export * from "./utils"; export * from "./types"; export * from "./builder"; diff --git a/src/4.0/exports/obfuscate.ts b/src/4.0/exports/obfuscate.ts index 98fe058f..8d8c90c8 100644 --- a/src/4.0/exports/obfuscate.ts +++ b/src/4.0/exports/obfuscate.ts @@ -1 +1 @@ -export { obfuscateVerifiableCredential as obfuscate, obfuscateErrors } from "../obfuscate"; +export { obfuscateVC as obfuscateVerifiableCredential, obfuscateErrors } from "../obfuscate"; diff --git a/src/4.0/exports/sign.ts b/src/4.0/exports/sign.ts index d313eb7b..a78063b6 100644 --- a/src/4.0/exports/sign.ts +++ b/src/4.0/exports/sign.ts @@ -1 +1 @@ -export { signDocument, signDocumentErrors } from "../sign"; +export { signVC, signDocumentErrors } from "../sign"; diff --git a/src/4.0/exports/validate.ts b/src/4.0/exports/validate.ts new file mode 100644 index 00000000..11c07374 --- /dev/null +++ b/src/4.0/exports/validate.ts @@ -0,0 +1 @@ +export { validateDigest } from "../validate"; diff --git a/src/4.0/exports/verify.ts b/src/4.0/exports/verify.ts deleted file mode 100644 index 28c88f93..00000000 --- a/src/4.0/exports/verify.ts +++ /dev/null @@ -1 +0,0 @@ -export { verify as verifySignature } from "../verify"; diff --git a/src/4.0/exports/wrap.ts b/src/4.0/exports/wrap.ts deleted file mode 100644 index ce57c015..00000000 --- a/src/4.0/exports/wrap.ts +++ /dev/null @@ -1 +0,0 @@ -export { wrapDocument, wrapDocuments, wrapDocumentErrors } from "../wrap"; diff --git a/src/4.0/hash.ts b/src/4.0/hash.ts new file mode 100644 index 00000000..4672f7d4 --- /dev/null +++ b/src/4.0/hash.ts @@ -0,0 +1,74 @@ +import { sortBy } from "lodash"; +import { keccak256 } from "js-sha3"; +import { W3cVerifiableCredential, Salt } from "./types"; +import { LeafValue, traverseAndFlatten } from "./traverseAndFlatten"; +import { hashToBuffer } from "../shared/utils/hashing"; + +export const genTargetHash = (document: W3cVerifiableCredential, salts: Salt[], obfuscatedData: string[]) => { + // find all leaf nodes in the document and hash them + // proof is not part of the digest + const { proof: _, ...documentWithoutProof } = document; + const saltsMap = new Map(salts.map((salt) => [salt.path, salt.value])); + const isEmptyDocument = Object.keys(documentWithoutProof).length === 0; + const hashedLeafNodes = + // skip if document without proof is empty as it will treat the empty document as a leaf node + isEmptyDocument + ? [] + : traverseAndFlatten(documentWithoutProof, ({ value, path }) => { + const salt = saltsMap.get(path); + if (!salt) throw new SaltNotFoundError(path); + return hashLeafNode({ path, salt, value }); + }); + + // combine both array and sort them to ensure determinism + const combinedHashes = obfuscatedData.concat(hashedLeafNodes); + const sortedHashes = sortBy(combinedHashes); + + // finally, return the digest of the entire set of data + return keccak256(JSON.stringify(sortedHashes)); +}; + +type HashParams = { + salt: string; + value: LeafValue; + path: string; +}; +type HashOptions = { + toHexString: true; +}; +export function hashLeafNode({ path, salt, value }: HashParams, options?: HashOptions) { + const type = deriveType(value); + const hash = keccak256(JSON.stringify({ [path]: `${salt}:${type}:${value}` })); + return !options?.toHexString ? hash : hashToBuffer(hash).toString("hex"); +} + +export function deriveType(value: unknown): "string" | "number" | "boolean" | "null" | "object" | "array" { + if (Array.isArray(value)) { + return "array"; + } else if (value === null) { + return "null"; + } else { + switch (typeof value) { + case "string": + return "string"; + case "number": + return "number"; + case "object": + return "object"; + case "boolean": + return "boolean"; + default: + throw new Error(`Unsupported type ${typeof value}`); + } + } +} + +export class SaltNotFoundError extends Error { + constructor(public path: string) { + super(`Salt not found for ${path}`); + + // we shd consider changing the compilation target to >= es6 + // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript + Object.setPrototypeOf(this, SaltNotFoundError.prototype); + } +} diff --git a/src/4.0/obfuscate.ts b/src/4.0/obfuscate.ts index 54bf3843..975771f1 100644 --- a/src/4.0/obfuscate.ts +++ b/src/4.0/obfuscate.ts @@ -2,7 +2,7 @@ import { cloneDeep, get, unset, pick, toPath } from "lodash"; import { decodeSalt, encodeSalt } from "./salt"; import { traverseAndFlatten } from "./traverseAndFlatten"; import { Override, PartialDeep, V4SignedWrappedDocument, V4WrappedDocument } from "./types"; -import { hashLeafNode } from "./digest"; +import { hashLeafNode } from "./hash"; const obfuscate = (_data: V4WrappedDocument, fields: string[] | string) => { const data = cloneDeep(_data); // Prevents alteration of original data @@ -86,12 +86,12 @@ export type ObfuscateVerifiableCredentialResult = O >; } >; -export const obfuscateVerifiableCredential = ( - document: T, +export const obfuscateVC = ( + vc: T, fields: string[] | string ): ObfuscateVerifiableCredentialResult => { - const { data, obfuscatedData } = obfuscate(document, fields); - const currentObfuscatedData = document.proof.privacy.obfuscated; + const { data, obfuscatedData } = obfuscate(vc, fields); + const currentObfuscatedData = vc.proof.privacy.obfuscated; const newObfuscatedData = currentObfuscatedData.concat(obfuscatedData); // assert that obfuscated is still compliant to our schema diff --git a/src/4.0/sign.ts b/src/4.0/sign.ts index 91e308b0..87ab8659 100644 --- a/src/4.0/sign.ts +++ b/src/4.0/sign.ts @@ -4,7 +4,7 @@ import { Signer } from "@ethersproject/abstract-signer"; import { V4OpenAttestationDocument, V4WrappedDocument, V4SignedWrappedDocument } from "./types"; import type { ZodError } from "zod"; -export const signDocument = async ( +export const signVC = async ( document: V4SignedWrappedDocument | V4WrappedDocument, algorithm: "Secp256k1VerificationKey2018", keyOrSigner: SigningKey | Signer diff --git a/src/4.0/verify.ts b/src/4.0/validate.ts similarity index 76% rename from src/4.0/verify.ts rename to src/4.0/validate.ts index 2f07b64e..c5a1e9fc 100644 --- a/src/4.0/verify.ts +++ b/src/4.0/validate.ts @@ -1,9 +1,9 @@ import { V4WrappedDocument } from "./types"; -import { SaltNotFoundError, digestCredential } from "./digest"; +import { SaltNotFoundError, genTargetHash } from "./hash"; import { checkProof } from "../shared/merkle"; import { decodeSalt } from "./salt"; -export const verify = (document: T): document is T => { +export const validateDigest = (document: T): document is T => { if (!document.proof) { return false; } @@ -15,7 +15,7 @@ export const verify = (document: T): document is T // Checks target hash try { - const digest = digestCredential(documentWithoutProof, decodedSalts, document.proof.privacy.obfuscated); + const digest = genTargetHash(documentWithoutProof, decodedSalts, document.proof.privacy.obfuscated); const targetHash = document.proof.targetHash; if (digest !== targetHash) return false; diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts deleted file mode 100644 index 28a27996..00000000 --- a/src/4.0/wrap.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { hashToBuffer } from "../shared/utils/hashing"; -import { MerkleTree } from "../shared/merkle"; -import { ContextUrl, ContextType, UnableToInterpretContextError, interpretContexts } from "./context"; -import { NoExtraProperties, V4OpenAttestationDocument, V4WrappedDocument, W3cVerifiableCredential } from "./types"; -import { digestCredential } from "../4.0/digest"; -import { encodeSalt, salt } from "./salt"; -import { ZodError } from "zod"; - -export const wrapDocument = async ( - // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict - document: NoExtraProperties -): Promise> => { - /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ - const oav4context = await V4OpenAttestationDocument.pick({ "@context": true }).passthrough().safeParseAsync(document); // Superficial check on user intention - let validatedRawDocument: W3cVerifiableCredential | undefined; - if (oav4context.success) { - const oav4 = await V4OpenAttestationDocument.safeParseAsync(document); - if (!oav4.success) { - throw new DataModelValidationError("Open Attestation v4.0", oav4.error); - } - validatedRawDocument = oav4.data; - } - - /* 1b. only if OA VC validation fail do we continue with W3C VC data model validation */ - if (!validatedRawDocument) { - const vc = await W3cVerifiableCredential.safeParseAsync(document); - if (!vc.success) { - throw new DataModelValidationError("Verifiable Credentials v2.0", vc.error); - } - validatedRawDocument = vc.data; - } - - /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ - await interpretContexts(validatedRawDocument); - - /* 3. Context validation */ - // Ensure that required contexts are present and in the correct order - // type: [Base, OA, ...] - const REQUIRED_CONTEXTS = [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4] as const; - const contexts = new Set(REQUIRED_CONTEXTS); - if (typeof validatedRawDocument["@context"] === "string") { - contexts.add(validatedRawDocument["@context"]); - } else if (isStringArray(validatedRawDocument["@context"])) { - validatedRawDocument["@context"].forEach((context) => contexts.add(context)); - } - REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); - const finalContexts: V4OpenAttestationDocument["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; - - /* 4. Type validation */ - // Ensure that required types are present and in the correct order - // type: ["VerifiableCredential", "OpenAttestationCredential", ...] - const REQUIRED_TYPES = [ContextType.BaseContext, ContextType.OAV4Context] as const; - const types = new Set([ContextType.BaseContext, ContextType.OAV4Context]); - if (typeof validatedRawDocument["type"] === "string") { - types.add(validatedRawDocument["type"]); - } else if (isStringArray(validatedRawDocument["type"])) { - types.forEach((type) => types.add(type)); - } - REQUIRED_TYPES.forEach((t) => types.delete(t)); - const finalTypes: V4OpenAttestationDocument["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; - - const documentReadyForWrapping = { - ...validatedRawDocument, - ...extractAndAssertAsV4OpenAttestationDocumentProps(validatedRawDocument, [ - "issuer", - "credentialStatus", - "credentialSubject", - ]), - "@context": finalContexts, - type: finalTypes, - } satisfies W3cVerifiableCredential; - - /* 5. OA wrapping */ - const salts = salt(documentReadyForWrapping); - const digest = digestCredential(documentReadyForWrapping, salts, []); - - const batchBuffers = [digest].map(hashToBuffer); - - const merkleTree = new MerkleTree(batchBuffers); - const merkleRoot = merkleTree.getRoot().toString("hex"); - const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer) => buffer.toString("hex")); - const verifiableCredential: V4WrappedDocument = { - ...documentReadyForWrapping, - proof: { - type: "OpenAttestationHashProof2018", - proofPurpose: "assertionMethod", - targetHash: digest, - proofs: merkleProof, - merkleRoot, - salts: encodeSalt(salts), - privacy: { - obfuscated: [], - }, - }, - }; - - return verifiableCredential as V4WrappedDocument; -}; - -export const wrapDocuments = async ( - // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict - documents: NoExtraProperties[] -): Promise[]> => { - // create individual verifiable credential - const verifiableCredentials = await Promise.all(documents.map((document) => wrapDocument(document))); - - // get all the target hashes to compute the merkle tree and the merkle root - const merkleTree = new MerkleTree( - verifiableCredentials.map((verifiableCredential) => verifiableCredential.proof.targetHash).map(hashToBuffer) - ); - const merkleRoot = merkleTree.getRoot().toString("hex"); - - // for each document, update the merkle root and add the proofs needed - return verifiableCredentials.map((verifiableCredential) => { - const digest = verifiableCredential.proof.targetHash; - const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer) => buffer.toString("hex")); - - return { - ...verifiableCredential, - proof: { - ...verifiableCredential.proof, - proofs: merkleProof, - merkleRoot, - }, - }; - }); -}; - -/** Extract a set of properties from w3cVerifiableCredential but only include the ones - * that are defined in the original document. For example, if we extract - * "a" and "b" from { b: "something" } we should only get { b: "something" } NOT - * { a: undefined, b: "something" }. We also assert that the extracted properties - * are of V4OpenAttestationDocument type. - **/ -function extractAndAssertAsV4OpenAttestationDocumentProps( - original: W3cVerifiableCredential, - keys: K[] -) { - const temp: Record = {}; - Object.entries(original).forEach(([k, v]) => { - if (keys.includes(k as K)) temp[k] = v; - }); - return temp as { [key in K]: V4OpenAttestationDocument[key] }; -} - -class DataModelValidationError extends Error { - constructor(dataModel: "Open Attestation v4.0" | "Verifiable Credentials v2.0", public error: ZodError) { - super(`Input document does not conform to ${dataModel} Data Model: \n ${JSON.stringify(error.format(), null, 2)}`); - Object.setPrototypeOf(this, DataModelValidationError.prototype); - } -} - -export const wrapDocumentErrors = { - DataModelValidationError, - UnableToInterpretContextError, -}; - -function isStringArray(input: unknown): input is string[] { - return Array.isArray(input) && input.every((i) => typeof i === "string"); -} diff --git a/src/index.ts b/src/index.ts index 905be848..7cd48c61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,11 +24,8 @@ import { digestCredential as digestCredentialV3 } from "./3.0/digest"; import { obfuscateVerifiableCredential as obfuscateVerifiableCredentialV3 } from "./3.0/obfuscate"; import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "./__generated__/schema.3.0"; -import { verify as verifyV4 } from "./4.0/verify"; -import { - ObfuscateVerifiableCredentialResult, - obfuscateVerifiableCredential as obfuscateVerifiableCredentialV4, -} from "./4.0/obfuscate"; +import { validateDigest as verifyV4 } from "./4.0/validate"; +import { ObfuscateVerifiableCredentialResult, obfuscateVC as obfuscateVerifiableCredentialV4 } from "./4.0/obfuscate"; import { v4Diagnose } from "./4.0/diagnose"; import { V4WrappedDocument, isV4WrappedDocument } from "./4.0/types"; From 2de1f0a0019a592f97e2130729fe505a567f51cc Mon Sep 17 00:00:00 2001 From: Kyle Huang Junyuan Date: Tue, 19 Nov 2024 12:13:39 +0800 Subject: [PATCH 3/6] feat: allow both OA and W3C VCs to be digested or signed --- scripts/generateV4JsonSchemas.ts | 20 +- src/4.0/__tests__/digest.test.ts | 22 ++- src/4.0/__tests__/documentBuilder.test.ts | 103 ++++++----- src/4.0/__tests__/e2e.test.ts | 105 +++++------ src/4.0/__tests__/guard.test.ts | 46 ++--- src/4.0/__tests__/hash.test.ts | 10 +- src/4.0/__tests__/obfuscate.test.ts | 114 ++++++------ src/4.0/__tests__/sign.test.ts | 96 ++++------ src/4.0/__tests__/validate.test.ts | 4 +- src/4.0/diagnose.ts | 14 +- src/4.0/digest.ts | 58 +++--- src/4.0/documentBuilder.ts | 175 ++++++++++-------- src/4.0/exports/builder.ts | 2 +- src/4.0/exports/digest.ts | 2 +- src/4.0/exports/obfuscate.ts | 2 +- src/4.0/exports/sign.ts | 2 +- src/4.0/exports/types.ts | 6 +- src/4.0/exports/utils.ts | 6 +- src/4.0/fixtures.ts | 24 +-- ...ema.json => v4-digested-oa-vc.schema.json} | 18 +- ...ument.schema.json => v4-oa-vc.schema.json} | 18 +- ...chema.json => v4-signed-oa-vc.schema.json} | 18 +- src/4.0/obfuscate.ts | 22 +-- src/4.0/sign.ts | 59 +++--- src/4.0/types.ts | 54 +++--- src/4.0/validate.ts | 8 +- src/index.ts | 40 ++-- src/shared/@types/document.ts | 7 +- src/shared/utils/guard.ts | 6 +- src/shared/utils/utils.ts | 46 ++--- 30 files changed, 546 insertions(+), 561 deletions(-) rename src/4.0/jsonSchemas/__generated__/{v4-wrapped-document.schema.json => v4-digested-oa-vc.schema.json} (95%) rename src/4.0/jsonSchemas/__generated__/{v4-document.schema.json => v4-oa-vc.schema.json} (95%) rename src/4.0/jsonSchemas/__generated__/{v4-signed-wrapped-document.schema.json => v4-signed-oa-vc.schema.json} (95%) diff --git a/scripts/generateV4JsonSchemas.ts b/scripts/generateV4JsonSchemas.ts index 93cb144b..2f2e8ffa 100644 --- a/scripts/generateV4JsonSchemas.ts +++ b/scripts/generateV4JsonSchemas.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import { zodToJsonSchema } from "zod-to-json-schema"; -import { V4OpenAttestationDocument, V4WrappedDocument, V4SignedWrappedDocument } from "../src/4.0/types"; +import { OAVerifiableCredential, DigestedOAVerifiableCredential, SignedOAVerifiableCredential } from "../src/4.0/types"; const OUTPUT_DIR = path.resolve("./src/4.0/jsonSchemas/__generated__"); @@ -13,19 +13,19 @@ fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const ZOD_SCHEMAS = [ { - filename: "v4-document.schema.json", - schemaName: "v4Document", - zodSchema: V4OpenAttestationDocument, + filename: "v4-oa-vc.schema.json", + schemaName: "OAVerifiableCredential", + zodSchema: OAVerifiableCredential, }, { - filename: "v4-wrapped-document.schema.json", - schemaName: "v4WrappedDocument", - zodSchema: V4WrappedDocument, + filename: "v4-digested-oa-vc.schema.json", + schemaName: "DigestedOAVerifiableCredential", + zodSchema: DigestedOAVerifiableCredential, }, { - filename: "v4-signed-wrapped-document.schema.json", - schemaName: "v4SignedWrappedDocument", - zodSchema: V4SignedWrappedDocument, + filename: "v4-signed-oa-vc.schema.json", + schemaName: "SignedOAVerifiableCredential", + zodSchema: SignedOAVerifiableCredential, }, ]; diff --git a/src/4.0/__tests__/digest.test.ts b/src/4.0/__tests__/digest.test.ts index 4d351327..688e31dd 100644 --- a/src/4.0/__tests__/digest.test.ts +++ b/src/4.0/__tests__/digest.test.ts @@ -1,9 +1,9 @@ -import { V4OpenAttestationDocument, V4WrappedDocument, W3cVerifiableCredential } from "../types"; -import { digestVC } from "../digest"; +import { OAVerifiableCredential, DigestedOAVerifiableCredential, W3cVerifiableCredential } from "../types"; +import { digestVc } from "../digest"; describe("V4.0 digest", () => { test("given a valid v4 document, should wrap correctly", async () => { - const wrapped = await digestVC({ + const wrapped = await digestVc({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -21,7 +21,7 @@ describe("V4.0 digest", () => { identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, }, }); - const parsedResults = V4WrappedDocument.safeParse(wrapped); + const parsedResults = DigestedOAVerifiableCredential.safeParse(wrapped); if (!parsedResults.success) { throw new Error("Parsing failed"); } @@ -37,7 +37,7 @@ describe("V4.0 digest", () => { test("given a document with explicit v4 contexts, but does not conform to the V4 document schema, should throw", async () => { await expect( - digestVC({ + digestVc({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -53,7 +53,7 @@ describe("V4.0 digest", () => { id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", name: "Government Technology Agency of Singapore (GovTech)", identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, - } as V4OpenAttestationDocument["issuer"], + } as OAVerifiableCredential["issuer"], }) ).rejects.toThrowErrorMatchingInlineSnapshot(` "Input document does not conform to Open Attestation v4.0 Data Model: @@ -73,7 +73,7 @@ describe("V4.0 digest", () => { test("given a valid v4 document but has an extra field, should throw", async () => { await expect( - digestVC({ + digestVc({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -94,7 +94,7 @@ describe("V4.0 digest", () => { }, // this should not exist extraField: "extra", - } as V4OpenAttestationDocument) + } as OAVerifiableCredential) ).rejects.toThrowErrorMatchingInlineSnapshot(` "Input document does not conform to Open Attestation v4.0 Data Model: { @@ -118,8 +118,10 @@ describe("V4.0 digest", () => { id: "https://example.com/issuer/123", }, }; - const wrapped = await digestVC(genericW3cVc as unknown as V4OpenAttestationDocument); - const parsedResults = V4WrappedDocument.pick({ "@context": true, type: true }).passthrough().safeParse(wrapped); + const wrapped = await digestVc(genericW3cVc as unknown as OAVerifiableCredential); + const parsedResults = DigestedOAVerifiableCredential.pick({ "@context": true, type: true }) + .passthrough() + .safeParse(wrapped); expect(parsedResults.success).toBe(true); expect(wrapped.proof.merkleRoot.length).toBe(64); expect(wrapped.proof.privacy.obfuscated).toEqual([]); diff --git a/src/4.0/__tests__/documentBuilder.test.ts b/src/4.0/__tests__/documentBuilder.test.ts index 6d64bf2d..0c2868cc 100644 --- a/src/4.0/__tests__/documentBuilder.test.ts +++ b/src/4.0/__tests__/documentBuilder.test.ts @@ -1,11 +1,11 @@ import { validateDigest } from "../validate"; -import { DocumentBuilder, DocumentBuilderErrors } from "../documentBuilder"; -import { isSignedWrappedDocument, isWrappedDocument, signVC } from "../exports"; +import { VcBuilder, VcBuilderErrors } from "../documentBuilder"; +import { isSignedOAVerifiableCredential, isDigestedOAVerifiableCredential, signVc } from "../exports"; import { SAMPLE_SIGNING_KEYS } from "../fixtures"; describe(`V4.0 DocumentBuilder`, () => { describe("given a single document", () => { - const document = new DocumentBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma" }) + const document = new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma" }) .embeddedRenderer({ rendererUrl: "https://example.com", templateName: "example", @@ -20,7 +20,7 @@ describe(`V4.0 DocumentBuilder`, () => { }); test("given sign and wrap is called, return a single signed document", async () => { - const signed = await document.wrapAndSign({ signer: SAMPLE_SIGNING_KEYS }); + const signed = await document.sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed.issuer).toMatchInlineSnapshot(` { "id": "did:example:123", @@ -52,12 +52,12 @@ describe(`V4.0 DocumentBuilder`, () => { "type": "OpenAttestationOcspResponder", } `); - expect(isSignedWrappedDocument(signed)).toBe(true); + expect(isSignedOAVerifiableCredential(signed)).toBe(true); expect(validateDigest(signed)).toBe(true); }); test("given wrap is called, return a wrapped document", async () => { - const wrapped = await document.justWrapWithoutSigning(); + const wrapped = await document.digest(); expect(wrapped.issuer).toMatchInlineSnapshot(` { "id": "did:example:123", @@ -89,13 +89,13 @@ describe(`V4.0 DocumentBuilder`, () => { "type": "OpenAttestationOcspResponder", } `); - expect(isWrappedDocument(wrapped)).toBe(true); - expect(isSignedWrappedDocument(wrapped)).toBe(false); + expect(isDigestedOAVerifiableCredential(wrapped)).toBe(true); + expect(isSignedOAVerifiableCredential(wrapped)).toBe(false); }); }); - describe("given a multiple documents", () => { - const document = new DocumentBuilder([ + describe("given multiple documents", () => { + const document = new VcBuilder([ { credentialSubject: { name: "John Doe" }, name: "Diploma" }, { credentialSubject: { name: "Jane Foster" }, name: "Degree" }, ]) @@ -110,8 +110,8 @@ describe(`V4.0 DocumentBuilder`, () => { issuerName: "Example University", }); - test("given sign and wrap is called, return a list of signed document", async () => { - const signed = await document.wrapAndSign({ signer: SAMPLE_SIGNING_KEYS }); + test("given sign is called, return a list of signed VCs", async () => { + const signed = await document.sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed[0].issuer).toMatchInlineSnapshot(` { "id": "did:example:123", @@ -138,7 +138,7 @@ describe(`V4.0 DocumentBuilder`, () => { ] `); expect(signed[0].credentialStatus).toBeUndefined(); - expect(isSignedWrappedDocument(signed[0])).toBe(true); + expect(isSignedOAVerifiableCredential(signed[0])).toBe(true); expect(validateDigest(signed[0])).toBe(true); expect(signed[1].issuer).toMatchInlineSnapshot(` @@ -167,12 +167,12 @@ describe(`V4.0 DocumentBuilder`, () => { ] `); expect(signed[1].credentialStatus).toBeUndefined(); - expect(isSignedWrappedDocument(signed[1])).toBe(true); + expect(isSignedOAVerifiableCredential(signed[1])).toBe(true); expect(validateDigest(signed[1])).toBe(true); }); test("given wrap is called, return a list of wrapped document", async () => { - const wrapped = await document.justWrapWithoutSigning(); + const wrapped = await document.digest(); expect(wrapped[0].issuer).toMatchInlineSnapshot(` { "id": "did:example:123", @@ -198,8 +198,8 @@ describe(`V4.0 DocumentBuilder`, () => { }, ] `); - expect(isWrappedDocument(wrapped[0])).toBe(true); - expect(isSignedWrappedDocument(wrapped[0])).toBe(false); + expect(isDigestedOAVerifiableCredential(wrapped[0])).toBe(true); + expect(isSignedOAVerifiableCredential(wrapped[0])).toBe(false); expect(wrapped[1].issuer).toMatchInlineSnapshot(` { @@ -226,13 +226,13 @@ describe(`V4.0 DocumentBuilder`, () => { }, ] `); - expect(isWrappedDocument(wrapped[1])).toBe(true); - expect(isSignedWrappedDocument(wrapped[1])).toBe(false); + expect(isDigestedOAVerifiableCredential(wrapped[1])).toBe(true); + expect(isSignedOAVerifiableCredential(wrapped[1])).toBe(false); }); }); test("given additional properties in constructor payload, should not be added into the document", async () => { - const signed = await new DocumentBuilder({ + const signed = await new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", anotherProperty: "value", @@ -247,13 +247,13 @@ describe(`V4.0 DocumentBuilder`, () => { issuerId: "did:example:123", issuerName: "Example University", }) - .wrapAndSign({ signer: SAMPLE_SIGNING_KEYS }); + .sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed).not.toHaveProperty("anotherProperty"); }); test("given svg rendering method, should be added into the document", async () => { - const signed = await new DocumentBuilder({ + const signed = await new VcBuilder({ credentialSubject: { name: "John Doe", attachments: [ @@ -276,7 +276,7 @@ describe(`V4.0 DocumentBuilder`, () => { issuerId: "did:example:123", issuerName: "Example University", }) - .wrapAndSign({ signer: SAMPLE_SIGNING_KEYS }); + .sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed.renderMethod).toMatchInlineSnapshot(` [ @@ -289,7 +289,7 @@ describe(`V4.0 DocumentBuilder`, () => { }); test("given no rendering method, should reflect in the output document", async () => { - const signed = await new DocumentBuilder({ + const signed = await new VcBuilder({ credentialSubject: { name: "John Doe", attachments: [ @@ -309,13 +309,13 @@ describe(`V4.0 DocumentBuilder`, () => { issuerId: "did:example:123", issuerName: "Example University", }) - .wrapAndSign({ signer: SAMPLE_SIGNING_KEYS }); + .sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed.renderMethod).toMatchInlineSnapshot(`undefined`); }); test("given attachment is added, should be added into the document", async () => { - const signed = await new DocumentBuilder({ + const signed = await new VcBuilder({ credentialSubject: { name: "John Doe", attachments: [ @@ -338,7 +338,7 @@ describe(`V4.0 DocumentBuilder`, () => { issuerId: "did:example:123", issuerName: "Example University", }) - .wrapAndSign({ signer: SAMPLE_SIGNING_KEYS }); + .sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed.credentialSubject.attachments).toMatchInlineSnapshot(` [ @@ -352,7 +352,7 @@ describe(`V4.0 DocumentBuilder`, () => { }); test("given revocation store revocation is added, should be added into credential status of the document", async () => { - const signed = await new DocumentBuilder({ + const signed = await new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", attachments: [ @@ -375,7 +375,7 @@ describe(`V4.0 DocumentBuilder`, () => { issuerId: "did:example:123", issuerName: "Example University", }) - .wrapAndSign({ signer: SAMPLE_SIGNING_KEYS }); + .sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed.credentialStatus).toMatchInlineSnapshot(` { @@ -385,8 +385,8 @@ describe(`V4.0 DocumentBuilder`, () => { `); }); - test("given wrap only is called, should be able to sign the wrapped document with the standalone sign fn", async () => { - const wrapped = await new DocumentBuilder({ + test("given digest is first called, should not be able to sign the digested document with the standalone sign fn", async () => { + const digested = await new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", }) @@ -400,16 +400,25 @@ describe(`V4.0 DocumentBuilder`, () => { issuerId: "did:example:123", issuerName: "Example University", }) - .justWrapWithoutSigning(); + .digest(); - const signed = await signVC(wrapped, "Secp256k1VerificationKey2018", SAMPLE_SIGNING_KEYS); - - expect(isSignedWrappedDocument(signed)).toBe(true); - expect(validateDigest(signed)).toBe(true); + let error; + await expect(async () => { + try { + await signVc(digested, "Secp256k1VerificationKey2018", SAMPLE_SIGNING_KEYS); + } catch (e) { + error = e; + throw e; + } + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "VC has already has proof object defined: + Either an unsigned or undigested VC must be provided" + `); + expect(error).toBeInstanceOf(VcBuilderErrors.VcProofNotEmptyError); }); test("given re-setting of values, should throw", async () => { - const builder = await new DocumentBuilder({ + const builder = await new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", }); @@ -424,11 +433,11 @@ describe(`V4.0 DocumentBuilder`, () => { rendererUrl: "https://another.com", templateName: "another", }) - ).toThrowError(DocumentBuilderErrors.ShouldNotModifyAfterSettingError); + ).toThrowError(VcBuilderErrors.ShouldNotModifyAfterSettingError); const documentWithNoRevocation = documentWithRenderMethod.noRevocation(); expect(() => documentWithRenderMethod.oscpRevocation({ oscpUrl: "https://oscp.example.com" })).toThrowError( - DocumentBuilderErrors.ShouldNotModifyAfterSettingError + VcBuilderErrors.ShouldNotModifyAfterSettingError ); documentWithNoRevocation.dnsTxtIssuance({ @@ -443,7 +452,7 @@ describe(`V4.0 DocumentBuilder`, () => { issuerId: "did:example:123", issuerName: "Example University", }) - ).toThrowError(DocumentBuilderErrors.ShouldNotModifyAfterSettingError); + ).toThrowError(VcBuilderErrors.ShouldNotModifyAfterSettingError); }); describe("given invalid props", () => { @@ -451,7 +460,7 @@ describe(`V4.0 DocumentBuilder`, () => { let error; expect(() => { try { - new DocumentBuilder({ + new VcBuilder({ credentialSubject: { name: "John Doe", attachments: [ @@ -467,12 +476,12 @@ describe(`V4.0 DocumentBuilder`, () => { error = e; throw e; } - }).toThrowError(DocumentBuilderErrors.PropsValidationError); - expect(error).toBeInstanceOf(DocumentBuilderErrors.PropsValidationError); + }).toThrowError(VcBuilderErrors.PropsValidationError); + expect(error).toBeInstanceOf(VcBuilderErrors.PropsValidationError); }); test("given an invalid identity identifier, should throw", () => { - const builder = new DocumentBuilder({ + const builder = new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", }) @@ -504,11 +513,11 @@ describe(`V4.0 DocumentBuilder`, () => { } }" `); - expect(error).toBeInstanceOf(DocumentBuilderErrors.PropsValidationError); + expect(error).toBeInstanceOf(VcBuilderErrors.PropsValidationError); }); test("given an invalid ethereum address for revocation store, should throw", () => { - const builder = new DocumentBuilder({ + const builder = new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", }).embeddedRenderer({ @@ -537,7 +546,7 @@ describe(`V4.0 DocumentBuilder`, () => { } }" `); - expect(error).toBeInstanceOf(DocumentBuilderErrors.PropsValidationError); + expect(error).toBeInstanceOf(VcBuilderErrors.PropsValidationError); }); }); }); diff --git a/src/4.0/__tests__/e2e.test.ts b/src/4.0/__tests__/e2e.test.ts index 38aa97bf..c7b06422 100644 --- a/src/4.0/__tests__/e2e.test.ts +++ b/src/4.0/__tests__/e2e.test.ts @@ -1,8 +1,13 @@ -import { obfuscate, validateSchema, verifySignature } from "../.."; import { cloneDeep, omit } from "lodash"; +import { + digestVc, + digestVcs, + obfuscateOAVerifiableCredential, + validateDigest, + isDigestedOAVerifiableCredential, +} from "../exports"; +import type { OAVerifiableCredential } from "../exports"; import { RAW_DOCUMENT_DID, SIGNED_WRAPPED_DOCUMENT_DID, WRAPPED_DOCUMENT_DID } from "../fixtures"; -import { V4OpenAttestationDocument } from "../types"; -import { digestVC, digestVCs } from "../digest"; const DOCUMENT_ONE = { ...RAW_DOCUMENT_DID, @@ -10,7 +15,7 @@ const DOCUMENT_ONE = { ...RAW_DOCUMENT_DID.credentialSubject, key1: "test", }, -} satisfies V4OpenAttestationDocument; +} satisfies OAVerifiableCredential; const DOCUMENT_TWO = { ...RAW_DOCUMENT_DID, credentialSubject: { @@ -18,7 +23,7 @@ const DOCUMENT_TWO = { key1: "hello", key2: "item2", }, -} satisfies V4OpenAttestationDocument; +} satisfies OAVerifiableCredential; const DOCUMENT_THREE = { ...RAW_DOCUMENT_DID, @@ -29,7 +34,7 @@ const DOCUMENT_THREE = { key3: 3.14159, key4: false, }, -} satisfies V4OpenAttestationDocument; +} satisfies OAVerifiableCredential; const DOCUMENT_FOUR = { ...RAW_DOCUMENT_DID, @@ -38,7 +43,7 @@ const DOCUMENT_FOUR = { key1: "item2", }, }; -const DATUM = [DOCUMENT_ONE, DOCUMENT_TWO, DOCUMENT_THREE, DOCUMENT_FOUR] satisfies V4OpenAttestationDocument[]; +const DATUM = [DOCUMENT_ONE, DOCUMENT_TWO, DOCUMENT_THREE, DOCUMENT_FOUR] satisfies OAVerifiableCredential[]; describe("V4.0 E2E Test Scenarios", () => { describe("Issuing a single document", () => { @@ -46,7 +51,7 @@ describe("V4.0 E2E Test Scenarios", () => { const missingData = { ...omit(cloneDeep(DOCUMENT_ONE), "issuer"), }; - await expect(digestVC(missingData as unknown as V4OpenAttestationDocument)).rejects + await expect(digestVc(missingData as unknown as OAVerifiableCredential)).rejects .toThrowErrorMatchingInlineSnapshot(` "Input document does not conform to Open Attestation v4.0 Data Model: { @@ -61,7 +66,7 @@ describe("V4.0 E2E Test Scenarios", () => { }); test("creates a wrapped document", async () => { - const wrappedDocument = await digestVC(RAW_DOCUMENT_DID); + const wrappedDocument = await digestVc(RAW_DOCUMENT_DID); expect(wrappedDocument["@context"]).toEqual([ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -75,37 +80,39 @@ describe("V4.0 E2E Test Scenarios", () => { }); test("checks that document is wrapped correctly", async () => { - const wrappedDocument = await digestVC(DOCUMENT_ONE); - const verified = verifySignature(wrappedDocument); + const wrappedDocument = await digestVc(DOCUMENT_ONE); + const verified = validateDigest(wrappedDocument); expect(verified).toBe(true); }); test("checks that document conforms to the schema", async () => { - const wrappedDocument = await digestVC(DOCUMENT_ONE); - expect(validateSchema(wrappedDocument)).toBe(true); + const wrappedDocument = await digestVc(DOCUMENT_ONE); + expect(isDigestedOAVerifiableCredential(wrappedDocument)).toBe(true); }); test("does not allow for the same merkle root to be generated", async () => { // This test takes some time to run, so we set the timeout to 14s - const wrappedDocument = await digestVC(DOCUMENT_ONE); - const newDocument = await digestVC(DOCUMENT_ONE); + const wrappedDocument = await digestVc(DOCUMENT_ONE); + const newDocument = await digestVc(DOCUMENT_ONE); expect(wrappedDocument.proof.merkleRoot).not.toBe(newDocument.proof.merkleRoot); }, 14000); test("obfuscate data correctly", async () => { - const newDocument = await digestVC(DOCUMENT_THREE); + const newDocument = await digestVc(DOCUMENT_THREE); expect(newDocument.credentialSubject.key2).toBeDefined(); - const obfuscatedDocument = obfuscate(newDocument, ["credentialSubject.key2"]); - expect(verifySignature(obfuscatedDocument)).toBe(true); - expect(validateSchema(obfuscatedDocument)).toBe(true); + const obfuscatedDocument = obfuscateOAVerifiableCredential(newDocument, ["credentialSubject.key2"]); + expect(validateDigest(obfuscatedDocument)).toBe(true); + expect(isDigestedOAVerifiableCredential(obfuscatedDocument)).toBe(true); expect(obfuscatedDocument.credentialSubject.key2).toBeUndefined(); }); test("obfuscate data transistively", async () => { - const newDocument = await digestVC(DOCUMENT_THREE); - const intermediateDocument = obfuscate(newDocument, ["credentialSubject.key2"]); - const obfuscatedDocument = obfuscate(intermediateDocument, ["credentialSubject.key3"]); - expect(obfuscate(newDocument, ["credentialSubject.key2", "credentialSubject.key3"])).toEqual(obfuscatedDocument); + const newDocument = await digestVc(DOCUMENT_THREE); + const intermediateDocument = obfuscateOAVerifiableCredential(newDocument, ["credentialSubject.key2"]); + const obfuscatedDocument = obfuscateOAVerifiableCredential(intermediateDocument, ["credentialSubject.key3"]); + expect( + obfuscateOAVerifiableCredential(newDocument, ["credentialSubject.key2", "credentialSubject.key3"]) + ).toEqual(obfuscatedDocument); }); describe("Issuing a batch of documents", () => { @@ -114,15 +121,15 @@ describe("V4.0 E2E Test Scenarios", () => { ...DATUM, { laurent: "task force, assemble!!", - } as unknown as V4OpenAttestationDocument, + } as unknown as OAVerifiableCredential, ]; - await expect(digestVCs(malformedDatum)).rejects.toThrow( + await expect(digestVcs(malformedDatum)).rejects.toThrow( "Input document does not conform to Verifiable Credentials" ); }); test("creates a batch of documents if all are in the right format", async () => { - const wrappedDocuments = await digestVCs(DATUM); + const wrappedDocuments = await digestVcs(DATUM); wrappedDocuments.forEach((doc, i: number) => { expect(doc.type).toEqual(["VerifiableCredential", "OpenAttestationCredential"]); expect(doc.proof.type).toBe("OpenAttestationHashProof2018"); @@ -135,35 +142,35 @@ describe("V4.0 E2E Test Scenarios", () => { }); test("checks that documents are wrapped correctly", async () => { - const wrappedDocuments = await digestVCs(DATUM); - const verified = wrappedDocuments.reduce((prev, curr) => verifySignature(curr) && prev, true); + const wrappedDocuments = await digestVcs(DATUM); + const verified = wrappedDocuments.reduce((prev, curr) => validateDigest(curr) && prev, true); expect(verified).toBe(true); }); test("checks that documents conforms to the schema", async () => { - const wrappedDocuments = await digestVCs(DATUM); + const wrappedDocuments = await digestVcs(DATUM); const validatedSchema = wrappedDocuments.reduce( - (prev: boolean, curr: any) => validateSchema(curr) && prev, + (prev: boolean, curr: any) => isDigestedOAVerifiableCredential(curr) && prev, true ); expect(validatedSchema).toBe(true); }); test("does not allow for same merkle root to be generated", async () => { - const wrappedDocuments = await digestVCs(DATUM); - const newWrappedDocuments = await digestVCs(DATUM); + const wrappedDocuments = await digestVcs(DATUM); + const newWrappedDocuments = await digestVcs(DATUM); expect(wrappedDocuments[0].proof.merkleRoot).not.toBe(newWrappedDocuments[0].proof.merkleRoot); }); }); }); - describe("validate", () => { + describe("validate schema", () => { test("should return true when document is a valid wrapped v4 document and identityProof is DNS-DID", () => { - expect(validateSchema(WRAPPED_DOCUMENT_DID)).toStrictEqual(true); + expect(isDigestedOAVerifiableCredential(WRAPPED_DOCUMENT_DID)).toStrictEqual(true); }); test("should return true when signed document is a valid signed wrapped v4 document and identityProof is DNS-DID", () => { - expect(validateSchema(SIGNED_WRAPPED_DOCUMENT_DID)).toStrictEqual(true); + expect(isDigestedOAVerifiableCredential(SIGNED_WRAPPED_DOCUMENT_DID)).toStrictEqual(true); }); test("should return false when document is invalid due to no DNS-DID identifier", () => { @@ -172,30 +179,8 @@ describe("V4.0 E2E Test Scenarios", () => { const credential = { ...RAW_DOCUMENT_DID, issuer: modifiedIssuer, - } satisfies V4OpenAttestationDocument; - expect(validateSchema(credential)).toStrictEqual(false); - }); - - test("should default to 2.0 when document is valid and version is undefined", () => { - expect( - validateSchema({ - version: undefined, - data: { - issuers: [ - { - name: "issuer.name", - certificateStore: "0x9178F546D3FF57D7A6352bD61B80cCCD46199C2d", - }, - ], - }, - signature: { - merkleRoot: "0xabc", - proof: [], - targetHash: "0xabc", - type: "SHA3MerkleProof", - }, - }) - ).toStrictEqual(true); + } satisfies OAVerifiableCredential; + expect(isDigestedOAVerifiableCredential(credential)).toStrictEqual(false); }); }); @@ -210,7 +195,7 @@ describe("V4.0 E2E Test Scenarios", () => { key4: "خحثىشففثسفشفهخى", }, }; - const wrapped = await digestVC(document); + const wrapped = await digestVc(document); expect(wrapped.proof.merkleRoot).toBeTruthy(); expect(wrapped.credentialSubject.key1).toBe(document.credentialSubject.key1); expect(wrapped.credentialSubject.key2).toBe(document.credentialSubject.key2); diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index 701763ce..fa1e7cab 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -1,13 +1,15 @@ import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign"; import { RAW_DOCUMENT_DID } from "../fixtures"; -import { signVC } from "../sign"; +import { digestVc } from "../digest"; +import { signVc } from "../sign"; import { W3cVerifiableCredential, - V4OpenAttestationDocument, - V4WrappedDocument, - V4SignedWrappedDocument, + OAVerifiableCredential, + Digested, + DigestedOAVerifiableCredential, + Signed, + SignedOAVerifiableCredential, } from "../types"; -import { digestVC } from "../digest"; const RAW_DOCUMENT = { ...RAW_DOCUMENT_DID, @@ -21,14 +23,14 @@ const RAW_DOCUMENT = { }, ], }, -} satisfies V4OpenAttestationDocument; +} satisfies OAVerifiableCredential; describe("V4.0 guard", () => { - let WRAPPED_DOCUMENT: V4WrappedDocument; - let SIGNED_WRAPPED_DOCUMENT: V4SignedWrappedDocument; + let WRAPPED_DOCUMENT: Digested; + let SIGNED_WRAPPED_DOCUMENT: Signed; beforeAll(async () => { - WRAPPED_DOCUMENT = await digestVC(RAW_DOCUMENT); - SIGNED_WRAPPED_DOCUMENT = await signVC(WRAPPED_DOCUMENT, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { + WRAPPED_DOCUMENT = await digestVc(RAW_DOCUMENT); + SIGNED_WRAPPED_DOCUMENT = await signVc(RAW_DOCUMENT, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", }); @@ -42,12 +44,12 @@ describe("V4.0 guard", () => { }); test("should pass document validation without removal of any data", () => { - const results = V4OpenAttestationDocument.parse(RAW_DOCUMENT_DID); + const results = OAVerifiableCredential.parse(RAW_DOCUMENT_DID); expect(results).toEqual(RAW_DOCUMENT_DID); }); test("should fail wrapped document validation", () => { - const results = V4WrappedDocument.safeParse(RAW_DOCUMENT_DID); + const results = DigestedOAVerifiableCredential.safeParse(RAW_DOCUMENT_DID); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -65,7 +67,7 @@ describe("V4.0 guard", () => { }); test("should fail signed wrapped document validation", () => { - const results = V4SignedWrappedDocument.safeParse(RAW_DOCUMENT_DID); + const results = SignedOAVerifiableCredential.safeParse(RAW_DOCUMENT_DID); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -91,18 +93,18 @@ describe("V4.0 guard", () => { }); test("should pass document validation without removal of any data", () => { - const v4Document: V4OpenAttestationDocument = WRAPPED_DOCUMENT; - const results = V4OpenAttestationDocument.parse(v4Document); + const v4Document: OAVerifiableCredential = WRAPPED_DOCUMENT; + const results = OAVerifiableCredential.parse(v4Document); expect(results).toEqual(WRAPPED_DOCUMENT); }); test("should pass wrapped document validation without removal of any data", () => { - const results = V4WrappedDocument.parse(WRAPPED_DOCUMENT); + const results = DigestedOAVerifiableCredential.parse(WRAPPED_DOCUMENT); expect(results).toEqual(WRAPPED_DOCUMENT); }); test("should fail signed wrapped document validation", () => { - const results = V4SignedWrappedDocument.safeParse(WRAPPED_DOCUMENT); + const results = SignedOAVerifiableCredential.safeParse(WRAPPED_DOCUMENT); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -139,19 +141,19 @@ describe("V4.0 guard", () => { }); test("should pass document validation without removal of any data", () => { - const v4Document: V4OpenAttestationDocument = SIGNED_WRAPPED_DOCUMENT; - const results = V4OpenAttestationDocument.parse(v4Document); + const v4Document: OAVerifiableCredential = SIGNED_WRAPPED_DOCUMENT; + const results = OAVerifiableCredential.parse(v4Document); expect(results).toEqual(SIGNED_WRAPPED_DOCUMENT); }); test("should pass wrapped document validation without removal of any data", () => { - const v4WrappedDocument: V4WrappedDocument = SIGNED_WRAPPED_DOCUMENT; - const results = V4WrappedDocument.parse(v4WrappedDocument); + const v4WrappedDocument: Digested = SIGNED_WRAPPED_DOCUMENT; + const results = DigestedOAVerifiableCredential.parse(v4WrappedDocument); expect(results).toEqual(SIGNED_WRAPPED_DOCUMENT); }); test("should pass signed wrapped document validation without removal of any data", () => { - const results = V4SignedWrappedDocument.parse(SIGNED_WRAPPED_DOCUMENT); + const results = SignedOAVerifiableCredential.parse(SIGNED_WRAPPED_DOCUMENT); expect(results).toEqual(SIGNED_WRAPPED_DOCUMENT); }); }); diff --git a/src/4.0/__tests__/hash.test.ts b/src/4.0/__tests__/hash.test.ts index 78dfc2dc..3e9083b8 100644 --- a/src/4.0/__tests__/hash.test.ts +++ b/src/4.0/__tests__/hash.test.ts @@ -1,8 +1,8 @@ import { genTargetHash } from "../hash"; import { decodeSalt } from "../salt"; import { SIGNED_WRAPPED_DOCUMENT_DID as ROOT_CREDENTIAL } from "../fixtures"; -import { V4SignedWrappedDocument } from "../types"; -import { obfuscateVC } from "../obfuscate"; +import { Signed } from "../types"; +import { obfuscateOAVerifiableCredential } from "../obfuscate"; // All obfuscated documents are generated from the ROOT_CREDENTIAL const ROOT_CREDENTIAL_TARGET_HASH = ROOT_CREDENTIAL.proof.targetHash; @@ -43,7 +43,7 @@ describe("V4.0 hash", () => { }); test("given a document with ONE element obfuscated, should digest and match the root credential's target hash", () => { - const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateVC(ROOT_CREDENTIAL, "credentialSubject.id"); + const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateOAVerifiableCredential(ROOT_CREDENTIAL, "credentialSubject.id"); expect(OBFUSCATED_WRAPPED_DOCUMENT.credentialSubject).toMatchInlineSnapshot(` { "licenses": [ @@ -80,7 +80,7 @@ describe("V4.0 hash", () => { }); test("given a document with THREE elements obfuscated, should digest and match the root credential's target hash", () => { - const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateVC(ROOT_CREDENTIAL, [ + const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateOAVerifiableCredential(ROOT_CREDENTIAL, [ "credentialSubject.id", "credentialSubject.name", "credentialSubject.licenses[0].description", @@ -163,7 +163,7 @@ describe("V4.0 hash", () => { signature: "0x949b76d8df493a56c1cf21303a74d6a54904461c1c10f4619b43ad7d339c64467c61eb4c0873f279cd21d5bdd044d3af5318f14d63f57acbd4cde30f271f3eb71c", }, - } as unknown as V4SignedWrappedDocument; + } as unknown as Signed; const digest = genTargetHash( OBFUSCATED_WRAPPED_DOCUMENT, diff --git a/src/4.0/__tests__/obfuscate.test.ts b/src/4.0/__tests__/obfuscate.test.ts index 6f481ecf..7e0e5484 100644 --- a/src/4.0/__tests__/obfuscate.test.ts +++ b/src/4.0/__tests__/obfuscate.test.ts @@ -1,19 +1,18 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { obfuscateVC } from "../obfuscate"; +import { obfuscateOAVerifiableCredential } from "../obfuscate"; import { get } from "lodash"; import { decodeSalt } from "../salt"; -import { digestVC } from "../digest"; -import { Salt, V4OpenAttestationDocument, V4WrappedDocument } from "../types"; -import { verifySignature } from "../../"; +import { digestVc } from "../digest"; +import { Salt, OAVerifiableCredential, Digested } from "../types"; +import { validateDigest } from "../validate"; import { RAW_DOCUMENT_DID, SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED, WRAPPED_DOCUMENT_DID } from "../fixtures"; import { hashLeafNode } from "../hash"; import { getObfuscatedData, isObfuscated } from "../../shared/utils"; -const makeV4RawDocument = >(props: T) => - ({ - ...RAW_DOCUMENT_DID, - ...(props as T), - } satisfies V4OpenAttestationDocument); +const makeOAVerifiableCredential = >(props: T) => { + const { credentialSubject, ...rest } = RAW_DOCUMENT_DID; + return { ...rest, ...(props as T) } satisfies OAVerifiableCredential; +}; const findSaltByPath = (salts: string, path: string): Salt | undefined => { return decodeSalt(salts).find((salt) => salt.path === path); @@ -26,11 +25,7 @@ const findSaltByPath = (salts: string, path: string): Salt | undefined => { * - the salt bound to the field has been removed * - the field has been removed */ -const expectRemovedFieldsWithoutArrayNotation = ( - field: string, - document: V4WrappedDocument, - obfuscatedDocument: V4WrappedDocument -) => { +const expectRemovedFieldsWithoutArrayNotation = (field: string, document: Digested, obfuscatedDocument: Digested) => { const value = get(document, field); const salt = findSaltByPath(document.proof.salts, field); @@ -47,31 +42,31 @@ describe("V4.0 obfuscate", () => { describe("obfuscateVC", () => { test("removes one field from the root object", async () => { const PATH_TO_REMOVE = "name"; - const wrappedDocument = await digestVC( - makeV4RawDocument({ credentialSubject: { id: "S1234567A", name: "John Doe" } }) + const digestedVc = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { id: "S1234567A", name: "John Doe" } }) ); - const obfuscatedDocument = obfuscateVC(wrappedDocument, PATH_TO_REMOVE); - const verified = verifySignature(obfuscatedDocument); + const obfuscatedVc = obfuscateOAVerifiableCredential(digestedVc, PATH_TO_REMOVE); + const verified = validateDigest(obfuscatedVc); expect(verified).toBe(true); - expectRemovedFieldsWithoutArrayNotation(PATH_TO_REMOVE, wrappedDocument, obfuscatedDocument); - expect(obfuscatedDocument.proof.privacy.obfuscated).toHaveLength(1); + expectRemovedFieldsWithoutArrayNotation(PATH_TO_REMOVE, digestedVc, obfuscatedVc); + expect(obfuscatedVc.proof.privacy.obfuscated).toHaveLength(1); }); test("removes paths that result in an invalid wrapped document, should throw", async () => { const PATHS_TO_REMOVE = ["credentialSubject", "renderMethod.0.id", "name"]; - const wrappedDocument = await digestVC( - makeV4RawDocument({ credentialSubject: { id: "S1234567A", name: "John Doe" } }) + const digestedVc = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { id: "S1234567A", name: "John Doe" } }) ); - expect(() => obfuscateVC(wrappedDocument, PATHS_TO_REMOVE)).toThrowError( + expect(() => obfuscateOAVerifiableCredential(digestedVc, PATHS_TO_REMOVE)).toThrowError( `"credentialSubject", "renderMethod.0.id"` ); }); test("removes one key of an object from an array", async () => { const PATH_TO_REMOVE = "credentialSubject.arrayOfObject[0].foo" as const; - const newDocument = await digestVC( - makeV4RawDocument({ + const digestedVc = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { id: "did:example:ebfeb1f712ebc6f1c276e12ec21", alumniOf: "Example University", @@ -83,37 +78,37 @@ describe("V4.0 obfuscate", () => { }, }) ); - const obfuscatedDocument = await obfuscateVC(newDocument, PATH_TO_REMOVE); + const obfuscatedVc = obfuscateOAVerifiableCredential(digestedVc, PATH_TO_REMOVE); - const verified = verifySignature(obfuscatedDocument); + const verified = validateDigest(obfuscatedVc); expect(verified).toBe(true); - const value = get(newDocument, PATH_TO_REMOVE); - const salt = findSaltByPath(newDocument.proof.salts, PATH_TO_REMOVE); + const value = get(digestedVc, PATH_TO_REMOVE); + const salt = findSaltByPath(digestedVc.proof.salts, PATH_TO_REMOVE); if (!salt) throw new Error(`Salt not found for ${PATH_TO_REMOVE}`); - expect(obfuscatedDocument.proof.privacy.obfuscated).toContain( + expect(obfuscatedVc.proof.privacy.obfuscated).toContain( hashLeafNode({ value, salt: salt.value, path: PATH_TO_REMOVE }, { toHexString: true }) ); - expect(findSaltByPath(obfuscatedDocument.proof.salts, PATH_TO_REMOVE)).toBeUndefined(); - expect(obfuscatedDocument.credentialSubject.arrayOfObject?.[0]).toStrictEqual({ doo: "foo" }); - expect(obfuscatedDocument.proof.privacy.obfuscated).toHaveLength(1); + expect(findSaltByPath(obfuscatedVc.proof.salts, PATH_TO_REMOVE)).toBeUndefined(); + expect(obfuscatedVc.credentialSubject.arrayOfObject?.[0]).toStrictEqual({ doo: "foo" }); + expect(obfuscatedVc.proof.privacy.obfuscated).toHaveLength(1); }); test("given an object is to be removed, should remove the object itself, as well as add each of its key's hash into privacy.obfuscated", async () => { const PATH_TO_REMOVE = "credentialSubject.hee"; - const wrappedDocument = await digestVC( - makeV4RawDocument({ + const wrappedDocument = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { hee: { foo: "bar", doo: "foo" }, haa: { foo: "baz", doo: "faz" }, }, }) ); - const obfuscatedDocument = obfuscateVC(wrappedDocument, PATH_TO_REMOVE); + const obfuscatedDocument = obfuscateOAVerifiableCredential(wrappedDocument, PATH_TO_REMOVE); - const verified = verifySignature(obfuscatedDocument); + const verified = validateDigest(obfuscatedDocument); expect(verified).toBe(true); // assert that each key of the object has been moved to privacy.obfuscated @@ -135,8 +130,8 @@ describe("V4.0 obfuscate", () => { test("given an entire array of objects to remove, should remove the array itself, then for every item, add each of its key's hash into privacy.obfuscated", async () => { const PATH_TO_REMOVE = "credentialSubject.attachments"; - const wrappedDocument = await digestVC( - makeV4RawDocument({ + const wrappedDocument = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { arrayOfObject: [ { foo: "bar", doo: "foo" }, @@ -157,9 +152,9 @@ describe("V4.0 obfuscate", () => { }, }) ); - const obfuscatedDocument = await obfuscateVC(wrappedDocument, PATH_TO_REMOVE); + const obfuscatedDocument = obfuscateOAVerifiableCredential(wrappedDocument, PATH_TO_REMOVE); - const verified = verifySignature(obfuscatedDocument); + const verified = validateDigest(obfuscatedDocument); expect(verified).toBe(true); [ @@ -186,8 +181,8 @@ describe("V4.0 obfuscate", () => { test("given multiple fields to be removed, should remove fields and add their hash into privacy.obfuscated", async () => { const PATHS_TO_REMOVE = ["credentialSubject.key1", "credentialSubject.key2"]; - const wrappedDocument = await digestVC( - makeV4RawDocument({ + const wrappedDocument = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { key1: "value1", key2: "value2", @@ -195,8 +190,8 @@ describe("V4.0 obfuscate", () => { }, }) ); - const obfuscatedDocument = await obfuscateVC(wrappedDocument, PATHS_TO_REMOVE); - const verified = verifySignature(obfuscatedDocument); + const obfuscatedDocument = obfuscateOAVerifiableCredential(wrappedDocument, PATHS_TO_REMOVE); + const verified = validateDigest(obfuscatedDocument); expect(verified).toBe(true); PATHS_TO_REMOVE.forEach((expectedRemovedField) => { @@ -206,8 +201,8 @@ describe("V4.0 obfuscate", () => { }); test("given a path to remove an entire item from an array, should throw", async () => { - const wrappedDocument = await digestVC( - makeV4RawDocument({ + const wrappedDocument = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { arrayOfObject: [ { foo: "bar", doo: "foo" }, @@ -235,13 +230,16 @@ describe("V4.0 obfuscate", () => { ); expect(() => - obfuscateVC(wrappedDocument, ["credentialSubject.attachments[0]", "credentialSubject.attachments[2]"]) + obfuscateOAVerifiableCredential(wrappedDocument, [ + "credentialSubject.attachments[0]", + "credentialSubject.attachments[2]", + ]) ).toThrow(); }); test("given a path to remove all elements in an object, should throw", async () => { - const wrappedDocument = await digestVC( - makeV4RawDocument({ + const wrappedDocument = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { arrayOfObject: [ { foo: "bar", doo: "foo" }, @@ -255,21 +253,23 @@ describe("V4.0 obfuscate", () => { ); expect(() => - obfuscateVC(wrappedDocument, [ + obfuscateOAVerifiableCredential(wrappedDocument, [ "credentialSubject.arrayOfObject[0].foo", "credentialSubject.arrayOfObject[0].doo", ]) ).toThrowErrorMatchingInlineSnapshot( `"Obfuscation of "credentialSubject.arrayOfObject[0].doo" has resulted in an empty {}, this is currently not supported. Alternatively, if the object is not part of an array, you may choose to obfuscate the parent of "credentialSubject.arrayOfObject[0].doo"."` ); - expect(() => obfuscateVC(wrappedDocument, ["credentialSubject.object.foo"])).toThrowErrorMatchingInlineSnapshot( + expect(() => + obfuscateOAVerifiableCredential(wrappedDocument, ["credentialSubject.object.foo"]) + ).toThrowErrorMatchingInlineSnapshot( `"Obfuscation of "credentialSubject.object.foo" has resulted in an empty {}, this is currently not supported. Alternatively, if the object is not part of an array, you may choose to obfuscate the parent of "credentialSubject.object.foo"."` ); }); test("is transitive", async () => { - const wrappedDocument = await digestVC( - makeV4RawDocument({ + const wrappedDocument = await digestVc( + makeOAVerifiableCredential({ credentialSubject: { key1: "value1", key2: "value2", @@ -277,9 +277,9 @@ describe("V4.0 obfuscate", () => { }, }) ); - const intermediateDoc = obfuscateVC(wrappedDocument, "key1"); - const finalDoc1 = obfuscateVC(intermediateDoc, "key2"); - const finalDoc2 = obfuscateVC(wrappedDocument, ["key1", "key2"]); + const intermediateDoc = obfuscateOAVerifiableCredential(wrappedDocument, "key1"); + const finalDoc1 = obfuscateOAVerifiableCredential(intermediateDoc, "key2"); + const finalDoc2 = obfuscateOAVerifiableCredential(wrappedDocument, ["key1", "key2"]); expect(finalDoc1).toEqual(finalDoc2); expect(intermediateDoc).not.toHaveProperty("key1"); diff --git a/src/4.0/__tests__/sign.test.ts b/src/4.0/__tests__/sign.test.ts index 58cb1a01..3cbe0f44 100644 --- a/src/4.0/__tests__/sign.test.ts +++ b/src/4.0/__tests__/sign.test.ts @@ -1,105 +1,73 @@ import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign"; import { Wallet } from "@ethersproject/wallet"; -import { WRAPPED_DOCUMENT_DID } from "../fixtures"; -import { V4SignedWrappedDocument } from "../types"; -import { signVC } from "../sign"; +import { RAW_DOCUMENT_DID } from "../fixtures"; +import { SignedOAVerifiableCredential } from "../types"; +import { signVc, signVcErrors } from "../sign"; describe("V4.0 sign", () => { it("should sign a document", async () => { - const signedWrappedDocument = await signVC( - WRAPPED_DOCUMENT_DID, + const signedWrappedDocument = await signVc( + RAW_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", } ); - const parsedResults = V4SignedWrappedDocument.safeParse(signedWrappedDocument); + const parsedResults = SignedOAVerifiableCredential.safeParse(signedWrappedDocument); if (!parsedResults.success) { throw new Error("Parsing failed"); } const { proof } = parsedResults.data; expect(Object.keys(proof).length).toBe(9); expect(proof.key).toBe("did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller"); - expect(proof.signature).toMatchInlineSnapshot( - `"0x625a9c8f7915c4f495fc872dd771d30fb289f405b11030862292a015f10602455451c7f4b5981109fb301915327fb502b4961beeb64b9acf3f9c9c8f8b42deeb1c"` - ); + expect(proof.signature).toBeDefined(); }); it("should sign a document with a wallet", async () => { const wallet = Wallet.fromMnemonic( "tourist quality multiply denial diary height funny calm disease buddy speed gold" ); - const signedWrappedDocument = await signVC( - WRAPPED_DOCUMENT_DID, + const signedWrappedDocument = await signVc( + RAW_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, wallet ); - const parsedResults = V4SignedWrappedDocument.safeParse(signedWrappedDocument); + const parsedResults = SignedOAVerifiableCredential.safeParse(signedWrappedDocument); if (!parsedResults.success) { throw new Error("Parsing failed"); } const { proof } = parsedResults.data; expect(Object.keys(proof).length).toBe(9); expect(proof.key).toBe("did:ethr:0x906FB815De8976b1e38D9a4C1014a3acE16Ce53C#controller"); - expect(proof.signature).toMatchInlineSnapshot( - `"0xde916f44e6d3a83ec082fd35eb0b85fc541deebe5e53082c2eaf07ec5ddd503f1929f650f3c39c6b4c224a56599e4e66d018dfd536019560f117b89adff6ead61b"` - ); + expect(proof.signature).toBeDefined(); }); - it("should a signed document to be resigned", async () => { - const signedDocument = await signVC( - WRAPPED_DOCUMENT_DID, - SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, - { - public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", - private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", - } - ); + it("should throw error if a signed document is resigned", async () => { + const signedVc = await signVc(RAW_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { + public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", + private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", + }); - const resignedDocument = await signVC( - WRAPPED_DOCUMENT_DID, - SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, - { - public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", - private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", + let error; + await expect(async () => { + try { + await signVc(signedVc, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { + public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", + private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", + }); + } catch (e) { + error = e; + throw e; } - ); - - expect(signedDocument).toEqual(resignedDocument); + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "VC has already has proof object defined: + Either an unsigned or undigested VC must be provided" + `); + expect(error).toBeInstanceOf(signVcErrors.VcProofNotEmptyError); }); - it("should throw error if a key or signer is invalid", async () => { await expect( - signVC(WRAPPED_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, {} as any) + signVc(RAW_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, {} as any) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Either a keypair or ethers.js Signer must be provided"`); }); - - it("should throw error if proof is malformed", async () => { - await expect( - signVC( - { - ...WRAPPED_DOCUMENT_DID, - proof: { ...WRAPPED_DOCUMENT_DID.proof, merkleRoot: undefined as unknown as string }, - }, - SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, - { - public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", - private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", - } - ) - ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Document has not been properly wrapped: - { - "_errors": [], - "proof": { - "_errors": [], - "merkleRoot": { - "_errors": [ - "Required" - ] - } - } - }" - `); - }); }); diff --git a/src/4.0/__tests__/validate.test.ts b/src/4.0/__tests__/validate.test.ts index 77b57adb..a6365ef7 100644 --- a/src/4.0/__tests__/validate.test.ts +++ b/src/4.0/__tests__/validate.test.ts @@ -1,6 +1,6 @@ import { cloneDeep } from "lodash"; import { BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID, SIGNED_WRAPPED_DOCUMENT_DID } from "../fixtures"; -import { V4SignedWrappedDocument } from "../types"; +import { Signed } from "../types"; import { validateDigest } from "../validate"; const TEST_DOCUMENTS = { @@ -41,7 +41,7 @@ describe("V4.0 validate", () => { issuer: { ...issuerWithoutName, fakename: name, // Key was originally "name" - } as unknown as V4SignedWrappedDocument["issuer"], + } as unknown as Signed["issuer"], }) ).toBe(false); }); diff --git a/src/4.0/diagnose.ts b/src/4.0/diagnose.ts index 0a17a8f3..9a547798 100644 --- a/src/4.0/diagnose.ts +++ b/src/4.0/diagnose.ts @@ -1,15 +1,17 @@ import type { Diagnose } from "../shared/utils/@types/diagnose"; -import { V4WrappedDocument, V4SignedWrappedDocument, V4OpenAttestationDocument } from "./types"; +import { OAVerifiableCredential, DigestedOAVerifiableCredential, SignedOAVerifiableCredential } from "./types"; export const v4Diagnose: Diagnose = ({ document, kind, debug }) => { - let Validator: typeof V4OpenAttestationDocument | typeof V4WrappedDocument | typeof V4SignedWrappedDocument = - V4OpenAttestationDocument; + let Validator: + | typeof OAVerifiableCredential + | typeof DigestedOAVerifiableCredential + | typeof SignedOAVerifiableCredential = OAVerifiableCredential; if (kind === "raw") { - Validator = V4OpenAttestationDocument; + Validator = OAVerifiableCredential; } else if (kind === "wrapped") { - Validator = V4WrappedDocument; + Validator = DigestedOAVerifiableCredential; } else { - Validator = V4SignedWrappedDocument; + Validator = SignedOAVerifiableCredential; } const results = Validator.safeParse(document); diff --git a/src/4.0/digest.ts b/src/4.0/digest.ts index 2b0a411d..acadf625 100644 --- a/src/4.0/digest.ts +++ b/src/4.0/digest.ts @@ -1,27 +1,26 @@ import { hashToBuffer } from "../shared/utils/hashing"; import { MerkleTree } from "../shared/merkle"; import { ContextUrl, ContextType, UnableToInterpretContextError, interpretContexts } from "./context"; -import { NoExtraProperties, V4OpenAttestationDocument, V4WrappedDocument, W3cVerifiableCredential } from "./types"; -import { genTargetHash } from "../4.0/hash"; +import { OAVerifiableCredential, Digested, W3cVerifiableCredential } from "./types"; +import { genTargetHash } from "./hash"; import { encodeSalt, salt } from "./salt"; import { ZodError } from "zod"; -export const digestVC = async ( - // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict - vc: NoExtraProperties -): Promise> => { - /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ - const oav4context = await V4OpenAttestationDocument.pick({ "@context": true }).passthrough().safeParseAsync(vc); // Superficial check on user intention +export const digestVc = async ( + vc: T +): Promise> => { + /* 1a. Try OpenAttestation VC validation, since most user will be issuing oa v4 */ + const oav4context = await OAVerifiableCredential.pick({ "@context": true }).passthrough().safeParseAsync(vc); // Superficial check on user intention let validatedRawDocument: W3cVerifiableCredential | undefined; if (oav4context.success) { - const oav4 = await V4OpenAttestationDocument.safeParseAsync(vc); + const oav4 = await OAVerifiableCredential.safeParseAsync(vc); if (!oav4.success) { throw new DataModelValidationError("Open Attestation v4.0", oav4.error); } validatedRawDocument = oav4.data; } - /* 1b. only if OA VC validation fail do we continue with W3C VC data model validation */ + /* 1b. Only if OA VC validation fail do we continue with W3C VC data model validation */ if (!validatedRawDocument) { const w3cVc = await W3cVerifiableCredential.safeParseAsync(vc); if (!w3cVc.success) { @@ -44,7 +43,7 @@ export const digestVC = async ( validatedRawDocument["@context"].forEach((context) => contexts.add(context)); } REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); - const finalContexts: V4OpenAttestationDocument["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; + const finalContexts: OAVerifiableCredential["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; /* 4. Type validation */ // Ensure that required types are present and in the correct order @@ -57,11 +56,11 @@ export const digestVC = async ( types.forEach((type) => types.add(type)); } REQUIRED_TYPES.forEach((t) => types.delete(t)); - const finalTypes: V4OpenAttestationDocument["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; + const finalTypes: OAVerifiableCredential["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; const documentReadyForWrapping = { ...validatedRawDocument, - ...extractAndAssertAsV4OpenAttestationDocumentProps(validatedRawDocument, [ + ...extractAndAssertAsOAVerifiableCredentialProps(validatedRawDocument, [ "issuer", "credentialStatus", "credentialSubject", @@ -70,39 +69,38 @@ export const digestVC = async ( type: finalTypes, } satisfies W3cVerifiableCredential; - /* 5. OA wrapping */ + /* 5. OA wrapping */ const salts = salt(documentReadyForWrapping); - const digest = genTargetHash(documentReadyForWrapping, salts, []); + const targetHash = genTargetHash(documentReadyForWrapping, salts, []); - const batchBuffers = [digest].map(hashToBuffer); + const batchBuffers = [targetHash].map(hashToBuffer); const merkleTree = new MerkleTree(batchBuffers); const merkleRoot = merkleTree.getRoot().toString("hex"); - const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer) => buffer.toString("hex")); - const verifiableCredential: V4WrappedDocument = { + const merkleProof = merkleTree.getProof(hashToBuffer(targetHash)).map((buffer) => buffer.toString("hex")); + + return { ...documentReadyForWrapping, proof: { type: "OpenAttestationHashProof2018", proofPurpose: "assertionMethod", - targetHash: digest, + targetHash, proofs: merkleProof, merkleRoot, salts: encodeSalt(salts), privacy: { - obfuscated: [], + obfuscated: [] as string[], // FIXME: Not sure why casting required here }, }, - }; - - return verifiableCredential as V4WrappedDocument; + } as Digested; }; -export const digestVCs = async ( +export const digestVcs = async ( // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict - vcs: NoExtraProperties[] -): Promise[]> => { + documents: T[] +): Promise[]> => { // create individual verifiable credential - const verifiableCredentials = await Promise.all(vcs.map((vc) => digestVC(vc))); + const verifiableCredentials = await Promise.all(documents.map((document) => digestVc(document))); // get all the target hashes to compute the merkle tree and the merkle root const merkleTree = new MerkleTree( @@ -132,7 +130,7 @@ export const digestVCs = async ( * { a: undefined, b: "something" }. We also assert that the extracted properties * are of V4OpenAttestationDocument type. **/ -function extractAndAssertAsV4OpenAttestationDocumentProps( +function extractAndAssertAsOAVerifiableCredentialProps( original: W3cVerifiableCredential, keys: K[] ) { @@ -140,7 +138,7 @@ function extractAndAssertAsV4OpenAttestationDocumentProps { if (keys.includes(k as K)) temp[k] = v; }); - return temp as { [key in K]: V4OpenAttestationDocument[key] }; + return temp as { [key in K]: OAVerifiableCredential[key] }; } class DataModelValidationError extends Error { @@ -150,7 +148,7 @@ class DataModelValidationError extends Error { } } -export const wrapDocumentErrors = { +export const wrapVcErrors = { DataModelValidationError, UnableToInterpretContextError, }; diff --git a/src/4.0/documentBuilder.ts b/src/4.0/documentBuilder.ts index b3d5e4a1..852221f0 100644 --- a/src/4.0/documentBuilder.ts +++ b/src/4.0/documentBuilder.ts @@ -1,25 +1,25 @@ -import { digestVC, digestVCs, wrapDocumentErrors } from "./digest"; -import { signVC, signDocumentErrors } from "./sign"; +import { digestVc, digestVcs, wrapVcErrors } from "./digest"; +import { signVc, signVcErrors } from "./sign"; import { Override, DecentralisedEmbeddedRenderer, SvgRenderer, OscpResponderRevocation, RevocationStoreRevocation, - V4OpenAttestationDocument, - V4SignedWrappedDocument, - V4WrappedDocument, + OAVerifiableCredential, + Signed, + Digested, } from "./types"; import { ContextType, ContextUrl } from "./context"; import { ZodError, z } from "zod"; -const SingleDocumentProps = z.object({ - name: V4OpenAttestationDocument.shape.name.unwrap(), - credentialSubject: V4OpenAttestationDocument.shape.credentialSubject, +const SingleVcProps = z.object({ + name: OAVerifiableCredential.shape.name.unwrap(), + credentialSubject: OAVerifiableCredential.shape.credentialSubject, }); -const DocumentProps = z.union([SingleDocumentProps, z.array(SingleDocumentProps)]); +const VcProps = z.union([SingleVcProps, z.array(SingleVcProps)]); const OscpRevocationProps = z.object({ oscpUrl: OscpResponderRevocation.shape.id, @@ -47,9 +47,9 @@ const SvgRendererProps = z.discriminatedUnion("type", [ ]); const DnsTextIssuanceProps = z.object({ - issuerId: V4OpenAttestationDocument.shape.issuer.shape.id, - issuerName: V4OpenAttestationDocument.shape.issuer.shape.name, - identityProofDomain: V4OpenAttestationDocument.shape.issuer.shape.identityProof.shape.identifier, + issuerId: OAVerifiableCredential.shape.issuer.shape.id, + issuerName: OAVerifiableCredential.shape.issuer.shape.name, + identityProofDomain: OAVerifiableCredential.shape.issuer.shape.identityProof.shape.identifier, }); /** @@ -72,43 +72,43 @@ class ShouldNotModifyAfterSettingError extends Error { } } -type DocumentProps = { +type VcProps = { /** - * Human readable name of the document. + * Human readable name of the VC. * * Maps to "name" */ name: string; /** - * Main content of the document. + * Main content of the VC. * * Maps to "credentialSubject" */ - credentialSubject: z.infer; + credentialSubject: z.infer; }; type State = { - documentMainProps: DocumentProps | DocumentProps[]; - renderMethod: V4OpenAttestationDocument["renderMethod"]; - issuer: V4OpenAttestationDocument["issuer"] | undefined; - credentialStatus: V4OpenAttestationDocument["credentialStatus"]; + vcMainProps: VcProps | VcProps[]; + renderMethod: OAVerifiableCredential["renderMethod"]; + issuer: OAVerifiableCredential["issuer"] | undefined; + credentialStatus: OAVerifiableCredential["credentialStatus"]; }; /** - * A builder class to facilitate the creation of OpenAttestation v4 documents. - * Assumes a shared issuer, render method, and credential status across all documents in batch mode. + * A builder class to facilitate the creation of OpenAttestation v4 VCs. + * Assumes a shared issuer, render method, and credential status across all VCs in batch mode. * For use cases requiring direct control, consider using the lower-level wrap and sign functions. */ -export class DocumentBuilder { +export class VcBuilder { private getState: () => State; private setState: (key: Key, value: Pick[Key]) => void; constructor(props: Props) { - const parsedResults = DocumentProps.safeParse(props); + const parsedResults = VcProps.safeParse(props); if (!parsedResults.success) throw new PropsValidationError(parsedResults.error); // define state here rather than in the instance to enforce immutability via setState. const state: State = { - documentMainProps: parsedResults.data, + vcMainProps: parsedResults.data, renderMethod: undefined, issuer: undefined, credentialStatus: undefined, @@ -123,13 +123,13 @@ export class DocumentBuilder { }; } - private wrap = async (): Promise> => { - const { documentMainProps: data, issuer, renderMethod, credentialStatus } = this.getState(); + private digest = async (): Promise> => { + const { vcMainProps: data, issuer, renderMethod, credentialStatus } = this.getState(); // this should never happen if (!issuer) throw new Error("Issuer is required"); if (Array.isArray(data)) { - const toWrap = data.map( + const toDigest = data.map( ({ name, credentialSubject }) => ({ "@context": [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4], @@ -139,17 +139,17 @@ export class DocumentBuilder { credentialSubject, ...(renderMethod && { renderMethod }), ...(credentialStatus && { credentialStatus }), - } satisfies V4OpenAttestationDocument) + } satisfies OAVerifiableCredential) ); - return digestVCs(toWrap) as unknown as WrappedReturn; + return digestVcs(toDigest) as unknown as DigestedReturn; } // this should never happen if (!data) throw new Error("CredentialSubject is required"); const { name, credentialSubject } = data; - return digestVC({ + return digestVc({ "@context": [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4], type: [ContextType.BaseContext, ContextType.OAV4Context], issuer, @@ -157,18 +157,48 @@ export class DocumentBuilder { credentialSubject, ...(renderMethod && { renderMethod }), ...(credentialStatus && { credentialStatus }), - }) as unknown as WrappedReturn; + }) as unknown as DigestedReturn; }; - private sign = async (props: { signer: Parameters[2] }): Promise> => { - const wrapped = await this.wrap(); - if (Array.isArray(wrapped)) { - return Promise.all(wrapped.map((d) => signVC(d, "Secp256k1VerificationKey2018", props.signer))) as Promise< - SignedReturn - >; + private sign = async (props: { signer: Parameters[2] }): Promise> => { + const { vcMainProps: data, issuer, renderMethod, credentialStatus } = this.getState(); + + // this should never happen + if (!issuer) throw new Error("Issuer is required"); + if (Array.isArray(data)) { + return Promise.all( + data.map(({ name, credentialSubject }) => + signVc( + { + "@context": [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4], + type: [ContextType.BaseContext, ContextType.OAV4Context], + issuer, + name, + credentialSubject, + ...(renderMethod && { renderMethod }), + ...(credentialStatus && { credentialStatus }), + } satisfies OAVerifiableCredential, + "Secp256k1VerificationKey2018", + props.signer + ) + ) + ) as unknown as Promise>; } - return signVC(wrapped, "Secp256k1VerificationKey2018", props.signer) as Promise>; + const { name, credentialSubject } = data; + return signVc( + { + "@context": [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4], + type: [ContextType.BaseContext, ContextType.OAV4Context], + issuer, + name, + credentialSubject, + ...(renderMethod && { renderMethod }), + ...(credentialStatus && { credentialStatus }), + } satisfies OAVerifiableCredential, + "Secp256k1VerificationKey2018", + props.signer + ) as unknown as SignedReturn; }; // add issuance methods here @@ -198,7 +228,7 @@ export class DocumentBuilder { // }, /** - * The document will be digitally signed, and identity proof will be provided via a DNS-TXT record. + * The VC will be digitally signed, and identity proof will be provided via a DNS-TXT record. * * Sets "issuer.type" to "OpenAttestationIssuer" and "issuer.identityProof.identityProofType" to "DNS-TXT" */ @@ -234,20 +264,12 @@ export class DocumentBuilder { identityProofType: "DNS-TXT", identifier: identityProofDomain, }, - } satisfies V4OpenAttestationDocument["issuer"]; + } satisfies OAVerifiableCredential["issuer"]; this.setState("issuer", issuer); return { - /** - * Wraps and signs the entire batch in a single operation. This method does not use internal batching logic, - * which could lead to too many concurrent remote calls when signing large batches. Use this function with caution in such scenarios. - */ - wrapAndSign: this.sign, - /** - * Provides an option to wrap documents without signing them, allowing for more control over the signing process. - * This is particularly useful when you need to sign documents in smaller batches or at different stages. - */ - justWrapWithoutSigning: this.wrap, + sign: this.sign, + digest: this.digest, }; }, } satisfies Record<`${string}Issuance`, (...args: any[]) => any>; @@ -255,7 +277,7 @@ export class DocumentBuilder { // add revocation methods here private REVOCATION_METHODS = { /** - * The document(s) will be issued without the capability for revocation; they remain valid indefinitely. + * The VC(s) will be issued without the capability for revocation; they remain valid indefinitely. */ noRevocation: () => { this.setState("credentialStatus", undefined); @@ -263,7 +285,7 @@ export class DocumentBuilder { return this.ISSUANCE_METHODS; }, /** - * The document(s) can be revoked using an OCSP responder, allowing for the verification of the revocation status through the specified URL. + * The VC(s) can be revoked using an OCSP responder, allowing for the verification of the revocation status through the specified URL. * * Sets "credentialStatus.type" to "OpenAttestationOcspResponder" */ @@ -282,13 +304,13 @@ export class DocumentBuilder { const credentialStatus = { id: oscpUrl, type: "OpenAttestationOcspResponder", - } satisfies V4OpenAttestationDocument["credentialStatus"]; + } satisfies OAVerifiableCredential["credentialStatus"]; this.setState("credentialStatus", credentialStatus); return this.ISSUANCE_METHODS; }, /** - * The document(s) can be revoked using a revocation store, implemented as a smart contract on the Ethereum blockchain. + * The VC(s) can be revoked using a revocation store, implemented as a smart contract on the Ethereum blockchain. * * Sets "credentialStatus.type" to "OpenAttestationRevocationStore" */ @@ -307,7 +329,7 @@ export class DocumentBuilder { const credentialStatus = { id: storeAddress, type: "OpenAttestationRevocationStore", - } satisfies V4OpenAttestationDocument["credentialStatus"]; + } satisfies OAVerifiableCredential["credentialStatus"]; this.setState("credentialStatus", credentialStatus); @@ -316,7 +338,7 @@ export class DocumentBuilder { } satisfies Record<`${string}Revocation`, (...args: any[]) => typeof this.ISSUANCE_METHODS>; /** - * Configures the document to be rendered using OpenAttestation's decentralized React components, + * Configures the VC to be rendered using OpenAttestation's decentralized React components, * see (https://github.com/Open-Attestation/decentralized-renderer-react-components). * * Sets "renderMethod[0].type" to "OpenAttestationEmbeddedRenderer" @@ -329,7 +351,7 @@ export class DocumentBuilder { */ rendererUrl: string; /** - * The identifier for the template used by the renderer to display the document correctly. + * The identifier for the template used by the renderer to display the VC correctly. * * Maps to "renderMethod[0].templateName" */ @@ -345,16 +367,16 @@ export class DocumentBuilder { type: "OpenAttestationEmbeddedRenderer", templateName, }, - ] satisfies V4OpenAttestationDocument["renderMethod"]; + ] satisfies OAVerifiableCredential["renderMethod"]; this.setState("renderMethod", renderMethod); return this.REVOCATION_METHODS; }; /** - * Renders the document using an SVG handlebar template, either embedded directly or hosted remotely. - * The root object of the handlebar template corresponds to the credentialSubject of the document. - * For instance, if the document credentialSubject is { name: "John Doe" }, + * Renders the VC using an SVG handlebar template, either embedded directly or hosted remotely. + * The root object of the handlebar template corresponds to the credentialSubject of the VC. + * For instance, if the VC credentialSubject is { name: "John Doe" }, * the handlebar template should reference the name as {{name}} to correctly map data fields. * * Sets "renderMethod[0].type" to "SvgRenderingTemplate2023" @@ -403,14 +425,14 @@ export class DocumentBuilder { type: "SvgRenderingTemplate2023" as const, digestMultibase: parsedResults.data.digestMultibase, }, - ] satisfies V4OpenAttestationDocument["renderMethod"]); + ] satisfies OAVerifiableCredential["renderMethod"]); this.setState("renderMethod", renderMethod); return this.REVOCATION_METHODS; }; /** - * Disables rendering for the document. + * Disables rendering for the VC. */ public noRenderer = () => { this.setState("renderMethod", undefined); @@ -418,17 +440,17 @@ export class DocumentBuilder { }; } -type SignedReturn = Props extends Array +type SignedReturn = Props extends Array ? Override< - V4SignedWrappedDocument, + Signed, { name: Props[number]["name"]; credentialSubject: Props[number]["credentialSubject"]; } >[] - : Props extends DocumentProps + : Props extends VcProps ? Override< - V4SignedWrappedDocument, + Signed, { name: Props["name"]; credentialSubject: Props["credentialSubject"]; @@ -436,17 +458,17 @@ type SignedReturn = Props extends > : never; -type WrappedReturn = Props extends Array +type DigestedReturn = Props extends Array ? Override< - V4WrappedDocument, + Digested, { name: Props[number]["name"]; credentialSubject: Props[number]["credentialSubject"]; } >[] - : Props extends DocumentProps + : Props extends VcProps ? Override< - V4WrappedDocument, + Digested, { name: Props["name"]; credentialSubject: Props["credentialSubject"]; @@ -454,11 +476,12 @@ type WrappedReturn = Props extend > : never; -const { UnableToInterpretContextError } = wrapDocumentErrors; -const { CouldNotSignDocumentError } = signDocumentErrors; -export const DocumentBuilderErrors = { +const { UnableToInterpretContextError } = wrapVcErrors; +const { CouldNotSignVcError, VcProofNotEmptyError } = signVcErrors; +export const VcBuilderErrors = { PropsValidationError, ShouldNotModifyAfterSettingError, UnableToInterpretContextError, - CouldNotSignDocumentError, + CouldNotSignVcError, + VcProofNotEmptyError, }; diff --git a/src/4.0/exports/builder.ts b/src/4.0/exports/builder.ts index 9d129cf1..bc1f5134 100644 --- a/src/4.0/exports/builder.ts +++ b/src/4.0/exports/builder.ts @@ -1 +1 @@ -export { DocumentBuilder, DocumentBuilderErrors } from "../documentBuilder"; +export { VcBuilder as DocumentBuilder, VcBuilderErrors as DocumentBuilderErrors } from "../documentBuilder"; diff --git a/src/4.0/exports/digest.ts b/src/4.0/exports/digest.ts index e8b38265..64182ef8 100644 --- a/src/4.0/exports/digest.ts +++ b/src/4.0/exports/digest.ts @@ -1 +1 @@ -export { digestVC, digestVCs, wrapDocumentErrors } from "../digest"; +export { digestVc, digestVcs, wrapVcErrors as wrapDocumentErrors } from "../digest"; diff --git a/src/4.0/exports/obfuscate.ts b/src/4.0/exports/obfuscate.ts index 8d8c90c8..cbf29ca8 100644 --- a/src/4.0/exports/obfuscate.ts +++ b/src/4.0/exports/obfuscate.ts @@ -1 +1 @@ -export { obfuscateVC as obfuscateVerifiableCredential, obfuscateErrors } from "../obfuscate"; +export { obfuscateOAVerifiableCredential, obfuscateErrors } from "../obfuscate"; diff --git a/src/4.0/exports/sign.ts b/src/4.0/exports/sign.ts index a78063b6..9280f63d 100644 --- a/src/4.0/exports/sign.ts +++ b/src/4.0/exports/sign.ts @@ -1 +1 @@ -export { signVC, signDocumentErrors } from "../sign"; +export { signVc, signVcErrors as signDocumentErrors } from "../sign"; diff --git a/src/4.0/exports/types.ts b/src/4.0/exports/types.ts index 70c99577..566f82d5 100644 --- a/src/4.0/exports/types.ts +++ b/src/4.0/exports/types.ts @@ -1,5 +1 @@ -export type { - V4OpenAttestationDocument as OpenAttestationDocument, - V4WrappedDocument as WrappedDocument, - V4SignedWrappedDocument as SignedWrappedDocument, -} from "../types"; +export type { OAVerifiableCredential, Digested, Signed } from "../types"; diff --git a/src/4.0/exports/utils.ts b/src/4.0/exports/utils.ts index 9e28fa7a..61f0daa1 100644 --- a/src/4.0/exports/utils.ts +++ b/src/4.0/exports/utils.ts @@ -1,7 +1,3 @@ export { v4Diagnose as diagnose } from "../diagnose"; -export { - isV4OpenAttestationDocument as isOpenAttestationDocument, - isV4WrappedDocument as isWrappedDocument, - isV4SignedWrappedDocument as isSignedWrappedDocument, -} from "../types"; +export { isOAVerifiableCredential, isDigestedOAVerifiableCredential, isSignedOAVerifiableCredential } from "../types"; export { computeDigestMultibase } from "../computeDigestMultibase"; diff --git a/src/4.0/fixtures.ts b/src/4.0/fixtures.ts index 45b4d9b7..ccf59216 100644 --- a/src/4.0/fixtures.ts +++ b/src/4.0/fixtures.ts @@ -1,4 +1,4 @@ -import { V4OpenAttestationDocument, V4SignedWrappedDocument, V4WrappedDocument } from "./types"; +import { OAVerifiableCredential, Digested, Signed } from "./types"; import { ContextUrl } from "./context"; const ISSUER_ID = "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90" as const; @@ -43,7 +43,7 @@ export const RAW_DOCUMENT_DID = freezeObject({ }, ], }, -} satisfies V4OpenAttestationDocument); +} satisfies OAVerifiableCredential); export const RAW_DOCUMENT_DID_OSCP = freezeObject({ "@context": [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4], @@ -84,7 +84,7 @@ export const RAW_DOCUMENT_DID_OSCP = freezeObject({ }, ], }, -} satisfies V4OpenAttestationDocument); +} satisfies OAVerifiableCredential); export const BATCHED_RAW_DOCUMENTS_DID = freezeObject([ { @@ -160,9 +160,9 @@ export const BATCHED_RAW_DOCUMENTS_DID = freezeObject([ }, ], }, -] satisfies V4OpenAttestationDocument[]); +] satisfies OAVerifiableCredential[]); -/* Wrapped */ +/* Digested */ export const WRAPPED_DOCUMENT_DID = freezeObject({ "@context": [ "https://www.w3.org/ns/credentials/v2", @@ -207,7 +207,7 @@ export const WRAPPED_DOCUMENT_DID = freezeObject({ "W3sidmFsdWUiOiJhOGEzMGE4ZTFjNWQ4ODk2NWI3NDZkZjBhYWYyMTMyN2Q4MDNkMzQ4ZThlOGRhMTlmNTNhMWU5ODFkOTFhMDQ0IiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6IjFmMzIwMzg4MjU3NTRkZTc1OGYwYmU2NjdiNjQ0ZjNjZGVkM2FlM2UwOGI0MTdhMmViZTljYmU1NmYyNGM0NTAiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiODQ0OTkwM2FhNDMxZDEzZTEzNTBiYjVhZTczMTM3OTRlMGQyMTMwNmM3NDA0YzI4NzJhY2Y3ZDY2NGIyMjNhZiIsInBhdGgiOiJuYW1lIn0seyJ2YWx1ZSI6ImFkN2Y1Mjg0OTc1MGViNjZhNjJlZmFmYWUwYjQxNGEwZGQ5OGUwNGJkMmI5YzU2NjliYWM1YzRiNDNjMDk3MTMiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJjY2I4ZDFkZDgyMDc2Y2EyOTQ5MWUxZTBjODAxOGM5MWY0Zjc5NGRiM2RkMDA1YmFjMGY4MzM1YmFmODFmZWRkIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiYmNlNzNhMjBlMDNiNmM0ZDM1M2VkY2IzMTM0NzZhOTZhNTRkMGNjYzVkNWQ1OWIzMjRhOWU1YTQ2NjQzZmFiNiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMjBhMDM0ZjcxMDliNDRmOGEyZTIxMWM1ZTE5YzQ2Nzk1NGY2OWU2NmQzOTZjZjFlYjk1NTViZDc2NjkyN2UyNSIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiIwNWVmYTdiNWM1MDFhZWIxNTE5NTE0MDczNzdmYjJmODc2MTk1ZTAzYzkwZjUzZTdhYWZjNGMzZmFhNDI1YjhhIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjEzYzE3YjQ5ZTc2YjQ3NjJjZGRiYmRjYjFiZDU2ZmUyNDIyZDEwYmJkMmY2MjAzZGZiNzRkZGRlYjBiZWNkYTMiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiNjBmM2JiMTY1YjhlMzcxOGJhZjQ0ZjVlMTdkNDljY2Y4ZGE5MGViYTMxNjUwZDRjM2IzODlkNmFiZGFiNTViYiIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6ImY1YjFjYjc3ZTZmNDQwM2NmMmM4NDg1MGIwNTcyMGI5NTk5Yjk0NmUwMWI2MzcwODUzZWY0YzUyYmQwYTZmZjEiLCJwYXRoIjoidmFsaWRGcm9tIn0seyJ2YWx1ZSI6ImM3MWM3ZDZjYTdhMjY5OWVhZjdjOTgwYzlmMjM1MWY3NDc3ZDliZDFlNzJlNGY2NTIxZjZhMzI0ZWEzYjdmMWYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiMDdlODkzMTgyZGFjNjRjOWVkZGU4MjMwYzdjZTdmMWM2NTRmZjgxN2Y5OGIzZTkxMWU4ZTg1Yzc4ODY0MWZhZCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6ImU4YzdmMjQyYTI5YThmYjJiMjEyMjVhYzlmOTk5ZmVhOTNlNmRhYzc5YTNlYjQwYWRlMTc2ZGRmYzFjMmRlMTgiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiI5ZDNkMThlMTY0YTg3YmQ3MmFlNDczYTIzZjc5ZjBkNzU2NTFiZjExODViMmI0N2ZlYjhiOGFiNWU3YWY1YzUyIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6Ijk4MjIwZTE5NmU4YmE4NWI3MDc2YzdiMzE1MDBkOTU0Nzk1MTk5NDQ4YmM1Y2IyMzM0ZjNhYjU1OTA3NGNkNTMiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiNTNlOTdmNDBkZTExNDkxMjNlNmNlMmNhN2I0MzlhMzI3NzYxMGZkNmZmZTZlMTcwYjEwMjdlOWMzNThmYjg2MSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjA1OTQ1MWQzMWNlZjM5MDg1YWMxNGVkYjE1NjJjYzFkNTE0YmYzZWQ0N2I3YzBkNWM0MjdiYmM0NGNlOGU5YmIiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiN2E3YmUzMzMzNjI4MDAyNmVkN2NkZmFlZDkwZWI1Zjg0ZDZiMGVkZjdiNTkxZjk5MjQ3NmYzNDBjMWViZjUzYyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiJkNzMxMDA3NmM1NzZmNzU0MzcwNjQ5MTYxOTEyNWY0YmQ5NDNlMDEwNWM3ZDM1ZjZjNThjZTI3ZjcwMzNiNjliIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifSx7InZhbHVlIjoiYTk3MzFhMzFkMzkzNmJmNjAyNzMxNTAwYjIzMjY3ZTA2MzcxOGEzZjJkMGFiZTI4MDhlOGJiMzQxODQxYWZlZCIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0uaWQifSx7InZhbHVlIjoiNDg4MjRkYjdjY2U3ZTY5MGQ3NjgyMGM1N2M1OWNlZGI5ZDZiNGVjMDlhMDY0ZDFmYTJhMmI0OGZhYzJlN2FhZCIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udHlwZSJ9LHsidmFsdWUiOiJjMzY1M2FkNzg4MzhkNDhmM2Q1ZGNkNmE2OGRmNGU0MmMxMTM1ZmY4MzhiYzI5MTY4NDQzMDdjZDljZmM4ZWY4IiwicGF0aCI6InJlbmRlck1ldGhvZFswXS50ZW1wbGF0ZU5hbWUifV0=", privacy: { obfuscated: [] }, }, -} satisfies V4WrappedDocument); +} satisfies Digested); export const WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ "@context": [ @@ -254,7 +254,7 @@ export const WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ "W3sidmFsdWUiOiI3N2RhNDUzOWYwY2M3ZmVmODg0ZmU0MTVkNzE2ZTRjODc5N2NiMDMyZGJlZDQzOWM2ZWViOTU2NmJlZDk1MmI0IiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6ImY2NWZhZWI4MzVmZTI4MzYyMDBhZGUyYTUzZjM4MzJkMGE2YTVjZjZiZjc2OGRlNmMxYjE3OTQ1OGIwMGI2MDIiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiMjVkOGNmNDY3MTAzMmMzMTUzZDdlY2I2OTQ1OGU2MzNkZWE0YmYwYTc0MGI4YzZiMDFlYjE5M2I4NzE2ZDYzYSIsInBhdGgiOiJuYW1lIn0seyJ2YWx1ZSI6ImQxNjEyODkzZGI2YjM3MDY0MjgzM2FkNjYwYjQ5N2ZiMTY0ZWZlZTZkNWY0ZDhhMjg0YjkxNWNkNGNhNzJkM2YiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJjZjkzMTg5ZTBjNTE0ZGUwMWJlOTI5ZWRhNjk4ZTdlOWQ5ZmRiMzJlOTVjZTdlOTM1NGM4OWJlYjc3Mzg1NjNkIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiYzgzNzJlYmU2NWJiMzdhOTI0YTljMmZiNGE3Yzc4MmQxMzI1ZjE0NTY3OTFjODJmZmI4NGUwY2FmYWFlMDg2OCIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMzg1MzJhNzJiMDA1Njc4Yjc2M2Y0NTdlY2IxZTI1NzhhMDVkYzQ5ZjdlZDhhYzk5N2EyNDJjZWNjNGY3MDcyMiIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiIzMTQ1OWY5ZmUyNTdkMDVlZTkwNjg4NmYxYmU3ZjBmOTU4YTUxZGM3YTJlNTY5N2EyOGNjZjI3YWVhOGRmNDg4IiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6ImVkOTQ0Mzk0ZmQ5YzY3OWI5MDg0MjNmNjJlZWU5M2YxODJmNjdmZmIxM2MxNGM2ODJjZDMyZmNkMTk3MmVlN2IiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiYTBjODBhOTI4ZGI3MDExYTI0ODIzYzUzZGJlNjNmNGU5ZTc3M2IyMjkyZWNjOThkMWFiNjZiMjVjYTBmYzY3YiIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6Ijk4Y2JlZjE3NDZkZjM1MmQ5Njg4NmYyYWQ1N2NmOWI5ODg2ZWJhZTJlYzA1ZTM4YWE1YTc5ZTM2YTE2OWY1NGMiLCJwYXRoIjoidmFsaWRGcm9tIn0seyJ2YWx1ZSI6IjlhYzhkMzA5ZGEyZGYzNWNhN2RkNDFkYTc3NzRkYzFhNWY4NTE3NmFiNGU3ZGY1MDgzNzBiNDNlNmU2Y2FhNGEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiZjkwYWU2YWVjNzlhODg0OTJkYzFlN2IwYThmNDExYWEwN2Y2YjY5NGMwZjQzNjhhZTMzZWVlNTllYzVhZDM2NyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6IjBhMWNhMTQwMmI4MDEwNWQzNGY4NmVjZjNjMDgxYTE3ZTVlODhiY2UwN2ZjNzgyMGRkYzdmZDY1OTA5ZDcwM2MiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiI2NTk1NGE0ZTNiZGRlNmQ5NGEyYjA4OTQ3YTU3YTdkOWEzYzAwNWEyN2ZmNzA0ZmNjMDI2MDI0MmNkNjczNGI1IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6Ijg3ZDc4NzBjYmVkOGZkYzIyNjA4MWMyZmY5ZmZmNmU3ZmJiZWYyMDUyMDg5YjU1MDg4MDg4MzliNWZlMWNlMGUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiMTgwMzBkZjQ5MzRhMDhlYmM3YTEwNjZlOWRlODZhMDAxYmZhNjcyNWI2Y2FiYjA5NGNmZWI5NzE4YTU3ZDViNiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImUyNWQ1MzFmMTIwNzM0ZWY2ZmY1MTU3MjViYjM5MGJkMjU4MTE2NWM4YTMxZTViMTRmNWUzZTMzM2I2OGZmNWUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiMTNiMjYyN2E4Yzk0YzkwYWI0M2JjZGExNDNkNTI2MDM0YWM0ZDVkNThhMTc2OTIzMDcwZTAzMGM2MTkwOWVlYiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiI2YjIzZWZkODVhZjZjZWZkMTBjM2EwNzczNjdlMjE4Mzc1MTlkN2ExYTBhMzVmODFkZDBhNWYzNTA0MTg4NjE4IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifSx7InZhbHVlIjoiMTI4MzY5ZDk5NTU2ZGYwOWMwOGE4NmZkODU4NmJlMWJlNWVjODcxYjY3NGQwNzJmN2U4ODdmNWZiYjViNjE5NiIsInBhdGgiOiJjcmVkZW50aWFsU3RhdHVzLmlkIn0seyJ2YWx1ZSI6ImYzNTBjOGYwNjlkNmIyM2M3NmE0MjQ3ZTIyOWRjOGM1MDVjMTFhZTNkNjFmYjE3ZDJlNDIxZWU1NzY4OGQ4YTMiLCJwYXRoIjoiY3JlZGVudGlhbFN0YXR1cy50eXBlIn0seyJ2YWx1ZSI6IjJkMTUzYzc1OGNiMTY1YjM1MTFhNjA4MjBkMzNiY2ZmYTViNmE3OWFiNWI5ZDNlMTA0NGZiNTk0NjNhNzM3MDUiLCJwYXRoIjoicmVuZGVyTWV0aG9kWzBdLmlkIn0seyJ2YWx1ZSI6ImJmOGJlY2M2Yjg1MDJkODBiNTg4ZmRmZmJhY2JmMmU1NTIzNjE1MzBjYmUxMGI4NzM5OTQ0NWYwZmZkYTkwOTAiLCJwYXRoIjoicmVuZGVyTWV0aG9kWzBdLnR5cGUifSx7InZhbHVlIjoiOTRmYmI5NWE1ZmNhZjU0YTcwYTAxODZiNjg1OWM5YmY5MzYzNWU0OTQ0N2U3ZmMxYWIyY2RmNTM5ZDllZjNiNyIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udGVtcGxhdGVOYW1lIn1d", privacy: { obfuscated: [] }, }, -} satisfies V4WrappedDocument); +} satisfies Digested); export const BATCHED_WRAPPED_DOCUMENTS_DID = freezeObject([ { @@ -350,7 +350,7 @@ export const BATCHED_WRAPPED_DOCUMENTS_DID = freezeObject([ privacy: { obfuscated: [] }, }, }, -] satisfies V4WrappedDocument[]); +] satisfies Digested[]); /* Signed */ export const SIGNED_WRAPPED_DOCUMENT_DID = freezeObject({ @@ -400,7 +400,7 @@ export const SIGNED_WRAPPED_DOCUMENT_DID = freezeObject({ signature: "0x949b76d8df493a56c1cf21303a74d6a54904461c1c10f4619b43ad7d339c64467c61eb4c0873f279cd21d5bdd044d3af5318f14d63f57acbd4cde30f271f3eb71c", }, -} satisfies V4SignedWrappedDocument); +} satisfies Signed); export const SIGNED_WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ "@context": [ @@ -450,7 +450,7 @@ export const SIGNED_WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ signature: "0xa9f89c00bac009044f02ca0e0c605389a927e4b011fa7c0f9a3bfd987598d8a442cd51218a31e387737ad42adeb9b9405c545a4d70ad75d06f7a7701e87440631c", }, -} satisfies V4SignedWrappedDocument); +} satisfies Signed); export const SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED = freezeObject({ "@context": [ @@ -495,7 +495,7 @@ export const SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED = freezeObject({ renderMethod: [ { id: "https://demo-renderer.opencerts.io", type: "OpenAttestationEmbeddedRenderer", templateName: "GOVTECH_DEMO" }, ], -} satisfies V4SignedWrappedDocument); +} satisfies Signed); export const BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID = freezeObject([ { @@ -597,7 +597,7 @@ export const BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID = freezeObject([ "0xc65309e0adf50ba6b91607c6913e15cd629412cf8180255e52c160cdf59bcfa0609f6eed71c379e3062b9fea39a5590dfc54323a352933c6ef9b694b63e2d74f1c", }, }, -] satisfies V4SignedWrappedDocument[]); +] satisfies Signed[]); // Freeze fixture to prevent accidental changes during tests function freezeObject(obj: T): T { diff --git a/src/4.0/jsonSchemas/__generated__/v4-wrapped-document.schema.json b/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json similarity index 95% rename from src/4.0/jsonSchemas/__generated__/v4-wrapped-document.schema.json rename to src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json index 9a516652..f9e48f6c 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-wrapped-document.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json @@ -1,7 +1,7 @@ { - "$ref": "#/definitions/v4WrappedDocument", + "$ref": "#/definitions/DigestedOAVerifiableCredential", "definitions": { - "v4WrappedDocument": { + "DigestedOAVerifiableCredential": { "type": "object", "properties": { "@context": { @@ -59,7 +59,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4WrappedDocument/properties/id" + "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -77,7 +77,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4WrappedDocument/properties/id" + "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -96,7 +96,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4WrappedDocument/properties/id" + "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string", @@ -225,7 +225,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4WrappedDocument/properties/id" + "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -242,7 +242,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4WrappedDocument/properties/id" + "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -262,7 +262,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4WrappedDocument/properties/id" + "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -279,7 +279,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4WrappedDocument/properties/id" + "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" diff --git a/src/4.0/jsonSchemas/__generated__/v4-document.schema.json b/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json similarity index 95% rename from src/4.0/jsonSchemas/__generated__/v4-document.schema.json rename to src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json index a112453a..843c42a3 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-document.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json @@ -1,7 +1,7 @@ { - "$ref": "#/definitions/v4Document", + "$ref": "#/definitions/OAVerifiableCredential", "definitions": { - "v4Document": { + "OAVerifiableCredential": { "type": "object", "properties": { "@context": { @@ -59,7 +59,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4Document/properties/id" + "$ref": "#/definitions/OAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -77,7 +77,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4Document/properties/id" + "$ref": "#/definitions/OAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -96,7 +96,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4Document/properties/id" + "$ref": "#/definitions/OAVerifiableCredential/properties/id" }, "type": { "type": "string", @@ -225,7 +225,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4Document/properties/id" + "$ref": "#/definitions/OAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -242,7 +242,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4Document/properties/id" + "$ref": "#/definitions/OAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -262,7 +262,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4Document/properties/id" + "$ref": "#/definitions/OAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -279,7 +279,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4Document/properties/id" + "$ref": "#/definitions/OAVerifiableCredential/properties/id" }, "type": { "type": "string" diff --git a/src/4.0/jsonSchemas/__generated__/v4-signed-wrapped-document.schema.json b/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json similarity index 95% rename from src/4.0/jsonSchemas/__generated__/v4-signed-wrapped-document.schema.json rename to src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json index 648460f3..d8cd7903 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-signed-wrapped-document.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json @@ -1,7 +1,7 @@ { - "$ref": "#/definitions/v4SignedWrappedDocument", + "$ref": "#/definitions/SignedOAVerifiableCredential", "definitions": { - "v4SignedWrappedDocument": { + "SignedOAVerifiableCredential": { "type": "object", "properties": { "@context": { @@ -59,7 +59,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4SignedWrappedDocument/properties/id" + "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -77,7 +77,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4SignedWrappedDocument/properties/id" + "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -96,7 +96,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4SignedWrappedDocument/properties/id" + "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" }, "type": { "type": "string", @@ -225,7 +225,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4SignedWrappedDocument/properties/id" + "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -242,7 +242,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4SignedWrappedDocument/properties/id" + "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -262,7 +262,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4SignedWrappedDocument/properties/id" + "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -279,7 +279,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/v4SignedWrappedDocument/properties/id" + "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" diff --git a/src/4.0/obfuscate.ts b/src/4.0/obfuscate.ts index 975771f1..8cfc9fc1 100644 --- a/src/4.0/obfuscate.ts +++ b/src/4.0/obfuscate.ts @@ -1,10 +1,10 @@ import { cloneDeep, get, unset, pick, toPath } from "lodash"; import { decodeSalt, encodeSalt } from "./salt"; import { traverseAndFlatten } from "./traverseAndFlatten"; -import { Override, PartialDeep, V4SignedWrappedDocument, V4WrappedDocument } from "./types"; +import { Override, PartialDeep, Signed, Digested, DigestedOAVerifiableCredential } from "./types"; import { hashLeafNode } from "./hash"; -const obfuscate = (_data: V4WrappedDocument, fields: string[] | string) => { +const obfuscate = (_data: Digested | Signed, fields: string[] | string) => { const data = cloneDeep(_data); // Prevents alteration of original data const fieldsAsArray = ([] as string[]).concat(fields); @@ -73,7 +73,7 @@ const obfuscate = (_data: V4WrappedDocument, fields: string[] | string) => { }; }; -export type ObfuscateVerifiableCredentialResult = Override< +export type ObfuscateOAVerifiableCredentialResult = Override< T, { credentialSubject: Override< @@ -86,16 +86,16 @@ export type ObfuscateVerifiableCredentialResult = O >; } >; -export const obfuscateVC = ( - vc: T, +export const obfuscateOAVerifiableCredential = ( + document: T, fields: string[] | string -): ObfuscateVerifiableCredentialResult => { - const { data, obfuscatedData } = obfuscate(vc, fields); - const currentObfuscatedData = vc.proof.privacy.obfuscated; +): ObfuscateOAVerifiableCredentialResult => { + const { data, obfuscatedData } = obfuscate(document, fields); + const currentObfuscatedData = document.proof.privacy.obfuscated; const newObfuscatedData = currentObfuscatedData.concat(obfuscatedData); // assert that obfuscated is still compliant to our schema - const parsedResults = V4WrappedDocument.safeParse({ + const parsedResults = DigestedOAVerifiableCredential.safeParse({ ...data, proof: { ...data.proof, @@ -109,13 +109,13 @@ export const obfuscateVC = path.join(".")); throw new CannotObfuscateProtectedPathsError(paths); } - return parsedResults.data as ObfuscateVerifiableCredentialResult; + return parsedResults.data as ObfuscateOAVerifiableCredentialResult; }; class CannotObfuscateProtectedPathsError extends Error { constructor(public paths: string[]) { super( - `The resultant obfuscated document is not V4 Wrapped Document compliant, please ensure that the following path(s) are not obfuscated: ${paths + `The resultant obfuscated document is not compliant with the OA v4 Verifiable Credential data model, please ensure that the following path(s) are not obfuscated: ${paths .map((val) => `"${val}"`) .join(", ")}` ); diff --git a/src/4.0/sign.ts b/src/4.0/sign.ts index 87ab8659..2690b25c 100644 --- a/src/4.0/sign.ts +++ b/src/4.0/sign.ts @@ -1,57 +1,68 @@ +import { Signer } from "@ethersproject/abstract-signer"; + import { sign } from "../shared/signer"; import { SigningKey } from "../shared/@types/sign"; -import { Signer } from "@ethersproject/abstract-signer"; -import { V4OpenAttestationDocument, V4WrappedDocument, V4SignedWrappedDocument } from "./types"; -import type { ZodError } from "zod"; +import { digestVc } from "./digest"; +import { Digested, OAVerifiableCredential, W3cVerifiableCredential, Signed } from "./types"; -export const signVC = async ( - document: V4SignedWrappedDocument | V4WrappedDocument, +export const signVc = async ( + unsignedVc: T, algorithm: "Secp256k1VerificationKey2018", keyOrSigner: SigningKey | Signer -): Promise> => { - const parsedResults = V4WrappedDocument.pick({ proof: true }).passthrough().safeParse(document); - if (!parsedResults.success) { - throw new WrappedDocumentValidationError(parsedResults.error); +): Promise> => { + /* 1. Input VC needs to be digested first */ + let validatedProof: Digested["proof"]; + if (!unsignedVc.proof) { + const wrappedDocument = await digestVc(unsignedVc); + validatedProof = wrappedDocument.proof; + } else { + // Do not accept a VC that already has proof object defined + throw new VcProofNotEmptyError(new Error("Either an unsigned or undigested VC must be provided")); } + const merkleRoot = `0x${validatedProof.merkleRoot}`; + + /* 2. Check if input keyOrSigner is valid */ if (!SigningKey.guard(keyOrSigner) && keyOrSigner.signMessage === undefined) { throw new Error(`Either a keypair or ethers.js Signer must be provided`); } - const { proof: validatedProof } = parsedResults.data; - const merkleRoot = `0x${validatedProof.merkleRoot}`; - + /* 3. Perform signing */ try { const signature = await sign(algorithm, merkleRoot, keyOrSigner); - const proof: V4SignedWrappedDocument["proof"] = { + const proof: Signed["proof"] = { ...validatedProof, key: "public" in keyOrSigner ? keyOrSigner.public : `did:ethr:${await keyOrSigner.getAddress()}#controller`, signature, }; - return { ...document, proof }; + return { ...unsignedVc, proof } as Signed; } catch (error) { - throw new CouldNotSignDocumentError(error); + throw new CouldNotSignVcError(error); } }; -class WrappedDocumentValidationError extends Error { - constructor(public error: ZodError) { - super(`Document has not been properly wrapped:\n${JSON.stringify(error.format(), null, 2)}`); - Object.setPrototypeOf(this, WrappedDocumentValidationError.prototype); +class VcProofNotEmptyError extends Error { + constructor(public error: unknown) { + super( + `VC has already has proof object defined:\n${ + error instanceof Error ? error.message : JSON.stringify(error, null, 2) + }` + ); + Object.setPrototypeOf(this, VcProofNotEmptyError.prototype); } } /** * Cases where this can be thrown includes: network error, invalid keys or signer */ -class CouldNotSignDocumentError extends Error { +class CouldNotSignVcError extends Error { constructor(public error: unknown) { super(`Could not sign document:\n${error instanceof Error ? error.message : JSON.stringify(error, null, 2)}`); - Object.setPrototypeOf(this, CouldNotSignDocumentError.prototype); + Object.setPrototypeOf(this, CouldNotSignVcError.prototype); } } -export const signDocumentErrors = { - WrappedDocumentValidationError, - CouldNotSignDocumentError, +export const signVcErrors = { + VcProofNotEmptyError, + CouldNotSignVcError, }; diff --git a/src/4.0/types.ts b/src/4.0/types.ts index e4e14bf4..0935001f 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -198,7 +198,7 @@ export const RevocationStoreRevocation = z.object({ type: z.literal("OpenAttestationRevocationStore"), }); -export const V4OpenAttestationDocument = _W3cVerifiableCredential +export const OAVerifiableCredential = _W3cVerifiableCredential .extend({ "@context": z @@ -241,7 +241,7 @@ export const V4OpenAttestationDocument = _W3cVerifiableCredential }) .strict(); -const WrappedProof = z.object({ +const DigestedProof = z.object({ type: z.literal("OpenAttestationHashProof2018"), proofPurpose: z.literal("assertionMethod"), targetHash: z.string(), @@ -250,33 +250,43 @@ const WrappedProof = z.object({ salts: z.string(), privacy: z.object({ obfuscated: z.array(z.string()) }), }); -const WrappedDocumentExtrasShape = { proof: WrappedProof.passthrough() } as const; -// V4WrappedDocument should never allow extra root properties -export const V4WrappedDocument = V4OpenAttestationDocument.extend(WrappedDocumentExtrasShape).strict(); - -const SignedWrappedProof = WrappedProof.extend({ key: z.string(), signature: z.string() }); -const SignedWrappedDocumentExtrasShape = { proof: SignedWrappedProof } as const; -// V4SignedWrappedDocument should never allow extra root properties -export const V4SignedWrappedDocument = V4OpenAttestationDocument.extend(SignedWrappedDocumentExtrasShape).strict(); +const DigestedOAVerifiableCredentialExtrasShape = { proof: DigestedProof.passthrough() } as const; +// DigestedOAVerifiableCredential should never allow extra root properties +export const DigestedOAVerifiableCredential = OAVerifiableCredential.extend( + DigestedOAVerifiableCredentialExtrasShape +).strict(); + +const SignedProof = DigestedProof.extend({ key: z.string(), signature: z.string() }); +const SignedOAVerifiableCredentialExtrasShape = { proof: SignedProof } as const; +// SignedOAVerifiableCredential should never allow extra root properties +export const SignedOAVerifiableCredential = OAVerifiableCredential.extend( + SignedOAVerifiableCredentialExtrasShape +).strict(); export type W3cVerifiableCredential = z.infer; // AssertStricterOrEqual is used to ensure that we have zod extended from the base type while // still being assignable to the base type. For example, if we accidentally extend and // replaced '@context' to a boolean, this would fail the assertion. -export type V4OpenAttestationDocument = AssertStricterOrEqual< +export type OAVerifiableCredential = AssertStricterOrEqual< W3cVerifiableCredential, - z.infer + z.infer >; -export type V4WrappedDocument = Override< +// export type VC = T extends OAVerifiableCredential +// ? OAVerifiableCredential +// : T extends W3cVerifiableCredential +// ? W3cVerifiableCredential +// : T; + +export type Digested = Override< T, - Pick, keyof typeof WrappedDocumentExtrasShape> + Pick, keyof typeof DigestedOAVerifiableCredentialExtrasShape> >; -export type V4SignedWrappedDocument = Override< +export type Signed = Override< T, - Pick, keyof typeof SignedWrappedDocumentExtrasShape> + Pick, keyof typeof SignedOAVerifiableCredentialExtrasShape> >; type IdentityProofType = z.infer; @@ -316,14 +326,14 @@ export type PartialDeep = T extends string | number | bigint | boolean | null [K in keyof T]?: PartialDeep; }; -export const isV4OpenAttestationDocument = (document: unknown): document is V4OpenAttestationDocument => { - return V4OpenAttestationDocument.safeParse(document).success; +export const isOAVerifiableCredential = (document: unknown): document is OAVerifiableCredential => { + return OAVerifiableCredential.safeParse(document).success; }; -export const isV4WrappedDocument = (document: unknown): document is V4WrappedDocument => { - return V4WrappedDocument.safeParse(document).success; +export const isDigestedOAVerifiableCredential = (document: unknown): document is Digested => { + return DigestedOAVerifiableCredential.safeParse(document).success; }; -export const isV4SignedWrappedDocument = (document: unknown): document is V4SignedWrappedDocument => { - return V4SignedWrappedDocument.safeParse(document).success; +export const isSignedOAVerifiableCredential = (document: unknown): document is Signed => { + return SignedOAVerifiableCredential.safeParse(document).success; }; diff --git a/src/4.0/validate.ts b/src/4.0/validate.ts index c5a1e9fc..23f8d04b 100644 --- a/src/4.0/validate.ts +++ b/src/4.0/validate.ts @@ -1,21 +1,21 @@ -import { V4WrappedDocument } from "./types"; +import { Digested } from "./types"; import { SaltNotFoundError, genTargetHash } from "./hash"; import { checkProof } from "../shared/merkle"; import { decodeSalt } from "./salt"; -export const validateDigest = (document: T): document is T => { +export const validateDigest = (document: T): document is T => { if (!document.proof) { return false; } // Remove proof from document // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars - const { proof, ...documentWithoutProof } = document; + const { proof, ...vcWithoutProof } = document; const decodedSalts = decodeSalt(document.proof.salts); // Checks target hash try { - const digest = genTargetHash(documentWithoutProof, decodedSalts, document.proof.privacy.obfuscated); + const digest = genTargetHash(vcWithoutProof, decodedSalts, document.proof.privacy.obfuscated); const targetHash = document.proof.targetHash; if (digest !== targetHash) return false; diff --git a/src/index.ts b/src/index.ts index 7cd48c61..c4005a89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,10 +24,7 @@ import { digestCredential as digestCredentialV3 } from "./3.0/digest"; import { obfuscateVerifiableCredential as obfuscateVerifiableCredentialV3 } from "./3.0/obfuscate"; import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "./__generated__/schema.3.0"; -import { validateDigest as verifyV4 } from "./4.0/validate"; -import { ObfuscateVerifiableCredentialResult, obfuscateVC as obfuscateVerifiableCredentialV4 } from "./4.0/obfuscate"; -import { v4Diagnose } from "./4.0/diagnose"; -import { V4WrappedDocument, isV4WrappedDocument } from "./4.0/types"; +import * as v4 from "./4.0/exports"; export function wrapDocument( data: T, @@ -68,9 +65,6 @@ export const validateSchema = (document: WrappedDocument): boolean => { return validate(document, getSchema(SchemaId.v2)).length === 0; else if (utils.isWrappedV3Document(document) || document?.version === SchemaId.v3) return validate(document, getSchema(SchemaId.v3)).length === 0; - else if (isV4WrappedDocument(document)) { - return v4Diagnose({ document, kind: "wrapped", debug: false, mode: "strict" }).length === 0; - } return validate(document, getSchema(`${document?.version || SchemaId.v2}`)).length === 0; }; @@ -78,11 +72,15 @@ export const validateSchema = (document: WrappedDocument): boolean => { export function verifySignature>(document: T) { if (utils.isWrappedV2Document(document)) return verify(document); else if (utils.isWrappedV3Document(document)) return verifyV3(document); - else if (isV4WrappedDocument(document)) return verifyV4(document); - throw new Error("Unsupported document type: Only OpenAttestation v2, v3 or v4 documents can be signature verified"); + throw new Error( + "Unsupported document type: Only OpenAttestation v2 or v3 documents can be signature verified with this function" + ); } +/** + * @deprecated will be removed in the next major release in favour of OpenAttestation v4.0 (more info: https://github.com/Open-Attestation/open-attestation/tree/beta) + */ export function digest(document: OpenAttestationDocumentV3, salts: v3.Salt[], obfuscatedData: string[]): string { if (utils.isRawV3Document(document)) return digestCredentialV3(document, salts, obfuscatedData); throw new Error( @@ -90,18 +88,18 @@ export function digest(document: OpenAttestationDocumentV3, salts: v3.Salt[], ob ); } -type ObfuscateReturn = T extends V4WrappedDocument ? ObfuscateVerifiableCredentialResult : T; export function obfuscate>( document: T, fields: string[] | string -): ObfuscateReturn { - if (utils.isWrappedV2Document(document)) return obfuscateDocumentV2(document, fields) as ObfuscateReturn; - else if (utils.isWrappedV3Document(document)) - return obfuscateVerifiableCredentialV3(document, fields) as ObfuscateReturn; - else if (isV4WrappedDocument(document)) - return obfuscateVerifiableCredentialV4(document, fields) as ObfuscateReturn; - - throw new Error("Unsupported document type: Only OpenAttestation v2, v3 or v4 documents can be obfuscated"); +): T { + if (utils.isWrappedV2Document(document)) return obfuscateDocumentV2(document, fields) as T; + else if (utils.isWrappedV3Document(document)) { + return obfuscateVerifiableCredentialV3(document, fields) as T; + } + + throw new Error( + "Unsupported document type: Only OpenAttestation v2 or v3 documents can be obfuscated with this function" + ); } export async function signDocument( @@ -120,7 +118,9 @@ export async function signDocument; - throw new Error("Unsupported document type: Only OpenAttestation v2 or v3 documents can be signed"); + throw new Error( + "Unsupported document type: Only OpenAttestation v2 or v3 documents can be signed with this function" + ); } export { digestDocument } from "./2.0/digest"; @@ -134,4 +134,4 @@ export * from "./shared/signer"; export { getData } from "./shared/utils"; // keep it to avoid breaking change, moved from privacy to utils export { v2 }; export { v3 }; -export * as v4 from "./4.0/exports"; +export { v4 }; diff --git a/src/shared/@types/document.ts b/src/shared/@types/document.ts index d3dd1dbf..97c8b08f 100644 --- a/src/shared/@types/document.ts +++ b/src/shared/@types/document.ts @@ -11,22 +11,17 @@ import { } from "../../3.0/types"; import { Literal, Static, String } from "runtypes"; import { isHexString } from "@ethersproject/bytes"; -import { V4OpenAttestationDocument, V4SignedWrappedDocument, V4WrappedDocument } from "../../4.0/types"; -export type OpenAttestationDocument = OpenAttestationDocumentV2 | OpenAttestationDocumentV3 | V4OpenAttestationDocument; +export type OpenAttestationDocument = OpenAttestationDocumentV2 | OpenAttestationDocumentV3; export type WrappedDocument = T extends OpenAttestationDocumentV2 ? WrappedDocumentV2 : T extends OpenAttestationDocumentV3 ? WrappedDocumentV3 - : T extends V4WrappedDocument - ? T : unknown; export type SignedWrappedDocument = T extends OpenAttestationDocumentV2 ? SignedWrappedDocumentV2 : T extends OpenAttestationDocumentV3 ? SignedWrappedDocumentV3 - : T extends V4SignedWrappedDocument - ? T : unknown; export enum SchemaId { diff --git a/src/shared/utils/guard.ts b/src/shared/utils/guard.ts index cfb52b3c..e8beb9cf 100644 --- a/src/shared/utils/guard.ts +++ b/src/shared/utils/guard.ts @@ -83,7 +83,7 @@ export const isSignedWrappedV3Document = ( }; export { - isV4OpenAttestationDocument as isRawV4Document, - isV4WrappedDocument as isWrappedV4Document, - isV4SignedWrappedDocument as isSignedWrappedV4Document, + isOAVerifiableCredential, + isDigestedOAVerifiableCredential, + isSignedOAVerifiableCredential, } from "../../4.0/types"; diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 25a5d8c9..67e7ea20 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -2,14 +2,9 @@ import { ErrorObject } from "ajv"; import * as v2 from "../../__generated__/schema.2.0"; import { getData } from "../../2.0/utils"; -import { WrappedDocument as WrappedDocumentV2 } from "../../2.0/types"; -import { OpenAttestationDocument as OpenAttestationDocumentV2 } from "../../__generated__/schema.2.0"; import * as v3 from "../../__generated__/schema.3.0"; -import { WrappedDocument as WrappedDocumentV3 } from "../../3.0/types"; -import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "../../__generated__/schema.3.0"; -import { V4WrappedDocument } from "../../4.0/types"; import { ContextUrl } from "../../4.0/context"; import { OpenAttestationDocument, WrappedDocument, SchemaId } from "../@types/document"; @@ -18,8 +13,9 @@ import { isWrappedV2Document, isRawV3Document, isWrappedV3Document, - isWrappedV4Document, - isRawV4Document, + isOAVerifiableCredential, + isDigestedOAVerifiableCredential, + isSignedOAVerifiableCredential, } from "./guard"; import { Version } from "./diagnose"; @@ -31,7 +27,7 @@ export function getIssuerAddress(document: any): any { return document.openAttestationMetadata.proof.value; } // TODO: OA v4 proof schema not updated to support document store issuance yet - // else if (isWrappedV4Document(document)) { + // else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { // return document.proof.? // } throw new Error( @@ -39,10 +35,11 @@ export function getIssuerAddress(document: any): any { ); } -export const getMerkleRoot = (document: any): string => { +export const getMerkleRoot = (document: unknown): string => { if (isWrappedV2Document(document)) return document.signature.merkleRoot; else if (isWrappedV3Document(document)) return document.proof.merkleRoot; - else if (isWrappedV4Document(document)) return document.proof.merkleRoot; + else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) + return document.proof.merkleRoot; throw new Error( "Unsupported document type: Only can retrieve merkle root from wrapped OpenAttestation v2, v3 & v4 documents." @@ -52,7 +49,8 @@ export const getMerkleRoot = (document: any): string => { export const getTargetHash = (document: any): string => { if (isWrappedV2Document(document)) return document.signature.targetHash; else if (isWrappedV3Document(document)) return document.proof.targetHash; - else if (isWrappedV4Document(document)) return document.proof.targetHash; + else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) + return document.proof.targetHash; throw new Error( "Unsupported document type: Only can retrieve target hash from wrapped OpenAttestation v2, v3 & v4 documents." @@ -70,7 +68,7 @@ export const getTemplateURL = (document: any): string | undefined => { else return document.$template?.url; } else if (isRawV3Document(document) || isWrappedV3Document(document)) { return document.openAttestationMetadata.template?.url; - } else if (isRawV4Document(document) || isWrappedV4Document(document)) { + } else if (isOAVerifiableCredential(document) || isDigestedOAVerifiableCredential(document)) { return document.renderMethod && document.renderMethod[0].id; } @@ -80,7 +78,7 @@ export const getTemplateURL = (document: any): string | undefined => { }; export const getDocumentData = (document: WrappedDocument): OpenAttestationDocument => { - if (isWrappedV3Document(document) || isWrappedV4Document(document)) { + if (isWrappedV3Document(document)) { const omit = (keys: any, obj: any): any => Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k))); return omit(["proof"], document); @@ -100,7 +98,7 @@ export const isTransferableAsset = (document: any): boolean => { ); }; -export const isDocumentRevokable = (document: any): boolean => { +export const isDocumentRevokable = (document: unknown): boolean => { if (isTransferableAsset(document)) { return false; } else if (isWrappedV2Document(document)) { @@ -123,7 +121,7 @@ export const isDocumentRevokable = (document: any): boolean => { !!document.openAttestationMetadata.proof.value; return isDocumentStoreRevokableV3 || isDidRevokableV3; - } else if (isWrappedV4Document(document)) { + } else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { if (typeof document.issuer === "string" || !document.credentialStatus) return false; const isDidRevokableV4 = document.issuer.identityProof?.identityProofType === "DNS-DID" @@ -160,17 +158,12 @@ export const isSchemaValidationError = (error: any): error is SchemaValidationEr // make it available for consumers export { keccak256 } from "js-sha3"; -export const isObfuscated = ( - document: - | WrappedDocumentV2 - | WrappedDocumentV3 - | V4WrappedDocument -): boolean => { +export const isObfuscated = (document: unknown): boolean => { if (isWrappedV2Document(document)) { return !!document.privacy?.obfuscatedData?.length; } else if (isWrappedV3Document(document)) { return !!document.proof.privacy.obfuscated.length; - } else if (isWrappedV4Document(document)) { + } else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { return !!document.proof.privacy.obfuscated.length; } @@ -179,17 +172,12 @@ export const isObfuscated = ( ); }; -export const getObfuscatedData = ( - document: - | WrappedDocumentV2 - | WrappedDocumentV3 - | V4WrappedDocument -): string[] => { +export const getObfuscatedData = (document: unknown): string[] => { if (isWrappedV2Document(document)) { return document.privacy?.obfuscatedData || []; } else if (isWrappedV3Document(document)) { return document.proof.privacy.obfuscated || []; - } else if (isWrappedV4Document(document)) { + } else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { return document.proof.privacy.obfuscated || []; } From 0315206ef1f5a1b4874c4321df9da801c493446d Mon Sep 17 00:00:00 2001 From: Kyle Huang Junyuan Date: Tue, 3 Dec 2024 13:59:17 +0800 Subject: [PATCH 4/6] refactor: use vc terminology --- src/4.0/__tests__/digest.test.ts | 34 ++-- src/4.0/__tests__/documentBuilder.test.ts | 80 ++++---- src/4.0/__tests__/e2e.test.ts | 186 ++++++++++-------- src/4.0/__tests__/guard.test.ts | 94 ++++----- src/4.0/__tests__/hash.test.ts | 52 ++--- src/4.0/__tests__/obfuscate.test.ts | 96 ++++----- src/4.0/__tests__/salt.test.ts | 40 ++-- src/4.0/__tests__/sign.test.ts | 34 ++-- src/4.0/__tests__/validate.test.ts | 71 ++++--- src/4.0/digest.ts | 49 +++-- src/4.0/exports/builder.ts | 2 +- src/4.0/exports/digest.ts | 2 +- src/4.0/exports/sign.ts | 2 +- src/4.0/fixtures.ts | 22 +-- src/4.0/hash.ts | 14 +- .../v4-digested-oa-vc.schema.json | 2 +- .../__generated__/v4-oa-vc.schema.json | 2 +- .../__generated__/v4-signed-oa-vc.schema.json | 2 +- src/4.0/obfuscate.ts | 10 +- src/4.0/sign.ts | 6 +- src/4.0/types.ts | 14 +- src/4.0/validate.ts | 18 +- src/4.0/{documentBuilder.ts => vcBuilder.ts} | 0 ...-did.json => digested-batched-vc-did.json} | 0 ...id-oscp.json => digested-vc-did-oscp.json} | 0 ...document-did.json => digested-vc-did.json} | 0 ...ments-did.json => raw-batched-vc-did.json} | 0 ...ent-did-oscp.json => raw-vc-did-oscp.json} | 0 ...{raw-document-did.json => raw-vc-did.json} | 0 ...ts-did.json => signed-batched-vc-did.json} | 0 ...ted.json => signed-vc-did-obfuscated.json} | 0 ...-did-oscp.json => signed-vc-did-oscp.json} | 0 ...d-document-did.json => signed-vc-did.json} | 0 33 files changed, 425 insertions(+), 407 deletions(-) rename src/4.0/{documentBuilder.ts => vcBuilder.ts} (100%) rename test/fixtures/v4/__generated__/{batched-wrapped-documents-did.json => digested-batched-vc-did.json} (100%) rename test/fixtures/v4/__generated__/{wrapped-document-did-oscp.json => digested-vc-did-oscp.json} (100%) rename test/fixtures/v4/__generated__/{wrapped-document-did.json => digested-vc-did.json} (100%) rename test/fixtures/v4/__generated__/{batched-raw-documents-did.json => raw-batched-vc-did.json} (100%) rename test/fixtures/v4/__generated__/{raw-document-did-oscp.json => raw-vc-did-oscp.json} (100%) rename test/fixtures/v4/__generated__/{raw-document-did.json => raw-vc-did.json} (100%) rename test/fixtures/v4/__generated__/{batched-signed-wrapped-documents-did.json => signed-batched-vc-did.json} (100%) rename test/fixtures/v4/__generated__/{signed-wrapped-document-did-obfuscated.json => signed-vc-did-obfuscated.json} (100%) rename test/fixtures/v4/__generated__/{signed-wrapped-document-did-oscp.json => signed-vc-did-oscp.json} (100%) rename test/fixtures/v4/__generated__/{signed-wrapped-document-did.json => signed-vc-did.json} (100%) diff --git a/src/4.0/__tests__/digest.test.ts b/src/4.0/__tests__/digest.test.ts index 688e31dd..582fad0e 100644 --- a/src/4.0/__tests__/digest.test.ts +++ b/src/4.0/__tests__/digest.test.ts @@ -2,8 +2,8 @@ import { OAVerifiableCredential, DigestedOAVerifiableCredential, W3cVerifiableCr import { digestVc } from "../digest"; describe("V4.0 digest", () => { - test("given a valid v4 document, should wrap correctly", async () => { - const wrapped = await digestVc({ + test("given a valid v4 VC, should digest correctly", async () => { + const digested = await digestVc({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -21,7 +21,7 @@ describe("V4.0 digest", () => { identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, }, }); - const parsedResults = DigestedOAVerifiableCredential.safeParse(wrapped); + const parsedResults = DigestedOAVerifiableCredential.safeParse(digested); if (!parsedResults.success) { throw new Error("Parsing failed"); } @@ -35,7 +35,7 @@ describe("V4.0 digest", () => { expect(proof.type).toBe("OpenAttestationHashProof2018"); }); - test("given a document with explicit v4 contexts, but does not conform to the V4 document schema, should throw", async () => { + test("given a VC with explicit v4 contexts, but does not conform to the V4 VC schema, should throw", async () => { await expect( digestVc({ "@context": [ @@ -56,7 +56,7 @@ describe("V4.0 digest", () => { } as OAVerifiableCredential["issuer"], }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Input document does not conform to Open Attestation v4.0 Data Model: + "Input VC does not conform to Open Attestation v4.0 Data Model: { "_errors": [], "issuer": { @@ -71,7 +71,7 @@ describe("V4.0 digest", () => { `); }); - test("given a valid v4 document but has an extra field, should throw", async () => { + test("given a valid v4 VC but has an extra field, should throw", async () => { await expect( digestVc({ "@context": [ @@ -96,7 +96,7 @@ describe("V4.0 digest", () => { extraField: "extra", } as OAVerifiableCredential) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Input document does not conform to Open Attestation v4.0 Data Model: + "Input VC does not conform to Open Attestation v4.0 Data Model: { "_errors": [ "Unrecognized key(s) in object: 'extraField'" @@ -105,7 +105,7 @@ describe("V4.0 digest", () => { `); }); - test("given a generic w3c vc, should wrap with context and type corrected", async () => { + test("given a generic W3C VC, should digest with context and type corrected", async () => { const genericW3cVc: W3cVerifiableCredential = { "@context": ["https://www.w3.org/ns/credentials/v2"], type: ["VerifiableCredential"], @@ -118,17 +118,17 @@ describe("V4.0 digest", () => { id: "https://example.com/issuer/123", }, }; - const wrapped = await digestVc(genericW3cVc as unknown as OAVerifiableCredential); + const digested = await digestVc(genericW3cVc as unknown as OAVerifiableCredential); const parsedResults = DigestedOAVerifiableCredential.pick({ "@context": true, type: true }) .passthrough() - .safeParse(wrapped); + .safeParse(digested); expect(parsedResults.success).toBe(true); - expect(wrapped.proof.merkleRoot.length).toBe(64); - expect(wrapped.proof.privacy.obfuscated).toEqual([]); - expect(wrapped.proof.proofPurpose).toBe("assertionMethod"); - expect(wrapped.proof.proofs).toEqual([]); - expect(wrapped.proof.salts.length).toBeGreaterThan(0); - expect(wrapped.proof.targetHash.length).toBe(64); - expect(wrapped.proof.type).toBe("OpenAttestationHashProof2018"); + expect(digested.proof.merkleRoot.length).toBe(64); + expect(digested.proof.privacy.obfuscated).toEqual([]); + expect(digested.proof.proofPurpose).toBe("assertionMethod"); + expect(digested.proof.proofs).toEqual([]); + expect(digested.proof.salts.length).toBeGreaterThan(0); + expect(digested.proof.targetHash.length).toBe(64); + expect(digested.proof.type).toBe("OpenAttestationHashProof2018"); }); }); diff --git a/src/4.0/__tests__/documentBuilder.test.ts b/src/4.0/__tests__/documentBuilder.test.ts index 0c2868cc..2de69937 100644 --- a/src/4.0/__tests__/documentBuilder.test.ts +++ b/src/4.0/__tests__/documentBuilder.test.ts @@ -1,11 +1,11 @@ import { validateDigest } from "../validate"; -import { VcBuilder, VcBuilderErrors } from "../documentBuilder"; +import { VcBuilder, VcBuilderErrors } from "../vcBuilder"; import { isSignedOAVerifiableCredential, isDigestedOAVerifiableCredential, signVc } from "../exports"; import { SAMPLE_SIGNING_KEYS } from "../fixtures"; -describe(`V4.0 DocumentBuilder`, () => { - describe("given a single document", () => { - const document = new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma" }) +describe(`V4.0 VcBuilder`, () => { + describe("given a single VC", () => { + const vc = new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma" }) .embeddedRenderer({ rendererUrl: "https://example.com", templateName: "example", @@ -19,8 +19,8 @@ describe(`V4.0 DocumentBuilder`, () => { issuerName: "Example University", }); - test("given sign and wrap is called, return a single signed document", async () => { - const signed = await document.sign({ signer: SAMPLE_SIGNING_KEYS }); + test("given sign is called, return a single signed VC", async () => { + const signed = await vc.sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed.issuer).toMatchInlineSnapshot(` { "id": "did:example:123", @@ -56,9 +56,9 @@ describe(`V4.0 DocumentBuilder`, () => { expect(validateDigest(signed)).toBe(true); }); - test("given wrap is called, return a wrapped document", async () => { - const wrapped = await document.digest(); - expect(wrapped.issuer).toMatchInlineSnapshot(` + test("given digest is called, return a digested VC", async () => { + const digested = await vc.digest(); + expect(digested.issuer).toMatchInlineSnapshot(` { "id": "did:example:123", "identityProof": { @@ -69,12 +69,12 @@ describe(`V4.0 DocumentBuilder`, () => { "type": "OpenAttestationIssuer", } `); - expect(wrapped.credentialSubject).toMatchInlineSnapshot(` + expect(digested.credentialSubject).toMatchInlineSnapshot(` { "name": "John Doe", } `); - expect(wrapped.renderMethod).toMatchInlineSnapshot(` + expect(digested.renderMethod).toMatchInlineSnapshot(` [ { "id": "https://example.com", @@ -83,19 +83,19 @@ describe(`V4.0 DocumentBuilder`, () => { }, ] `); - expect(wrapped.credentialStatus).toMatchInlineSnapshot(` + expect(digested.credentialStatus).toMatchInlineSnapshot(` { "id": "https://oscp.example.com", "type": "OpenAttestationOcspResponder", } `); - expect(isDigestedOAVerifiableCredential(wrapped)).toBe(true); - expect(isSignedOAVerifiableCredential(wrapped)).toBe(false); + expect(isDigestedOAVerifiableCredential(digested)).toBe(true); + expect(isSignedOAVerifiableCredential(digested)).toBe(false); }); }); - describe("given multiple documents", () => { - const document = new VcBuilder([ + describe("given multiple VCs", () => { + const vc = new VcBuilder([ { credentialSubject: { name: "John Doe" }, name: "Diploma" }, { credentialSubject: { name: "Jane Foster" }, name: "Degree" }, ]) @@ -111,7 +111,7 @@ describe(`V4.0 DocumentBuilder`, () => { }); test("given sign is called, return a list of signed VCs", async () => { - const signed = await document.sign({ signer: SAMPLE_SIGNING_KEYS }); + const signed = await vc.sign({ signer: SAMPLE_SIGNING_KEYS }); expect(signed[0].issuer).toMatchInlineSnapshot(` { "id": "did:example:123", @@ -171,9 +171,9 @@ describe(`V4.0 DocumentBuilder`, () => { expect(validateDigest(signed[1])).toBe(true); }); - test("given wrap is called, return a list of wrapped document", async () => { - const wrapped = await document.digest(); - expect(wrapped[0].issuer).toMatchInlineSnapshot(` + test("given digest is called, return a list of digested VCs", async () => { + const digested = await vc.digest(); + expect(digested[0].issuer).toMatchInlineSnapshot(` { "id": "did:example:123", "identityProof": { @@ -184,12 +184,12 @@ describe(`V4.0 DocumentBuilder`, () => { "type": "OpenAttestationIssuer", } `); - expect(wrapped[0].credentialSubject).toMatchInlineSnapshot(` + expect(digested[0].credentialSubject).toMatchInlineSnapshot(` { "name": "John Doe", } `); - expect(wrapped[0].renderMethod).toMatchInlineSnapshot(` + expect(digested[0].renderMethod).toMatchInlineSnapshot(` [ { "id": "https://example.com", @@ -198,10 +198,10 @@ describe(`V4.0 DocumentBuilder`, () => { }, ] `); - expect(isDigestedOAVerifiableCredential(wrapped[0])).toBe(true); - expect(isSignedOAVerifiableCredential(wrapped[0])).toBe(false); + expect(isDigestedOAVerifiableCredential(digested[0])).toBe(true); + expect(isSignedOAVerifiableCredential(digested[0])).toBe(false); - expect(wrapped[1].issuer).toMatchInlineSnapshot(` + expect(digested[1].issuer).toMatchInlineSnapshot(` { "id": "did:example:123", "identityProof": { @@ -212,12 +212,12 @@ describe(`V4.0 DocumentBuilder`, () => { "type": "OpenAttestationIssuer", } `); - expect(wrapped[1].credentialSubject).toMatchInlineSnapshot(` + expect(digested[1].credentialSubject).toMatchInlineSnapshot(` { "name": "Jane Foster", } `); - expect(wrapped[1].renderMethod).toMatchInlineSnapshot(` + expect(digested[1].renderMethod).toMatchInlineSnapshot(` [ { "id": "https://example.com", @@ -226,12 +226,12 @@ describe(`V4.0 DocumentBuilder`, () => { }, ] `); - expect(isDigestedOAVerifiableCredential(wrapped[1])).toBe(true); - expect(isSignedOAVerifiableCredential(wrapped[1])).toBe(false); + expect(isDigestedOAVerifiableCredential(digested[1])).toBe(true); + expect(isSignedOAVerifiableCredential(digested[1])).toBe(false); }); }); - test("given additional properties in constructor payload, should not be added into the document", async () => { + test("given additional properties in constructor payload, should not be added into the VC", async () => { const signed = await new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", @@ -252,7 +252,7 @@ describe(`V4.0 DocumentBuilder`, () => { expect(signed).not.toHaveProperty("anotherProperty"); }); - test("given svg rendering method, should be added into the document", async () => { + test("given svg rendering method, should be added into the VC", async () => { const signed = await new VcBuilder({ credentialSubject: { name: "John Doe", @@ -288,7 +288,7 @@ describe(`V4.0 DocumentBuilder`, () => { `); }); - test("given no rendering method, should reflect in the output document", async () => { + test("given no rendering method, should reflect in the output VC", async () => { const signed = await new VcBuilder({ credentialSubject: { name: "John Doe", @@ -314,7 +314,7 @@ describe(`V4.0 DocumentBuilder`, () => { expect(signed.renderMethod).toMatchInlineSnapshot(`undefined`); }); - test("given attachment is added, should be added into the document", async () => { + test("given attachment is added, should be added into the VC", async () => { const signed = await new VcBuilder({ credentialSubject: { name: "John Doe", @@ -351,7 +351,7 @@ describe(`V4.0 DocumentBuilder`, () => { `); }); - test("given revocation store revocation is added, should be added into credential status of the document", async () => { + test("given revocation store revocation is added, should be added into credential status of the VC", async () => { const signed = await new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", @@ -385,7 +385,7 @@ describe(`V4.0 DocumentBuilder`, () => { `); }); - test("given digest is first called, should not be able to sign the digested document with the standalone sign fn", async () => { + test("given digest is first called, should not be able to sign the digested VC with the standalone sign fn", async () => { const digested = await new VcBuilder({ credentialSubject: { name: "John Doe" }, name: "Diploma", @@ -423,7 +423,7 @@ describe(`V4.0 DocumentBuilder`, () => { name: "Diploma", }); - const documentWithRenderMethod = builder.embeddedRenderer({ + const vcWithRenderMethod = builder.embeddedRenderer({ rendererUrl: "https://example.com", templateName: "example", }); @@ -435,19 +435,19 @@ describe(`V4.0 DocumentBuilder`, () => { }) ).toThrowError(VcBuilderErrors.ShouldNotModifyAfterSettingError); - const documentWithNoRevocation = documentWithRenderMethod.noRevocation(); - expect(() => documentWithRenderMethod.oscpRevocation({ oscpUrl: "https://oscp.example.com" })).toThrowError( + const vcWithNoRevocation = vcWithRenderMethod.noRevocation(); + expect(() => vcWithRenderMethod.oscpRevocation({ oscpUrl: "https://oscp.example.com" })).toThrowError( VcBuilderErrors.ShouldNotModifyAfterSettingError ); - documentWithNoRevocation.dnsTxtIssuance({ + vcWithNoRevocation.dnsTxtIssuance({ identityProofDomain: "example.com", issuerId: "did:example:123", issuerName: "Example University", }); expect(() => - documentWithNoRevocation.dnsTxtIssuance({ + vcWithNoRevocation.dnsTxtIssuance({ identityProofDomain: "another.com", issuerId: "did:example:123", issuerName: "Example University", diff --git a/src/4.0/__tests__/e2e.test.ts b/src/4.0/__tests__/e2e.test.ts index c7b06422..e002a6e5 100644 --- a/src/4.0/__tests__/e2e.test.ts +++ b/src/4.0/__tests__/e2e.test.ts @@ -7,28 +7,28 @@ import { isDigestedOAVerifiableCredential, } from "../exports"; import type { OAVerifiableCredential } from "../exports"; -import { RAW_DOCUMENT_DID, SIGNED_WRAPPED_DOCUMENT_DID, WRAPPED_DOCUMENT_DID } from "../fixtures"; +import { RAW_VC_DID, SIGNED_VC_DID, DIGESTED_VC_DID } from "../fixtures"; -const DOCUMENT_ONE = { - ...RAW_DOCUMENT_DID, +const VC_ONE = { + ...RAW_VC_DID, credentialSubject: { - ...RAW_DOCUMENT_DID.credentialSubject, + ...RAW_VC_DID.credentialSubject, key1: "test", }, } satisfies OAVerifiableCredential; -const DOCUMENT_TWO = { - ...RAW_DOCUMENT_DID, +const VC_TWO = { + ...RAW_VC_DID, credentialSubject: { - ...RAW_DOCUMENT_DID.credentialSubject, + ...RAW_VC_DID.credentialSubject, key1: "hello", key2: "item2", }, } satisfies OAVerifiableCredential; -const DOCUMENT_THREE = { - ...RAW_DOCUMENT_DID, +const VC_THREE = { + ...RAW_VC_DID, credentialSubject: { - ...RAW_DOCUMENT_DID.credentialSubject, + ...RAW_VC_DID.credentialSubject, key1: "item1", key2: "true", key3: 3.14159, @@ -36,24 +36,24 @@ const DOCUMENT_THREE = { }, } satisfies OAVerifiableCredential; -const DOCUMENT_FOUR = { - ...RAW_DOCUMENT_DID, +const VC_FOUR = { + ...RAW_VC_DID, credentialSubject: { - ...RAW_DOCUMENT_DID.credentialSubject, + ...RAW_VC_DID.credentialSubject, key1: "item2", }, }; -const DATUM = [DOCUMENT_ONE, DOCUMENT_TWO, DOCUMENT_THREE, DOCUMENT_FOUR] satisfies OAVerifiableCredential[]; +const DATUM = [VC_ONE, VC_TWO, VC_THREE, VC_FOUR] satisfies OAVerifiableCredential[]; describe("V4.0 E2E Test Scenarios", () => { - describe("Issuing a single document", () => { + describe("Issuing a single VC", () => { test("fails for missing data", async () => { const missingData = { - ...omit(cloneDeep(DOCUMENT_ONE), "issuer"), + ...omit(cloneDeep(VC_ONE), "issuer"), }; await expect(digestVc(missingData as unknown as OAVerifiableCredential)).rejects .toThrowErrorMatchingInlineSnapshot(` - "Input document does not conform to Open Attestation v4.0 Data Model: + "Input VC does not conform to Open Attestation v4.0 Data Model: { "_errors": [], "issuer": { @@ -65,72 +65,100 @@ describe("V4.0 E2E Test Scenarios", () => { `); }); - test("creates a wrapped document", async () => { - const wrappedDocument = await digestVc(RAW_DOCUMENT_DID); - expect(wrappedDocument["@context"]).toEqual([ + test("creates a digested VC", async () => { + const digested = await digestVc(RAW_VC_DID); + expect(digested["@context"]).toEqual([ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", ]); - expect(wrappedDocument.type).toEqual(["VerifiableCredential", "OpenAttestationCredential"]); - expect(wrappedDocument.proof.type).toBe("OpenAttestationHashProof2018"); - expect(wrappedDocument.proof.targetHash).toBeDefined(); - expect(wrappedDocument.proof.merkleRoot).toBeDefined(); - expect(wrappedDocument.proof.proofs).toEqual([]); - expect(wrappedDocument.proof.merkleRoot).toBe(wrappedDocument.proof.targetHash); + expect(digested.type).toEqual(["VerifiableCredential", "OpenAttestationCredential"]); + expect(digested.proof.type).toBe("OpenAttestationHashProof2018"); + expect(digested.proof.targetHash).toBeDefined(); + expect(digested.proof.merkleRoot).toBeDefined(); + expect(digested.proof.proofs).toEqual([]); + expect(digested.proof.merkleRoot).toBe(digested.proof.targetHash); }); - test("checks that document is wrapped correctly", async () => { - const wrappedDocument = await digestVc(DOCUMENT_ONE); - const verified = validateDigest(wrappedDocument); + test("checks that VC is digested correctly", async () => { + const digested = await digestVc(VC_ONE); + const verified = validateDigest(digested); expect(verified).toBe(true); }); - test("checks that document conforms to the schema", async () => { - const wrappedDocument = await digestVc(DOCUMENT_ONE); - expect(isDigestedOAVerifiableCredential(wrappedDocument)).toBe(true); + test("checks that VC conforms to the schema", async () => { + const digested = await digestVc(VC_ONE); + expect(isDigestedOAVerifiableCredential(digested)).toBe(true); }); test("does not allow for the same merkle root to be generated", async () => { // This test takes some time to run, so we set the timeout to 14s - const wrappedDocument = await digestVc(DOCUMENT_ONE); - const newDocument = await digestVc(DOCUMENT_ONE); - expect(wrappedDocument.proof.merkleRoot).not.toBe(newDocument.proof.merkleRoot); + const digested = await digestVc(VC_ONE); + const newDigested = await digestVc(VC_ONE); + expect(digested.proof.merkleRoot).not.toBe(newDigested.proof.merkleRoot); }, 14000); test("obfuscate data correctly", async () => { - const newDocument = await digestVc(DOCUMENT_THREE); - expect(newDocument.credentialSubject.key2).toBeDefined(); - const obfuscatedDocument = obfuscateOAVerifiableCredential(newDocument, ["credentialSubject.key2"]); - expect(validateDigest(obfuscatedDocument)).toBe(true); - expect(isDigestedOAVerifiableCredential(obfuscatedDocument)).toBe(true); - expect(obfuscatedDocument.credentialSubject.key2).toBeUndefined(); + const newDigested = await digestVc(VC_THREE); + expect(newDigested.credentialSubject.key2).toBeDefined(); + const obfuscatedVc = obfuscateOAVerifiableCredential(newDigested, ["credentialSubject.key2"]); + expect(validateDigest(obfuscatedVc)).toBe(true); + expect(isDigestedOAVerifiableCredential(obfuscatedVc)).toBe(true); + expect(obfuscatedVc.credentialSubject.key2).toBeUndefined(); }); test("obfuscate data transistively", async () => { - const newDocument = await digestVc(DOCUMENT_THREE); - const intermediateDocument = obfuscateOAVerifiableCredential(newDocument, ["credentialSubject.key2"]); - const obfuscatedDocument = obfuscateOAVerifiableCredential(intermediateDocument, ["credentialSubject.key3"]); + const newDigested = await digestVc(VC_THREE); + const intermediateVc = obfuscateOAVerifiableCredential(newDigested, ["credentialSubject.key2"]); + const obfuscatedVc = obfuscateOAVerifiableCredential(intermediateVc, ["credentialSubject.key3"]); expect( - obfuscateOAVerifiableCredential(newDocument, ["credentialSubject.key2", "credentialSubject.key3"]) - ).toEqual(obfuscatedDocument); + obfuscateOAVerifiableCredential(newDigested, ["credentialSubject.key2", "credentialSubject.key3"]) + ).toEqual(obfuscatedVc); }); - describe("Issuing a batch of documents", () => { - test("fails if there is a malformed document", async () => { + describe("Issuing a batch of VCs", () => { + test("fails if there is a malformed VC", async () => { const malformedDatum = [ ...DATUM, { laurent: "task force, assemble!!", } as unknown as OAVerifiableCredential, ]; - await expect(digestVcs(malformedDatum)).rejects.toThrow( - "Input document does not conform to Verifiable Credentials" - ); + await expect(digestVcs(malformedDatum)).rejects.toThrowErrorMatchingInlineSnapshot(` + "Input VC does not conform to Verifiable Credentials v2.0 Data Model: + { + "_errors": [], + "@context": { + "_errors": [ + "Required", + "Required", + "Required" + ] + }, + "issuer": { + "_errors": [ + "Required", + "Required" + ] + }, + "type": { + "_errors": [ + "Required", + "Required" + ] + }, + "credentialSubject": { + "_errors": [ + "Required", + "Required" + ] + } + }" + `); }); - test("creates a batch of documents if all are in the right format", async () => { - const wrappedDocuments = await digestVcs(DATUM); - wrappedDocuments.forEach((doc, i: number) => { + test("creates a batch of VC if all are in the right format", async () => { + const digestedVcs = await digestVcs(DATUM); + digestedVcs.forEach((doc, i: number) => { expect(doc.type).toEqual(["VerifiableCredential", "OpenAttestationCredential"]); expect(doc.proof.type).toBe("OpenAttestationHashProof2018"); expect(doc.proof.type).toBe("OpenAttestationHashProof2018"); @@ -141,15 +169,15 @@ describe("V4.0 E2E Test Scenarios", () => { }); }); - test("checks that documents are wrapped correctly", async () => { - const wrappedDocuments = await digestVcs(DATUM); - const verified = wrappedDocuments.reduce((prev, curr) => validateDigest(curr) && prev, true); + test("checks that VCs are digested correctly", async () => { + const digestedVcs = await digestVcs(DATUM); + const verified = digestedVcs.reduce((prev, curr) => validateDigest(curr) && prev, true); expect(verified).toBe(true); }); - test("checks that documents conforms to the schema", async () => { - const wrappedDocuments = await digestVcs(DATUM); - const validatedSchema = wrappedDocuments.reduce( + test("checks that VCs conforms to the schema", async () => { + const digestedVcs = await digestVcs(DATUM); + const validatedSchema = digestedVcs.reduce( (prev: boolean, curr: any) => isDigestedOAVerifiableCredential(curr) && prev, true ); @@ -157,27 +185,27 @@ describe("V4.0 E2E Test Scenarios", () => { }); test("does not allow for same merkle root to be generated", async () => { - const wrappedDocuments = await digestVcs(DATUM); - const newWrappedDocuments = await digestVcs(DATUM); - expect(wrappedDocuments[0].proof.merkleRoot).not.toBe(newWrappedDocuments[0].proof.merkleRoot); + const digestedVcs = await digestVcs(DATUM); + const newDigestedVcs = await digestVcs(DATUM); + expect(digestedVcs[0].proof.merkleRoot).not.toBe(newDigestedVcs[0].proof.merkleRoot); }); }); }); describe("validate schema", () => { - test("should return true when document is a valid wrapped v4 document and identityProof is DNS-DID", () => { - expect(isDigestedOAVerifiableCredential(WRAPPED_DOCUMENT_DID)).toStrictEqual(true); + test("should return true when VC is a valid digested v4 VC and identityProof is DNS-DID", () => { + expect(isDigestedOAVerifiableCredential(DIGESTED_VC_DID)).toStrictEqual(true); }); - test("should return true when signed document is a valid signed wrapped v4 document and identityProof is DNS-DID", () => { - expect(isDigestedOAVerifiableCredential(SIGNED_WRAPPED_DOCUMENT_DID)).toStrictEqual(true); + test("should return true when signed VC is a valid signed v4 VC and identityProof is DNS-DID", () => { + expect(isDigestedOAVerifiableCredential(SIGNED_VC_DID)).toStrictEqual(true); }); - test("should return false when document is invalid due to no DNS-DID identifier", () => { - const modifiedIssuer = cloneDeep(RAW_DOCUMENT_DID.issuer); + test("should return false when VC is invalid due to no DNS-DID identifier", () => { + const modifiedIssuer = cloneDeep(RAW_VC_DID.issuer); delete (modifiedIssuer as any).id; const credential = { - ...RAW_DOCUMENT_DID, + ...RAW_VC_DID, issuer: modifiedIssuer, } satisfies OAVerifiableCredential; expect(isDigestedOAVerifiableCredential(credential)).toStrictEqual(false); @@ -185,9 +213,9 @@ describe("V4.0 E2E Test Scenarios", () => { }); describe("unicode", () => { - test("should not corrupt unicode document", async () => { - const document = { - ...RAW_DOCUMENT_DID, + test("should not corrupt unicode VC", async () => { + const vc = { + ...RAW_VC_DID, credentialSubject: { key1: "哦喷啊特特是他题哦你", key2: "นยำืฟะะำหะฟะรนื", @@ -195,12 +223,12 @@ describe("V4.0 E2E Test Scenarios", () => { key4: "خحثىشففثسفشفهخى", }, }; - const wrapped = await digestVc(document); - expect(wrapped.proof.merkleRoot).toBeTruthy(); - expect(wrapped.credentialSubject.key1).toBe(document.credentialSubject.key1); - expect(wrapped.credentialSubject.key2).toBe(document.credentialSubject.key2); - expect(wrapped.credentialSubject.key3).toBe(document.credentialSubject.key3); - expect(wrapped.credentialSubject.key4).toBe(document.credentialSubject.key4); + const digested = await digestVc(vc); + expect(digested.proof.merkleRoot).toBeTruthy(); + expect(digested.credentialSubject.key1).toBe(vc.credentialSubject.key1); + expect(digested.credentialSubject.key2).toBe(vc.credentialSubject.key2); + expect(digested.credentialSubject.key3).toBe(vc.credentialSubject.key3); + expect(digested.credentialSubject.key4).toBe(vc.credentialSubject.key4); }); }); }); diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index fa1e7cab..3dc9807a 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -1,5 +1,5 @@ import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign"; -import { RAW_DOCUMENT_DID } from "../fixtures"; +import { RAW_VC_DID } from "../fixtures"; import { digestVc } from "../digest"; import { signVc } from "../sign"; import { @@ -11,10 +11,10 @@ import { SignedOAVerifiableCredential, } from "../types"; -const RAW_DOCUMENT = { - ...RAW_DOCUMENT_DID, +const RAW_VC = { + ...RAW_VC_DID, credentialSubject: { - ...RAW_DOCUMENT_DID.credentialSubject, + ...RAW_VC_DID.credentialSubject, attachments: [ { mimeType: "image/png", @@ -26,30 +26,30 @@ const RAW_DOCUMENT = { } satisfies OAVerifiableCredential; describe("V4.0 guard", () => { - let WRAPPED_DOCUMENT: Digested; - let SIGNED_WRAPPED_DOCUMENT: Signed; + let DIGESTED_VC: Digested; + let SIGNED_VC: Signed; beforeAll(async () => { - WRAPPED_DOCUMENT = await digestVc(RAW_DOCUMENT); - SIGNED_WRAPPED_DOCUMENT = await signVc(RAW_DOCUMENT, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { + DIGESTED_VC = await digestVc(RAW_VC); + SIGNED_VC = await signVc(RAW_VC, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", }); }); - describe("given a raw document", () => { - test("should pass w3c vc validation without removal of any data", () => { - const w3cVerifiableCredential: W3cVerifiableCredential = RAW_DOCUMENT_DID; + describe("given a raw VC", () => { + test("should pass W3C VC validation without removal of any data", () => { + const w3cVerifiableCredential: W3cVerifiableCredential = RAW_VC_DID; const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); - expect(results).toEqual(RAW_DOCUMENT_DID); + expect(results).toEqual(RAW_VC_DID); }); - test("should pass document validation without removal of any data", () => { - const results = OAVerifiableCredential.parse(RAW_DOCUMENT_DID); - expect(results).toEqual(RAW_DOCUMENT_DID); + test("should pass VC validation without removal of any data", () => { + const results = OAVerifiableCredential.parse(RAW_VC_DID); + expect(results).toEqual(RAW_VC_DID); }); - test("should fail wrapped document validation", () => { - const results = DigestedOAVerifiableCredential.safeParse(RAW_DOCUMENT_DID); + test("should fail digested VC validation", () => { + const results = DigestedOAVerifiableCredential.safeParse(RAW_VC_DID); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -66,8 +66,8 @@ describe("V4.0 guard", () => { `); }); - test("should fail signed wrapped document validation", () => { - const results = SignedOAVerifiableCredential.safeParse(RAW_DOCUMENT_DID); + test("should fail signed VC validation", () => { + const results = SignedOAVerifiableCredential.safeParse(RAW_VC_DID); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -85,26 +85,26 @@ describe("V4.0 guard", () => { }); }); - describe("given a wrapped document", () => { - test("should pass w3c vc validation without removal of any data", () => { - const w3cVerifiableCredential: W3cVerifiableCredential = WRAPPED_DOCUMENT; + describe("given a digested VC", () => { + test("should pass W3C VC validation without removal of any data", () => { + const w3cVerifiableCredential: W3cVerifiableCredential = DIGESTED_VC; const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); - expect(results).toEqual(WRAPPED_DOCUMENT); + expect(results).toEqual(DIGESTED_VC); }); - test("should pass document validation without removal of any data", () => { - const v4Document: OAVerifiableCredential = WRAPPED_DOCUMENT; - const results = OAVerifiableCredential.parse(v4Document); - expect(results).toEqual(WRAPPED_DOCUMENT); + test("should pass VC validation without removal of any data", () => { + const oaVc: OAVerifiableCredential = DIGESTED_VC; + const results = OAVerifiableCredential.parse(oaVc); + expect(results).toEqual(DIGESTED_VC); }); - test("should pass wrapped document validation without removal of any data", () => { - const results = DigestedOAVerifiableCredential.parse(WRAPPED_DOCUMENT); - expect(results).toEqual(WRAPPED_DOCUMENT); + test("should pass digested VC validation without removal of any data", () => { + const results = DigestedOAVerifiableCredential.parse(DIGESTED_VC); + expect(results).toEqual(DIGESTED_VC); }); - test("should fail signed wrapped document validation", () => { - const results = SignedOAVerifiableCredential.safeParse(WRAPPED_DOCUMENT); + test("should fail signed VC validation", () => { + const results = SignedOAVerifiableCredential.safeParse(DIGESTED_VC); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -133,28 +133,28 @@ describe("V4.0 guard", () => { }); }); - describe("given a signed wrapped document", () => { - test("should pass w3c vc validation without removal of any data", () => { - const w3cVerifiableCredential: W3cVerifiableCredential = SIGNED_WRAPPED_DOCUMENT; + describe("given a signed VC", () => { + test("should pass W3C VC validation without removal of any data", () => { + const w3cVerifiableCredential: W3cVerifiableCredential = SIGNED_VC; const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); - expect(results).toEqual(SIGNED_WRAPPED_DOCUMENT); + expect(results).toEqual(SIGNED_VC); }); - test("should pass document validation without removal of any data", () => { - const v4Document: OAVerifiableCredential = SIGNED_WRAPPED_DOCUMENT; - const results = OAVerifiableCredential.parse(v4Document); - expect(results).toEqual(SIGNED_WRAPPED_DOCUMENT); + test("should pass VC validation without removal of any data", () => { + const oaVc: OAVerifiableCredential = SIGNED_VC; + const results = OAVerifiableCredential.parse(oaVc); + expect(results).toEqual(SIGNED_VC); }); - test("should pass wrapped document validation without removal of any data", () => { - const v4WrappedDocument: Digested = SIGNED_WRAPPED_DOCUMENT; - const results = DigestedOAVerifiableCredential.parse(v4WrappedDocument); - expect(results).toEqual(SIGNED_WRAPPED_DOCUMENT); + test("should pass digested VC validation without removal of any data", () => { + const oaDigestedVc: Digested = SIGNED_VC; + const results = DigestedOAVerifiableCredential.parse(oaDigestedVc); + expect(results).toEqual(SIGNED_VC); }); - test("should pass signed wrapped document validation without removal of any data", () => { - const results = SignedOAVerifiableCredential.parse(SIGNED_WRAPPED_DOCUMENT); - expect(results).toEqual(SIGNED_WRAPPED_DOCUMENT); + test("should pass signed VC validation without removal of any data", () => { + const results = SignedOAVerifiableCredential.parse(SIGNED_VC); + expect(results).toEqual(SIGNED_VC); }); }); }); diff --git a/src/4.0/__tests__/hash.test.ts b/src/4.0/__tests__/hash.test.ts index 3e9083b8..aa32d882 100644 --- a/src/4.0/__tests__/hash.test.ts +++ b/src/4.0/__tests__/hash.test.ts @@ -1,20 +1,20 @@ import { genTargetHash } from "../hash"; import { decodeSalt } from "../salt"; -import { SIGNED_WRAPPED_DOCUMENT_DID as ROOT_CREDENTIAL } from "../fixtures"; +import { SIGNED_VC_DID as ROOT_CREDENTIAL } from "../fixtures"; import { Signed } from "../types"; import { obfuscateOAVerifiableCredential } from "../obfuscate"; -// All obfuscated documents are generated from the ROOT_CREDENTIAL +// All obfuscated VCs are generated from the ROOT_CREDENTIAL const ROOT_CREDENTIAL_TARGET_HASH = ROOT_CREDENTIAL.proof.targetHash; describe("V4.0 hash", () => { - test("given that obfuscated documents are generated from the ROOT_CREDENTIAL, ROOT_CREDENTIAL_TARGET_HASH should match snapshot", () => { + test("given that obfuscated VCs are generated from the ROOT_CREDENTIAL, ROOT_CREDENTIAL_TARGET_HASH should match snapshot", () => { expect(ROOT_CREDENTIAL_TARGET_HASH).toMatchInlineSnapshot( `"0b1f90bc8e87cfce8ec49cea60d406291ad130ddedc26e866a8c4f2152747abc"` ); }); - test("given a document with ALL FIELDS VISIBLE, should digest and match the root credential's target hash", () => { + test("given a VC with ALL FIELDS VISIBLE, should digest and match the root credential's target hash", () => { expect(ROOT_CREDENTIAL.credentialSubject).toMatchInlineSnapshot(` { "id": "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42", @@ -42,9 +42,9 @@ describe("V4.0 hash", () => { expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); }); - test("given a document with ONE element obfuscated, should digest and match the root credential's target hash", () => { - const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateOAVerifiableCredential(ROOT_CREDENTIAL, "credentialSubject.id"); - expect(OBFUSCATED_WRAPPED_DOCUMENT.credentialSubject).toMatchInlineSnapshot(` + test("given a VC with ONE element obfuscated, should digest and match the root credential's target hash", () => { + const OBFUSCATED_DIGESTED_VC = obfuscateOAVerifiableCredential(ROOT_CREDENTIAL, "credentialSubject.id"); + expect(OBFUSCATED_DIGESTED_VC.credentialSubject).toMatchInlineSnapshot(` { "licenses": [ { @@ -64,28 +64,28 @@ describe("V4.0 hash", () => { ], } `); - expect(OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated).toMatchInlineSnapshot(` + expect(OBFUSCATED_DIGESTED_VC.proof.privacy.obfuscated).toMatchInlineSnapshot(` [ "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", ] `); const digest = genTargetHash( - OBFUSCATED_WRAPPED_DOCUMENT, - decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), - OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated + OBFUSCATED_DIGESTED_VC, + decodeSalt(OBFUSCATED_DIGESTED_VC.proof.salts), + OBFUSCATED_DIGESTED_VC.proof.privacy.obfuscated ); expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); - expect(digest).toBe(OBFUSCATED_WRAPPED_DOCUMENT.proof.targetHash); + expect(digest).toBe(OBFUSCATED_DIGESTED_VC.proof.targetHash); }); - test("given a document with THREE elements obfuscated, should digest and match the root credential's target hash", () => { - const OBFUSCATED_WRAPPED_DOCUMENT = obfuscateOAVerifiableCredential(ROOT_CREDENTIAL, [ + test("given a VC with THREE elements obfuscated, should digest and match the root credential's target hash", () => { + const OBFUSCATED_DIGESTED_VC = obfuscateOAVerifiableCredential(ROOT_CREDENTIAL, [ "credentialSubject.id", "credentialSubject.name", "credentialSubject.licenses[0].description", ]); - expect(OBFUSCATED_WRAPPED_DOCUMENT.credentialSubject).toMatchInlineSnapshot(` + expect(OBFUSCATED_DIGESTED_VC.credentialSubject).toMatchInlineSnapshot(` { "licenses": [ { @@ -103,7 +103,7 @@ describe("V4.0 hash", () => { ], } `); - expect(OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated).toMatchInlineSnapshot(` + expect(OBFUSCATED_DIGESTED_VC.proof.privacy.obfuscated).toMatchInlineSnapshot(` [ "31744f7aac0af84e23e752611279933657ff78a9065330f8c5029ec5205979a3", "f49443c7e5fcb9f20dad4463a5e0b2cb3e341c430d4792cb87cb11bce0efd9b0", @@ -112,18 +112,18 @@ describe("V4.0 hash", () => { `); const digest = genTargetHash( - OBFUSCATED_WRAPPED_DOCUMENT, - decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), - OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated + OBFUSCATED_DIGESTED_VC, + decodeSalt(OBFUSCATED_DIGESTED_VC.proof.salts), + OBFUSCATED_DIGESTED_VC.proof.privacy.obfuscated ); expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); - expect(digest).toBe(OBFUSCATED_WRAPPED_DOCUMENT.proof.targetHash); + expect(digest).toBe(OBFUSCATED_DIGESTED_VC.proof.targetHash); }); - test("given a document with NO VISIBLE FIELDS, should digest and match the root credential's target hash", () => { + test("given a VC with NO VISIBLE FIELDS, should digest and match the root credential's target hash", () => { // this has to be manually generated, since obfuscateVerifiableCredential does not allow obfuscating fields that - // result in a non compliant V4 OA document - const OBFUSCATED_WRAPPED_DOCUMENT = { + // result in a non compliant V4 OA VC + const OBFUSCATED_DIGESTED_VC = { // no visible fields proof: { type: "OpenAttestationHashProof2018", @@ -166,9 +166,9 @@ describe("V4.0 hash", () => { } as unknown as Signed; const digest = genTargetHash( - OBFUSCATED_WRAPPED_DOCUMENT, - decodeSalt(OBFUSCATED_WRAPPED_DOCUMENT.proof.salts), - OBFUSCATED_WRAPPED_DOCUMENT.proof.privacy.obfuscated + OBFUSCATED_DIGESTED_VC, + decodeSalt(OBFUSCATED_DIGESTED_VC.proof.salts), + OBFUSCATED_DIGESTED_VC.proof.privacy.obfuscated ); expect(digest).toBe(ROOT_CREDENTIAL_TARGET_HASH); }); diff --git a/src/4.0/__tests__/obfuscate.test.ts b/src/4.0/__tests__/obfuscate.test.ts index 7e0e5484..5e3fd985 100644 --- a/src/4.0/__tests__/obfuscate.test.ts +++ b/src/4.0/__tests__/obfuscate.test.ts @@ -5,12 +5,12 @@ import { decodeSalt } from "../salt"; import { digestVc } from "../digest"; import { Salt, OAVerifiableCredential, Digested } from "../types"; import { validateDigest } from "../validate"; -import { RAW_DOCUMENT_DID, SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED, WRAPPED_DOCUMENT_DID } from "../fixtures"; +import { RAW_VC_DID, SIGNED_VC_DID_OBFUSCATED, DIGESTED_VC_DID } from "../fixtures"; import { hashLeafNode } from "../hash"; import { getObfuscatedData, isObfuscated } from "../../shared/utils"; const makeOAVerifiableCredential = >(props: T) => { - const { credentialSubject, ...rest } = RAW_DOCUMENT_DID; + const { credentialSubject, ...rest } = RAW_VC_DID; return { ...rest, ...(props as T) } satisfies OAVerifiableCredential; }; @@ -25,17 +25,17 @@ const findSaltByPath = (salts: string, path: string): Salt | undefined => { * - the salt bound to the field has been removed * - the field has been removed */ -const expectRemovedFieldsWithoutArrayNotation = (field: string, document: Digested, obfuscatedDocument: Digested) => { - const value = get(document, field); - const salt = findSaltByPath(document.proof.salts, field); +const expectRemovedFieldsWithoutArrayNotation = (field: string, vc: Digested, obfuscatedVc: Digested) => { + const value = get(vc, field); + const salt = findSaltByPath(vc.proof.salts, field); if (!salt) throw new Error("Salt not found for ${field}"); - expect(obfuscatedDocument.proof.privacy.obfuscated).toContain( + expect(obfuscatedVc.proof.privacy.obfuscated).toContain( hashLeafNode({ value, salt: salt.value, path: salt.path }, { toHexString: true }) ); - expect(findSaltByPath(obfuscatedDocument.proof.salts, field)).toBeUndefined(); - expect(obfuscatedDocument).not.toHaveProperty(field); + expect(findSaltByPath(obfuscatedVc.proof.salts, field)).toBeUndefined(); + expect(obfuscatedVc).not.toHaveProperty(field); }; describe("V4.0 obfuscate", () => { @@ -53,7 +53,7 @@ describe("V4.0 obfuscate", () => { expect(obfuscatedVc.proof.privacy.obfuscated).toHaveLength(1); }); - test("removes paths that result in an invalid wrapped document, should throw", async () => { + test("removes paths that result in an invalid digested VC, should throw", async () => { const PATHS_TO_REMOVE = ["credentialSubject", "renderMethod.0.id", "name"]; const digestedVc = await digestVc( makeOAVerifiableCredential({ credentialSubject: { id: "S1234567A", name: "John Doe" } }) @@ -98,7 +98,7 @@ describe("V4.0 obfuscate", () => { test("given an object is to be removed, should remove the object itself, as well as add each of its key's hash into privacy.obfuscated", async () => { const PATH_TO_REMOVE = "credentialSubject.hee"; - const wrappedDocument = await digestVc( + const digested = await digestVc( makeOAVerifiableCredential({ credentialSubject: { hee: { foo: "bar", doo: "foo" }, @@ -106,31 +106,31 @@ describe("V4.0 obfuscate", () => { }, }) ); - const obfuscatedDocument = obfuscateOAVerifiableCredential(wrappedDocument, PATH_TO_REMOVE); + const obfuscatedVc = obfuscateOAVerifiableCredential(digested, PATH_TO_REMOVE); - const verified = validateDigest(obfuscatedDocument); + const verified = validateDigest(obfuscatedVc); expect(verified).toBe(true); // assert that each key of the object has been moved to privacy.obfuscated ["credentialSubject.hee.foo", "credentialSubject.hee.doo"].forEach((expectedRemovedField) => { - const value = get(wrappedDocument, expectedRemovedField); - const salt = findSaltByPath(wrappedDocument.proof.salts, expectedRemovedField); + const value = get(digested, expectedRemovedField); + const salt = findSaltByPath(digested.proof.salts, expectedRemovedField); if (!salt) throw new Error(`Salt not found for ${expectedRemovedField}`); - expect(obfuscatedDocument.proof.privacy.obfuscated).toContain( + expect(obfuscatedVc.proof.privacy.obfuscated).toContain( hashLeafNode({ value, salt: salt.value, path: expectedRemovedField }, { toHexString: true }) ); - expect(findSaltByPath(obfuscatedDocument.proof.salts, expectedRemovedField)).toBeUndefined(); + expect(findSaltByPath(obfuscatedVc.proof.salts, expectedRemovedField)).toBeUndefined(); }); - expect(obfuscatedDocument.credentialSubject?.hee).toBeUndefined(); // let's make sure only the first item has been removed - expect(obfuscatedDocument.proof.privacy.obfuscated).toHaveLength(2); + expect(obfuscatedVc.credentialSubject?.hee).toBeUndefined(); // let's make sure only the first item has been removed + expect(obfuscatedVc.proof.privacy.obfuscated).toHaveLength(2); }); test("given an entire array of objects to remove, should remove the array itself, then for every item, add each of its key's hash into privacy.obfuscated", async () => { const PATH_TO_REMOVE = "credentialSubject.attachments"; - const wrappedDocument = await digestVc( + const digested = await digestVc( makeOAVerifiableCredential({ credentialSubject: { arrayOfObject: [ @@ -152,9 +152,9 @@ describe("V4.0 obfuscate", () => { }, }) ); - const obfuscatedDocument = obfuscateOAVerifiableCredential(wrappedDocument, PATH_TO_REMOVE); + const obfuscatedVc = obfuscateOAVerifiableCredential(digested, PATH_TO_REMOVE); - const verified = validateDigest(obfuscatedDocument); + const verified = validateDigest(obfuscatedVc); expect(verified).toBe(true); [ @@ -165,23 +165,23 @@ describe("V4.0 obfuscate", () => { "credentialSubject.attachments[1].filename", "credentialSubject.attachments[1].data", ].forEach((expectedRemovedField) => { - const value = get(wrappedDocument, expectedRemovedField); - const salt = findSaltByPath(wrappedDocument.proof.salts, expectedRemovedField); + const value = get(digested, expectedRemovedField); + const salt = findSaltByPath(digested.proof.salts, expectedRemovedField); if (!salt) throw new Error(`Salt not found for ${expectedRemovedField}`); - expect(obfuscatedDocument.proof.privacy.obfuscated).toContain( + expect(obfuscatedVc.proof.privacy.obfuscated).toContain( hashLeafNode({ value, salt: salt.value, path: expectedRemovedField }, { toHexString: true }) ); - expect(findSaltByPath(obfuscatedDocument.proof.salts, expectedRemovedField)).toBeUndefined(); + expect(findSaltByPath(obfuscatedVc.proof.salts, expectedRemovedField)).toBeUndefined(); }); - expect(obfuscatedDocument.credentialSubject.attachments).toBeUndefined(); - expect(obfuscatedDocument.proof.privacy.obfuscated).toHaveLength(6); + expect(obfuscatedVc.credentialSubject.attachments).toBeUndefined(); + expect(obfuscatedVc.proof.privacy.obfuscated).toHaveLength(6); }); test("given multiple fields to be removed, should remove fields and add their hash into privacy.obfuscated", async () => { const PATHS_TO_REMOVE = ["credentialSubject.key1", "credentialSubject.key2"]; - const wrappedDocument = await digestVc( + const digested = await digestVc( makeOAVerifiableCredential({ credentialSubject: { key1: "value1", @@ -190,18 +190,18 @@ describe("V4.0 obfuscate", () => { }, }) ); - const obfuscatedDocument = obfuscateOAVerifiableCredential(wrappedDocument, PATHS_TO_REMOVE); - const verified = validateDigest(obfuscatedDocument); + const obfuscatedVc = obfuscateOAVerifiableCredential(digested, PATHS_TO_REMOVE); + const verified = validateDigest(obfuscatedVc); expect(verified).toBe(true); PATHS_TO_REMOVE.forEach((expectedRemovedField) => { - expectRemovedFieldsWithoutArrayNotation(expectedRemovedField, wrappedDocument, obfuscatedDocument); + expectRemovedFieldsWithoutArrayNotation(expectedRemovedField, digested, obfuscatedVc); }); - expect(obfuscatedDocument.proof.privacy.obfuscated).toHaveLength(2); + expect(obfuscatedVc.proof.privacy.obfuscated).toHaveLength(2); }); test("given a path to remove an entire item from an array, should throw", async () => { - const wrappedDocument = await digestVc( + const digested = await digestVc( makeOAVerifiableCredential({ credentialSubject: { arrayOfObject: [ @@ -230,7 +230,7 @@ describe("V4.0 obfuscate", () => { ); expect(() => - obfuscateOAVerifiableCredential(wrappedDocument, [ + obfuscateOAVerifiableCredential(digested, [ "credentialSubject.attachments[0]", "credentialSubject.attachments[2]", ]) @@ -238,7 +238,7 @@ describe("V4.0 obfuscate", () => { }); test("given a path to remove all elements in an object, should throw", async () => { - const wrappedDocument = await digestVc( + const digested = await digestVc( makeOAVerifiableCredential({ credentialSubject: { arrayOfObject: [ @@ -253,7 +253,7 @@ describe("V4.0 obfuscate", () => { ); expect(() => - obfuscateOAVerifiableCredential(wrappedDocument, [ + obfuscateOAVerifiableCredential(digested, [ "credentialSubject.arrayOfObject[0].foo", "credentialSubject.arrayOfObject[0].doo", ]) @@ -261,14 +261,14 @@ describe("V4.0 obfuscate", () => { `"Obfuscation of "credentialSubject.arrayOfObject[0].doo" has resulted in an empty {}, this is currently not supported. Alternatively, if the object is not part of an array, you may choose to obfuscate the parent of "credentialSubject.arrayOfObject[0].doo"."` ); expect(() => - obfuscateOAVerifiableCredential(wrappedDocument, ["credentialSubject.object.foo"]) + obfuscateOAVerifiableCredential(digested, ["credentialSubject.object.foo"]) ).toThrowErrorMatchingInlineSnapshot( `"Obfuscation of "credentialSubject.object.foo" has resulted in an empty {}, this is currently not supported. Alternatively, if the object is not part of an array, you may choose to obfuscate the parent of "credentialSubject.object.foo"."` ); }); test("is transitive", async () => { - const wrappedDocument = await digestVc( + const digested = await digestVc( makeOAVerifiableCredential({ credentialSubject: { key1: "value1", @@ -277,9 +277,9 @@ describe("V4.0 obfuscate", () => { }, }) ); - const intermediateDoc = obfuscateOAVerifiableCredential(wrappedDocument, "key1"); + const intermediateDoc = obfuscateOAVerifiableCredential(digested, "key1"); const finalDoc1 = obfuscateOAVerifiableCredential(intermediateDoc, "key2"); - const finalDoc2 = obfuscateOAVerifiableCredential(wrappedDocument, ["key1", "key2"]); + const finalDoc2 = obfuscateOAVerifiableCredential(digested, ["key1", "key2"]); expect(finalDoc1).toEqual(finalDoc2); expect(intermediateDoc).not.toHaveProperty("key1"); @@ -291,12 +291,12 @@ describe("V4.0 obfuscate", () => { }); describe("getObfuscated", () => { - test("should return empty array when there is no obfuscated data in document", () => { - expect(getObfuscatedData(WRAPPED_DOCUMENT_DID)).toHaveLength(0); + test("should return empty array when there is no obfuscated data in VC", () => { + expect(getObfuscatedData(DIGESTED_VC_DID)).toHaveLength(0); }); - test("should return array of hashes when there is obfuscated data in document", () => { - const obfuscatedData = getObfuscatedData(SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED); + test("should return array of hashes when there is obfuscated data in VC", () => { + const obfuscatedData = getObfuscatedData(SIGNED_VC_DID_OBFUSCATED); expect(obfuscatedData.length).toBe(1); expect(obfuscatedData?.[0]).toMatchInlineSnapshot( `"7f2ecdae29b49b3a971d5acdfbbf9225a193e735ce41b89b0d84cca801794fc9"` @@ -305,12 +305,12 @@ describe("V4.0 obfuscate", () => { }); describe("isObfuscated", () => { - test("should return false when there is no obfuscated data in document", () => { - expect(isObfuscated(WRAPPED_DOCUMENT_DID)).toBe(false); + test("should return false when there is no obfuscated data in VC", () => { + expect(isObfuscated(DIGESTED_VC_DID)).toBe(false); }); - test("should return true where there is obfuscated data in document", () => { - expect(isObfuscated(SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED)).toBe(true); + test("should return true where there is obfuscated data in VC", () => { + expect(isObfuscated(SIGNED_VC_DID_OBFUSCATED)).toBe(true); }); }); }); diff --git a/src/4.0/__tests__/salt.test.ts b/src/4.0/__tests__/salt.test.ts index afe20bdc..f3eb9f28 100644 --- a/src/4.0/__tests__/salt.test.ts +++ b/src/4.0/__tests__/salt.test.ts @@ -4,78 +4,78 @@ import { Base64 } from "js-base64"; describe("V4.0 salt", () => { describe("salt", () => { test("handles shadowed keys correctly (type 1: root, dot notation)", () => { - const document = { + const vc = { "credentialSubject.alumniOf": "0xSomeMaliciousDocumentStore, this would be at credentialSubject.alumniOf after flatMap if uncaught", }; expect(() => { - salt(document); + salt(vc); }).toThrow("Key names must not have . in them"); }); test("handles shadowed keys correctly (type 2: root, array index)", () => { - const document = { + const vc = { "type[1]": "MaliciousCredential, this would be at type[1] after flatMap if uncaught", }; expect(() => { - salt(document); + salt(vc); }).toThrow("Key names must not have '[' or ']' in them"); }); test("handles shadowed keys correctly (type 3: nested as object, dot notation)", () => { - const document = { + const vc = { nested: { "credentialSubject.alumniOf": "0xSomeMaliciousDocumentStore, this would be at nested.credentialSubject.alumniOf after flatMap if uncaught", }, }; expect(() => { - salt(document); + salt(vc); }).toThrow("Key names must not have . in them"); }); test("handles shadowed keys correctly (type 4: nested as object, array index)", () => { - const document = { + const vc = { nested: { "type[1]": "this would be at nested.type[1] after flatMap if uncaught" }, }; expect(() => { - salt(document); + salt(vc); }).toThrow("Key names must not have '[' or ']' in them"); }); test("handles shadowed keys correctly (type 5: nested as array, dot notation)", () => { - const document = { + const vc = { nested: [{ "shadowed.key": "this would be at nested[0].shadowed.key after flatMap if uncaught" }], }; expect(() => { - salt(document); + salt(vc); }).toThrow("Key names must not have . in them"); }); test("handles shadowed keys correctly (type 6: nested as array, array index)", () => { - const document = { + const vc = { nested: [{ "type[1]": "this would be at nested[0].type[1] after flatMap if uncaught" }], }; expect(() => { - salt(document); + salt(vc); }).toThrow("Key names must not have '[' or ']' in them"); }); test("handles null values correctly", () => { - const document = { + const vc = { grades: null, }; - const salted = salt(document); + const salted = salt(vc); expect(salted).toContainEqual(expect.objectContaining({ path: "grades" })); }); test("handles undefined values correctly", () => { - const document = { + const vc = { grades: undefined, }; expect(() => { - salt(document); + salt(vc); }).toThrow("Unexpected data 'undefined' in 'grades'"); // Cannot convert undefined or null to object? }); test("handles numbers and booleans correctly", () => { - const document = { + const vc = { grades: ["A+", 100, 50.28, true, "B+"], }; - const salted = salt(document); + const salted = salt(vc); expect(salted).toContainEqual(expect.objectContaining({ path: "grades[0]" })); expect(salted).toContainEqual(expect.objectContaining({ path: "grades[1]" })); expect(salted).toContainEqual(expect.objectContaining({ path: "grades[2]" })); @@ -83,10 +83,10 @@ describe("V4.0 salt", () => { expect(salted).toContainEqual(expect.objectContaining({ path: "grades[4]" })); }); test("throw on sparse arrays (we do not support obfuscation of array item as JSON turns empty slots into null values)", () => { - const document = { + const vc = { grades: ["A+", 100, , , , true, "B+"], }; - expect(() => salt(document)).toThrow(`Unexpected data 'undefined'`); + expect(() => salt(vc)).toThrow(`Unexpected data 'undefined'`); }); }); diff --git a/src/4.0/__tests__/sign.test.ts b/src/4.0/__tests__/sign.test.ts index 3cbe0f44..26cb77ce 100644 --- a/src/4.0/__tests__/sign.test.ts +++ b/src/4.0/__tests__/sign.test.ts @@ -1,20 +1,16 @@ import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign"; import { Wallet } from "@ethersproject/wallet"; -import { RAW_DOCUMENT_DID } from "../fixtures"; +import { RAW_VC_DID } from "../fixtures"; import { SignedOAVerifiableCredential } from "../types"; import { signVc, signVcErrors } from "../sign"; describe("V4.0 sign", () => { - it("should sign a document", async () => { - const signedWrappedDocument = await signVc( - RAW_DOCUMENT_DID, - SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, - { - public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", - private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", - } - ); - const parsedResults = SignedOAVerifiableCredential.safeParse(signedWrappedDocument); + it("should sign a VC", async () => { + const signed = await signVc(RAW_VC_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { + public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", + private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", + }); + const parsedResults = SignedOAVerifiableCredential.safeParse(signed); if (!parsedResults.success) { throw new Error("Parsing failed"); } @@ -23,16 +19,12 @@ describe("V4.0 sign", () => { expect(proof.key).toBe("did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller"); expect(proof.signature).toBeDefined(); }); - it("should sign a document with a wallet", async () => { + it("should sign a VC with a wallet", async () => { const wallet = Wallet.fromMnemonic( "tourist quality multiply denial diary height funny calm disease buddy speed gold" ); - const signedWrappedDocument = await signVc( - RAW_DOCUMENT_DID, - SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, - wallet - ); - const parsedResults = SignedOAVerifiableCredential.safeParse(signedWrappedDocument); + const signed = await signVc(RAW_VC_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, wallet); + const parsedResults = SignedOAVerifiableCredential.safeParse(signed); if (!parsedResults.success) { throw new Error("Parsing failed"); } @@ -42,8 +34,8 @@ describe("V4.0 sign", () => { expect(proof.signature).toBeDefined(); }); - it("should throw error if a signed document is resigned", async () => { - const signedVc = await signVc(RAW_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { + it("should throw error if a signed VC is resigned", async () => { + const signedVc = await signVc(RAW_VC_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", }); @@ -67,7 +59,7 @@ describe("V4.0 sign", () => { }); it("should throw error if a key or signer is invalid", async () => { await expect( - signVc(RAW_DOCUMENT_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, {} as any) + signVc(RAW_VC_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, {} as any) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Either a keypair or ethers.js Signer must be provided"`); }); }); diff --git a/src/4.0/__tests__/validate.test.ts b/src/4.0/__tests__/validate.test.ts index a6365ef7..41d4c5dd 100644 --- a/src/4.0/__tests__/validate.test.ts +++ b/src/4.0/__tests__/validate.test.ts @@ -1,31 +1,30 @@ import { cloneDeep } from "lodash"; -import { BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID, SIGNED_WRAPPED_DOCUMENT_DID } from "../fixtures"; +import { SIGNED_BATCHED_VC_DID, SIGNED_VC_DID } from "../fixtures"; import { Signed } from "../types"; import { validateDigest } from "../validate"; -const TEST_DOCUMENTS = { - "Documents without proofs mean these documents are wrapped individually (i.e. targetHash == merkleRoot)": - SIGNED_WRAPPED_DOCUMENT_DID, - "Documents with proofs mean these documents are wrapped as a batch (i.e. proofs exist, and targetHash !== merkleRoot)": - BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID[0], +const TEST_VCS = { + "VCs without proofs mean these VCs are digested individually (i.e. targetHash == merkleRoot)": SIGNED_VC_DID, + "VCs with proofs mean these VCs are digested as a batch (i.e. proofs exist, and targetHash !== merkleRoot)": + SIGNED_BATCHED_VC_DID[0], } as const; describe("V4.0 validate", () => { - Object.entries(TEST_DOCUMENTS).forEach(([description, document]) => { + Object.entries(TEST_VCS).forEach(([description, vc]) => { describe(`${description}`, () => { - test("given a document wiht unaltered data, should return true", () => { - expect(validateDigest(document)).toBe(true); + test("given a VC with unaltered data, should return true", () => { + expect(validateDigest(vc)).toBe(true); }); describe("tempering", () => { test("given a value of a key in object is changed, should return false", () => { const newName = "Fake Name"; - expect(document.issuer.name).not.toBe(newName); + expect(vc.issuer.name).not.toBe(newName); expect( validateDigest({ - ...document, + ...vc, issuer: { - ...document.issuer, + ...vc.issuer, name: "Fake Name", // Value was originally "DEMO STORE" }, }) @@ -33,11 +32,11 @@ describe("V4.0 validate", () => { }); test("given a key in an object is altered (value kept the same), should return false", () => { - const { name, ...issuerWithoutName } = document.issuer; + const { name, ...issuerWithoutName } = vc.issuer; expect( validateDigest({ - ...document, + ...vc, issuer: { ...issuerWithoutName, fakename: name, // Key was originally "name" @@ -47,7 +46,7 @@ describe("V4.0 validate", () => { }); test("given a new array item is added, should return false", () => { - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); expect(modifiedCredentialSubject.licenses[2]).toBeUndefined(); modifiedCredentialSubject.licenses.push({ class: "Class 2A", @@ -58,21 +57,21 @@ describe("V4.0 validate", () => { expect( validateDigest({ - ...document, + ...vc, credentialSubject: modifiedCredentialSubject, }) ).toBe(false); }); test("given a key in an item is removed, should return false", () => { - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); expect(modifiedCredentialSubject.licenses[0].description).toBeDefined(); delete (modifiedCredentialSubject.licenses[0] as any).description; expect(modifiedCredentialSubject.licenses[0].description).toBeUndefined(); expect( validateDigest({ - ...document, + ...vc, credentialSubject: modifiedCredentialSubject, }) ).toBe(false); @@ -82,9 +81,9 @@ describe("V4.0 validate", () => { test("given insertion into an object", () => { expect( validateDigest({ - ...document, + ...vc, credentialSubject: { - ...document.credentialSubject, + ...vc.credentialSubject, newField: {}, }, }) @@ -92,14 +91,14 @@ describe("V4.0 validate", () => { }); test("given insertion into an array", () => { - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); expect(modifiedCredentialSubject.licenses[2]).toBeUndefined(); modifiedCredentialSubject.licenses.push({} as any); expect(modifiedCredentialSubject.licenses[2]).toEqual({}); expect( validateDigest({ - ...document, + ...vc, credentialSubject: modifiedCredentialSubject, }) ).toBe(false); @@ -110,9 +109,9 @@ describe("V4.0 validate", () => { test("given insertion into an object", () => { expect( validateDigest({ - ...document, + ...vc, credentialSubject: { - ...document.credentialSubject, + ...vc.credentialSubject, newField: [], }, }) @@ -120,14 +119,14 @@ describe("V4.0 validate", () => { }); test("given insertion into an array", () => { - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); expect(modifiedCredentialSubject.licenses[2]).toBeUndefined(); modifiedCredentialSubject.licenses.push([] as any); expect(modifiedCredentialSubject.licenses[2]).toEqual([]); expect( validateDigest({ - ...document, + ...vc, credentialSubject: modifiedCredentialSubject, }) ).toBe(false); @@ -137,57 +136,57 @@ describe("V4.0 validate", () => { test("given insertion of a null value into an object, should return false", () => { expect( validateDigest({ - ...document, + ...vc, credentialSubject: { - ...document.credentialSubject, + ...vc.credentialSubject, newField: null, }, }) ).toBe(false); - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); expect(modifiedCredentialSubject.licenses[2]).toBeUndefined(); modifiedCredentialSubject.licenses.push({} as any); expect(modifiedCredentialSubject.licenses[2]).toEqual({}); expect( validateDigest({ - ...document, + ...vc, credentialSubject: modifiedCredentialSubject, }) ).toBe(false); }); test("given a null value is inserted into an array, should return false", () => { - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); expect(modifiedCredentialSubject.licenses[2]).toBeUndefined(); modifiedCredentialSubject.licenses.push(null as any); expect(modifiedCredentialSubject.licenses[2]).toBe(null); expect( validateDigest({ - ...document, + ...vc, credentialSubject: modifiedCredentialSubject, }) ).toBe(false); }); test("given an altered value type that string coerce to the same value, should return false", () => { - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); expect(typeof modifiedCredentialSubject.licenses[0].class).toBe("string"); modifiedCredentialSubject.licenses[0].class = 3 as unknown as string; expect(typeof modifiedCredentialSubject.licenses[0].class).toBe("number"); expect( validateDigest({ - ...document, + ...vc, credentialSubject: modifiedCredentialSubject, }) ).toBe(false); }); test("given a key and value is moved, should return false", () => { - const modifiedCredentialSubject = cloneDeep(document.credentialSubject); + const modifiedCredentialSubject = cloneDeep(vc.credentialSubject); // move "id" from credentialSubject to root expect(modifiedCredentialSubject.id).toBe("urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42"); @@ -197,7 +196,7 @@ describe("V4.0 validate", () => { expect( validateDigest({ - ...document, + ...vc, id, credentialSubject: modifiedCredentialSubject, }) diff --git a/src/4.0/digest.ts b/src/4.0/digest.ts index acadf625..2e8f5264 100644 --- a/src/4.0/digest.ts +++ b/src/4.0/digest.ts @@ -11,36 +11,36 @@ export const digestVc = async > => { /* 1a. Try OpenAttestation VC validation, since most user will be issuing oa v4 */ const oav4context = await OAVerifiableCredential.pick({ "@context": true }).passthrough().safeParseAsync(vc); // Superficial check on user intention - let validatedRawDocument: W3cVerifiableCredential | undefined; + let validatedUndigestedVc: W3cVerifiableCredential | undefined; if (oav4context.success) { const oav4 = await OAVerifiableCredential.safeParseAsync(vc); if (!oav4.success) { throw new DataModelValidationError("Open Attestation v4.0", oav4.error); } - validatedRawDocument = oav4.data; + validatedUndigestedVc = oav4.data; } /* 1b. Only if OA VC validation fail do we continue with W3C VC data model validation */ - if (!validatedRawDocument) { + if (!validatedUndigestedVc) { const w3cVc = await W3cVerifiableCredential.safeParseAsync(vc); if (!w3cVc.success) { throw new DataModelValidationError("Verifiable Credentials v2.0", w3cVc.error); } - validatedRawDocument = w3cVc.data; + validatedUndigestedVc = w3cVc.data; } /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ - await interpretContexts(validatedRawDocument); + await interpretContexts(validatedUndigestedVc); /* 3. Context validation */ // Ensure that required contexts are present and in the correct order // type: [Base, OA, ...] const REQUIRED_CONTEXTS = [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4] as const; const contexts = new Set(REQUIRED_CONTEXTS); - if (typeof validatedRawDocument["@context"] === "string") { - contexts.add(validatedRawDocument["@context"]); - } else if (isStringArray(validatedRawDocument["@context"])) { - validatedRawDocument["@context"].forEach((context) => contexts.add(context)); + if (typeof validatedUndigestedVc["@context"] === "string") { + contexts.add(validatedUndigestedVc["@context"]); + } else if (isStringArray(validatedUndigestedVc["@context"])) { + validatedUndigestedVc["@context"].forEach((context) => contexts.add(context)); } REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); const finalContexts: OAVerifiableCredential["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; @@ -50,17 +50,17 @@ export const digestVc = async ([ContextType.BaseContext, ContextType.OAV4Context]); - if (typeof validatedRawDocument["type"] === "string") { - types.add(validatedRawDocument["type"]); - } else if (isStringArray(validatedRawDocument["type"])) { + if (typeof validatedUndigestedVc["type"] === "string") { + types.add(validatedUndigestedVc["type"]); + } else if (isStringArray(validatedUndigestedVc["type"])) { types.forEach((type) => types.add(type)); } REQUIRED_TYPES.forEach((t) => types.delete(t)); const finalTypes: OAVerifiableCredential["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; - const documentReadyForWrapping = { - ...validatedRawDocument, - ...extractAndAssertAsOAVerifiableCredentialProps(validatedRawDocument, [ + const vcReadyForDigesting = { + ...validatedUndigestedVc, + ...extractAndAssertAsOAVerifiableCredentialProps(validatedUndigestedVc, [ "issuer", "credentialStatus", "credentialSubject", @@ -70,8 +70,8 @@ export const digestVc = async buffer.toString("hex")); return { - ...documentReadyForWrapping, + ...vcReadyForDigesting, proof: { type: "OpenAttestationHashProof2018", proofPurpose: "assertionMethod", @@ -96,11 +96,10 @@ export const digestVc = async ( - // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict - documents: T[] + vcs: T[] ): Promise[]> => { // create individual verifiable credential - const verifiableCredentials = await Promise.all(documents.map((document) => digestVc(document))); + const verifiableCredentials = await Promise.all(vcs.map((vc) => digestVc(vc))); // get all the target hashes to compute the merkle tree and the merkle root const merkleTree = new MerkleTree( @@ -108,7 +107,7 @@ export const digestVcs = async { const digest = verifiableCredential.proof.targetHash; const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer) => buffer.toString("hex")); @@ -125,10 +124,10 @@ export const digestVcs = async ( original: W3cVerifiableCredential, @@ -143,7 +142,7 @@ function extractAndAssertAsOAVerifiableCredentialProps); -export const WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ +export const DIGESTED_VC_DID_OSCP = freezeObject({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -256,7 +256,7 @@ export const WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ }, } satisfies Digested); -export const BATCHED_WRAPPED_DOCUMENTS_DID = freezeObject([ +export const DIGESTED_BATCHED_VC_DID = freezeObject([ { "@context": [ "https://www.w3.org/ns/credentials/v2", @@ -353,7 +353,7 @@ export const BATCHED_WRAPPED_DOCUMENTS_DID = freezeObject([ ] satisfies Digested[]); /* Signed */ -export const SIGNED_WRAPPED_DOCUMENT_DID = freezeObject({ +export const SIGNED_VC_DID = freezeObject({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -402,7 +402,7 @@ export const SIGNED_WRAPPED_DOCUMENT_DID = freezeObject({ }, } satisfies Signed); -export const SIGNED_WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ +export const SIGNED_VC_DID_OSCP = freezeObject({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -452,7 +452,7 @@ export const SIGNED_WRAPPED_DOCUMENT_DID_OSCP = freezeObject({ }, } satisfies Signed); -export const SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED = freezeObject({ +export const SIGNED_VC_DID_OBFUSCATED = freezeObject({ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://schemata.openattestation.com/com/openattestation/4.0/context.json", @@ -497,7 +497,7 @@ export const SIGNED_WRAPPED_DOCUMENT_DID_OBFUSCATED = freezeObject({ ], } satisfies Signed); -export const BATCHED_SIGNED_WRAPPED_DOCUMENTS_DID = freezeObject([ +export const SIGNED_BATCHED_VC_DID = freezeObject([ { "@context": [ "https://www.w3.org/ns/credentials/v2", diff --git a/src/4.0/hash.ts b/src/4.0/hash.ts index 4672f7d4..6ad3eaa5 100644 --- a/src/4.0/hash.ts +++ b/src/4.0/hash.ts @@ -4,17 +4,17 @@ import { W3cVerifiableCredential, Salt } from "./types"; import { LeafValue, traverseAndFlatten } from "./traverseAndFlatten"; import { hashToBuffer } from "../shared/utils/hashing"; -export const genTargetHash = (document: W3cVerifiableCredential, salts: Salt[], obfuscatedData: string[]) => { - // find all leaf nodes in the document and hash them +export const genTargetHash = (vc: W3cVerifiableCredential, salts: Salt[], obfuscatedData: string[]) => { + // find all leaf nodes in the vc and hash them // proof is not part of the digest - const { proof: _, ...documentWithoutProof } = document; + const { proof: _, ...vcWithoutProof } = vc; const saltsMap = new Map(salts.map((salt) => [salt.path, salt.value])); - const isEmptyDocument = Object.keys(documentWithoutProof).length === 0; + const isEmptyVc = Object.keys(vcWithoutProof).length === 0; const hashedLeafNodes = - // skip if document without proof is empty as it will treat the empty document as a leaf node - isEmptyDocument + // skip if vc without proof is empty as it will treat the empty vc as a leaf node + isEmptyVc ? [] - : traverseAndFlatten(documentWithoutProof, ({ value, path }) => { + : traverseAndFlatten(vcWithoutProof, ({ value, path }) => { const salt = saltsMap.get(path); if (!salt) throw new SaltNotFoundError(path); return hashLeafNode({ path, salt, value }); diff --git a/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json b/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json index f9e48f6c..d274ec90 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json @@ -356,7 +356,7 @@ "id": { "type": "string", "format": "uri", - "description": "URL of a decentralised renderer to render this document" + "description": "URL of a decentralised renderer to render this VC" }, "type": { "type": "string", diff --git a/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json b/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json index 843c42a3..21e23006 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json @@ -334,7 +334,7 @@ "id": { "type": "string", "format": "uri", - "description": "URL of a decentralised renderer to render this document" + "description": "URL of a decentralised renderer to render this VC" }, "type": { "type": "string", diff --git a/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json b/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json index d8cd7903..cc1f4c92 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json @@ -364,7 +364,7 @@ "id": { "type": "string", "format": "uri", - "description": "URL of a decentralised renderer to render this document" + "description": "URL of a decentralised renderer to render this VC" }, "type": { "type": "string", diff --git a/src/4.0/obfuscate.ts b/src/4.0/obfuscate.ts index 8cfc9fc1..2d8e09dd 100644 --- a/src/4.0/obfuscate.ts +++ b/src/4.0/obfuscate.ts @@ -37,7 +37,7 @@ const obfuscate = (_data: Digested | Signed, fields: string[] | string) => { const isRemoved = unset(data, path); if (isRemoved) { // assertions to ensure that obfuscation does not result in additional leaf nodes - // that would render the resultant document as invalid + // that would render the resultant vc as invalid path.pop(); if (path.length > 0) { const parent = get(data, path); @@ -87,11 +87,11 @@ export type ObfuscateOAVerifiableCredentialResult = Override } >; export const obfuscateOAVerifiableCredential = ( - document: T, + vc: T, fields: string[] | string ): ObfuscateOAVerifiableCredentialResult => { - const { data, obfuscatedData } = obfuscate(document, fields); - const currentObfuscatedData = document.proof.privacy.obfuscated; + const { data, obfuscatedData } = obfuscate(vc, fields); + const currentObfuscatedData = vc.proof.privacy.obfuscated; const newObfuscatedData = currentObfuscatedData.concat(obfuscatedData); // assert that obfuscated is still compliant to our schema @@ -115,7 +115,7 @@ export const obfuscateOAVerifiableCredential = ( class CannotObfuscateProtectedPathsError extends Error { constructor(public paths: string[]) { super( - `The resultant obfuscated document is not compliant with the OA v4 Verifiable Credential data model, please ensure that the following path(s) are not obfuscated: ${paths + `The resultant obfuscated VC is not compliant with the OA v4 Verifiable Credential data model, please ensure that the following path(s) are not obfuscated: ${paths .map((val) => `"${val}"`) .join(", ")}` ); diff --git a/src/4.0/sign.ts b/src/4.0/sign.ts index 2690b25c..48c53a69 100644 --- a/src/4.0/sign.ts +++ b/src/4.0/sign.ts @@ -13,8 +13,8 @@ export const signVc = async = T extends string | number | bigint | boolean | null [K in keyof T]?: PartialDeep; }; -export const isOAVerifiableCredential = (document: unknown): document is OAVerifiableCredential => { - return OAVerifiableCredential.safeParse(document).success; +export const isOAVerifiableCredential = (vc: unknown): vc is OAVerifiableCredential => { + return OAVerifiableCredential.safeParse(vc).success; }; -export const isDigestedOAVerifiableCredential = (document: unknown): document is Digested => { - return DigestedOAVerifiableCredential.safeParse(document).success; +export const isDigestedOAVerifiableCredential = (vc: unknown): vc is Digested => { + return DigestedOAVerifiableCredential.safeParse(vc).success; }; -export const isSignedOAVerifiableCredential = (document: unknown): document is Signed => { - return SignedOAVerifiableCredential.safeParse(document).success; +export const isSignedOAVerifiableCredential = (vc: unknown): vc is Signed => { + return SignedOAVerifiableCredential.safeParse(vc).success; }; diff --git a/src/4.0/validate.ts b/src/4.0/validate.ts index 23f8d04b..b00e8ae3 100644 --- a/src/4.0/validate.ts +++ b/src/4.0/validate.ts @@ -3,24 +3,24 @@ import { SaltNotFoundError, genTargetHash } from "./hash"; import { checkProof } from "../shared/merkle"; import { decodeSalt } from "./salt"; -export const validateDigest = (document: T): document is T => { - if (!document.proof) { +export const validateDigest = (vc: T): vc is T => { + if (!vc.proof) { return false; } - // Remove proof from document + // Remove proof from VC // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars - const { proof, ...vcWithoutProof } = document; - const decodedSalts = decodeSalt(document.proof.salts); + const { proof, ...vcWithoutProof } = vc; + const decodedSalts = decodeSalt(vc.proof.salts); // Checks target hash try { - const digest = genTargetHash(vcWithoutProof, decodedSalts, document.proof.privacy.obfuscated); - const targetHash = document.proof.targetHash; + const digest = genTargetHash(vcWithoutProof, decodedSalts, vc.proof.privacy.obfuscated); + const targetHash = vc.proof.targetHash; if (digest !== targetHash) return false; - // Calculates merkle root from target hash and proof, then compare to merkle root in document - return checkProof(document.proof.proofs, document.proof.merkleRoot, document.proof.targetHash); + // Calculates merkle root from target hash and proof, then compare to merkle root in VC + return checkProof(vc.proof.proofs, vc.proof.merkleRoot, vc.proof.targetHash); } catch (error: unknown) { if (error instanceof SaltNotFoundError) { return false; diff --git a/src/4.0/documentBuilder.ts b/src/4.0/vcBuilder.ts similarity index 100% rename from src/4.0/documentBuilder.ts rename to src/4.0/vcBuilder.ts diff --git a/test/fixtures/v4/__generated__/batched-wrapped-documents-did.json b/test/fixtures/v4/__generated__/digested-batched-vc-did.json similarity index 100% rename from test/fixtures/v4/__generated__/batched-wrapped-documents-did.json rename to test/fixtures/v4/__generated__/digested-batched-vc-did.json diff --git a/test/fixtures/v4/__generated__/wrapped-document-did-oscp.json b/test/fixtures/v4/__generated__/digested-vc-did-oscp.json similarity index 100% rename from test/fixtures/v4/__generated__/wrapped-document-did-oscp.json rename to test/fixtures/v4/__generated__/digested-vc-did-oscp.json diff --git a/test/fixtures/v4/__generated__/wrapped-document-did.json b/test/fixtures/v4/__generated__/digested-vc-did.json similarity index 100% rename from test/fixtures/v4/__generated__/wrapped-document-did.json rename to test/fixtures/v4/__generated__/digested-vc-did.json diff --git a/test/fixtures/v4/__generated__/batched-raw-documents-did.json b/test/fixtures/v4/__generated__/raw-batched-vc-did.json similarity index 100% rename from test/fixtures/v4/__generated__/batched-raw-documents-did.json rename to test/fixtures/v4/__generated__/raw-batched-vc-did.json diff --git a/test/fixtures/v4/__generated__/raw-document-did-oscp.json b/test/fixtures/v4/__generated__/raw-vc-did-oscp.json similarity index 100% rename from test/fixtures/v4/__generated__/raw-document-did-oscp.json rename to test/fixtures/v4/__generated__/raw-vc-did-oscp.json diff --git a/test/fixtures/v4/__generated__/raw-document-did.json b/test/fixtures/v4/__generated__/raw-vc-did.json similarity index 100% rename from test/fixtures/v4/__generated__/raw-document-did.json rename to test/fixtures/v4/__generated__/raw-vc-did.json diff --git a/test/fixtures/v4/__generated__/batched-signed-wrapped-documents-did.json b/test/fixtures/v4/__generated__/signed-batched-vc-did.json similarity index 100% rename from test/fixtures/v4/__generated__/batched-signed-wrapped-documents-did.json rename to test/fixtures/v4/__generated__/signed-batched-vc-did.json diff --git a/test/fixtures/v4/__generated__/signed-wrapped-document-did-obfuscated.json b/test/fixtures/v4/__generated__/signed-vc-did-obfuscated.json similarity index 100% rename from test/fixtures/v4/__generated__/signed-wrapped-document-did-obfuscated.json rename to test/fixtures/v4/__generated__/signed-vc-did-obfuscated.json diff --git a/test/fixtures/v4/__generated__/signed-wrapped-document-did-oscp.json b/test/fixtures/v4/__generated__/signed-vc-did-oscp.json similarity index 100% rename from test/fixtures/v4/__generated__/signed-wrapped-document-did-oscp.json rename to test/fixtures/v4/__generated__/signed-vc-did-oscp.json diff --git a/test/fixtures/v4/__generated__/signed-wrapped-document-did.json b/test/fixtures/v4/__generated__/signed-vc-did.json similarity index 100% rename from test/fixtures/v4/__generated__/signed-wrapped-document-did.json rename to test/fixtures/v4/__generated__/signed-vc-did.json From 180c9b8fd1881802d8849bfbce7fe05646b58ec6 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 10 Jan 2025 10:37:45 +0800 Subject: [PATCH 5/6] fix: suggestions to mix n match (#320) * fix: make types more granular * fix: use new types, assert that vc is proofless * refactor: bring w3cVc schema definitions tgt * fix: use new types * fix: update references to guards and types * fix: update references to guards and types * fix: tests * refactor: rename wrap to digest * refactor: already checking for proofless data model error in digest * fix: naming * refactor: reference proofless schema, custom err msg * fix: builder error shd have digest and sign errors * fix: tests * refactor: oaDigested should mean only digested, and not include signed documents * refactor: useless alias --- scripts/generateV4JsonSchemas.ts | 14 ++- src/4.0/__tests__/digest.test.ts | 15 ++- src/4.0/__tests__/documentBuilder.test.ts | 40 +++--- src/4.0/__tests__/e2e.test.ts | 27 ++-- src/4.0/__tests__/guard.test.ts | 56 +++++---- src/4.0/__tests__/hash.test.ts | 4 +- src/4.0/__tests__/obfuscate.test.ts | 10 +- src/4.0/__tests__/sign.test.ts | 31 +++-- src/4.0/__tests__/validate.test.ts | 4 +- src/4.0/diagnose.ts | 10 +- src/4.0/digest.ts | 45 ++++--- src/4.0/exports/digest.ts | 2 +- src/4.0/exports/types.ts | 2 +- src/4.0/exports/utils.ts | 6 +- src/4.0/fixtures.ts | 16 +-- src/4.0/hash.ts | 4 +- .../v4-digested-oa-vc.schema.json | 20 +-- .../__generated__/v4-oa-vc.schema.json | 98 ++++++++++++--- .../__generated__/v4-signed-oa-vc.schema.json | 18 +-- src/4.0/obfuscate.ts | 10 +- src/4.0/salt.ts | 10 +- src/4.0/sign.ts | 35 ++---- src/4.0/types.ts | 118 ++++++++++-------- src/4.0/validate.ts | 4 +- src/4.0/vcBuilder.ts | 20 ++- src/shared/utils/guard.ts | 4 +- src/shared/utils/utils.ts | 18 +-- 27 files changed, 373 insertions(+), 268 deletions(-) diff --git a/scripts/generateV4JsonSchemas.ts b/scripts/generateV4JsonSchemas.ts index 2f2e8ffa..9f5ad221 100644 --- a/scripts/generateV4JsonSchemas.ts +++ b/scripts/generateV4JsonSchemas.ts @@ -1,7 +1,11 @@ import fs from "fs"; import path from "path"; import { zodToJsonSchema } from "zod-to-json-schema"; -import { OAVerifiableCredential, DigestedOAVerifiableCredential, SignedOAVerifiableCredential } from "../src/4.0/types"; +import { + OAVerifiableCredential, + OADigestedOAVerifiableCredential, + OASignedOAVerifiableCredential, +} from "../src/4.0/types"; const OUTPUT_DIR = path.resolve("./src/4.0/jsonSchemas/__generated__"); @@ -19,13 +23,13 @@ const ZOD_SCHEMAS = [ }, { filename: "v4-digested-oa-vc.schema.json", - schemaName: "DigestedOAVerifiableCredential", - zodSchema: DigestedOAVerifiableCredential, + schemaName: "OADigestedOAVerifiableCredential", + zodSchema: OADigestedOAVerifiableCredential, }, { filename: "v4-signed-oa-vc.schema.json", - schemaName: "SignedOAVerifiableCredential", - zodSchema: SignedOAVerifiableCredential, + schemaName: "OASignedOAVerifiableCredential", + zodSchema: OASignedOAVerifiableCredential, }, ]; diff --git a/src/4.0/__tests__/digest.test.ts b/src/4.0/__tests__/digest.test.ts index 582fad0e..e97a519f 100644 --- a/src/4.0/__tests__/digest.test.ts +++ b/src/4.0/__tests__/digest.test.ts @@ -1,4 +1,9 @@ -import { OAVerifiableCredential, DigestedOAVerifiableCredential, W3cVerifiableCredential } from "../types"; +import { + OAVerifiableCredential, + ProoflessOAVerifiableCredential, + OADigestedOAVerifiableCredential, + W3cVerifiableCredential, +} from "../types"; import { digestVc } from "../digest"; describe("V4.0 digest", () => { @@ -21,7 +26,7 @@ describe("V4.0 digest", () => { identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, }, }); - const parsedResults = DigestedOAVerifiableCredential.safeParse(digested); + const parsedResults = OADigestedOAVerifiableCredential.safeParse(digested); if (!parsedResults.success) { throw new Error("Parsing failed"); } @@ -94,7 +99,7 @@ describe("V4.0 digest", () => { }, // this should not exist extraField: "extra", - } as OAVerifiableCredential) + } as ProoflessOAVerifiableCredential) ).rejects.toThrowErrorMatchingInlineSnapshot(` "Input VC does not conform to Open Attestation v4.0 Data Model: { @@ -118,8 +123,8 @@ describe("V4.0 digest", () => { id: "https://example.com/issuer/123", }, }; - const digested = await digestVc(genericW3cVc as unknown as OAVerifiableCredential); - const parsedResults = DigestedOAVerifiableCredential.pick({ "@context": true, type: true }) + const digested = await digestVc(genericW3cVc as unknown as ProoflessOAVerifiableCredential); + const parsedResults = OADigestedOAVerifiableCredential.pick({ "@context": true, type: true }) .passthrough() .safeParse(digested); expect(parsedResults.success).toBe(true); diff --git a/src/4.0/__tests__/documentBuilder.test.ts b/src/4.0/__tests__/documentBuilder.test.ts index 2de69937..bee67f37 100644 --- a/src/4.0/__tests__/documentBuilder.test.ts +++ b/src/4.0/__tests__/documentBuilder.test.ts @@ -1,7 +1,8 @@ import { validateDigest } from "../validate"; import { VcBuilder, VcBuilderErrors } from "../vcBuilder"; -import { isSignedOAVerifiableCredential, isDigestedOAVerifiableCredential, signVc } from "../exports"; +import { isOASignedOAVerifiableCredential, isOADigestedOAVerifiableCredential, signVc } from "../exports"; import { SAMPLE_SIGNING_KEYS } from "../fixtures"; +import { ProoflessOAVerifiableCredential } from "../types"; describe(`V4.0 VcBuilder`, () => { describe("given a single VC", () => { @@ -52,7 +53,7 @@ describe(`V4.0 VcBuilder`, () => { "type": "OpenAttestationOcspResponder", } `); - expect(isSignedOAVerifiableCredential(signed)).toBe(true); + expect(isOASignedOAVerifiableCredential(signed)).toBe(true); expect(validateDigest(signed)).toBe(true); }); @@ -89,8 +90,8 @@ describe(`V4.0 VcBuilder`, () => { "type": "OpenAttestationOcspResponder", } `); - expect(isDigestedOAVerifiableCredential(digested)).toBe(true); - expect(isSignedOAVerifiableCredential(digested)).toBe(false); + expect(isOADigestedOAVerifiableCredential(digested)).toBe(true); + expect(isOASignedOAVerifiableCredential(digested)).toBe(false); }); }); @@ -138,7 +139,7 @@ describe(`V4.0 VcBuilder`, () => { ] `); expect(signed[0].credentialStatus).toBeUndefined(); - expect(isSignedOAVerifiableCredential(signed[0])).toBe(true); + expect(isOASignedOAVerifiableCredential(signed[0])).toBe(true); expect(validateDigest(signed[0])).toBe(true); expect(signed[1].issuer).toMatchInlineSnapshot(` @@ -167,7 +168,7 @@ describe(`V4.0 VcBuilder`, () => { ] `); expect(signed[1].credentialStatus).toBeUndefined(); - expect(isSignedOAVerifiableCredential(signed[1])).toBe(true); + expect(isOASignedOAVerifiableCredential(signed[1])).toBe(true); expect(validateDigest(signed[1])).toBe(true); }); @@ -198,8 +199,8 @@ describe(`V4.0 VcBuilder`, () => { }, ] `); - expect(isDigestedOAVerifiableCredential(digested[0])).toBe(true); - expect(isSignedOAVerifiableCredential(digested[0])).toBe(false); + expect(isOADigestedOAVerifiableCredential(digested[0])).toBe(true); + expect(isOASignedOAVerifiableCredential(digested[0])).toBe(false); expect(digested[1].issuer).toMatchInlineSnapshot(` { @@ -226,8 +227,8 @@ describe(`V4.0 VcBuilder`, () => { }, ] `); - expect(isDigestedOAVerifiableCredential(digested[1])).toBe(true); - expect(isSignedOAVerifiableCredential(digested[1])).toBe(false); + expect(isOADigestedOAVerifiableCredential(digested[1])).toBe(true); + expect(isOASignedOAVerifiableCredential(digested[1])).toBe(false); }); }); @@ -405,16 +406,27 @@ describe(`V4.0 VcBuilder`, () => { let error; await expect(async () => { try { - await signVc(digested, "Secp256k1VerificationKey2018", SAMPLE_SIGNING_KEYS); + await signVc( + digested as unknown as ProoflessOAVerifiableCredential, + "Secp256k1VerificationKey2018", + SAMPLE_SIGNING_KEYS + ); } catch (e) { error = e; throw e; } }).rejects.toThrowErrorMatchingInlineSnapshot(` - "VC has already has proof object defined: - Either an unsigned or undigested VC must be provided" + "Input VC does not conform to Open Attestation v4.0 Data Model: + { + "_errors": [], + "proof": { + "_errors": [ + "VC has to be unsigned" + ] + } + }" `); - expect(error).toBeInstanceOf(VcBuilderErrors.VcProofNotEmptyError); + expect(error).toBeInstanceOf(VcBuilderErrors.DataModelValidationError); }); test("given re-setting of values, should throw", async () => { diff --git a/src/4.0/__tests__/e2e.test.ts b/src/4.0/__tests__/e2e.test.ts index e002a6e5..0ac66984 100644 --- a/src/4.0/__tests__/e2e.test.ts +++ b/src/4.0/__tests__/e2e.test.ts @@ -1,13 +1,8 @@ import { cloneDeep, omit } from "lodash"; -import { - digestVc, - digestVcs, - obfuscateOAVerifiableCredential, - validateDigest, - isDigestedOAVerifiableCredential, -} from "../exports"; +import { digestVc, digestVcs, obfuscateOAVerifiableCredential, validateDigest } from "../exports"; import type { OAVerifiableCredential } from "../exports"; import { RAW_VC_DID, SIGNED_VC_DID, DIGESTED_VC_DID } from "../fixtures"; +import { isOADigestedOAVerifiableCredential, ProoflessOAVerifiableCredential } from "../types"; const VC_ONE = { ...RAW_VC_DID, @@ -51,7 +46,7 @@ describe("V4.0 E2E Test Scenarios", () => { const missingData = { ...omit(cloneDeep(VC_ONE), "issuer"), }; - await expect(digestVc(missingData as unknown as OAVerifiableCredential)).rejects + await expect(digestVc(missingData as unknown as ProoflessOAVerifiableCredential)).rejects .toThrowErrorMatchingInlineSnapshot(` "Input VC does not conform to Open Attestation v4.0 Data Model: { @@ -87,7 +82,7 @@ describe("V4.0 E2E Test Scenarios", () => { test("checks that VC conforms to the schema", async () => { const digested = await digestVc(VC_ONE); - expect(isDigestedOAVerifiableCredential(digested)).toBe(true); + expect(isOADigestedOAVerifiableCredential(digested)).toBe(true); }); test("does not allow for the same merkle root to be generated", async () => { @@ -102,7 +97,7 @@ describe("V4.0 E2E Test Scenarios", () => { expect(newDigested.credentialSubject.key2).toBeDefined(); const obfuscatedVc = obfuscateOAVerifiableCredential(newDigested, ["credentialSubject.key2"]); expect(validateDigest(obfuscatedVc)).toBe(true); - expect(isDigestedOAVerifiableCredential(obfuscatedVc)).toBe(true); + expect(isOADigestedOAVerifiableCredential(obfuscatedVc)).toBe(true); expect(obfuscatedVc.credentialSubject.key2).toBeUndefined(); }); @@ -121,7 +116,7 @@ describe("V4.0 E2E Test Scenarios", () => { ...DATUM, { laurent: "task force, assemble!!", - } as unknown as OAVerifiableCredential, + } as unknown as ProoflessOAVerifiableCredential, ]; await expect(digestVcs(malformedDatum)).rejects.toThrowErrorMatchingInlineSnapshot(` "Input VC does not conform to Verifiable Credentials v2.0 Data Model: @@ -178,7 +173,7 @@ describe("V4.0 E2E Test Scenarios", () => { test("checks that VCs conforms to the schema", async () => { const digestedVcs = await digestVcs(DATUM); const validatedSchema = digestedVcs.reduce( - (prev: boolean, curr: any) => isDigestedOAVerifiableCredential(curr) && prev, + (prev: boolean, curr: any) => isOADigestedOAVerifiableCredential(curr) && prev, true ); expect(validatedSchema).toBe(true); @@ -194,11 +189,11 @@ describe("V4.0 E2E Test Scenarios", () => { describe("validate schema", () => { test("should return true when VC is a valid digested v4 VC and identityProof is DNS-DID", () => { - expect(isDigestedOAVerifiableCredential(DIGESTED_VC_DID)).toStrictEqual(true); + expect(isOADigestedOAVerifiableCredential(DIGESTED_VC_DID)).toStrictEqual(true); }); - test("should return true when signed VC is a valid signed v4 VC and identityProof is DNS-DID", () => { - expect(isDigestedOAVerifiableCredential(SIGNED_VC_DID)).toStrictEqual(true); + test("should return false when signed VC is a valid signed v4 VC and identityProof is DNS-DID", () => { + expect(isOADigestedOAVerifiableCredential(SIGNED_VC_DID)).toStrictEqual(false); }); test("should return false when VC is invalid due to no DNS-DID identifier", () => { @@ -208,7 +203,7 @@ describe("V4.0 E2E Test Scenarios", () => { ...RAW_VC_DID, issuer: modifiedIssuer, } satisfies OAVerifiableCredential; - expect(isDigestedOAVerifiableCredential(credential)).toStrictEqual(false); + expect(isOADigestedOAVerifiableCredential(credential)).toStrictEqual(false); }); }); diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index 3dc9807a..e9b94ed5 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -5,10 +5,12 @@ import { signVc } from "../sign"; import { W3cVerifiableCredential, OAVerifiableCredential, - Digested, - DigestedOAVerifiableCredential, - Signed, - SignedOAVerifiableCredential, + OADigestedOAVerifiableCredential, + OASignedOAVerifiableCredential, + OADigested, + OASigned, + OADigestedW3cVerifiableCredential, + OASignedW3cVerifiableCredential, } from "../types"; const RAW_VC = { @@ -26,8 +28,8 @@ const RAW_VC = { } satisfies OAVerifiableCredential; describe("V4.0 guard", () => { - let DIGESTED_VC: Digested; - let SIGNED_VC: Signed; + let DIGESTED_VC: OADigested; + let SIGNED_VC: OASigned; beforeAll(async () => { DIGESTED_VC = await digestVc(RAW_VC); SIGNED_VC = await signVc(RAW_VC, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { @@ -49,25 +51,25 @@ describe("V4.0 guard", () => { }); test("should fail digested VC validation", () => { - const results = DigestedOAVerifiableCredential.safeParse(RAW_VC_DID); + const results = OADigestedOAVerifiableCredential.safeParse(RAW_VC_DID); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` - [ZodError: [ - { - "code": "invalid_type", - "expected": "object", - "received": "undefined", - "path": [ - "proof" - ], - "message": "Required" - } - ]] - `); + [ZodError: [ + { + "code": "invalid_type", + "expected": "object", + "received": "undefined", + "path": [ + "proof" + ], + "message": "Required" + } + ]] + `); }); test("should fail signed VC validation", () => { - const results = SignedOAVerifiableCredential.safeParse(RAW_VC_DID); + const results = OASignedOAVerifiableCredential.safeParse(RAW_VC_DID); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -99,12 +101,12 @@ describe("V4.0 guard", () => { }); test("should pass digested VC validation without removal of any data", () => { - const results = DigestedOAVerifiableCredential.parse(DIGESTED_VC); + const results = OADigestedOAVerifiableCredential.parse(DIGESTED_VC); expect(results).toEqual(DIGESTED_VC); }); test("should fail signed VC validation", () => { - const results = SignedOAVerifiableCredential.safeParse(DIGESTED_VC); + const results = OASignedOAVerifiableCredential.safeParse(DIGESTED_VC); expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ @@ -146,14 +148,14 @@ describe("V4.0 guard", () => { expect(results).toEqual(SIGNED_VC); }); - test("should pass digested VC validation without removal of any data", () => { - const oaDigestedVc: Digested = SIGNED_VC; - const results = DigestedOAVerifiableCredential.parse(oaDigestedVc); - expect(results).toEqual(SIGNED_VC); + test("should fail digested VC validation without removal of any data", () => { + const oaDigestedVc: OASigned = SIGNED_VC; + const results = OADigestedW3cVerifiableCredential.safeParse(oaDigestedVc); + expect(results.success).toBe(false); }); test("should pass signed VC validation without removal of any data", () => { - const results = SignedOAVerifiableCredential.parse(SIGNED_VC); + const results = OASignedW3cVerifiableCredential.parse(SIGNED_VC); expect(results).toEqual(SIGNED_VC); }); }); diff --git a/src/4.0/__tests__/hash.test.ts b/src/4.0/__tests__/hash.test.ts index aa32d882..d8cdbee3 100644 --- a/src/4.0/__tests__/hash.test.ts +++ b/src/4.0/__tests__/hash.test.ts @@ -1,7 +1,7 @@ import { genTargetHash } from "../hash"; import { decodeSalt } from "../salt"; import { SIGNED_VC_DID as ROOT_CREDENTIAL } from "../fixtures"; -import { Signed } from "../types"; +import { OASigned } from "../types"; import { obfuscateOAVerifiableCredential } from "../obfuscate"; // All obfuscated VCs are generated from the ROOT_CREDENTIAL @@ -163,7 +163,7 @@ describe("V4.0 hash", () => { signature: "0x949b76d8df493a56c1cf21303a74d6a54904461c1c10f4619b43ad7d339c64467c61eb4c0873f279cd21d5bdd044d3af5318f14d63f57acbd4cde30f271f3eb71c", }, - } as unknown as Signed; + } as unknown as OASigned; const digest = genTargetHash( OBFUSCATED_DIGESTED_VC, diff --git a/src/4.0/__tests__/obfuscate.test.ts b/src/4.0/__tests__/obfuscate.test.ts index 5e3fd985..3f7992c0 100644 --- a/src/4.0/__tests__/obfuscate.test.ts +++ b/src/4.0/__tests__/obfuscate.test.ts @@ -3,7 +3,7 @@ import { obfuscateOAVerifiableCredential } from "../obfuscate"; import { get } from "lodash"; import { decodeSalt } from "../salt"; import { digestVc } from "../digest"; -import { Salt, OAVerifiableCredential, Digested } from "../types"; +import { OADigested, OASigned, OASalt, OAVerifiableCredential } from "../types"; import { validateDigest } from "../validate"; import { RAW_VC_DID, SIGNED_VC_DID_OBFUSCATED, DIGESTED_VC_DID } from "../fixtures"; import { hashLeafNode } from "../hash"; @@ -14,7 +14,7 @@ const makeOAVerifiableCredential = { +const findSaltByPath = (salts: string, path: string): OASalt | undefined => { return decodeSalt(salts).find((salt) => salt.path === path); }; @@ -25,7 +25,11 @@ const findSaltByPath = (salts: string, path: string): Salt | undefined => { * - the salt bound to the field has been removed * - the field has been removed */ -const expectRemovedFieldsWithoutArrayNotation = (field: string, vc: Digested, obfuscatedVc: Digested) => { +const expectRemovedFieldsWithoutArrayNotation = ( + field: string, + vc: OADigested | OASigned, + obfuscatedVc: OADigested | OASigned +) => { const value = get(vc, field); const salt = findSaltByPath(vc.proof.salts, field); diff --git a/src/4.0/__tests__/sign.test.ts b/src/4.0/__tests__/sign.test.ts index 26cb77ce..d683435e 100644 --- a/src/4.0/__tests__/sign.test.ts +++ b/src/4.0/__tests__/sign.test.ts @@ -1,8 +1,8 @@ import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign"; import { Wallet } from "@ethersproject/wallet"; import { RAW_VC_DID } from "../fixtures"; -import { SignedOAVerifiableCredential } from "../types"; import { signVc, signVcErrors } from "../sign"; +import { OASignedOAVerifiableCredential, ProoflessOAVerifiableCredential } from "../types"; describe("V4.0 sign", () => { it("should sign a VC", async () => { @@ -10,7 +10,7 @@ describe("V4.0 sign", () => { public: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", private: "0x497c85ed89f1874ba37532d1e33519aba15bd533cdcb90774cc497bfe3cde655", }); - const parsedResults = SignedOAVerifiableCredential.safeParse(signed); + const parsedResults = OASignedOAVerifiableCredential.safeParse(signed); if (!parsedResults.success) { throw new Error("Parsing failed"); } @@ -24,7 +24,7 @@ describe("V4.0 sign", () => { "tourist quality multiply denial diary height funny calm disease buddy speed gold" ); const signed = await signVc(RAW_VC_DID, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, wallet); - const parsedResults = SignedOAVerifiableCredential.safeParse(signed); + const parsedResults = OASignedOAVerifiableCredential.safeParse(signed); if (!parsedResults.success) { throw new Error("Parsing failed"); } @@ -43,19 +43,30 @@ describe("V4.0 sign", () => { let error; await expect(async () => { try { - await signVc(signedVc, SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, { - public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", - private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", - }); + await signVc( + signedVc as unknown as ProoflessOAVerifiableCredential, + SUPPORTED_SIGNING_ALGORITHM.Secp256k1VerificationKey2018, + { + public: "did:ethr:0xb6De3744E1259e1aB692f5a277f053B79429c5a2#controller", + private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5", + } + ); } catch (e) { error = e; throw e; } }).rejects.toThrowErrorMatchingInlineSnapshot(` - "VC has already has proof object defined: - Either an unsigned or undigested VC must be provided" + "Input VC does not conform to Open Attestation v4.0 Data Model: + { + "_errors": [], + "proof": { + "_errors": [ + "VC has to be unsigned" + ] + } + }" `); - expect(error).toBeInstanceOf(signVcErrors.VcProofNotEmptyError); + expect(error).toBeInstanceOf(signVcErrors.DataModelValidationError); }); it("should throw error if a key or signer is invalid", async () => { await expect( diff --git a/src/4.0/__tests__/validate.test.ts b/src/4.0/__tests__/validate.test.ts index 41d4c5dd..d7d74e7b 100644 --- a/src/4.0/__tests__/validate.test.ts +++ b/src/4.0/__tests__/validate.test.ts @@ -1,6 +1,6 @@ import { cloneDeep } from "lodash"; import { SIGNED_BATCHED_VC_DID, SIGNED_VC_DID } from "../fixtures"; -import { Signed } from "../types"; +import { OASigned } from "../types"; import { validateDigest } from "../validate"; const TEST_VCS = { @@ -40,7 +40,7 @@ describe("V4.0 validate", () => { issuer: { ...issuerWithoutName, fakename: name, // Key was originally "name" - } as unknown as Signed["issuer"], + } as unknown as OASigned["issuer"], }) ).toBe(false); }); diff --git a/src/4.0/diagnose.ts b/src/4.0/diagnose.ts index 9a547798..cd2e0cc2 100644 --- a/src/4.0/diagnose.ts +++ b/src/4.0/diagnose.ts @@ -1,17 +1,17 @@ import type { Diagnose } from "../shared/utils/@types/diagnose"; -import { OAVerifiableCredential, DigestedOAVerifiableCredential, SignedOAVerifiableCredential } from "./types"; +import { OADigestedOAVerifiableCredential, OASignedOAVerifiableCredential, OAVerifiableCredential } from "./types"; export const v4Diagnose: Diagnose = ({ document, kind, debug }) => { let Validator: | typeof OAVerifiableCredential - | typeof DigestedOAVerifiableCredential - | typeof SignedOAVerifiableCredential = OAVerifiableCredential; + | typeof OADigestedOAVerifiableCredential + | typeof OASignedOAVerifiableCredential = OAVerifiableCredential; if (kind === "raw") { Validator = OAVerifiableCredential; } else if (kind === "wrapped") { - Validator = DigestedOAVerifiableCredential; + Validator = OADigestedOAVerifiableCredential; } else { - Validator = SignedOAVerifiableCredential; + Validator = OASignedOAVerifiableCredential; } const results = Validator.safeParse(document); diff --git a/src/4.0/digest.ts b/src/4.0/digest.ts index 2e8f5264..c4638e4e 100644 --- a/src/4.0/digest.ts +++ b/src/4.0/digest.ts @@ -1,19 +1,24 @@ import { hashToBuffer } from "../shared/utils/hashing"; import { MerkleTree } from "../shared/merkle"; import { ContextUrl, ContextType, UnableToInterpretContextError, interpretContexts } from "./context"; -import { OAVerifiableCredential, Digested, W3cVerifiableCredential } from "./types"; +import { + ProoflessW3cVerifiableCredential, + ProoflessOAVerifiableCredential, + OADigested, + W3cVerifiableCredential, +} from "./types"; import { genTargetHash } from "./hash"; import { encodeSalt, salt } from "./salt"; import { ZodError } from "zod"; -export const digestVc = async ( +export const digestVc = async ( vc: T -): Promise> => { +): Promise> => { /* 1a. Try OpenAttestation VC validation, since most user will be issuing oa v4 */ - const oav4context = await OAVerifiableCredential.pick({ "@context": true }).passthrough().safeParseAsync(vc); // Superficial check on user intention - let validatedUndigestedVc: W3cVerifiableCredential | undefined; + const oav4context = await ProoflessOAVerifiableCredential.pick({ "@context": true }).passthrough().safeParseAsync(vc); // Superficial check on user intention + let validatedUndigestedVc: ProoflessW3cVerifiableCredential | undefined; if (oav4context.success) { - const oav4 = await OAVerifiableCredential.safeParseAsync(vc); + const oav4 = await ProoflessOAVerifiableCredential.safeParseAsync(vc); if (!oav4.success) { throw new DataModelValidationError("Open Attestation v4.0", oav4.error); } @@ -22,7 +27,7 @@ export const digestVc = async contexts.add(context)); } REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); - const finalContexts: OAVerifiableCredential["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; + const finalContexts: ProoflessOAVerifiableCredential["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; /* 4. Type validation */ // Ensure that required types are present and in the correct order @@ -56,7 +61,7 @@ export const digestVc = async types.add(type)); } REQUIRED_TYPES.forEach((t) => types.delete(t)); - const finalTypes: OAVerifiableCredential["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; + const finalTypes: ProoflessOAVerifiableCredential["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; const vcReadyForDigesting = { ...validatedUndigestedVc, @@ -67,7 +72,7 @@ export const digestVc = async buffer.toString("hex")); - return { + const unsignedOADigestedVc: OADigested = { ...vcReadyForDigesting, proof: { type: "OpenAttestationHashProof2018", @@ -89,15 +94,17 @@ export const digestVc = async ; + }; + + return unsignedOADigestedVc as OADigested; }; -export const digestVcs = async ( +export const digestVcs = async ( vcs: T[] -): Promise[]> => { +): Promise[]> => { // create individual verifiable credential const verifiableCredentials = await Promise.all(vcs.map((vc) => digestVc(vc))); @@ -129,15 +136,15 @@ export const digestVcs = async ( - original: W3cVerifiableCredential, +function extractAndAssertAsOAVerifiableCredentialProps( + original: ProoflessW3cVerifiableCredential, keys: K[] ) { const temp: Record = {}; Object.entries(original).forEach(([k, v]) => { if (keys.includes(k as K)) temp[k] = v; }); - return temp as { [key in K]: OAVerifiableCredential[key] }; + return temp as { [key in K]: ProoflessOAVerifiableCredential[key] }; } class DataModelValidationError extends Error { @@ -147,7 +154,7 @@ class DataModelValidationError extends Error { } } -export const wrapVcErrors = { +export const digestVcErrors = { DataModelValidationError, UnableToInterpretContextError, }; diff --git a/src/4.0/exports/digest.ts b/src/4.0/exports/digest.ts index 8e549bf6..2ae329b1 100644 --- a/src/4.0/exports/digest.ts +++ b/src/4.0/exports/digest.ts @@ -1 +1 @@ -export { digestVc, digestVcs, wrapVcErrors } from "../digest"; +export { digestVc, digestVcs, digestVcErrors } from "../digest"; diff --git a/src/4.0/exports/types.ts b/src/4.0/exports/types.ts index 566f82d5..b863cb5b 100644 --- a/src/4.0/exports/types.ts +++ b/src/4.0/exports/types.ts @@ -1 +1 @@ -export type { OAVerifiableCredential, Digested, Signed } from "../types"; +export type { OAVerifiableCredential, OADigested, OASigned } from "../types"; diff --git a/src/4.0/exports/utils.ts b/src/4.0/exports/utils.ts index 61f0daa1..e01b88c3 100644 --- a/src/4.0/exports/utils.ts +++ b/src/4.0/exports/utils.ts @@ -1,3 +1,7 @@ export { v4Diagnose as diagnose } from "../diagnose"; -export { isOAVerifiableCredential, isDigestedOAVerifiableCredential, isSignedOAVerifiableCredential } from "../types"; +export { + isOAVerifiableCredential, + isOADigestedOAVerifiableCredential, + isOASignedOAVerifiableCredential, +} from "../types"; export { computeDigestMultibase } from "../computeDigestMultibase"; diff --git a/src/4.0/fixtures.ts b/src/4.0/fixtures.ts index dad0dafb..f95cd46d 100644 --- a/src/4.0/fixtures.ts +++ b/src/4.0/fixtures.ts @@ -1,4 +1,4 @@ -import { OAVerifiableCredential, Digested, Signed } from "./types"; +import { OAVerifiableCredential, OADigested, OASigned } from "./types"; import { ContextUrl } from "./context"; const ISSUER_ID = "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90" as const; @@ -207,7 +207,7 @@ export const DIGESTED_VC_DID = freezeObject({ "W3sidmFsdWUiOiJhOGEzMGE4ZTFjNWQ4ODk2NWI3NDZkZjBhYWYyMTMyN2Q4MDNkMzQ4ZThlOGRhMTlmNTNhMWU5ODFkOTFhMDQ0IiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6IjFmMzIwMzg4MjU3NTRkZTc1OGYwYmU2NjdiNjQ0ZjNjZGVkM2FlM2UwOGI0MTdhMmViZTljYmU1NmYyNGM0NTAiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiODQ0OTkwM2FhNDMxZDEzZTEzNTBiYjVhZTczMTM3OTRlMGQyMTMwNmM3NDA0YzI4NzJhY2Y3ZDY2NGIyMjNhZiIsInBhdGgiOiJuYW1lIn0seyJ2YWx1ZSI6ImFkN2Y1Mjg0OTc1MGViNjZhNjJlZmFmYWUwYjQxNGEwZGQ5OGUwNGJkMmI5YzU2NjliYWM1YzRiNDNjMDk3MTMiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJjY2I4ZDFkZDgyMDc2Y2EyOTQ5MWUxZTBjODAxOGM5MWY0Zjc5NGRiM2RkMDA1YmFjMGY4MzM1YmFmODFmZWRkIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiYmNlNzNhMjBlMDNiNmM0ZDM1M2VkY2IzMTM0NzZhOTZhNTRkMGNjYzVkNWQ1OWIzMjRhOWU1YTQ2NjQzZmFiNiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMjBhMDM0ZjcxMDliNDRmOGEyZTIxMWM1ZTE5YzQ2Nzk1NGY2OWU2NmQzOTZjZjFlYjk1NTViZDc2NjkyN2UyNSIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiIwNWVmYTdiNWM1MDFhZWIxNTE5NTE0MDczNzdmYjJmODc2MTk1ZTAzYzkwZjUzZTdhYWZjNGMzZmFhNDI1YjhhIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjEzYzE3YjQ5ZTc2YjQ3NjJjZGRiYmRjYjFiZDU2ZmUyNDIyZDEwYmJkMmY2MjAzZGZiNzRkZGRlYjBiZWNkYTMiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiNjBmM2JiMTY1YjhlMzcxOGJhZjQ0ZjVlMTdkNDljY2Y4ZGE5MGViYTMxNjUwZDRjM2IzODlkNmFiZGFiNTViYiIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6ImY1YjFjYjc3ZTZmNDQwM2NmMmM4NDg1MGIwNTcyMGI5NTk5Yjk0NmUwMWI2MzcwODUzZWY0YzUyYmQwYTZmZjEiLCJwYXRoIjoidmFsaWRGcm9tIn0seyJ2YWx1ZSI6ImM3MWM3ZDZjYTdhMjY5OWVhZjdjOTgwYzlmMjM1MWY3NDc3ZDliZDFlNzJlNGY2NTIxZjZhMzI0ZWEzYjdmMWYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiMDdlODkzMTgyZGFjNjRjOWVkZGU4MjMwYzdjZTdmMWM2NTRmZjgxN2Y5OGIzZTkxMWU4ZTg1Yzc4ODY0MWZhZCIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6ImU4YzdmMjQyYTI5YThmYjJiMjEyMjVhYzlmOTk5ZmVhOTNlNmRhYzc5YTNlYjQwYWRlMTc2ZGRmYzFjMmRlMTgiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiI5ZDNkMThlMTY0YTg3YmQ3MmFlNDczYTIzZjc5ZjBkNzU2NTFiZjExODViMmI0N2ZlYjhiOGFiNWU3YWY1YzUyIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6Ijk4MjIwZTE5NmU4YmE4NWI3MDc2YzdiMzE1MDBkOTU0Nzk1MTk5NDQ4YmM1Y2IyMzM0ZjNhYjU1OTA3NGNkNTMiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiNTNlOTdmNDBkZTExNDkxMjNlNmNlMmNhN2I0MzlhMzI3NzYxMGZkNmZmZTZlMTcwYjEwMjdlOWMzNThmYjg2MSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6IjA1OTQ1MWQzMWNlZjM5MDg1YWMxNGVkYjE1NjJjYzFkNTE0YmYzZWQ0N2I3YzBkNWM0MjdiYmM0NGNlOGU5YmIiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiN2E3YmUzMzMzNjI4MDAyNmVkN2NkZmFlZDkwZWI1Zjg0ZDZiMGVkZjdiNTkxZjk5MjQ3NmYzNDBjMWViZjUzYyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiJkNzMxMDA3NmM1NzZmNzU0MzcwNjQ5MTYxOTEyNWY0YmQ5NDNlMDEwNWM3ZDM1ZjZjNThjZTI3ZjcwMzNiNjliIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifSx7InZhbHVlIjoiYTk3MzFhMzFkMzkzNmJmNjAyNzMxNTAwYjIzMjY3ZTA2MzcxOGEzZjJkMGFiZTI4MDhlOGJiMzQxODQxYWZlZCIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0uaWQifSx7InZhbHVlIjoiNDg4MjRkYjdjY2U3ZTY5MGQ3NjgyMGM1N2M1OWNlZGI5ZDZiNGVjMDlhMDY0ZDFmYTJhMmI0OGZhYzJlN2FhZCIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udHlwZSJ9LHsidmFsdWUiOiJjMzY1M2FkNzg4MzhkNDhmM2Q1ZGNkNmE2OGRmNGU0MmMxMTM1ZmY4MzhiYzI5MTY4NDQzMDdjZDljZmM4ZWY4IiwicGF0aCI6InJlbmRlck1ldGhvZFswXS50ZW1wbGF0ZU5hbWUifV0=", privacy: { obfuscated: [] }, }, -} satisfies Digested); +} satisfies OADigested); export const DIGESTED_VC_DID_OSCP = freezeObject({ "@context": [ @@ -254,7 +254,7 @@ export const DIGESTED_VC_DID_OSCP = freezeObject({ "W3sidmFsdWUiOiI3N2RhNDUzOWYwY2M3ZmVmODg0ZmU0MTVkNzE2ZTRjODc5N2NiMDMyZGJlZDQzOWM2ZWViOTU2NmJlZDk1MmI0IiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6ImY2NWZhZWI4MzVmZTI4MzYyMDBhZGUyYTUzZjM4MzJkMGE2YTVjZjZiZjc2OGRlNmMxYjE3OTQ1OGIwMGI2MDIiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiMjVkOGNmNDY3MTAzMmMzMTUzZDdlY2I2OTQ1OGU2MzNkZWE0YmYwYTc0MGI4YzZiMDFlYjE5M2I4NzE2ZDYzYSIsInBhdGgiOiJuYW1lIn0seyJ2YWx1ZSI6ImQxNjEyODkzZGI2YjM3MDY0MjgzM2FkNjYwYjQ5N2ZiMTY0ZWZlZTZkNWY0ZDhhMjg0YjkxNWNkNGNhNzJkM2YiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiJjZjkzMTg5ZTBjNTE0ZGUwMWJlOTI5ZWRhNjk4ZTdlOWQ5ZmRiMzJlOTVjZTdlOTM1NGM4OWJlYjc3Mzg1NjNkIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiYzgzNzJlYmU2NWJiMzdhOTI0YTljMmZiNGE3Yzc4MmQxMzI1ZjE0NTY3OTFjODJmZmI4NGUwY2FmYWFlMDg2OCIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMzg1MzJhNzJiMDA1Njc4Yjc2M2Y0NTdlY2IxZTI1NzhhMDVkYzQ5ZjdlZDhhYzk5N2EyNDJjZWNjNGY3MDcyMiIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiIzMTQ1OWY5ZmUyNTdkMDVlZTkwNjg4NmYxYmU3ZjBmOTU4YTUxZGM3YTJlNTY5N2EyOGNjZjI3YWVhOGRmNDg4IiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6ImVkOTQ0Mzk0ZmQ5YzY3OWI5MDg0MjNmNjJlZWU5M2YxODJmNjdmZmIxM2MxNGM2ODJjZDMyZmNkMTk3MmVlN2IiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiYTBjODBhOTI4ZGI3MDExYTI0ODIzYzUzZGJlNjNmNGU5ZTc3M2IyMjkyZWNjOThkMWFiNjZiMjVjYTBmYzY3YiIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6Ijk4Y2JlZjE3NDZkZjM1MmQ5Njg4NmYyYWQ1N2NmOWI5ODg2ZWJhZTJlYzA1ZTM4YWE1YTc5ZTM2YTE2OWY1NGMiLCJwYXRoIjoidmFsaWRGcm9tIn0seyJ2YWx1ZSI6IjlhYzhkMzA5ZGEyZGYzNWNhN2RkNDFkYTc3NzRkYzFhNWY4NTE3NmFiNGU3ZGY1MDgzNzBiNDNlNmU2Y2FhNGEiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiZjkwYWU2YWVjNzlhODg0OTJkYzFlN2IwYThmNDExYWEwN2Y2YjY5NGMwZjQzNjhhZTMzZWVlNTllYzVhZDM2NyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6IjBhMWNhMTQwMmI4MDEwNWQzNGY4NmVjZjNjMDgxYTE3ZTVlODhiY2UwN2ZjNzgyMGRkYzdmZDY1OTA5ZDcwM2MiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiI2NTk1NGE0ZTNiZGRlNmQ5NGEyYjA4OTQ3YTU3YTdkOWEzYzAwNWEyN2ZmNzA0ZmNjMDI2MDI0MmNkNjczNGI1IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6Ijg3ZDc4NzBjYmVkOGZkYzIyNjA4MWMyZmY5ZmZmNmU3ZmJiZWYyMDUyMDg5YjU1MDg4MDg4MzliNWZlMWNlMGUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiMTgwMzBkZjQ5MzRhMDhlYmM3YTEwNjZlOWRlODZhMDAxYmZhNjcyNWI2Y2FiYjA5NGNmZWI5NzE4YTU3ZDViNiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImUyNWQ1MzFmMTIwNzM0ZWY2ZmY1MTU3MjViYjM5MGJkMjU4MTE2NWM4YTMxZTViMTRmNWUzZTMzM2I2OGZmNWUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiMTNiMjYyN2E4Yzk0YzkwYWI0M2JjZGExNDNkNTI2MDM0YWM0ZDVkNThhMTc2OTIzMDcwZTAzMGM2MTkwOWVlYiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiI2YjIzZWZkODVhZjZjZWZkMTBjM2EwNzczNjdlMjE4Mzc1MTlkN2ExYTBhMzVmODFkZDBhNWYzNTA0MTg4NjE4IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifSx7InZhbHVlIjoiMTI4MzY5ZDk5NTU2ZGYwOWMwOGE4NmZkODU4NmJlMWJlNWVjODcxYjY3NGQwNzJmN2U4ODdmNWZiYjViNjE5NiIsInBhdGgiOiJjcmVkZW50aWFsU3RhdHVzLmlkIn0seyJ2YWx1ZSI6ImYzNTBjOGYwNjlkNmIyM2M3NmE0MjQ3ZTIyOWRjOGM1MDVjMTFhZTNkNjFmYjE3ZDJlNDIxZWU1NzY4OGQ4YTMiLCJwYXRoIjoiY3JlZGVudGlhbFN0YXR1cy50eXBlIn0seyJ2YWx1ZSI6IjJkMTUzYzc1OGNiMTY1YjM1MTFhNjA4MjBkMzNiY2ZmYTViNmE3OWFiNWI5ZDNlMTA0NGZiNTk0NjNhNzM3MDUiLCJwYXRoIjoicmVuZGVyTWV0aG9kWzBdLmlkIn0seyJ2YWx1ZSI6ImJmOGJlY2M2Yjg1MDJkODBiNTg4ZmRmZmJhY2JmMmU1NTIzNjE1MzBjYmUxMGI4NzM5OTQ0NWYwZmZkYTkwOTAiLCJwYXRoIjoicmVuZGVyTWV0aG9kWzBdLnR5cGUifSx7InZhbHVlIjoiOTRmYmI5NWE1ZmNhZjU0YTcwYTAxODZiNjg1OWM5YmY5MzYzNWU0OTQ0N2U3ZmMxYWIyY2RmNTM5ZDllZjNiNyIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udGVtcGxhdGVOYW1lIn1d", privacy: { obfuscated: [] }, }, -} satisfies Digested); +} satisfies OADigested); export const DIGESTED_BATCHED_VC_DID = freezeObject([ { @@ -350,7 +350,7 @@ export const DIGESTED_BATCHED_VC_DID = freezeObject([ privacy: { obfuscated: [] }, }, }, -] satisfies Digested[]); +] satisfies OADigested[]); /* Signed */ export const SIGNED_VC_DID = freezeObject({ @@ -400,7 +400,7 @@ export const SIGNED_VC_DID = freezeObject({ signature: "0x949b76d8df493a56c1cf21303a74d6a54904461c1c10f4619b43ad7d339c64467c61eb4c0873f279cd21d5bdd044d3af5318f14d63f57acbd4cde30f271f3eb71c", }, -} satisfies Signed); +} satisfies OASigned); export const SIGNED_VC_DID_OSCP = freezeObject({ "@context": [ @@ -450,7 +450,7 @@ export const SIGNED_VC_DID_OSCP = freezeObject({ signature: "0xa9f89c00bac009044f02ca0e0c605389a927e4b011fa7c0f9a3bfd987598d8a442cd51218a31e387737ad42adeb9b9405c545a4d70ad75d06f7a7701e87440631c", }, -} satisfies Signed); +} satisfies OASigned); export const SIGNED_VC_DID_OBFUSCATED = freezeObject({ "@context": [ @@ -495,7 +495,7 @@ export const SIGNED_VC_DID_OBFUSCATED = freezeObject({ renderMethod: [ { id: "https://demo-renderer.opencerts.io", type: "OpenAttestationEmbeddedRenderer", templateName: "GOVTECH_DEMO" }, ], -} satisfies Signed); +} satisfies OASigned); export const SIGNED_BATCHED_VC_DID = freezeObject([ { @@ -597,7 +597,7 @@ export const SIGNED_BATCHED_VC_DID = freezeObject([ "0xc65309e0adf50ba6b91607c6913e15cd629412cf8180255e52c160cdf59bcfa0609f6eed71c379e3062b9fea39a5590dfc54323a352933c6ef9b694b63e2d74f1c", }, }, -] satisfies Signed[]); +] satisfies OASigned[]); // Freeze fixture to prevent accidental changes during tests function freezeObject(obj: T): T { diff --git a/src/4.0/hash.ts b/src/4.0/hash.ts index 6ad3eaa5..5d8961c8 100644 --- a/src/4.0/hash.ts +++ b/src/4.0/hash.ts @@ -1,10 +1,10 @@ import { sortBy } from "lodash"; import { keccak256 } from "js-sha3"; -import { W3cVerifiableCredential, Salt } from "./types"; +import { W3cVerifiableCredential, OASalt } from "./types"; import { LeafValue, traverseAndFlatten } from "./traverseAndFlatten"; import { hashToBuffer } from "../shared/utils/hashing"; -export const genTargetHash = (vc: W3cVerifiableCredential, salts: Salt[], obfuscatedData: string[]) => { +export const genTargetHash = (vc: W3cVerifiableCredential, salts: OASalt[], obfuscatedData: string[]) => { // find all leaf nodes in the vc and hash them // proof is not part of the digest const { proof: _, ...vcWithoutProof } = vc; diff --git a/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json b/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json index d274ec90..54454c71 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-digested-oa-vc.schema.json @@ -1,7 +1,7 @@ { - "$ref": "#/definitions/DigestedOAVerifiableCredential", + "$ref": "#/definitions/OADigestedOAVerifiableCredential", "definitions": { - "DigestedOAVerifiableCredential": { + "OADigestedOAVerifiableCredential": { "type": "object", "properties": { "@context": { @@ -59,7 +59,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OADigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -77,7 +77,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OADigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -96,7 +96,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OADigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string", @@ -225,7 +225,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OADigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -242,7 +242,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OADigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -262,7 +262,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OADigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -279,7 +279,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/DigestedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OADigestedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -344,7 +344,7 @@ "salts", "privacy" ], - "additionalProperties": true + "additionalProperties": false }, "renderMethod": { "type": "array", diff --git a/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json b/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json index 21e23006..ae9e54e4 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-oa-vc.schema.json @@ -299,28 +299,98 @@ "type": "object", "properties": { "type": { + "type": "string", + "const": "OpenAttestationHashProof2018" + }, + "proofPurpose": { + "type": "string", + "const": "assertionMethod" + }, + "targetHash": { + "type": "string" + }, + "proofs": { + "type": "array", + "items": { + "type": "string" + } + }, + "merkleRoot": { "type": "string" + }, + "salts": { + "type": "string" + }, + "privacy": { + "type": "object", + "properties": { + "obfuscated": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "obfuscated" + ], + "additionalProperties": false } }, "required": [ - "type" + "type", + "proofPurpose", + "targetHash", + "proofs", + "merkleRoot", + "salts", + "privacy" ], - "additionalProperties": true + "additionalProperties": false }, { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - } + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/OAVerifiableCredential/properties/proof/anyOf/0/properties/type" }, - "required": [ - "type" - ], - "additionalProperties": true - } + "proofPurpose": { + "$ref": "#/definitions/OAVerifiableCredential/properties/proof/anyOf/0/properties/proofPurpose" + }, + "targetHash": { + "$ref": "#/definitions/OAVerifiableCredential/properties/proof/anyOf/0/properties/targetHash" + }, + "proofs": { + "$ref": "#/definitions/OAVerifiableCredential/properties/proof/anyOf/0/properties/proofs" + }, + "merkleRoot": { + "$ref": "#/definitions/OAVerifiableCredential/properties/proof/anyOf/0/properties/merkleRoot" + }, + "salts": { + "$ref": "#/definitions/OAVerifiableCredential/properties/proof/anyOf/0/properties/salts" + }, + "privacy": { + "$ref": "#/definitions/OAVerifiableCredential/properties/proof/anyOf/0/properties/privacy" + }, + "key": { + "type": "string" + }, + "signature": { + "type": "string" + } + }, + "required": [ + "type", + "proofPurpose", + "targetHash", + "proofs", + "merkleRoot", + "salts", + "privacy", + "key", + "signature" + ], + "additionalProperties": false } ] }, diff --git a/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json b/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json index cc1f4c92..35b97abc 100644 --- a/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json +++ b/src/4.0/jsonSchemas/__generated__/v4-signed-oa-vc.schema.json @@ -1,7 +1,7 @@ { - "$ref": "#/definitions/SignedOAVerifiableCredential", + "$ref": "#/definitions/OASignedOAVerifiableCredential", "definitions": { - "SignedOAVerifiableCredential": { + "OASignedOAVerifiableCredential": { "type": "object", "properties": { "@context": { @@ -59,7 +59,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OASignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -77,7 +77,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OASignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -96,7 +96,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OASignedOAVerifiableCredential/properties/id" }, "type": { "type": "string", @@ -225,7 +225,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OASignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -242,7 +242,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OASignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -262,7 +262,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OASignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" @@ -279,7 +279,7 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/SignedOAVerifiableCredential/properties/id" + "$ref": "#/definitions/OASignedOAVerifiableCredential/properties/id" }, "type": { "type": "string" diff --git a/src/4.0/obfuscate.ts b/src/4.0/obfuscate.ts index 2d8e09dd..5927aef7 100644 --- a/src/4.0/obfuscate.ts +++ b/src/4.0/obfuscate.ts @@ -1,10 +1,10 @@ import { cloneDeep, get, unset, pick, toPath } from "lodash"; import { decodeSalt, encodeSalt } from "./salt"; import { traverseAndFlatten } from "./traverseAndFlatten"; -import { Override, PartialDeep, Signed, Digested, DigestedOAVerifiableCredential } from "./types"; +import { OADigested, OASigned, Override, PartialDeep, OADigestedOrSignedOAVerifiableCredential } from "./types"; import { hashLeafNode } from "./hash"; -const obfuscate = (_data: Digested | Signed, fields: string[] | string) => { +const obfuscate = (_data: OADigested | OASigned, fields: string[] | string) => { const data = cloneDeep(_data); // Prevents alteration of original data const fieldsAsArray = ([] as string[]).concat(fields); @@ -73,7 +73,7 @@ const obfuscate = (_data: Digested | Signed, fields: string[] | string) => { }; }; -export type ObfuscateOAVerifiableCredentialResult = Override< +export type ObfuscateOAVerifiableCredentialResult = Override< T, { credentialSubject: Override< @@ -86,7 +86,7 @@ export type ObfuscateOAVerifiableCredentialResult = Override >; } >; -export const obfuscateOAVerifiableCredential = ( +export const obfuscateOAVerifiableCredential = ( vc: T, fields: string[] | string ): ObfuscateOAVerifiableCredentialResult => { @@ -95,7 +95,7 @@ export const obfuscateOAVerifiableCredential = ( const newObfuscatedData = currentObfuscatedData.concat(obfuscatedData); // assert that obfuscated is still compliant to our schema - const parsedResults = DigestedOAVerifiableCredential.safeParse({ + const parsedResults = OADigestedOrSignedOAVerifiableCredential.safeParse({ ...data, proof: { ...data.proof, diff --git a/src/4.0/salt.ts b/src/4.0/salt.ts index 5de5a92d..fa42c0e4 100644 --- a/src/4.0/salt.ts +++ b/src/4.0/salt.ts @@ -1,4 +1,4 @@ -import { Salt } from "./types"; +import { OASalt } from "./types"; import { Base64 } from "js-base64"; import { traverseAndFlatten } from "./traverseAndFlatten"; import randomBytes from "randombytes"; @@ -23,15 +23,15 @@ const illegalCharactersCheck = (data: Record) => { // Using hex encoding as compared to base64 for constant string length export const secureRandomString = () => randomBytes(ENTROPY_IN_BYTES).toString("hex"); -export const salt = (data: any): Salt[] => { +export const salt = (data: any): OASalt[] => { // Check for illegal characters e.g. '.', '[' or ']' illegalCharactersCheck(data); return traverseAndFlatten(data, ({ path }) => ({ value: secureRandomString(), path })); }; -export const encodeSalt = (salts: Salt[]): string => Base64.encode(JSON.stringify(salts)); -export const decodeSalt = (salts: string): Salt[] => { - const decoded: Salt[] = JSON.parse(Base64.decode(salts)); +export const encodeSalt = (salts: OASalt[]): string => Base64.encode(JSON.stringify(salts)); +export const decodeSalt = (salts: string): OASalt[] => { + const decoded: OASalt[] = JSON.parse(Base64.decode(salts)); decoded.forEach((salt) => { if (salt.value.length !== ENTROPY_IN_BYTES * 2) throw new Error(`Salt must be ${ENTROPY_IN_BYTES} bytes`); }); diff --git a/src/4.0/sign.ts b/src/4.0/sign.ts index 48c53a69..b48c55a6 100644 --- a/src/4.0/sign.ts +++ b/src/4.0/sign.ts @@ -2,23 +2,17 @@ import { Signer } from "@ethersproject/abstract-signer"; import { sign } from "../shared/signer"; import { SigningKey } from "../shared/@types/sign"; -import { digestVc } from "./digest"; -import { Digested, OAVerifiableCredential, W3cVerifiableCredential, Signed } from "./types"; +import { digestVc, digestVcErrors } from "./digest"; +import type { ProoflessOAVerifiableCredential, ProoflessW3cVerifiableCredential, OASigned } from "./types"; -export const signVc = async ( +export const signVc = async ( unsignedVc: T, algorithm: "Secp256k1VerificationKey2018", keyOrSigner: SigningKey | Signer -): Promise> => { +): Promise> => { /* 1. Input VC needs to be digested first */ - let validatedProof: Digested["proof"]; - if (!unsignedVc.proof) { - const digestedVc = await digestVc(unsignedVc); - validatedProof = digestedVc.proof; - } else { - // Do not accept a VC that already has proof object defined - throw new VcProofNotEmptyError(new Error("Either an unsigned or undigested VC must be provided")); - } + const digestedVc = await digestVc(unsignedVc); + const validatedProof = digestedVc.proof; const merkleRoot = `0x${validatedProof.merkleRoot}`; @@ -30,28 +24,17 @@ export const signVc = async ; + return { ...unsignedVc, proof } as OASigned; } catch (error) { throw new CouldNotSignVcError(error); } }; -class VcProofNotEmptyError extends Error { - constructor(public error: unknown) { - super( - `VC has already has proof object defined:\n${ - error instanceof Error ? error.message : JSON.stringify(error, null, 2) - }` - ); - Object.setPrototypeOf(this, VcProofNotEmptyError.prototype); - } -} - /** * Cases where this can be thrown includes: network error, invalid keys or signer */ @@ -63,6 +46,6 @@ class CouldNotSignVcError extends Error { } export const signVcErrors = { - VcProofNotEmptyError, + ...digestVcErrors, CouldNotSignVcError, }; diff --git a/src/4.0/types.ts b/src/4.0/types.ts index d0b115b0..006e23b7 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -150,11 +150,9 @@ const _W3cVerifiableCredential = z.object({ ]) .optional(), }); -// W3cVerifiableCredential should always allow extra root properties -export const W3cVerifiableCredential = _W3cVerifiableCredential.passthrough(); -const IdentityProofType = z.union([z.literal("DNS-TXT"), z.literal("DNS-DID"), z.literal("DID")]); -const Salt = z.object({ value: z.string(), path: z.string() }); +const OAIdentityProofType = z.union([z.literal("DNS-TXT"), z.literal("DNS-DID"), z.literal("DID")]); +const OASalt = z.object({ value: z.string(), path: z.string() }); export const DecentralisedEmbeddedRenderer = z.object({ // Must have id match url pattern @@ -198,6 +196,21 @@ export const RevocationStoreRevocation = z.object({ type: z.literal("OpenAttestationRevocationStore"), }); +const OADigestedProof = z + .object({ + type: z.literal("OpenAttestationHashProof2018"), + proofPurpose: z.literal("assertionMethod"), + targetHash: z.string(), + proofs: z.array(z.string()), + merkleRoot: z.string(), + salts: z.string(), + privacy: z.object({ obfuscated: z.array(z.string()) }), + }) + .strict(); + +const OASignedProof = OADigestedProof.extend({ key: z.string(), signature: z.string() }).strict(); +const OAProof = z.union([OADigestedProof, OASignedProof]); + export const OAVerifiableCredential = _W3cVerifiableCredential .extend({ "@context": z @@ -219,7 +232,7 @@ export const OAVerifiableCredential = _W3cVerifiableCredential type: z.literal("OpenAttestationIssuer"), name: z.string(), identityProof: z.object({ - identityProofType: IdentityProofType, + identityProofType: OAIdentityProofType, identifier: z.string(), }), }), @@ -238,60 +251,63 @@ export const OAVerifiableCredential = _W3cVerifiableCredential // [Optional] Render Method renderMethod: z.array(z.discriminatedUnion("type", [DecentralisedEmbeddedRenderer, SvgRenderer])).optional(), + + proof: OAProof.optional(), }) .strict(); -const DigestedProof = z.object({ - type: z.literal("OpenAttestationHashProof2018"), - proofPurpose: z.literal("assertionMethod"), - targetHash: z.string(), - proofs: z.array(z.string()), - merkleRoot: z.string(), - salts: z.string(), - privacy: z.object({ obfuscated: z.array(z.string()) }), +const Proofless = z + .undefined({ + message: "VC has to be unsigned", + }) + .optional(); + +export const ProoflessOAVerifiableCredential = OAVerifiableCredential.extend({ + proof: Proofless, +}); +export const OADigestedOrSignedOAVerifiableCredential = OAVerifiableCredential.extend({ + proof: OAProof, +}); +export const OADigestedOAVerifiableCredential = OAVerifiableCredential.extend({ + proof: OADigestedProof, +}); +export const OASignedOAVerifiableCredential = OAVerifiableCredential.extend({ + proof: OASignedProof, }); -const DigestedOAVerifiableCredentialExtrasShape = { proof: DigestedProof.passthrough() } as const; -// DigestedOAVerifiableCredential should never allow extra root properties -export const DigestedOAVerifiableCredential = OAVerifiableCredential.extend( - DigestedOAVerifiableCredentialExtrasShape -).strict(); - -const SignedProof = DigestedProof.extend({ key: z.string(), signature: z.string() }); -const SignedOAVerifiableCredentialExtrasShape = { proof: SignedProof } as const; -// SignedOAVerifiableCredential should never allow extra root properties -export const SignedOAVerifiableCredential = OAVerifiableCredential.extend( - SignedOAVerifiableCredentialExtrasShape -).strict(); -export type W3cVerifiableCredential = z.infer; +// W3cVerifiableCredential should always allow extra root properties +export const W3cVerifiableCredential = _W3cVerifiableCredential.passthrough(); +export const ProoflessW3cVerifiableCredential = W3cVerifiableCredential.extend({ + proof: Proofless, +}); +export const OADigestedOrSignedW3cVerifiableCredential = W3cVerifiableCredential.extend({ + proof: OAProof, +}); +export const OADigestedW3cVerifiableCredential = W3cVerifiableCredential.extend({ + proof: OADigestedProof, +}); +export const OASignedW3cVerifiableCredential = W3cVerifiableCredential.extend({ + proof: OASignedProof, +}); -// AssertStricterOrEqual is used to ensure that we have zod extended from the base type while -// still being assignable to the base type. For example, if we accidentally extend and -// replaced '@context' to a boolean, this would fail the assertion. -export type OAVerifiableCredential = AssertStricterOrEqual< - W3cVerifiableCredential, - z.infer ->; +// types +export type W3cVerifiableCredential = z.infer; +export type ProoflessW3cVerifiableCredential = z.infer; -// export type VC = T extends OAVerifiableCredential -// ? OAVerifiableCredential -// : T extends W3cVerifiableCredential -// ? W3cVerifiableCredential -// : T; +export type OAVerifiableCredential = z.infer; +export type ProoflessOAVerifiableCredential = z.infer; -export type Digested = Override< +export type OADigested = Override< T, - Pick, keyof typeof DigestedOAVerifiableCredentialExtrasShape> + Pick, "proof"> >; -export type Signed = Override< +export type OASigned = Override< T, - Pick, keyof typeof SignedOAVerifiableCredentialExtrasShape> + Pick, "proof"> >; -type IdentityProofType = z.infer; - -export type Salt = z.infer; +export type OASalt = z.infer; // Utility types /** Overrides properties in the Target (a & b does not override a props with b props) */ @@ -301,10 +317,6 @@ export type Override, OverrideWith extend > & OverrideWith; -/** Used to assert that StricterType is a stricter or equal version of LooserType, and most importantly, that - * StricterType is STILL assignable to LooserType. */ -type AssertStricterOrEqual = StricterType extends LooserType ? StricterType : never; - // eslint-disable-next-line @typescript-eslint/no-unused-vars export type NoExtraProperties = NewObj extends Reference & infer _ExtraProps ? Reference : NewObj; @@ -330,10 +342,10 @@ export const isOAVerifiableCredential = (vc: unknown): vc is OAVerifiableCredent return OAVerifiableCredential.safeParse(vc).success; }; -export const isDigestedOAVerifiableCredential = (vc: unknown): vc is Digested => { - return DigestedOAVerifiableCredential.safeParse(vc).success; +export const isOADigestedOAVerifiableCredential = (vc: unknown): vc is OADigested => { + return OADigestedOAVerifiableCredential.safeParse(vc).success; }; -export const isSignedOAVerifiableCredential = (vc: unknown): vc is Signed => { - return SignedOAVerifiableCredential.safeParse(vc).success; +export const isOASignedOAVerifiableCredential = (vc: unknown): vc is OASigned => { + return OASignedOAVerifiableCredential.safeParse(vc).success; }; diff --git a/src/4.0/validate.ts b/src/4.0/validate.ts index b00e8ae3..536388e8 100644 --- a/src/4.0/validate.ts +++ b/src/4.0/validate.ts @@ -1,9 +1,9 @@ -import { Digested } from "./types"; import { SaltNotFoundError, genTargetHash } from "./hash"; import { checkProof } from "../shared/merkle"; import { decodeSalt } from "./salt"; +import { OADigested, W3cVerifiableCredential } from "./types"; -export const validateDigest = (vc: T): vc is T => { +export const validateDigest = (vc: OADigested): vc is OADigested => { if (!vc.proof) { return false; } diff --git a/src/4.0/vcBuilder.ts b/src/4.0/vcBuilder.ts index 852221f0..4a2f09d9 100644 --- a/src/4.0/vcBuilder.ts +++ b/src/4.0/vcBuilder.ts @@ -1,4 +1,4 @@ -import { digestVc, digestVcs, wrapVcErrors } from "./digest"; +import { digestVc, digestVcs } from "./digest"; import { signVc, signVcErrors } from "./sign"; import { Override, @@ -7,8 +7,8 @@ import { OscpResponderRevocation, RevocationStoreRevocation, OAVerifiableCredential, - Signed, - Digested, + OASigned, + OADigested, } from "./types"; import { ContextType, ContextUrl } from "./context"; @@ -442,7 +442,7 @@ export class VcBuilder { type SignedReturn = Props extends Array ? Override< - Signed, + OASigned, { name: Props[number]["name"]; credentialSubject: Props[number]["credentialSubject"]; @@ -450,7 +450,7 @@ type SignedReturn = Props extends Array[] : Props extends VcProps ? Override< - Signed, + OASigned, { name: Props["name"]; credentialSubject: Props["credentialSubject"]; @@ -460,7 +460,7 @@ type SignedReturn = Props extends Array = Props extends Array ? Override< - Digested, + OADigested, { name: Props[number]["name"]; credentialSubject: Props[number]["credentialSubject"]; @@ -468,7 +468,7 @@ type DigestedReturn = Props extends Array[] : Props extends VcProps ? Override< - Digested, + OADigested, { name: Props["name"]; credentialSubject: Props["credentialSubject"]; @@ -476,12 +476,8 @@ type DigestedReturn = Props extends Array : never; -const { UnableToInterpretContextError } = wrapVcErrors; -const { CouldNotSignVcError, VcProofNotEmptyError } = signVcErrors; export const VcBuilderErrors = { + ...signVcErrors, PropsValidationError, ShouldNotModifyAfterSettingError, - UnableToInterpretContextError, - CouldNotSignVcError, - VcProofNotEmptyError, }; diff --git a/src/shared/utils/guard.ts b/src/shared/utils/guard.ts index e8beb9cf..ee5708d8 100644 --- a/src/shared/utils/guard.ts +++ b/src/shared/utils/guard.ts @@ -84,6 +84,6 @@ export const isSignedWrappedV3Document = ( export { isOAVerifiableCredential, - isDigestedOAVerifiableCredential, - isSignedOAVerifiableCredential, + isOADigestedOAVerifiableCredential, + isOASignedOAVerifiableCredential, } from "../../4.0/types"; diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 67e7ea20..52069e79 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -14,8 +14,8 @@ import { isRawV3Document, isWrappedV3Document, isOAVerifiableCredential, - isDigestedOAVerifiableCredential, - isSignedOAVerifiableCredential, + isOADigestedOAVerifiableCredential, + isOASignedOAVerifiableCredential, } from "./guard"; import { Version } from "./diagnose"; @@ -27,7 +27,7 @@ export function getIssuerAddress(document: any): any { return document.openAttestationMetadata.proof.value; } // TODO: OA v4 proof schema not updated to support document store issuance yet - // else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { + // else if (isOADigestedOAVerifiableCredential(document) || isOASignedOAVerifiableCredential(document)) { // return document.proof.? // } throw new Error( @@ -38,7 +38,7 @@ export function getIssuerAddress(document: any): any { export const getMerkleRoot = (document: unknown): string => { if (isWrappedV2Document(document)) return document.signature.merkleRoot; else if (isWrappedV3Document(document)) return document.proof.merkleRoot; - else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) + else if (isOADigestedOAVerifiableCredential(document) || isOASignedOAVerifiableCredential(document)) return document.proof.merkleRoot; throw new Error( @@ -49,7 +49,7 @@ export const getMerkleRoot = (document: unknown): string => { export const getTargetHash = (document: any): string => { if (isWrappedV2Document(document)) return document.signature.targetHash; else if (isWrappedV3Document(document)) return document.proof.targetHash; - else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) + else if (isOADigestedOAVerifiableCredential(document) || isOASignedOAVerifiableCredential(document)) return document.proof.targetHash; throw new Error( @@ -68,7 +68,7 @@ export const getTemplateURL = (document: any): string | undefined => { else return document.$template?.url; } else if (isRawV3Document(document) || isWrappedV3Document(document)) { return document.openAttestationMetadata.template?.url; - } else if (isOAVerifiableCredential(document) || isDigestedOAVerifiableCredential(document)) { + } else if (isOAVerifiableCredential(document) || isOADigestedOAVerifiableCredential(document)) { return document.renderMethod && document.renderMethod[0].id; } @@ -121,7 +121,7 @@ export const isDocumentRevokable = (document: unknown): boolean => { !!document.openAttestationMetadata.proof.value; return isDocumentStoreRevokableV3 || isDidRevokableV3; - } else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { + } else if (isOADigestedOAVerifiableCredential(document) || isOASignedOAVerifiableCredential(document)) { if (typeof document.issuer === "string" || !document.credentialStatus) return false; const isDidRevokableV4 = document.issuer.identityProof?.identityProofType === "DNS-DID" @@ -163,7 +163,7 @@ export const isObfuscated = (document: unknown): boolean => { return !!document.privacy?.obfuscatedData?.length; } else if (isWrappedV3Document(document)) { return !!document.proof.privacy.obfuscated.length; - } else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { + } else if (isOADigestedOAVerifiableCredential(document) || isOASignedOAVerifiableCredential(document)) { return !!document.proof.privacy.obfuscated.length; } @@ -177,7 +177,7 @@ export const getObfuscatedData = (document: unknown): string[] => { return document.privacy?.obfuscatedData || []; } else if (isWrappedV3Document(document)) { return document.proof.privacy.obfuscated || []; - } else if (isDigestedOAVerifiableCredential(document) || isSignedOAVerifiableCredential(document)) { + } else if (isOADigestedOAVerifiableCredential(document) || isOASignedOAVerifiableCredential(document)) { return document.proof.privacy.obfuscated || []; } From 1cc6b3c3376214b3e65e4266b1ee1ab657a4fd09 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Tue, 14 Jan 2025 15:04:25 +0800 Subject: [PATCH 6/6] fix: allow digesting and signing of modified oa vcs (#321) --- src/4.0/__tests__/digest.test.ts | 5 ++-- src/4.0/__tests__/e2e.test.ts | 15 +++++------- src/4.0/digest.ts | 40 +++++++++++++++++++------------- src/4.0/sign.ts | 20 ++++++++++++---- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/4.0/__tests__/digest.test.ts b/src/4.0/__tests__/digest.test.ts index e97a519f..88d734bd 100644 --- a/src/4.0/__tests__/digest.test.ts +++ b/src/4.0/__tests__/digest.test.ts @@ -3,6 +3,7 @@ import { ProoflessOAVerifiableCredential, OADigestedOAVerifiableCredential, W3cVerifiableCredential, + ProoflessW3cVerifiableCredential, } from "../types"; import { digestVc } from "../digest"; @@ -110,7 +111,7 @@ describe("V4.0 digest", () => { `); }); - test("given a generic W3C VC, should digest with context and type corrected", async () => { + test("given a generic W3C VC and with validate with OA data model disabled, should digest with context and type corrected", async () => { const genericW3cVc: W3cVerifiableCredential = { "@context": ["https://www.w3.org/ns/credentials/v2"], type: ["VerifiableCredential"], @@ -123,7 +124,7 @@ describe("V4.0 digest", () => { id: "https://example.com/issuer/123", }, }; - const digested = await digestVc(genericW3cVc as unknown as ProoflessOAVerifiableCredential); + const digested = await digestVc(genericW3cVc as unknown as ProoflessW3cVerifiableCredential, true); const parsedResults = OADigestedOAVerifiableCredential.pick({ "@context": true, type: true }) .passthrough() .safeParse(digested); diff --git a/src/4.0/__tests__/e2e.test.ts b/src/4.0/__tests__/e2e.test.ts index 0ac66984..d27dc78f 100644 --- a/src/4.0/__tests__/e2e.test.ts +++ b/src/4.0/__tests__/e2e.test.ts @@ -119,31 +119,28 @@ describe("V4.0 E2E Test Scenarios", () => { } as unknown as ProoflessOAVerifiableCredential, ]; await expect(digestVcs(malformedDatum)).rejects.toThrowErrorMatchingInlineSnapshot(` - "Input VC does not conform to Verifiable Credentials v2.0 Data Model: + "Input VC does not conform to Open Attestation v4.0 Data Model: { - "_errors": [], + "_errors": [ + "Unrecognized key(s) in object: 'laurent'" + ], "@context": { "_errors": [ - "Required", - "Required", "Required" ] }, - "issuer": { + "type": { "_errors": [ - "Required", "Required" ] }, - "type": { + "issuer": { "_errors": [ - "Required", "Required" ] }, "credentialSubject": { "_errors": [ - "Required", "Required" ] } diff --git a/src/4.0/digest.ts b/src/4.0/digest.ts index c4638e4e..fa1735d9 100644 --- a/src/4.0/digest.ts +++ b/src/4.0/digest.ts @@ -11,22 +11,24 @@ import { genTargetHash } from "./hash"; import { encodeSalt, salt } from "./salt"; import { ZodError } from "zod"; -export const digestVc = async ( - vc: T -): Promise> => { - /* 1a. Try OpenAttestation VC validation, since most user will be issuing oa v4 */ - const oav4context = await ProoflessOAVerifiableCredential.pick({ "@context": true }).passthrough().safeParseAsync(vc); // Superficial check on user intention +export async function digestVc(vc: T): Promise>; +export async function digestVc( + vc: T, + isUseW3cDataModel: boolean +): Promise>; +export async function digestVc( + vc: T, + isUseW3cDataModel?: boolean +): Promise> { + // 1. Validate vc against OA or W3C data model let validatedUndigestedVc: ProoflessW3cVerifiableCredential | undefined; - if (oav4context.success) { + if (!isUseW3cDataModel) { const oav4 = await ProoflessOAVerifiableCredential.safeParseAsync(vc); if (!oav4.success) { throw new DataModelValidationError("Open Attestation v4.0", oav4.error); } validatedUndigestedVc = oav4.data; - } - - /* 1b. Only if OA VC validation fail do we continue with W3C VC data model validation */ - if (!validatedUndigestedVc) { + } else { const w3cVc = await ProoflessW3cVerifiableCredential.safeParseAsync(vc); if (!w3cVc.success) { throw new DataModelValidationError("Verifiable Credentials v2.0", w3cVc.error); @@ -100,13 +102,19 @@ export const digestVc = async ; -}; +} -export const digestVcs = async ( - vcs: T[] -): Promise[]> => { +export async function digestVcs(vcs: T[]): Promise[]>; +export async function digestVcs( + vcs: T[], + isUseW3cDataModel: boolean +): Promise[]>; +export async function digestVcs( + vcs: T[], + isUseW3cDataModel?: boolean +): Promise[]> { // create individual verifiable credential - const verifiableCredentials = await Promise.all(vcs.map((vc) => digestVc(vc))); + const verifiableCredentials = await Promise.all(vcs.map((vc) => digestVc(vc, isUseW3cDataModel ?? false))); // get all the target hashes to compute the merkle tree and the merkle root const merkleTree = new MerkleTree( @@ -128,7 +136,7 @@ export const digestVcs = async ( +export async function signVc( unsignedVc: T, algorithm: "Secp256k1VerificationKey2018", keyOrSigner: SigningKey | Signer -): Promise> => { +): Promise>; +export async function signVc( + unsignedVc: T, + algorithm: "Secp256k1VerificationKey2018", + keyOrSigner: SigningKey | Signer, + isUseW3cDataModel: boolean +): Promise>; +export async function signVc( + unsignedVc: T, + algorithm: "Secp256k1VerificationKey2018", + keyOrSigner: SigningKey | Signer, + isUseW3cDataModel?: boolean +): Promise> { /* 1. Input VC needs to be digested first */ - const digestedVc = await digestVc(unsignedVc); + const digestedVc = await digestVc(unsignedVc, isUseW3cDataModel ?? false); const validatedProof = digestedVc.proof; const merkleRoot = `0x${validatedProof.merkleRoot}`; @@ -33,7 +45,7 @@ export const signVc = async