Skip to content

Commit

Permalink
feat: implement verifier manager
Browse files Browse the repository at this point in the history
  • Loading branch information
Nebulis committed Dec 12, 2019
1 parent e2f888d commit acbd650
Show file tree
Hide file tree
Showing 29 changed files with 3,473 additions and 2,704 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"prettier/prettier": "error",
"import/no-unresolved": "off",
"import/prefer-default-export": "off",
"import/extensions": "off",
"@typescript-eslint/explicit-function-return-type":"off",
"@typescript-eslint/no-explicit-any":"off"
}
Expand Down
4,430 changes: 2,669 additions & 1,761 deletions package-lock.json

Large diffs are not rendered by default.

36 changes: 20 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,31 @@
"author": "",
"license": "GPL-3.0",
"dependencies": {
"@govtechsg/open-attestation": "^1.1.41",
"ethers": "^4.0.27"
"@govtechsg/dnsprove": "^2.0.5",
"@govtechsg/open-attestation": "^1.1.42",
"ethers": "^4.0.40"
},
"devDependencies": {
"@commitlint/cli": "8.2.0",
"@commitlint/config-conventional": "8.2.0",
"@commitlint/prompt": "8.2.0",
"@ls-age/commitlint-circle": "1.0.0",
"@types/jest": "^24.0.18",
"@types/jest": "^24.0.23",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.6.0",
"@typescript-eslint/parser": "^2.11.0",
"commitizen": "4.0.3",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jest": "^22.4.1",
"eslint-plugin-prettier": "^3.0.1",
"git-cz": "3.2.1",
"jest": "^24.7.1",
"prettier": "^1.16.4",
"semantic-release": "^15.13.24",
"ts-jest": "^24.1.0",
"typescript": "^3.6.3"
"eslint": "^6.7.2",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-jest": "^23.1.1",
"eslint-plugin-prettier": "^3.1.1",
"git-cz": "^3.3.0",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"semantic-release": "^15.13.31",
"ts-jest": "^24.2.0",
"typescript": "^3.7.3"
},
"publishConfig": {
"access": "public"
Expand All @@ -57,5 +58,8 @@
"commitizen": {
"path": "node_modules/@commitlint/prompt"
}
},
"prettier": {
"printWidth": 120
}
}
8 changes: 2 additions & 6 deletions src/common/smartContract/contractInstance.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import * as ethers from "ethers";
import { INFURA_API_KEY } from "../../config";
import { Hash } from "../../types";
import { Hash } from "../../types/core";

