Skip to content

Commit

Permalink
fix: loose json schema validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Nebulis committed May 4, 2021
1 parent 63c39fc commit e58aaeb
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 20 deletions.
3 changes: 2 additions & 1 deletion src/2.0/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { hashToBuffer, SchemaValidationError } from "../shared/utils";
import { SchemaId } from "../shared/@types/document";
import { SchematisedDocument, Signature, WrappedDocument } from "./types";
import { OpenAttestationDocument } from "../__generated__/schema.2.0";
import { getSchema, validateSchema as validate } from "../shared/validate";
import { validateSchema as validate } from "../shared/validate";
import { saltData } from "./salt";
import { WrapDocumentOption, WrapDocumentOptionV2 } from "../shared/@types/wrap";
import { getSchema } from "../shared/ajv";

const createDocument = <T extends OpenAttestationDocument = OpenAttestationDocument>(
data: any,
Expand Down
3 changes: 2 additions & 1 deletion src/3.0/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { MerkleTree } from "../shared/merkle";
import { SchemaId } from "../shared/@types/document";
import { WrappedDocument } from "./types";
import { digestCredential } from "../3.0/digest";
import { getSchema, validateSchema as validate } from "../shared/validate";
import { validateSchema as validate } from "../shared/validate";
import { WrapDocumentOptionV3 } from "../shared/@types/wrap";
import { OpenAttestationDocument } from "../__generated__/schema.3.0";
import { encodeSalt, salt } from "./salt";
import { validateW3C } from "./validate";
import { getSchema } from "../shared/ajv";

const getExternalSchema = (schema?: string) => (schema ? { schema } : {});

Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getSchema, validateSchema as validate } from "./shared/validate";
import { validateSchema as validate } from "./shared/validate";
import { verify } from "./2.0/verify";
import { verify as verifyV3 } from "./3.0/verify";
import { wrapDocument as wrapV2Document, wrapDocuments as wrapV2Documents } from "./2.0/wrap";
Expand All @@ -19,6 +19,7 @@ import { WrapDocumentOptionV2, WrapDocumentOptionV3 } from "./shared/@types/wrap
import { SchemaValidationError } from "./shared/utils";
import { SigningKey, SUPPORTED_SIGNING_ALGORITHM } from "./shared/@types/sign";
import { ethers, Signer } from "ethers";
import { getSchema } from "./shared/ajv";

export function __unsafe__use__it__at__your__own__risks__wrapDocument<T extends OpenAttestationDocumentV3>(
data: T,
Expand Down
27 changes: 27 additions & 0 deletions src/shared/ajv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Ajv from "ajv";
import addFormats from "ajv-formats";
import openAttestationSchemav2 from "../2.0/schema/schema.json";
import openAttestationSchemav3 from "../3.0/schema/schema.json";
import { CurrentOptions } from "ajv/dist/core";

const defaultTransform = (schema: Record<string, any>) => schema;
export const buildAjv = (
options: CurrentOptions & { transform: (schema: Record<string, any>) => Record<string, any> } = {
transform: defaultTransform,
}
): Ajv => {
const { transform, ...ajvOptions } = options;
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, ...ajvOptions });
addFormats(ajv);
ajv.addKeyword("deprecationMessage");
ajv.compile(transform(openAttestationSchemav2));
ajv.compile(transform(openAttestationSchemav3));
return ajv;
};

