diff --git a/src/cr1/types.ts b/src/cr1/types.ts index 6cda8d9..5cef9e5 100644 --- a/src/cr1/types.ts +++ b/src/cr1/types.ts @@ -244,10 +244,12 @@ export type ConformanceWarningMessage = { reference: string } +export type SchemaValidation = 'succeeded' | 'failed' | 'ignored' + export type ValidationResult = { valid: boolean content: VerifiableCredential - schema: Record + schema: Record status: Record warnings: ConformanceWarningMessage[] } diff --git a/src/cr1/validator/index.ts b/src/cr1/validator/index.ts index daf8f53..ce7c5f6 100644 --- a/src/cr1/validator/index.ts +++ b/src/cr1/validator/index.ts @@ -42,6 +42,10 @@ export const validator = ({ resolver }: RequestValidator) => { id: schema.id, type: 'application/schema+json', }) + if (credentialSchema === true) { + validation.schema[schema.id] = { validation: 'ignored' } as any + continue; + } const schemaContent = decoder.decode(credentialSchema.content) const parsedSchemaContent = JSON.parse(schemaContent) let valid: any; @@ -58,7 +62,7 @@ export const validator = ({ resolver }: RequestValidator) => { } catch (e) { valid = false } - validation.schema[schema.id] = { valid } + validation.schema[schema.id] = { validation: valid ? 'succeeded' : 'failed' } if (!valid) { validation.valid = false validation.schema[schema.id].errors = compiledSchemaValidator.errors as JsonSchemaError[] diff --git a/test/json-schema-tests/better-schema-errors.test.ts b/test/json-schema-tests/better-schema-errors.test.ts index 1878277..2ca2bed 100644 --- a/test/json-schema-tests/better-schema-errors.test.ts +++ b/test/json-schema-tests/better-schema-errors.test.ts @@ -120,7 +120,7 @@ credentialSubject: expect(validation1.valid).toBe(false); expect(validation1.schema).toEqual({ "https://vendor.example/api/schemas/product-passport": { - "valid": false, + "validation": "failed", "errors": [ { "instancePath": "/credentialSubject", diff --git a/test/json-schema-tests/optional-schema-validation.test.ts b/test/json-schema-tests/optional-schema-validation.test.ts new file mode 100644 index 0000000..ae91459 --- /dev/null +++ b/test/json-schema-tests/optional-schema-validation.test.ts @@ -0,0 +1,100 @@ +import * as jose from "jose"; +import moment from "moment"; + +import * as transmute from "../../src"; + +const alg = `ES256`; +const issuer = `did:example:123`; +const baseURL = `https://vendor.example/api`; + +let publicKey: any; +let issued: any; + +beforeAll(async () => { + const privateKey = await transmute.key.generate({ + alg, + type: "application/jwk+json", + }); + publicKey = await transmute.key.publicFromPrivate({ + type: "application/jwk+json", + content: privateKey, + }); + issued = await transmute + .issuer({ + alg, + type: "application/vc+ld+json+jwt", + signer: { + sign: async (bytes: Uint8Array) => { + const jws = await new jose.CompactSign(bytes) + .setProtectedHeader({ kid: `${issuer}#key-42`, alg }) + .sign( + await transmute.key.importKeyLike({ + type: "application/jwk+json", + content: privateKey, + }) + ); + return transmute.text.encoder.encode(jws); + }, + }, + }) + .issue({ + claimset: transmute.text.encoder.encode(` +"@context": + - https://www.w3.org/ns/credentials/v2 + - ${baseURL}/context/v2 +id: ${baseURL}/credentials/3732 +type: + - VerifiableCredential + - ExampleDegreeCredential +issuer: + id: ${issuer} +name: "Example University" +validFrom: ${moment().toISOString()} +credentialSchema: + id: ${baseURL}/schemas/product-passport + type: JsonSchema +credentialSubject: + id: did:example:ebfeb1f712ebc6f1c276e12ec21 + unexpectedProperty: unexpectedValue + degree: + type: ExampleBachelorDegree + subtype: Bachelor of Science and Arts +`), + }); +}) + +it("can disable schema validation", async () => { + const validator = await transmute.validator({ + resolver: { + resolve: async (opts: any) => { + // console.log(opts) + const { id, type, content } = opts + // Resolve external resources according to verifier policy + // In this case, we return inline exampes... + if (id === `${baseURL}/schemas/product-passport`) { + return true; // resolving the special case "true" ignores validation + } + if (content != undefined && type === `application/vc+ld+json+jwt`) { + const { kid } = jose.decodeProtectedHeader( + transmute.text.decoder.decode(content) + ); + // lookup public key on a resolver + if (kid === `did:example:123#key-42`) { + return { + type: "application/jwk+json", + content: publicKey, + }; + } + } + throw new Error("Resolver option not supported."); + }, + }, + }); + const validation1 = await validator.validate({ + type: "application/vc+ld+json+jwt", + content: issued, + }); + expect(validation1.valid).toBe(true); + // console.log(JSON.stringify(validation1, null, 2)) + expect(validation1.schema['https://vendor.example/api/schemas/product-passport'].validation).toBe('ignored') +}); \ No newline at end of file diff --git a/test/jwt-product-passports/integration.test.ts b/test/jwt-product-passports/integration.test.ts index 5e429c9..8a02529 100644 --- a/test/jwt-product-passports/integration.test.ts +++ b/test/jwt-product-passports/integration.test.ts @@ -214,7 +214,7 @@ credentialSubject: content: issued, }) expect(validated.valid).toBe(true) - expect(validated.schema[`${baseURL}/schemas/product-passport`].valid).toBe(true) + expect(validated.schema[`${baseURL}/schemas/product-passport`].validation).toBe('succeeded') expect(validated.status[`${baseURL}/credentials/status/3#${revocationIndex}`].valid).toBe(false) expect(validated.status[`${baseURL}/credentials/status/4#${suspensionIndex}`].valid).toBe(false) diff --git a/test/w3c-cr-1/3-schema.test.ts b/test/w3c-cr-1/3-schema.test.ts index 4829e65..512d6b4 100644 --- a/test/w3c-cr-1/3-schema.test.ts +++ b/test/w3c-cr-1/3-schema.test.ts @@ -89,7 +89,7 @@ credentialSubject: }), }) expect(validation.valid).toBe(true); - expect(validation.schema).toEqual({ 'https://issuer.example/schemas/42': { valid: true } }); + expect(validation.schema).toEqual({ 'https://issuer.example/schemas/42': { validation: 'succeeded' } }); }) it('failure', async () => { @@ -157,7 +157,7 @@ credentialSubject: expect(validation.valid).toBe(false); expect(validation.schema).toEqual({ "https://issuer.example/schemas/52": { - "valid": false, + "validation": 'failed', "errors": [ { "instancePath": "/credentialSubject/id",