From e1e040d64b9f297728b9050c57765a1e8947db85 Mon Sep 17 00:00:00 2001 From: Laurent Maillet Date: Wed, 8 Jan 2020 17:09:46 +0800 Subject: [PATCH] fix: dns verifier (#77) * fix: dns verifier --- .eslintrc.json | 3 +- README.md | 2 +- package.json | 2 +- src/index.ts | 1 + src/verifiers/openAttestationDnsTxt.ts | 93 ++-- .../openAttestationDnsTxt.v2.test.ts | 415 +++++++++++++----- .../openAttestationDnsTxt.v3.test.ts | 4 +- src/verify.v2.integration.test.ts | 8 +- 8 files changed, 379 insertions(+), 149 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 840eadf8..561ce400 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,6 +25,7 @@ "import/extensions": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-explicit-any": "off", - "no-unused-expressions": "off" + "no-unused-expressions": "off", + "no-else-return": "off" } } 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..c36bd35b 100644 --- a/src/verifiers/openAttestationDnsTxt.ts +++ b/src/verifiers/openAttestationDnsTxt.ts @@ -3,20 +3,18 @@ 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; - error?: string | Error; + status: "INVALID"; + value: string; }; // Resolve identity of an issuer, currently supporting only DNS-TXT +// DNS-TXT is explained => https://github.com/Open-Attestation/adr/blob/master/decentralized_identity_proof_DNS-TXT.md const resolveIssuerIdentity = async ( issuer: v2.Issuer | v3.Issuer, smartContractAddress: string, @@ -36,13 +34,13 @@ const resolveIssuerIdentity = async ( ); return matchingRecord ? { - identified: true, + status: "VALID", dns: location, - smartContract: smartContractAddress + value: smartContractAddress } : { - identified: false, - smartContract: smartContractAddress + status: "INVALID", + 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 === "INVALID"); if (invalidIdentity !== -1) { + const smartContractAddress = + 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 ${smartContractAddress} is invalid`, status: "INVALID" }; } @@ -100,29 +107,31 @@ export const openAttestationDnsTxt: Verifier< data: identities, status: "VALID" }; - } - const documentData = getData(document); - const identity = await resolveIssuerIdentity(documentData.issuer, documentData.proof.value, options); - if (!identity.identified) { + } else { + // we have a v3 document + const documentData = getData(document); + const identity = await resolveIssuerIdentity(documentData.issuer, documentData.proof.value, options); + if (identity.status === "INVALID") { + return { + name, + type, + data: { + type: documentData.issuer.identityProof.type, + location: documentData.issuer.identityProof.location, + value: documentData.proof.value + }, + message: "Certificate issuer identity is invalid", + status: "INVALID" + }; + } + return { name, type, - data: { - type: documentData.issuer.identityProof.type, - location: documentData.issuer.identityProof.location, - value: documentData.proof.value - }, - message: "Certificate issuer identity is invalid", - status: "INVALID" + data: identity, + status: "VALID" }; } - - return { - name, - type, - data: identity, - status: "VALID" - }; } catch (e) { return { name, diff --git a/src/verifiers/openAttestationDnsTxt.v2.test.ts b/src/verifiers/openAttestationDnsTxt.v2.test.ts index 195bc31d..b91f5e8c 100644 --- a/src/verifiers/openAttestationDnsTxt.v2.test.ts +++ b/src/verifiers/openAttestationDnsTxt.v2.test.ts @@ -1,107 +1,243 @@ import { openAttestationDnsTxt } from "./openAttestationDnsTxt"; import { documentRopstenValidWithToken } from "../../test/fixtures/v2/documentRopstenValidWithToken"; +import { verificationBuilder } from "./verificationBuilder"; +const verify = verificationBuilder([openAttestationDnsTxt]); describe("OpenAttestationDnsTxt v2 document", () => { - it("should return a valid fragment when document has valid identity", async () => { - const fragment = await openAttestationDnsTxt.verify(documentRopstenValidWithToken, { - network: "ropsten" + 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([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: [ + { + dns: "example.tradetrust.io", + status: "VALID", + value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + } + ], + status: "VALID" + } + ]); }); - expect(fragment).toStrictEqual({ - type: "ISSUER_IDENTITY", - name: "OpenAttestationDnsTxt", - data: [ + 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([ { - 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" + 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 verify(document, { + network: "ropsten" + }); + expect(fragment).toStrictEqual([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: [{ status: "INVALID", value: "0xabcd" }], + message: "Certificate issuer identity for 0xabcd is invalid", + status: "INVALID" + } + ]); }); - 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 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 type", async () => { - const document = { - ...documentRopstenValidWithToken, - data: { - ...documentRopstenValidWithToken.data, - issuers: [ - { - ...documentRopstenValidWithToken.data.issuers[0], - identityProof: { - ...documentRopstenValidWithToken.data.issuers[0].identityProof, - type: null - } - } - ] - } - }; - // @ts-ignore valid error, need to ignore - const fragment = await openAttestationDnsTxt.verify(document, { - network: "ropsten" + 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 + } + ] + } + }; + 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("Identity type not supported"), - message: "Identity type not supported", - 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" + } + ]); }); - }); - 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 openAttestationDnsTxt.verify(document, { - network: "ropsten" + 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", + 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" + } + ]); }); - 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 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: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" + } + ]); }); }); - describe("test", () => { - it("should return true if at least one issuer has a documentStore", () => { + 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: { @@ -122,23 +258,44 @@ 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 true if at least one issuer has a tokenRegistry", () => { + it("should return an invalid fragment when document has one issuer with document store/valid identity and a second issuer with invalid identity", 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", + documentStore: "1d337929-6770-4a05-ace0-1f07c25c7615:string:0xabcd", + identityProof: { + type: "1350e9f5-920b-496d-b95c-2a2793f5bff6:string:DNS-TXT", + location: "291a5524-f1c6-45f8-aebc-d691cf020fdd:string:example.tradetrust.io" + } }, { 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 +305,66 @@ describe("OpenAttestationDnsTxt v2 document", () => { } }; expect( - openAttestationDnsTxt.test(document, { + await verify(document, { network: "ropsten" }) - ).toStrictEqual(true); + ).toStrictEqual([ + { + type: "ISSUER_IDENTITY", + name: "OpenAttestationDnsTxt", + data: [ + { + status: "INVALID", + value: "0xabcd" + }, + { + dns: "example.tradetrust.io", + status: "VALID", + value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" + } + ], + message: "Certificate issuer identity for 0xabcd is invalid", + status: "INVALID" + } + ]); }); - 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 +381,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",