Skip to content

Commit

Permalink
Merge pull request #444 from docknetwork/fix-schema-verify-id-field
Browse files Browse the repository at this point in the history
Fix verifying schema with only ID field in subject, dont expand for schema validation
  • Loading branch information
cykoder authored Aug 12, 2024
2 parents c5f7ee9 + a19fa64 commit ceef434
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 75 deletions.
3 changes: 1 addition & 2 deletions example/standard_schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ async function validateSchema(schema, credential) {
await Schema.validateSchema(schema);
console.log('Validating credential against schema...');

const expanded = await expandJSONLD(credential);
await validateCredentialSchema(expanded, schema, credential['@context']);
await validateCredentialSchema(credential, schema);
console.log('Success!');
}

Expand Down
4 changes: 1 addition & 3 deletions src/utils/vc/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
expandJSONLD, getKeyFromDIDDocument, getSuiteFromKeyDoc, processIfKvac,
} from './helpers';
import {
credentialContextField,
DEFAULT_CONTEXT_V1_URL,
DockStatusList2021Qualifier,
PrivateStatusList2021EntryType,
Expand Down Expand Up @@ -323,8 +322,7 @@ export async function verifyCredential(
const isAnoncredsDerived = isAnoncredsProofType(credential);
if (!skipSchemaCheck && !isAnoncredsDerived) {
await getAndValidateSchemaIfPresent(
expandedCredential,
credential[credentialContextField],
credential,
docLoader,
);
}
Expand Down
40 changes: 9 additions & 31 deletions src/utils/vc/schema.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
import jsonld from 'jsonld';
import { validate } from 'jsonschema';
import defaultDocumentLoader from './document-loader';

import {
expandedSubjectProperty,
expandedSchemaProperty,
credentialIDField,
credentialContextField,
} from './constants';

/**
* The function uses `jsonschema` package to verify that the expanded `credential`'s subject `credentialSubject` has the JSON
* schema `schema`
* @param {object} credential - The credential to use, must be expanded JSON-LD
* @param {object} schema - The schema to use
* @param context
* @param documentLoader
* @returns {Promise<Boolean>} - Returns promise to a boolean or throws error
* @returns {Boolean} - Returns a boolean or throws error
*/
export async function validateCredentialSchema(
export function validateCredentialSchema(
credential,
schema,
context,
documentLoader,
) {
const requiresID = schema.required && schema.required.indexOf('id') > -1;
const credentialSubject = credential[expandedSubjectProperty] || [];
const credentialSubject = credential.credentialSubject || [];
const subjects = credentialSubject.length
? credentialSubject
: [credentialSubject];
Expand All @@ -36,22 +27,13 @@ export async function validateCredentialSchema(
delete subject[credentialIDField];
}

// eslint-disable-next-line
const compacted = await jsonld.compact(subject, context, {
documentLoader: documentLoader || defaultDocumentLoader(),
});
delete compacted[credentialContextField];

if (Object.keys(compacted).length === 0) {
throw new Error('Compacted subject is empty, likely invalid');
}

const schemaObj = schema.schema || schema;
const subjectSchema = (schemaObj.properties && schemaObj.properties.credentialSubject)
|| schemaObj;

validate(compacted, subjectSchema, {
validate(subject, subjectSchema, {
throwError: true,
throwFirst: true,
});
}
return true;
Expand All @@ -60,21 +42,19 @@ export async function validateCredentialSchema(
/**
* Get schema and run validation on credential if it contains both a credentialSubject and credentialSchema
* @param {object} credential - a verifiable credential JSON object
* @param {object} context - the context
* @param {object} documentLoader - the document loader
* @returns {Promise<void>}
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export async function getAndValidateSchemaIfPresent(
credential,
context,
documentLoader,
) {
const schemaList = credential[expandedSchemaProperty];
const schemaList = Array.isArray(credential.credentialSchema) ? credential.credentialSchema : [credential.credentialSchema];
if (schemaList) {
const schema = schemaList[0];
if (credential[expandedSubjectProperty] && schema) {
const schemaUri = schema[credentialIDField];
if (credential.credentialSubject && schema) {
const schemaUri = schema.id;
let schemaObj;

const { document } = await documentLoader(schemaUri);
Expand All @@ -95,11 +75,9 @@ export async function getAndValidateSchemaIfPresent(
}

try {
await validateCredentialSchema(
validateCredentialSchema(
credential,
schemaObj,
context,
documentLoader,
);
} catch (e) {
throw new Error(`Schema validation failed: ${e}`);
Expand Down
4 changes: 1 addition & 3 deletions src/verifiable-credential.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
expandJSONLD,
issueCredential,
verifyCredential,
DEFAULT_CONTEXT,
Expand Down Expand Up @@ -107,8 +106,7 @@ class VerifiableCredential {
throw new Error('No credential subject defined');
}

const expanded = await expandJSONLD(this.toJSON());
return validateCredentialSchema(expanded, schema, this.context);
return validateCredentialSchema(this.toJSON(), schema);
}

/**
Expand Down
59 changes: 23 additions & 36 deletions tests/unit/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,10 @@ import VerifiableCredential from '../../src/verifiable-credential';
import Schema from '../../src/modules/schema';
import { DockBlobQualifier } from '../../src/modules/blob';

import {
expandJSONLD,
} from '../../src/utils/vc/helpers';

import {
validateCredentialSchema,
} from '../../src/utils/vc/schema';

import {
expandedSubjectProperty,
} from '../../src/utils/vc/constants';

import exampleCredential from '../example-credential';
import exampleSchema from '../example-schema';

Expand Down Expand Up @@ -92,59 +84,54 @@ describe('Validate Credential Schema utility', () => {
const schema = new Schema();
schema.setJSONSchema(exampleSchema);

let expandedCredential;
beforeAll(async () => {
expandedCredential = await expandJSONLD(exampleCredential);
}, 10000);

test('credentialSubject has same fields and fields have same types as JSON-schema', () => {
expect(validateCredentialSchema(expandedCredential, schema, exampleCredential['@context'])).toBeDefined();
expect(validateCredentialSchema(exampleCredential, schema, exampleCredential['@context'])).toBe(true);
});

test('credentialSubject has same fields but fields have different type than JSON-schema', async () => {
await expect(validateCredentialSchema({
[`${expandedSubjectProperty}`]: {
invalid: true,
expect(() => validateCredentialSchema({
credentialSubject: {
emailAddress: true,
},
}, schema, exampleCredential['@context'])).rejects.toThrow();
}, schema, exampleCredential['@context'])).toThrow();
}, 100000);

test('credentialSubject is missing required fields from the JSON-schema and it should fail to validate.', async () => {
const credentialSubject = { ...expandedCredential[expandedSubjectProperty] };
delete credentialSubject['https://schema.org/alumniOf'];
await expect(validateCredentialSchema({
[`${expandedSubjectProperty}`]: credentialSubject,
}, schema, exampleCredential['@context'])).rejects.toThrow();
const credentialSubject = { ...exampleCredential.credentialSubject };
delete credentialSubject.alumniOf;
expect(() => validateCredentialSchema({
credentialSubject,
}, schema, exampleCredential['@context'])).toThrow();
}, 100000);

test('The schema\'s properties is missing the required key and credentialSubject can omit some of the properties.', async () => {
const nonRequiredSchema = { ...exampleSchema };
delete nonRequiredSchema.required;
await schema.setJSONSchema(nonRequiredSchema);

const credentialSubject = { ...expandedCredential[expandedSubjectProperty][0] };
delete credentialSubject['https://schema.org/alumniOf'];
const credentialSubject = { ...exampleCredential.credentialSubject };
delete credentialSubject.alumniOf;

await expect(validateCredentialSchema({
[`${expandedSubjectProperty}`]: credentialSubject,
}, schema, exampleCredential['@context'])).resolves.toBeDefined();
expect(validateCredentialSchema({
credentialSubject,
}, schema, exampleCredential['@context'])).toBe(true);
});

test('credentialSubject has extra fields than given schema specifies and additionalProperties has certain type.', async () => {
const credentialSubject = { ...expandedCredential[expandedSubjectProperty][0], additionalString: 'mystring' };
const credentialSubject = { ...exampleCredential.credentialSubject, additionalString: 'mystring' };
await schema.setJSONSchema({
...exampleSchema,
additionalProperties: { type: 'string' },
});

await expect(validateCredentialSchema({
[`${expandedSubjectProperty}`]: credentialSubject,
}, schema, exampleCredential['@context'])).resolves.toBeDefined();
expect(validateCredentialSchema({
credentialSubject,
}, schema, exampleCredential['@context'])).toBe(true);
});

test('credentialSubject has nested fields and given schema specifies the nested structure.', async () => {
const credentialSubject = {
...expandedCredential[expandedSubjectProperty][0],
...exampleCredential.credentialSubject,
nestedFields: {
test: true,
},
Expand All @@ -164,8 +151,8 @@ describe('Validate Credential Schema utility', () => {
},
});

await expect(validateCredentialSchema({
[`${expandedSubjectProperty}`]: credentialSubject,
}, schema, exampleCredential['@context'])).resolves.toBeDefined();
expect(validateCredentialSchema({
credentialSubject,
}, schema, exampleCredential['@context'])).toBe(true);
});
});

0 comments on commit ceef434

Please sign in to comment.