Skip to content

Commit

Permalink
feat: 🎸 verify signed proof blocks (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamJLemmon authored May 11, 2020
1 parent bca629c commit 7cb7499
Show file tree
Hide file tree
Showing 12 changed files with 1,428 additions and 452 deletions.
1,302 changes: 868 additions & 434 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { v2, v3, WrappedDocument } from "@govtechsg/open-attestation";
import { SignedWrappedDocument, v2, v3, WrappedDocument } from "@govtechsg/open-attestation";
import { verificationBuilder } from "./verifiers/verificationBuilder";
import { Verifier } from "./types/core";
import { Verifier, Verifiers } from "./types/core";
import { openAttestationHash } from "./verifiers/hash/openAttestationHash";
import { openAttestationDnsTxt, Identity } from "./verifiers/dnsText/openAttestationDnsTxt";
import { Identity, openAttestationDnsTxt } from "./verifiers/dnsText/openAttestationDnsTxt";
import { openAttestationEthereumDocumentStoreIssued } from "./verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued";
import { openAttestationSignedProof } from "./verifiers/signedProof/openAttestationSignedProof";
import { openAttestationEthereumDocumentStoreRevoked } from "./verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked";
import { isValid } from "./validator";
import { openAttestationEthereumTokenRegistryMinted } from "./verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted";

const openAttestationVerifiers: Verifier<
WrappedDocument<v2.OpenAttestationDocument> | WrappedDocument<v3.OpenAttestationDocument>
>[] = [
const openAttestationVerifiers: Verifiers[] = [
openAttestationHash,
openAttestationSignedProof,
openAttestationEthereumDocumentStoreIssued,
openAttestationEthereumTokenRegistryMinted,
openAttestationEthereumDocumentStoreRevoked,
openAttestationDnsTxt,
];

const verify = verificationBuilder(openAttestationVerifiers);
const verify = verificationBuilder<
| SignedWrappedDocument<v2.OpenAttestationDocument>
| WrappedDocument<v2.OpenAttestationDocument>
| WrappedDocument<v3.OpenAttestationDocument>
>(openAttestationVerifiers);

export * from "./types/core";
export * from "./types/error";
Expand All @@ -31,6 +35,7 @@ export {
Identity,
openAttestationHash,
openAttestationEthereumDocumentStoreRevoked,
openAttestationSignedProof,
openAttestationEthereumDocumentStoreIssued,
openAttestationDnsTxt,
openAttestationEthereumTokenRegistryMinted,
Expand Down
21 changes: 14 additions & 7 deletions src/types/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Contract } from "ethers";
import { v2, v3, WrappedDocument } from "@govtechsg/open-attestation";
import { SignedWrappedDocument, v2, v3, WrappedDocument } from "@govtechsg/open-attestation";
import { Reason } from "./error";

/**
Expand Down Expand Up @@ -60,8 +59,16 @@ export interface Verifier<
}
export type Hash = string;

export interface OpenAttestationContract {
address: Hash;
type: v3.Method;
instance: Contract;
}
export type DocumentsToVerify =
| WrappedDocument<v2.OpenAttestationDocument>
| WrappedDocument<v3.OpenAttestationDocument>
| SignedWrappedDocument<v2.OpenAttestationDocument>;

// TODO move to open-attestation
export const isSignedWrappedDocument = (
document: any
): document is SignedWrappedDocument<v2.OpenAttestationDocument> => {
return Object.keys(document).includes("proof");
};

export type Verifiers = Verifier<DocumentsToVerify>;
6 changes: 6 additions & 0 deletions src/types/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export enum OpenAttestationEthereumDocumentStoreIssuedCode {
SKIPPED = 4,
CONTRACT_NOT_FOUND = 404,
}
export enum OpenAttestationDocumentSignedCode {
UNEXPECTED_ERROR = 0,
DOCUMENT_PROOF_INVALID = 1,
DOCUMENT_PROOF_ERROR = 2,
SKIPPED = 4,
}
export enum OpenAttestationEthereumDocumentStoreRevokedCode {
UNEXPECTED_ERROR = 0,
DOCUMENT_REVOKED = 1,
Expand Down
81 changes: 81 additions & 0 deletions src/verifiers/signedProof/openAttestationSignedProof.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { verificationBuilder } from "../verificationBuilder";
import { openAttestationSignedProof } from "./openAttestationSignedProof";
import { documentSignedProofValid } from "../../../test/fixtures/v2/documentSignedProofValid";
import { documentSignedProofInvalidProofType } from "../../../test/fixtures/v2/documentSignedProofInvalidProofType";
import { documentSignedProofInvalidSignature } from "../../../test/fixtures/v2/documentSignedProofInvalidSignature";
import { documentMainnetValidWithCertificateStore } from "../../../test/fixtures/v2/documentMainnetValidWithCertificateStore";

const verify = verificationBuilder([openAttestationSignedProof]);

describe("OpenAttestationSignedProof", () => {
describe("SKIPPED", () => {
it("should return a valid SKIPPED fragment when document does not have a proof a block", async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore error is valid, inoring for test purpose
const fragment = await verify(documentMainnetValidWithCertificateStore, {
network: "ropsten",
});
expect(fragment).toStrictEqual([
{
name: "OpenAttestationSignedProof",
type: "DOCUMENT_STATUS",
reason: {
code: 4,
codeString: "SKIPPED",
message: "Document does not have a proof block",
},
status: "SKIPPED",
},
]);
});
});
describe("v2", () => {
it("should return a valid fragment when document has valid signed proof", async () => {
const fragment = await verify(documentSignedProofValid, {
network: "ropsten",
});
expect(fragment).toStrictEqual([
{
name: "OpenAttestationSignedProof",
type: "DOCUMENT_STATUS",
status: "VALID",
},
]);
});
it("should return an invalid fragment when document has an invalid proof signature", async () => {
const fragment = await verify(documentSignedProofInvalidSignature, {
network: "ropsten",
});
expect(fragment).toStrictEqual([
{
name: "OpenAttestationSignedProof",
type: "DOCUMENT_STATUS",
reason: {
code: 1,
codeString: "DOCUMENT_PROOF_INVALID",
message: "Certificate proof is invalid",
},
status: "INVALID",
},
]);
});
it("should return an invalid fragment when document proof uses an unsupported proof type", async () => {
const fragment = await verify(documentSignedProofInvalidProofType, {
network: "ropsten",
});
expect(fragment).toStrictEqual([
{
name: "OpenAttestationSignedProof",
type: "DOCUMENT_STATUS",
data: new Error(`Proof type: notSupported is not supported.`),
reason: {
code: 2,
codeString: "DOCUMENT_PROOF_ERROR",
message: `Proof type: notSupported is not supported.`,
},
status: "ERROR",
},
]);
});
});
});
64 changes: 64 additions & 0 deletions src/verifiers/signedProof/openAttestationSignedProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as ethers from "ethers";
import { DocumentsToVerify, isSignedWrappedDocument, Verifier } from "../../types/core";
import { OpenAttestationDocumentSignedCode } from "../../types/error";

const name = "OpenAttestationSignedProof";
const type = "DOCUMENT_STATUS";

export const openAttestationSignedProof: Verifier<DocumentsToVerify> = {
skip: () => {
return Promise.resolve({
status: "SKIPPED",
type,
name,
reason: {
code: OpenAttestationDocumentSignedCode.SKIPPED,
codeString: OpenAttestationDocumentSignedCode[OpenAttestationDocumentSignedCode.SKIPPED],
message: `Document does not have a proof block`,
},
});
},
test: (document) => {
return isSignedWrappedDocument(document);
},
verify: async (document) => {
try {
if (!isSignedWrappedDocument(document)) throw new Error(`No proof was found in document.`); // Optional param, silence undefined type error
// Note that proof.verificationMethod currently only supports a publicKey, no URLs ie. DIDs
const { proof, signature } = document;
let proofValid = false;

if (proof.type === "EcdsaSecp256k1Signature2019") {
// Existing targetHash is being signed
const msg = signature.targetHash;
const recoverAddress = ethers.utils.verifyMessage(msg, proof.signature);
proofValid = recoverAddress.toLowerCase() === proof.verificationMethod.toLowerCase();
} else {
throw new Error(`Proof type: ${proof.type} is not supported.`);
}
if (proofValid) {
const status = "VALID";
return { name, type, status };
} else {
const status = "INVALID";
const message = "Certificate proof is invalid";
const reason = {
code: OpenAttestationDocumentSignedCode.DOCUMENT_PROOF_INVALID,
codeString: OpenAttestationDocumentSignedCode[OpenAttestationDocumentSignedCode.DOCUMENT_PROOF_INVALID],
message,
};
return { name, type, status, reason };
}
} catch (e) {
const data = e;
const { message } = e;
const status = "ERROR";
const reason = {
code: OpenAttestationDocumentSignedCode.DOCUMENT_PROOF_ERROR,
codeString: OpenAttestationDocumentSignedCode[OpenAttestationDocumentSignedCode.DOCUMENT_PROOF_ERROR],
message,
};
return { name, type, data, reason, status };
}
},
};
8 changes: 4 additions & 4 deletions src/verifiers/verificationBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { v3, WrappedDocument } from "@govtechsg/open-attestation";
import { VerificationFragment, VerificationManagerOptions, Verifier } from "../types/core";

/**
* A verification manager will run a list of {@link Verifier} over a signed document.
* Before running each verifier, the manager will make sure the verifier can handle the specific document by calling its exposed test function.
* The manager will return the consolidated list of {@link VerificationFragment}
*/
export const verificationBuilder = <Document = WrappedDocument<v3.OpenAttestationDocument>>(
verifiers: Verifier<Document>[]
) => (document: Document, options: VerificationManagerOptions): Promise<VerificationFragment[]> => {
export const verificationBuilder = <Document>(verifiers: Verifier<Document>[]) => (
document: Document,
options: VerificationManagerOptions
): Promise<VerificationFragment[]> => {
const promises: Promise<VerificationFragment>[] = verifiers.map((verifier) => {
if (verifier.test(document, options)) {
return verifier.verify(document, options);
Expand Down
Loading

0 comments on commit 7cb7499

Please sign in to comment.