diff --git a/package-lock.json b/package-lock.json index 29c0217..10b5027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1477,9 +1477,10 @@ } }, "node_modules/@transmute/cose/node_modules/jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "version": "4.15.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.7.tgz", + "integrity": "sha512-L7ioP+JAuZe8v+T5+zVI9Tx8LtU8BL7NxkyDFVMv+Qr3JW0jSoYDedLtodaXwfqMpeCyx4WXFNyu9tJt4WvC1A==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -1510,9 +1511,10 @@ } }, "node_modules/@transmute/vc-jwt-sd/node_modules/jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "version": "4.15.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.7.tgz", + "integrity": "sha512-L7ioP+JAuZe8v+T5+zVI9Tx8LtU8BL7NxkyDFVMv+Qr3JW0jSoYDedLtodaXwfqMpeCyx4WXFNyu9tJt4WvC1A==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -2228,12 +2230,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3071,10 +3074,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3459,6 +3463,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -5135,6 +5140,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -6685,9 +6691,9 @@ }, "dependencies": { "jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + "version": "4.15.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.7.tgz", + "integrity": "sha512-L7ioP+JAuZe8v+T5+zVI9Tx8LtU8BL7NxkyDFVMv+Qr3JW0jSoYDedLtodaXwfqMpeCyx4WXFNyu9tJt4WvC1A==" } } }, @@ -6714,9 +6720,9 @@ "integrity": "sha512-bTCCiR0brj9RShibl2wirK+y99JuZBhCLXo114N7HtwjKnLa43D14X9Ay0SdIslCYhyOH6kagtMp9HhVkqyPqQ==" }, "jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + "version": "4.15.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.7.tgz", + "integrity": "sha512-L7ioP+JAuZe8v+T5+zVI9Tx8LtU8BL7NxkyDFVMv+Qr3JW0jSoYDedLtodaXwfqMpeCyx4WXFNyu9tJt4WvC1A==" } } }, @@ -7255,12 +7261,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "brorand": { @@ -7869,9 +7875,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" diff --git a/src/cr1/credential/issuer.ts b/src/cr1/credential/issuer.ts index 24400d9..06546b1 100644 --- a/src/cr1/credential/issuer.ts +++ b/src/cr1/credential/issuer.ts @@ -13,7 +13,7 @@ const coseSign1CredentialIssuer = (issuer: RequestCredentialIssuer) => { if (issuer.signer === undefined) { throw new Error('No signer available.') } - const claims = claimset.parse(decoder.decode(credential.claimset)) as any + const claims = claimset.parse(decoder.decode(credential.claimset)) return issuer.signer.sign(encoder.encode(JSON.stringify(claims))) } } @@ -25,7 +25,7 @@ const jwtCredentialIssuer = (issuer: RequestCredentialIssuer) => { if (issuer.signer === undefined) { throw new Error('No signer available.') } - const claims = claimset.parse(decoder.decode(credential.claimset)) as any + const claims = claimset.parse(decoder.decode(credential.claimset)) return issuer.signer.sign(encoder.encode(JSON.stringify(claims))) } } diff --git a/src/cr1/types.ts b/src/cr1/types.ts index 6cda8d9..32a35c8 100644 --- a/src/cr1/types.ts +++ b/src/cr1/types.ts @@ -158,8 +158,14 @@ export type SecuredContentType = { content: Uint8Array } +export type VerifierResolutionRequest = { + type: SupportedCredentialFormats | SupportedPresentationFormats | SupportedJwtSignatureFormats | SupportedSdJwtSignatureFormats | SupportedCoseSign1Formats + content: Uint8Array + purpose: ValidatorResolutionPurpose +} + export type VerifierResolver = { - resolve: (req: SecuredContentType) => Promise + resolve: (req: VerifierResolutionRequest) => Promise } export type RequestVerifier = { @@ -167,10 +173,13 @@ export type RequestVerifier = { } +export type ValidatorResolutionPurpose = 'schema-validation' | 'status-check' | 'verification-material' + export type ValidatorContentType = { id?: string type: any content?: Uint8Array + purpose: ValidatorResolutionPurpose } @@ -244,11 +253,19 @@ export type ConformanceWarningMessage = { reference: string } +export type SchemaValidation = 'succeeded' | 'failed' | 'ignored' + +export type StatusCheckResult = { + errors?: StatusListError[] +} & Record // because of enumerations. + +export type SchemaCheckResult = { validation?: SchemaValidation, errors?: JsonSchemaError[], } + export type ValidationResult = { - valid: boolean + verified: boolean content: VerifiableCredential - schema: Record - status: Record + schema: Record + status: Record warnings: ConformanceWarningMessage[] } diff --git a/src/cr1/validator/index.ts b/src/cr1/validator/index.ts index daf8f53..e022373 100644 --- a/src/cr1/validator/index.ts +++ b/src/cr1/validator/index.ts @@ -26,7 +26,7 @@ export const validator = ({ resolver }: RequestValidator) => { validate: async ({ type, content }: SecuredContentType) => { const verified = await verifier({ resolver }).verify({ type, content }) const validation: ValidationResult = { - valid: true, + verified: true, content: verified, schema: {}, status: {}, @@ -41,7 +41,12 @@ export const validator = ({ resolver }: RequestValidator) => { // prefer to resolve this one by id, instead of content id: schema.id, type: 'application/schema+json', + purpose: 'schema-validation' }) + if (credentialSchema === true) { + validation.schema[schema.id] = { validation: 'ignored' } + continue; + } const schemaContent = decoder.decode(credentialSchema.content) const parsedSchemaContent = JSON.parse(schemaContent) let valid: any; @@ -58,9 +63,8 @@ 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[] } } @@ -73,23 +77,21 @@ export const validator = ({ resolver }: RequestValidator) => { const statusListCredential = await resolver.resolve({ // prefer to resolve this one by id, instead of content id: status.statusListCredential, - type: type // we do not support mixed type credential and status lists! + type: type, // we do not support mixed type credential and status lists! + purpose: 'status-check' }) const verified = await verifier({ resolver }).verify(statusListCredential) // confirm purpose matches if (status.statusPurpose !== verified.credentialSubject.statusPurpose) { - validation.valid = false validation.status[`${status.id}`] = { - valid: false, purpose: status.statusPurpose, errors: [{ + [status.statusPurpose]: false, + errors: [{ message: 'status list purpose does not match credential status' }] } } else { const bit = bs(verified.credentialSubject.encodedList).get(parseInt(status.statusListIndex, 10)) - if (bit) { - validation.valid = false - } - validation.status[`${status.id}`] = { valid: bit, purpose: status.statusPurpose } + validation.status[`${status.id}`] = { [status.statusPurpose]: bit } } } diff --git a/src/cr1/verifier/verifier.ts b/src/cr1/verifier/verifier.ts index 25a5d4a..0ad5e76 100644 --- a/src/cr1/verifier/verifier.ts +++ b/src/cr1/verifier/verifier.ts @@ -23,7 +23,7 @@ const acceptableAudience = (expectedAud: string, receivedAud: string | string[]) } const verifyJwt = async ({ resolver }: RequestVerifier, { type, content, audience, nonce }: RequestVerify) => { - const key = await resolver.resolve({ type, content }) + const key = await resolver.resolve({ type, content, purpose: 'verification-material' }) const publicKey = await importKeyLike(key) const jwt = decoder.decode(content) const { payload } = await jose.jwtVerify(jwt, publicKey, { @@ -46,7 +46,8 @@ const verifyCoseSign1 resolve: async () => { const key = await resolver.resolve({ type, - content + content, + purpose: 'verification-material' }) return importJWK(key) } @@ -86,7 +87,8 @@ const verifySdJwtCredential = async ({ resolver }: RequestVerifier, { type, cont resolve: async () => { const key = await resolver.resolve({ type, - content + content, + purpose: 'verification-material' }) return importJWK(key) } @@ -106,7 +108,8 @@ const verifySdJwtPresentation = async ({ resolver }: RequestVerifier, { type, co resolve: async () => { const key = await resolver.resolve({ type, - content // same a token + content, // same a token + purpose: 'verification-material' }) return importJWK(key) } diff --git a/test/json-schema-tests/better-schema-errors.test.ts b/test/json-schema-tests/better-schema-errors.test.ts index 1878277..5444be7 100644 --- a/test/json-schema-tests/better-schema-errors.test.ts +++ b/test/json-schema-tests/better-schema-errors.test.ts @@ -117,10 +117,10 @@ credentialSubject: type: "application/vc+ld+json+jwt", content: issued, }); - expect(validation1.valid).toBe(false); + expect(validation1.verified).toBe(true); 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/json-schema-tests.test.ts b/test/json-schema-tests/json-schema-tests.test.ts index fbb7e0f..630c70c 100644 --- a/test/json-schema-tests/json-schema-tests.test.ts +++ b/test/json-schema-tests/json-schema-tests.test.ts @@ -221,12 +221,12 @@ credentialSubject: type: "application/vc+ld+json+jwt", content: issued, }); - expect(valid1.valid).toBe(true); + expect(valid1.verified).toBe(true); const valid2 = await validator.validate({ type: "application/vc+ld+json+jwt", content: issued, }); - expect(valid2.valid).toBe(true); + expect(valid2.verified).toBe(true); }); }); 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..7a041ca --- /dev/null +++ b/test/json-schema-tests/optional-schema-validation.test.ts @@ -0,0 +1,90 @@ +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 ({ purpose }) => { + if (purpose === 'schema-validation') { + return true; // resolving the special case "true" ignores validation + } + if (purpose === 'verification-material') { + 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.verified).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..fd6d0bf 100644 --- a/test/jwt-product-passports/integration.test.ts +++ b/test/jwt-product-passports/integration.test.ts @@ -213,10 +213,10 @@ credentialSubject: type: 'application/vc+ld+json+jwt', content: issued, }) - expect(validated.valid).toBe(true) - expect(validated.schema[`${baseURL}/schemas/product-passport`].valid).toBe(true) - expect(validated.status[`${baseURL}/credentials/status/3#${revocationIndex}`].valid).toBe(false) - expect(validated.status[`${baseURL}/credentials/status/4#${suspensionIndex}`].valid).toBe(false) + expect(validated.verified).toBe(true) + expect(validated.schema[`${baseURL}/schemas/product-passport`].validation).toBe('succeeded') + expect(validated.status[`${baseURL}/credentials/status/3#${revocationIndex}`]).toEqual({ "revocation": false, }) + expect(validated.status[`${baseURL}/credentials/status/4#${suspensionIndex}`]).toEqual({ "suspension": false, }) const vp = await transmute diff --git a/test/w3c-cr-1/3-schema.test.ts b/test/w3c-cr-1/3-schema.test.ts index 4829e65..068cc19 100644 --- a/test/w3c-cr-1/3-schema.test.ts +++ b/test/w3c-cr-1/3-schema.test.ts @@ -88,8 +88,8 @@ credentialSubject: `) }), }) - expect(validation.valid).toBe(true); - expect(validation.schema).toEqual({ 'https://issuer.example/schemas/42': { valid: true } }); + expect(validation.verified).toBe(true); + expect(validation.schema).toEqual({ 'https://issuer.example/schemas/42': { validation: 'succeeded' } }); }) it('failure', async () => { @@ -154,10 +154,10 @@ credentialSubject: `) }), }) - expect(validation.valid).toBe(false); + expect(validation.verified).toBe(true); expect(validation.schema).toEqual({ "https://issuer.example/schemas/52": { - "valid": false, + "validation": 'failed', "errors": [ { "instancePath": "/credentialSubject/id", diff --git a/test/w3c-cr-1/4-status.test.ts b/test/w3c-cr-1/4-status.test.ts index 453d667..20e118c 100644 --- a/test/w3c-cr-1/4-status.test.ts +++ b/test/w3c-cr-1/4-status.test.ts @@ -96,9 +96,11 @@ credentialSubject: `) }), }) - expect(validation.valid).toBe(true); - expect(validation.status['https://example.com/credentials/status/3#94567'].valid).toBe(false); - expect(validation.status['https://example.com/credentials/status/3#94567'].purpose).toBe('revocation'); + expect(validation.verified).toBe(true); + expect(validation.status['https://example.com/credentials/status/3#94567']).toEqual({ + "revocation": false, + }); + }) // failure here means REVOKED @@ -182,9 +184,10 @@ credentialSubject: `) }), }) - expect(validation.valid).toBe(false); - expect(validation.status['https://example.com/credentials/status/3#94567'].valid).toBe(true); - expect(validation.status['https://example.com/credentials/status/3#94567'].purpose).toBe('revocation'); + expect(validation.verified).toBe(true); + expect(validation.status['https://example.com/credentials/status/3#94567']).toEqual({ + "revocation": true, + }); }) }) @@ -258,9 +261,11 @@ credentialSubject: `) }), }) - expect(validation.valid).toBe(true); - expect(validation.status['https://example.com/credentials/status/3#94567'].valid).toBe(false); - expect(validation.status['https://example.com/credentials/status/3#94567'].purpose).toBe('suspension'); + expect(validation.verified).toBe(true); + expect(validation.status['https://example.com/credentials/status/3#94567']).toEqual({ + "suspension": false + }); + }) // failure here means REVOKED @@ -344,9 +349,11 @@ credentialSubject: `) }), }) - expect(validation.valid).toBe(false); - expect(validation.status['https://example.com/credentials/status/3#94567'].valid).toBe(true); - expect(validation.status['https://example.com/credentials/status/3#94567'].purpose).toBe('suspension'); + expect(validation.verified).toBe(true); + expect(validation.status['https://example.com/credentials/status/3#94567']).toEqual({ + "suspension": true + }); + }) }) })