interface ContractInstance {
abi: any; // type is any of json file in abi folder
network: string;
contractAddress: Hash;
}
export const contractInstance = ({
abi,
network,
contractAddress
}: ContractInstance) => {
export const contractInstance = ({ abi, network, contractAddress }: ContractInstance) => {
const provider = new ethers.providers.InfuraProvider(network, INFURA_API_KEY);
return new ethers.Contract(contractAddress, abi, provider);
};
7 changes: 2 additions & 5 deletions src/common/smartContract/documentToSmartContracts.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { Document, getData } from "@govtechsg/open-attestation";
import { issuerToSmartContract } from "./issuerToSmartContract";
import { Issuer } from "../../types";
import { Issuer } from "../../types/core";

// Given a list of issuers, convert to smart contract
const mapIssuersToSmartContracts = (issuers: Issuer[], network: string) =>
issuers.map(issuer => issuerToSmartContract(issuer, network));

// Given a raw document, return list of all smart contracts
export const documentToSmartContracts = (
document: Document,
network: string
) => {
export const documentToSmartContracts = (document: Document, network: string) => {
const data = getData(document);
const issuers = data.issuers || [];

Expand Down
7 changes: 2 additions & 5 deletions src/common/smartContract/issuerToSmartContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ import { contractInstance } from "./contractInstance";
import { TYPES } from "./constants";
import tokenRegistryAbi from "./abi/tokenRegistry.json";
import documentStoreAbi from "./abi/documentStore.json";
import { Issuer, OpenAttestationContract } from "../../types";
import { Issuer, OpenAttestationContract } from "../../types/core";

export const issuerToSmartContract = (
issuer: Issuer,
network: string
): OpenAttestationContract => {
export const issuerToSmartContract = (issuer: Issuer, network: string): OpenAttestationContract => {
switch (true) {
case "tokenRegistry" in issuer:
return {
Expand Down
4 changes: 2 additions & 2 deletions src/hash/hash.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { verifyHash } from "./hash";
import { document } from "../../test/fixtures/document";
import { documentTampered } from "../../test/fixtures/tampered-document";
import { documentTamperedWithCertificateStore } from "../../test/fixtures/tampered-document";

describe("verify/hash", () => {
describe("verifyHash", () => {
it("should return true for untampered document", async () => {
expect(await verifyHash(document)).toEqual({ checksumMatch: true });
});
it("should return false for tampered document", async () => {
expect(await verifyHash(documentTampered)).toEqual({
expect(await verifyHash(documentTamperedWithCertificateStore)).toEqual({
checksumMatch: false
});
});
Expand Down
116 changes: 22 additions & 94 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,24 @@
import { SignedDocument } from "@govtechsg/open-attestation";
import { verifyHash } from "./hash/hash";
import { verifyIssued } from "./issued/verify";
import { verifyRevoked } from "./revoked/verify";
import { documentToSmartContracts } from "./common/smartContract/documentToSmartContracts";
import { OpenAttestationContract } from "./types";

/**
* Unwraps the resolve type of promises
* e.g.
* type foo = () => Promise<boolean>;
* type bar = ResolveType<foo>; // bar is a boolean
*/
type ResolveType<T> = T extends (...args: any[]) => Promise<infer U> ? U : T;

type VerificationChecks = [
ReturnType<typeof verifyHash>,
ReturnType<typeof verifyIssued>,
ReturnType<typeof verifyRevoked>
];

type VerificationChecksWithValidity = [
ReturnType<typeof verifyHash>,
ReturnType<typeof verifyIssued>,
ReturnType<typeof verifyRevoked>,
Promise<boolean>
import { verificationManager } from "./verifiers/verificationManager";
import { Verifier } from "./types/core";
import { openAttestationTamperCheck } from "./verifiers/openAttestationTamperCheck";
import { openAttestationDnsTxtIdentity } from "./verifiers/openAttestationDnsTxtIdentity";
import { openAttestationEthereumDocumentStoreIssued } from "./verifiers/openAttestationEthereumDocumentStoreIssued";
import { openAttestationEthereumDocumentStoreRevoked } from "./verifiers/openAttestationEthereumDocumentStoreRevoked";

const defaultVerifiers: Verifier[] = [
openAttestationTamperCheck,
openAttestationEthereumDocumentStoreIssued,
openAttestationEthereumDocumentStoreRevoked,
openAttestationDnsTxtIdentity
];

const getAllChecks = (
document: SignedDocument,
smartContracts: OpenAttestationContract[]
): VerificationChecks => [
verifyHash(document),
verifyIssued(document, smartContracts),
verifyRevoked(document, smartContracts)
];

const isDocumentValid = (
hash: ResolveType<typeof verifyHash>,
issued: ResolveType<typeof verifyIssued>,
revoked: ResolveType<typeof verifyRevoked>
) => hash.checksumMatch && issued.issuedOnAll && !revoked.revokedOnAny;

/**
* @param {object} document Entire document object to be validated
* @param {string} network Network to check against, defaults to "homestead". Other valid choices: "ropsten", "kovan", etc
* @returns
*/
export const verify = async (
document: SignedDocument,
network = "homestead"
) => {
const smartContracts = documentToSmartContracts(document, network);
const checks = getAllChecks(document, smartContracts);
const [hash, issued, revoked] = await Promise.all(checks);

return {
hash,
issued,
revoked,
valid: isDocumentValid(hash, issued, revoked)
};
const defaultVerificationManager = verificationManager(defaultVerifiers);

export {
verificationManager,
openAttestationEthereumDocumentStoreIssued,
openAttestationDnsTxtIdentity,
openAttestationTamperCheck,
openAttestationEthereumDocumentStoreRevoked,
defaultVerifiers,
defaultVerificationManager
};

/**
* @param {object} document Entire document object to be validated
* @param {string} network Network to check against, defaults to "homestead". Other valid choices: "ropsten", "kovan", etc
* @returns {array} Array of promises, each promise corresponds to a verification check.
* The last promise resolves to the overall validity based on all the checks.
*/
export const verifyWithIndividualChecks = (
document: SignedDocument,
network = "homestead"
): VerificationChecksWithValidity => {
const smartContracts = documentToSmartContracts(document, network);
const [hash, issued, revoked] = getAllChecks(document, smartContracts);

// If any of the checks are invalid, resolve the overall validity early
const valid = Promise.all([
new Promise(async (resolve, reject) =>
(await hash).checksumMatch ? resolve() : reject()
),
new Promise(async (resolve, reject) =>
(await issued).issuedOnAll ? resolve() : reject()
),
new Promise(async (resolve, reject) =>
(await revoked).revokedOnAny ? reject() : resolve()
)
])
.then(() => true)
.catch(() => false);

return [hash, issued, revoked, valid];
};

export default verify; // backward compatible
17 changes: 4 additions & 13 deletions src/issued/contractInterface.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import { constants } from "ethers";
import { Hash, OpenAttestationContract } from "../types";
import { Hash, OpenAttestationContract } from "../types/core";
import { TYPES } from "../common/smartContract/constants";

// Return issued status given a smart contract instance (documentStore/tokenRegistry)
export const isIssuedOnTokenRegistry = async (
smartContract: OpenAttestationContract,
hash: Hash
): Promise<boolean> => {
export const isIssuedOnTokenRegistry = async (smartContract: OpenAttestationContract, hash: Hash): Promise<boolean> => {
const owner = await smartContract.instance.functions.ownerOf(hash);
return !(owner === constants.AddressZero);
};

export const isIssuedOnDocumentStore = async (
smartContract: OpenAttestationContract,
hash: Hash
): Promise<boolean> => {
export const isIssuedOnDocumentStore = async (smartContract: OpenAttestationContract, hash: Hash): Promise<boolean> => {
return smartContract.instance.functions.isIssued(hash);
};

export const isIssued = (
smartContract: OpenAttestationContract,
hash: Hash
) => {
export const isIssued = (smartContract: OpenAttestationContract, hash: Hash) => {
switch (smartContract.type) {
case TYPES.TOKEN_REGISTRY:
return isIssuedOnTokenRegistry(smartContract, hash);
Expand Down
16 changes: 4 additions & 12 deletions src/issued/verify.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { SignedDocument } from "@govtechsg/open-attestation";
import { isIssued } from "./contractInterface";
import { Hash, OpenAttestationContract } from "../types";
import { Hash, OpenAttestationContract } from "../types/core";

export const issuedStatusOnContracts = async (
smartContracts: OpenAttestationContract[] = [],
hash: Hash
) => {
export const issuedStatusOnContracts = async (smartContracts: OpenAttestationContract[] = [], hash: Hash) => {
const issueStatusesDeferred = smartContracts.map(smartContract =>
isIssued(smartContract, hash)
.then(issued => ({
Expand All @@ -21,17 +18,12 @@ export const issuedStatusOnContracts = async (
return Promise.all(issueStatusesDeferred);
};

export const isIssuedOnAll = (
statuses: { address: Hash; issued: boolean }[]
) => {
export const isIssuedOnAll = (statuses: { address: Hash; issued: boolean }[]) => {
if (!statuses || statuses.length === 0) return false;
return statuses.every(status => status.issued);
};

export const verifyIssued = async (
document: SignedDocument,
smartContracts: OpenAttestationContract[] = []
) => {
export const verifyIssued = async (document: SignedDocument, smartContracts: OpenAttestationContract[] = []) => {
const hash = `0x${document.signature.merkleRoot}`;
const details = await issuedStatusOnContracts(smartContracts, hash);
const issuedOnAll = isIssuedOnAll(details);
Expand Down
7 changes: 2 additions & 5 deletions src/revoked/contractInterface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { constants } from "ethers";
import { Hash, OpenAttestationContract } from "../types";
import { Hash, OpenAttestationContract } from "../types/core";
import { TYPES } from "../common/smartContract/constants";

export const isRevokedOnTokenRegistry = async (
Expand All @@ -17,10 +17,7 @@ export const isRevokedOnDocumentStore = async (
return smartContract.instance.functions.isRevoked(hash);
};

export const isRevoked = (
smartContract: OpenAttestationContract,
hash: Hash
) => {
export const isRevoked = (smartContract: OpenAttestationContract, hash: Hash) => {
switch (smartContract.type) {
case TYPES.TOKEN_REGISTRY:
return isRevokedOnTokenRegistry(smartContract, hash);
Expand Down
30 changes: 7 additions & 23 deletions src/revoked/verify.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { SignedDocument, utils } from "@govtechsg/open-attestation";
import { Hash, OpenAttestationContract } from "../types";
import { Hash, OpenAttestationContract } from "../types/core";
import { isRevoked } from "./contractInterface";

// Given a list of hashes, check against one smart contract if any of the hash has been revoked
export const isAnyHashRevokedOnStore = async (
smartContract: OpenAttestationContract,
intermediateHashes: Hash[]
) => {
const revokedStatusDeferred = intermediateHashes.map(hash =>
isRevoked(smartContract, hash)
);
export const isAnyHashRevokedOnStore = async (smartContract: OpenAttestationContract, intermediateHashes: Hash[]) => {
const revokedStatusDeferred = intermediateHashes.map(hash => isRevoked(smartContract, hash));
const revokedStatuses = await Promise.all(revokedStatusDeferred);
return revokedStatuses.some(status => status);
};
Expand All @@ -33,17 +28,12 @@ export const revokedStatusOnContracts = async (
return Promise.all(revokeStatusesDefered);
};

export const isRevokedOnAny = (
statuses: { address: Hash; revoked: boolean }[]
) => {
export const isRevokedOnAny = (statuses: { address: Hash; revoked: boolean }[]) => {
if (!statuses || statuses.length === 0) return false;
return statuses.some(status => status.revoked);
};

export const getIntermediateHashes = (
targetHash: Hash,
proofs: Hash[] = []
) => {
export const getIntermediateHashes = (targetHash: Hash, proofs: Hash[] = []) => {
const hashes = [`0x${targetHash}`];
proofs.reduce((prev, curr) => {
const next = utils.combineHashString(prev, curr);
Expand All @@ -53,17 +43,11 @@ export const getIntermediateHashes = (
return hashes;
};

export const verifyRevoked = async (
document: SignedDocument,
smartContracts: OpenAttestationContract[] = []
) => {
export const verifyRevoked = async (document: SignedDocument, smartContracts: OpenAttestationContract[] = []) => {
const { targetHash } = document.signature;
const proofs = document.signature.proof || [];
const intermediateHashes = getIntermediateHashes(targetHash, proofs);
const details = await revokedStatusOnContracts(
smartContracts,
intermediateHashes
);
const details = await revokedStatusOnContracts(smartContracts, intermediateHashes);
const revokedOnAny = isRevokedOnAny(details);

return {
Expand Down
Loading

0 comments on commit acbd650

Please sign in to comment.