const localAjv = buildAjv();
export const getSchema = (key: string, ajv = localAjv) => {
const schema = ajv.getSchema(key);
if (!schema) throw new Error(`Could not find ${key} schema`);
return schema;
};
24 changes: 24 additions & 0 deletions src/shared/utils/__tests__/guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ describe("guard", () => {
test("should not be valid when document.data.issuers is missing", () => {
expect(isWrappedV2Document(omit(cloneDeep(wrappedV2Document), "data.issuers"))).toBe(false);
});
test("should be valid when document.data.issuers.documentStore does not have correct length", () => {
expect(isWrappedV2Document(set(cloneDeep(wrappedV2Document), "data.issuers[0].documentStore", "0xabcd"))).toBe(
true
);
});
test("should not be valid when document.data.issuers.documentStore does not have correct length on strict mode", () => {
expect(
isWrappedV2Document(set(cloneDeep(wrappedV2Document), "data.issuers[0].documentStore", "0xabcd"), {
mode: "strict",
})
).toBe(false);
});
test("should not be valid when document.signature is an empty object", () => {
expect(isWrappedV2Document(omit(cloneDeep(wrappedV2Document), "signature"))).toBe(false);
});
Expand Down Expand Up @@ -239,6 +251,18 @@ describe("guard", () => {
test("should not be valid when document.proof.targetHash is missing", () => {
expect(isWrappedV3Document(omit(cloneDeep(wrappedV3Document), "proof.targetHash"))).toBe(false);
});
test("should be valid when openAttestationMetadata.proof.method does not have correct length", () => {
expect(
isWrappedV3Document(set(cloneDeep(wrappedV3Document), "openAttestationMetadata.proof.method", "abcd"))
).toBe(true);
});
test("should not be valid when openAttestationMetadata.proof.method does not have correct length on strict mode", () => {
expect(
isWrappedV3Document(set(cloneDeep(wrappedV3Document), "openAttestationMetadata.proof.method", "abcd"), {
mode: "strict",
})
).toBe(false);
});
test("should be valid when document.proof.targetHash is a string", () => {
expect(isWrappedV3Document(set(cloneDeep(wrappedV3Document), "proof.targetHash", "oops"))).toBe(true);
});
Expand Down
38 changes: 36 additions & 2 deletions src/shared/utils/diagnose.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { logger } from "ethers";
import { SchemaId } from "../..";
import { getSchema, validateSchema as validate } from "../validate";
import { validateSchema as validate } from "../validate";
import {
VerifiableCredentialSignedProof,
VerifiableCredentialWrappedProof,
VerifiableCredentialWrappedProofStrict,
} from "../../3.0/types";
import { ArrayProof, Signature, SignatureStrict } from "../../2.0/types";
import { clone, cloneDeepWith } from "lodash";
import { buildAjv, getSchema } from "../ajv";

type Version = "2.0" | "3.0";
type Kind = "wrapped" | "signed";
Expand All @@ -25,6 +27,35 @@ const handleError = (debug: boolean, ...messages: string[]) => {
return messages.map((message) => ({ message }));
};

// remove enum and pattern from the schema
function transformSchema(schema: Record<string, any>): Record<string, any> {
const excludeKeys = ["enum", "pattern"];
function omit(value: any) {
if (value && typeof value === "object") {
const key = excludeKeys.find((key) => value[key]);
if (key) {
const node = clone(value);
excludeKeys.forEach((key) => {
delete node[key];
});
return node;
}
}
}

const newSchema = cloneDeepWith(schema, omit);
// because we remove check on enum (DNS-DID, DNS-TXT, etc.) the identity proof can match multiple sub schema in v2.
// so here we change oneOf to anyOf, so that if more than one identityProof matches, it still passes
if (newSchema?.definitions?.identityProof?.oneOf) {
newSchema.definitions.identityProof.anyOf = newSchema?.definitions?.identityProof?.oneOf;
delete newSchema?.definitions?.identityProof?.oneOf;
}
return newSchema;
}
// custom ajv for loose schema validation
// it will allow invalid format, invalid pattern and invalid enum
const ajv = buildAjv({ transform: transformSchema, validateFormats: false });

/**
* Tools to give information about the validity of a document. It will return and eventually output the errors found.
* @param version 2.0 or 3.0
Expand Down Expand Up @@ -53,7 +84,10 @@ export const diagnose = ({
return handleError(debug, "The document must be an object");
}

const errors = validate(document, getSchema(version === "3.0" ? SchemaId.v3 : SchemaId.v2));
const errors = validate(
document,
getSchema(version === "3.0" ? SchemaId.v3 : SchemaId.v2, mode === "non-strict" ? ajv : undefined)
);
if (errors.length > 0) {
// TODO this can be improved later
return handleError(
Expand Down
17 changes: 2 additions & 15 deletions src/shared/validate/validate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import Ajv, { ErrorObject, ValidateFunction } from "ajv";
import addFormats from "ajv-formats";
import { ErrorObject, ValidateFunction } from "ajv";
import { getLogger } from "../logger";
import openAttestationSchemav2 from "../../2.0/schema/schema.json";
import openAttestationSchemav3 from "../../3.0/schema/schema.json";
import { getData } from "../utils";
import { SchemaId } from "../@types/document";
import { getData } from "../utils";

const logger = getLogger("validate");

Expand All @@ -21,13 +18,3 @@ export const validateSchema = (document: any, validator: ValidateFunction): Erro
logger.debug(`Document is a valid open attestation document v${document.version}`);
return [];
};
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true });
addFormats(ajv);
ajv.addKeyword("deprecationMessage");
ajv.compile(openAttestationSchemav2);
ajv.compile(openAttestationSchemav3);
export const getSchema = (key: string) => {
const schema = ajv.getSchema(key);
if (!schema) throw new Error(`Could not find ${key} schema`);
return schema;
};

0 comments on commit e58aaeb

Please sign in to comment.