From c32880dee6da92e89a5ab28a25de5953e3875c17 Mon Sep 17 00:00:00 2001 From: Nebulis Date: Tue, 31 Dec 2019 17:51:11 +0800 Subject: [PATCH] fix: dns verifier --- README.md | 2 +- package.json | 2 +- src/index.ts | 1 + src/verifiers/openAttestationDnsTxt.ts | 55 +-- .../openAttestationDnsTxt.v2.test.ts | 375 +++++++++++++----- .../openAttestationDnsTxt.v3.test.ts | 4 +- src/verify.v2.integration.test.ts | 8 +- 7 files changed, 313 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 453344b6..a71e7932 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CircleCI](https://circleci.com/gh/Open-Attestation/oa-verify.svg?style=svg)](https://circleci.com/gh/Open-Attestation/oa-verify) -Library to verify any [OpenAttestation](https://github.com/OpenCerts/open-attestation) document. This library implements [the verifier ADR](https://github.com/Open-Attestation/adr/blob/master/verifier.md). +Library to verify any [OpenAttestation](https://github.com/Open-Attestation/open-attestation) document. This library implements [the verifier ADR](https://github.com/Open-Attestation/adr/blob/master/verifier.md). ## Installation diff --git a/package.json b/package.json index e808fb0c..2d2344c8 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "git-cz": "^3.3.0", "jest": "^24.9.0", "prettier": "^1.19.1", - "semantic-release": "^15.13.31", + "semantic-release": "^15.14.0", "ts-jest": "^24.2.0", "typescript": "^3.7.3" }, diff --git a/src/index.ts b/src/index.ts index 74b0a582..8800090c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,4 +20,5 @@ const openAttestationVerifiers: Verifier< const verify = verificationBuilder(openAttestationVerifiers); +export * from "./types/core"; export { verificationBuilder, openAttestationVerifiers, isValid, verify, Verifier }; diff --git a/src/verifiers/openAttestationDnsTxt.ts b/src/verifiers/openAttestationDnsTxt.ts index 6d01e7f3..a9cd6eeb 100644 --- a/src/verifiers/openAttestationDnsTxt.ts +++ b/src/verifiers/openAttestationDnsTxt.ts @@ -3,17 +3,15 @@ import { getDocumentStoreRecords } from "@govtechsg/dnsprove"; import { getNetwork } from "ethers/utils"; import { isWrappedV2Document, VerificationFragmentType, VerificationManagerOptions, Verifier } from "../types/core"; -const getSmartContractAddress = (issuer: v2.Issuer) => issuer.documentStore || issuer.tokenRegistry; - type Identity = | { - identified: true; + status: "VALID"; dns: string; - smartContract: string; + value: string; } | { - identified: false; - smartContract: string; + status: "ERROR"; + value: string; error?: string | Error; }; // Resolve identity of an issuer, currently supporting only DNS-TXT @@ -36,13 +34,13 @@ const resolveIssuerIdentity = async ( ); return matchingRecord ? { - identified: true, + status: "VALID", dns: location, - smartContract: smartContractAddress + value: smartContractAddress } : { - identified: false, - smartContract: smartContractAddress + status: "ERROR", + value: smartContractAddress }; }; @@ -62,7 +60,12 @@ export const openAttestationDnsTxt: Verifier< test: document => { if (isWrappedV2Document(document)) { const documentData = getData(document); - return documentData.issuers.some(getSmartContractAddress); + // at least one issuer uses DNS-TXT + return documentData.issuers.some(issuer => { + return ( + (issuer.documentStore || issuer.tokenRegistry) && issuer.identityProof?.type === v2.IdentityProofType.DNSTxt + ); + }); } const documentData = getData(document); return documentData.issuer.identityProof.type === v3.IdentityProofType.DNSTxt; @@ -73,24 +76,28 @@ export const openAttestationDnsTxt: Verifier< if (isWrappedV2Document(document)) { const documentData = getData(document); const identities = await Promise.all( - // we expect the test function to prevent this issue => smart contract address MUST be populated - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - documentData.issuers.map(issuer => resolveIssuerIdentity(issuer, getSmartContractAddress(issuer)!, options)) + documentData.issuers.map(issuer => { + if (issuer.identityProof?.type === v2.IdentityProofType.DNSTxt) { + // we expect the test function to prevent this issue => smart contract address MUST be populated + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return resolveIssuerIdentity(issuer, (issuer.documentStore || issuer.tokenRegistry)!, options); + } + return { + status: "SKIPPED" + }; + }) ); - const invalidIdentity = identities.findIndex(identity => !identity.identified); + const invalidIdentity = identities.findIndex(identity => identity.status === "ERROR"); if (invalidIdentity !== -1) { + const value = + documentData.issuers[invalidIdentity].documentStore || documentData.issuers[invalidIdentity].tokenRegistry; + return { name, type, - data: { - type: documentData.issuers[invalidIdentity].identityProof?.type, - location: documentData.issuers[invalidIdentity].identityProof?.location, - value: - documentData.issuers[invalidIdentity].documentStore || - documentData.issuers[invalidIdentity].tokenRegistry - }, - message: "Certificate issuer identity is invalid", + data: identities, + message: `Certificate issuer identity for ${value} is invalid`, status: "INVALID" }; } @@ -103,7 +110,7 @@ export const openAttestationDnsTxt: Verifier< } const documentData = getData(document); const identity = await resolveIssuerIdentity(documentData.issuer, documentData.proof.value, options); - if (!identity.identified) { + if (identity.status === "ERROR") { return { name, type, diff --git a/src/verifiers/openAttestationDnsTxt.v2.test.ts b/src/verifiers/openAttestationDnsTxt.v2.test.ts index 195bc31d..ef5be6cc 100644 --- a/src/verifiers/openAttestationDnsTxt.v2.test.ts +++ b/src/verifiers/openAttestationDnsTxt.v2.test.ts @@ -1,120 +1,220 @@ import { openAttestationDnsTxt } from "./openAttestationDnsTxt"; import { documentRopstenValidWithToken } from "../../test/fixtures/v2/documentRopstenValidWithToken"; +import { verificationBuilder } from "./verificationBuilder"; +const verify = verificationBuilder([openAttestationDnsTxt]); +// TODO suggestion to use verification manager to test in a more realistic way. describe("OpenAttestationDnsTxt v2 document", () => { - it("should return a valid fragment when document has valid identity", async () => { - const fragment = await openAttestationDnsTxt.verify(documentRopstenValidWithToken, { - network: "ropsten" - }); - expect(fragment).toStrictEqual({ - type: "ISSUER_IDENTITY", - name: "OpenAttestationDnsTxt", - data: [ + describe("with one issuer", () => { + it("should return a valid fragment when document has valid identity", async () => { + const fragment = await verify(documentRopstenValidWithToken, { + network: "ropsten" + }); + expect(fragment).toStrictEqual([ { - dns: "example.tradetrust.io", - identified: true, - smartContract: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: [ + { + dns: "example.tradetrust.io", + status: "VALID", + value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + } + ], + status: "VALID" } - ], - status: "VALID" - }); - }); - it("should return an invalid fragment when document identity does not match", async () => { - const document = { - ...documentRopstenValidWithToken, - data: { - ...documentRopstenValidWithToken.data, - issuers: [ - { - ...documentRopstenValidWithToken.data.issuers[0], - tokenRegistry: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xabcd" - } - ] - } - }; - const fragment = await openAttestationDnsTxt.verify(document, { - network: "ropsten" + ]); }); - expect(fragment).toStrictEqual({ - type: "ISSUER_IDENTITY", - name: "OpenAttestationDnsTxt", - data: { location: "example.tradetrust.io", value: "0xabcd", type: "DNS-TXT" }, - message: "Certificate issuer identity is invalid", - status: "INVALID" + it("should return a valid fragment when document has valid identity and uses document store", async () => { + const document = { + ...documentRopstenValidWithToken, + data: { + ...documentRopstenValidWithToken.data, + issuers: [ + { + name: "2433e228-5bee-4863-9b98-2337f4f90306:string:DEMO STORE", + documentStore: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe", + identityProof: { + type: "1350e9f5-920b-496d-b95c-2a2793f5bff6:string:DNS-TXT", + location: "291a5524-f1c6-45f8-aebc-d691cf020fdd:string:example.tradetrust.io" + } + } + ] + } + }; + + const fragment = await verify(document, { + network: "ropsten" + }); + expect(fragment).toStrictEqual([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: [ + { + dns: "example.tradetrust.io", + status: "VALID", + value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + } + ], + status: "VALID" + } + ]); }); - }); - it("should return an error fragment when document has no identity type", async () => { - const document = { - ...documentRopstenValidWithToken, - data: { - ...documentRopstenValidWithToken.data, - issuers: [ - { - ...documentRopstenValidWithToken.data.issuers[0], - identityProof: { - ...documentRopstenValidWithToken.data.issuers[0].identityProof, - type: null + it("should return an invalid fragment when document identity does not match", async () => { + const document = { + ...documentRopstenValidWithToken, + data: { + ...documentRopstenValidWithToken.data, + issuers: [ + { + ...documentRopstenValidWithToken.data.issuers[0], + tokenRegistry: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xabcd" } - } - ] - } - }; - // @ts-ignore valid error, need to ignore - const fragment = await openAttestationDnsTxt.verify(document, { - network: "ropsten" + ] + } + }; + const fragment = await verify(document, { + network: "ropsten" + }); + expect(fragment).toStrictEqual([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: { location: "example.tradetrust.io", value: "0xabcd", type: "DNS-TXT" }, + message: "Certificate issuer identity for 0xabcd is invalid", + status: "INVALID" + } + ]); }); - expect(fragment).toStrictEqual({ - type: "ISSUER_IDENTITY", - name: "OpenAttestationDnsTxt", - data: new Error("Identity type not supported"), - message: "Identity type not supported", - status: "ERROR" + it("should return an error fragment when document has no identity location", async () => { + const document = { + ...documentRopstenValidWithToken, + data: { + ...documentRopstenValidWithToken.data, + issuers: [ + { + ...documentRopstenValidWithToken.data.issuers[0], + identityProof: { + ...documentRopstenValidWithToken.data.issuers[0].identityProof, + location: null + } + } + ] + } + }; + // @ts-ignore valid error, need to ignore + const fragment = await verify(document, { + network: "ropsten" + }); + expect(fragment).toStrictEqual([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: new Error("Location is missing"), + message: "Location is missing", + status: "ERROR" + } + ]); }); - }); - it("should return an error fragment when document has no identity location", async () => { - const document = { - ...documentRopstenValidWithToken, - data: { - ...documentRopstenValidWithToken.data, - issuers: [ - { - ...documentRopstenValidWithToken.data.issuers[0], - identityProof: { - ...documentRopstenValidWithToken.data.issuers[0].identityProof, - location: null + it("should return a skipped fragment if issuer has a tokenRegistry but does not provide identity proof", async () => { + const document = { + ...documentRopstenValidWithToken, + data: { + ...documentRopstenValidWithToken.data, + issuers: [ + { + name: "2433e228-5bee-4863-9b98-2337f4f90306:string:DEMO STORE", + tokenRegistry: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe", + identityProof: undefined } - } - ] - } - }; - // @ts-ignore valid error, need to ignore - const fragment = await openAttestationDnsTxt.verify(document, { - network: "ropsten" + ] + } + }; + expect( + await verify(document, { + network: "ropsten" + }) + ).toStrictEqual([ + { + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + name: "OpenAttestationDnsTxt", + status: "SKIPPED", + type: "ISSUER_IDENTITY" + } + ]); }); - expect(fragment).toStrictEqual({ - type: "ISSUER_IDENTITY", - name: "OpenAttestationDnsTxt", - data: new Error("Location is missing"), - message: "Location is missing", - status: "ERROR" + it("should return a skipped fragment if issuer has a document store but does not provide identity proof", async () => { + const document = { + ...documentRopstenValidWithToken, + data: { + ...documentRopstenValidWithToken.data, + issuers: [ + { + name: "2433e228-5bee-4863-9b98-2337f4f90306:string:DEMO STORE", + documentStore: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe", + identityProof: undefined + } + ] + } + }; + expect( + await verify(document, { + network: "ropsten" + }) + ).toStrictEqual([ + { + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + name: "OpenAttestationDnsTxt", + status: "SKIPPED", + type: "ISSUER_IDENTITY" + } + ]); }); - }); - - describe("test", () => { - it("should return true if at least one issuer has a documentStore", () => { + it("should return a skipped if issuer has a tokenRegistry but does not use DNS-TXT as identity proof", async () => { const document = { ...documentRopstenValidWithToken, data: { ...documentRopstenValidWithToken.data, issuers: [ { - name: "2433e228-5bee-4863-9b98-2337f4f90306:string:DEMO STORE" - }, + name: "2433e228-5bee-4863-9b98-2337f4f90306:string:DEMO STORE", + tokenRegistry: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe", + identityProof: { + type: "1350e9f5-920b-496d-b95c-2a2793f5bff6:string:OTHER-METHOD", + location: "291a5524-f1c6-45f8-aebc-d691cf020fdd:string:example.tradetrust.io" + } + } + ] + } + }; + expect( + await verify(document, { + network: "ropsten" + }) + ).toStrictEqual([ + { + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + name: "OpenAttestationDnsTxt", + status: "SKIPPED", + type: "ISSUER_IDENTITY" + } + ]); + }); + it("should return a skipped if issuer has a documentStore but does not use DNS-TXT as identity proof", async () => { + const document = { + ...documentRopstenValidWithToken, + data: { + ...documentRopstenValidWithToken.data, + issuers: [ { name: "2433e228-5bee-4863-9b98-2337f4f90306:string:DEMO STORE", documentStore: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe", identityProof: { - type: "1350e9f5-920b-496d-b95c-2a2793f5bff6:string:DNS-TXT", + type: "1350e9f5-920b-496d-b95c-2a2793f5bff6:string:OTHER-METHOD", location: "291a5524-f1c6-45f8-aebc-d691cf020fdd:string:example.tradetrust.io" } } @@ -122,12 +222,23 @@ describe("OpenAttestationDnsTxt v2 document", () => { } }; expect( - openAttestationDnsTxt.test(document, { + await verify(document, { network: "ropsten" }) - ).toStrictEqual(true); + ).toStrictEqual([ + { + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + name: "OpenAttestationDnsTxt", + status: "SKIPPED", + type: "ISSUER_IDENTITY" + } + ]); }); - it("should return true if at least one issuer has a tokenRegistry", () => { + }); + + describe("with multiple issuers", () => { + it("should return a valid fragment when document has one issuer with document store/valid identity and a second issuer without identity", async () => { const document = { ...documentRopstenValidWithToken, data: { @@ -138,7 +249,7 @@ describe("OpenAttestationDnsTxt v2 document", () => { }, { name: "2433e228-5bee-4863-9b98-2337f4f90306:string:DEMO STORE", - tokenRegistry: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe", + documentStore: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe", identityProof: { type: "1350e9f5-920b-496d-b95c-2a2793f5bff6:string:DNS-TXT", location: "291a5524-f1c6-45f8-aebc-d691cf020fdd:string:example.tradetrust.io" @@ -148,12 +259,64 @@ describe("OpenAttestationDnsTxt v2 document", () => { } }; expect( - openAttestationDnsTxt.test(document, { + await verify(document, { network: "ropsten" }) - ).toStrictEqual(true); + ).toStrictEqual([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: [ + { + status: "SKIPPED" + }, + { + dns: "example.tradetrust.io", + status: "VALID", + value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + } + ], + status: "VALID" + } + ]); }); - it("should return false if no issuer has a tokenRegistry or documentStore", () => { + it("should return a valid fragment when document has one issuer with token registry/valid identity and a second issuer without identity", async () => { + const document = { + ...documentRopstenValidWithToken, + data: { + ...documentRopstenValidWithToken.data, + issuers: [ + documentRopstenValidWithToken.data.issuers[0], + { + ...documentRopstenValidWithToken.data.issuers[0], + identityProof: undefined + } + ] + } + }; + + const fragment = await verify(document, { + network: "ropsten" + }); + expect(fragment).toStrictEqual([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: [ + { + dns: "example.tradetrust.io", + status: "VALID", + value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + }, + { + status: "SKIPPED" + } + ], + status: "VALID" + } + ]); + }); + it("should return skipped fragment if no issuer has a tokenRegistry or documentStore", async () => { const document = { ...documentRopstenValidWithToken, data: { @@ -170,10 +333,18 @@ describe("OpenAttestationDnsTxt v2 document", () => { } }; expect( - openAttestationDnsTxt.test(document, { + await verify(document, { network: "ropsten" }) - ).toStrictEqual(false); + ).toStrictEqual([ + { + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + name: "OpenAttestationDnsTxt", + status: "SKIPPED", + type: "ISSUER_IDENTITY" + } + ]); }); }); }); diff --git a/src/verifiers/openAttestationDnsTxt.v3.test.ts b/src/verifiers/openAttestationDnsTxt.v3.test.ts index f1988dec..6aa83424 100644 --- a/src/verifiers/openAttestationDnsTxt.v3.test.ts +++ b/src/verifiers/openAttestationDnsTxt.v3.test.ts @@ -26,8 +26,8 @@ describe("OpenAttestationDnsTxt v3 document", () => { name: "OpenAttestationDnsTxt", data: { dns: "example.openattestation.com", - identified: true, - smartContract: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + status: "VALID", + value: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" }, status: "VALID" }); diff --git a/src/verify.v2.integration.test.ts b/src/verify.v2.integration.test.ts index aa34e713..e50724cf 100644 --- a/src/verify.v2.integration.test.ts +++ b/src/verify.v2.integration.test.ts @@ -231,8 +231,8 @@ describe("verify(integration)", () => { data: [ { dns: "example.tradetrust.io", - identified: true, - smartContract: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + status: "VALID", + value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" } ], status: "VALID", @@ -290,8 +290,8 @@ describe("verify(integration)", () => { data: [ { dns: "tradetrust.io", - identified: true, - smartContract: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3" + status: "VALID", + value: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3" } ], status: "VALID",