Skip to content

Commit

Permalink
feat: ocsp revocation against merkle root & intermediate hashes (#228)
Browse files Browse the repository at this point in the history
* feat: add ocsp response v2 type guard

* refactor: deprecate isRevokedByOcspResponder() and re-order functions

* feat: ocsp revocation against merkle root & intermediate hashes

* test: add test for new ocsp responder

* refactor: move try/catch to parent caller

* refactor: rename ocsp location to use ocsp.example.com
  • Loading branch information
HJunyuan authored Jun 13, 2022
1 parent 870c28a commit fd5d5f0
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 96 deletions.
162 changes: 142 additions & 20 deletions src/verifiers/documentStatus/didSigned/didSignedDocumentStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ describe("verify", () => {
beforeEach(() => {
mockGetPublicKey.mockReset();
});

describe("v2", () => {
it("should pass for documents using `DID` and is correctly signed", async () => {
whenPublicKeyResolvesSuccessfully("0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89");
Expand Down Expand Up @@ -451,12 +452,11 @@ describe("verify", () => {
}
`);
});

it("should pass when DID document is signed and is not revoked by an OCSP", async () => {
whenPublicKeyResolvesSuccessfully("0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89");
it("should pass when DID document is signed and is not revoked by an OCSP v1", async () => {
whenPublicKeyResolvesSuccessfully();

const handlers = [
rest.get("https://www.ica.gov.sg/ocsp/SGCNM21566327", (_, res, ctx) => {
rest.get("https://ocsp.example.com/SGCNM21566327", (_, res, ctx) => {
return res(
ctx.json({
certificateId: "SGCNM21566327",
Expand All @@ -476,13 +476,13 @@ describe("verify", () => {
"details": Object {
"issuance": Array [
Object {
"did": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89",
"did": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90",
"issued": true,
},
],
"revocation": Array [
Object {
"address": "https://www.ica.gov.sg/ocsp",
"address": "https://ocsp.example.com",
"revoked": false,
},
],
Expand All @@ -498,12 +498,11 @@ describe("verify", () => {

server.close();
});

it("should fail when DID document is signed but is found by an OCSP", async () => {
whenPublicKeyResolvesSuccessfully("0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89");
it("should fail when DID document is signed but is found by an OCSP v1", async () => {
whenPublicKeyResolvesSuccessfully();

const handlers = [
rest.get("https://www.ica.gov.sg/ocsp/SGCNM21566327", (_, res, ctx) => {
rest.get("https://ocsp.example.com/SGCNM21566327", (_, res, ctx) => {
return res(
ctx.json({
certificateId: "SGCNM21566327",
Expand All @@ -526,13 +525,13 @@ describe("verify", () => {
"details": Object {
"issuance": Array [
Object {
"did": "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89",
"did": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90",
"issued": true,
},
],
"revocation": Array [
Object {
"address": "https://www.ica.gov.sg/ocsp",
"address": "https://ocsp.example.com",
"reason": Object {
"code": 4,
"codeString": "SUPERSEDED",
Expand All @@ -556,6 +555,137 @@ describe("verify", () => {
}
`);

server.close();
});
it("should pass when DID document is signed and is not revoked by an OCSP v2", async () => {
whenPublicKeyResolvesSuccessfully();

const handlers = [
rest.get(
"https://ocsp.example.com/0x28b221f6287d8e4f8da09a835bcb750537cc8385e2535ff63591fdf0162be824",
(_, res, ctx) => {
return res(
ctx.json({
revoked: false,
documentHash: "0x28b221f6287d8e4f8da09a835bcb750537cc8385e2535ff63591fdf0162be824",
})
);
}
),
rest.get(
"https://ocsp.example.com/0x56961854a82feafe9a56eb57acfe3b97f17eda5d497b622c9acc9f03c412618c",
(_, res, ctx) => {
return res(
ctx.json({
revoked: false,
documentHash: "0x56961854a82feafe9a56eb57acfe3b97f17eda5d497b622c9acc9f03c412618c",
})
);
}
),
];

const server: SetupServerApi = setupServer(...handlers);
server.listen();

const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcsp, options);
expect(res).toMatchInlineSnapshot(`
Object {
"data": Object {
"details": Object {
"issuance": Array [
Object {
"did": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90",
"issued": true,
},
],
"revocation": Array [
Object {
"address": "https://ocsp.example.com",
"revoked": false,
},
],
},
"issuedOnAll": true,
"revokedOnAny": false,
},
"name": "OpenAttestationDidSignedDocumentStatus",
"status": "VALID",
"type": "DOCUMENT_STATUS",
}
`);

server.close();
});
it("should fail when DID document is signed but is found by an OCSP v2", async () => {
whenPublicKeyResolvesSuccessfully();

const handlers = [
rest.get(
"https://ocsp.example.com/0x28b221f6287d8e4f8da09a835bcb750537cc8385e2535ff63591fdf0162be824",
(_, res, ctx) => {
return res(
ctx.json({
revoked: true,
documentHash: "0x28b221f6287d8e4f8da09a835bcb750537cc8385e2535ff63591fdf0162be824",
reasonCode: 4,
})
);
}
),
rest.get(
"https://ocsp.example.com/0x56961854a82feafe9a56eb57acfe3b97f17eda5d497b622c9acc9f03c412618c",
(_, res, ctx) => {
return res(
ctx.json({
revoked: false,
documentHash: "0x56961854a82feafe9a56eb57acfe3b97f17eda5d497b622c9acc9f03c412618c",
})
);
}
),
];

const server: SetupServerApi = setupServer(...handlers);
server.listen();

const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcsp, options);
expect(res).toMatchInlineSnapshot(`
Object {
"data": Object {
"details": Object {
"issuance": Array [
Object {
"did": "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90",
"issued": true,
},
],
"revocation": Array [
Object {
"address": "https://ocsp.example.com",
"reason": Object {
"code": 4,
"codeString": "SUPERSEDED",
"message": "Document 0x56961854a82feafe9a56eb57acfe3b97f17eda5d497b622c9acc9f03c412618c has been revoked under OCSP Responder: https://ocsp.example.com",
},
"revoked": true,
},
],
},
"issuedOnAll": true,
"revokedOnAny": true,
},
"name": "OpenAttestationDidSignedDocumentStatus",
"reason": Object {
"code": 4,
"codeString": "SUPERSEDED",
"message": "Document 0x56961854a82feafe9a56eb57acfe3b97f17eda5d497b622c9acc9f03c412618c has been revoked under OCSP Responder: https://ocsp.example.com",
},
"status": "INVALID",
"type": "DOCUMENT_STATUS",
}
`);

server.close();
});
});
Expand Down Expand Up @@ -602,7 +732,6 @@ describe("verify", () => {
}
`);
});

it("should pass for documents using `DID` and is correctly signed, and is not revoked on a document store (if specified)", async () => {
whenPublicKeyResolvesSuccessfully();
const res = await openAttestationDidSignedDocumentStatus.verify(didSignedRevocationStoreNotRevokedV3, options);
Expand All @@ -628,7 +757,6 @@ describe("verify", () => {
}
`);
});

it("should pass for documents using `DID-DNS` and is correctly signed", async () => {
whenPublicKeyResolvesSuccessfully();
const res = await openAttestationDidSignedDocumentStatus.verify(dnsDidSignedV3, options);
Expand All @@ -653,7 +781,6 @@ describe("verify", () => {
}
`);
});

it("should pass for documents using `DID-DNS` and is correctly signed, and is not revoked on a document store (if specified)", async () => {
whenPublicKeyResolvesSuccessfully();
const res = await openAttestationDidSignedDocumentStatus.verify(dnsDidSignedRevocationStoreNotRevokedV3, options);
Expand All @@ -679,7 +806,6 @@ describe("verify", () => {
}
`);
});

it("should fail when revocation block is missing", async () => {
whenPublicKeyResolvesSuccessfully();
const docWithoutRevocationBlock = {
Expand Down Expand Up @@ -716,7 +842,6 @@ describe("verify", () => {
}
`);
});

it("should throw an unrecognized revocation type error when revocation is not set to NONE or REVOCATION_STORE", async () => {
whenPublicKeyResolvesSuccessfully();
const docWithIncorrectRevocation = {
Expand Down Expand Up @@ -748,7 +873,6 @@ describe("verify", () => {
}
`);
});

it("should fail for documents using `DID` and is correctly signed, and is revoked on a document store (if specified)", async () => {
whenPublicKeyResolvesSuccessfully();
const res = await openAttestationDidSignedDocumentStatus.verify(didSignedRevocationStoreButRevokedV3, options);
Expand Down Expand Up @@ -784,7 +908,6 @@ describe("verify", () => {
}
`);
});

it("should fail for documents using `DID-DNS` and is correctly signed, and is revoked on a document store (if specified)", async () => {
whenPublicKeyResolvesSuccessfully();
const res = await openAttestationDidSignedDocumentStatus.verify(dnsDidSignedRevocationStoreButRevokedV3, options);
Expand Down Expand Up @@ -820,7 +943,6 @@ describe("verify", () => {
}
`);
});

it("should fail when signature is wrong", async () => {
whenPublicKeyResolvesSuccessfully();
const documentWithWrongSig = {
Expand Down
18 changes: 12 additions & 6 deletions src/verifiers/documentStatus/didSigned/didSignedDocumentStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OpenAttestationDidSignedDocumentStatusCode, Reason } from "../../../typ
import { DidVerificationStatus, ValidDidVerificationStatus, verifySignature } from "../../../did/verifier";
import { CodedError } from "../../../common/error";
import { withCodedErrorHandler } from "../../../common/errorHandler";
import { isRevokedByOcspResponder, isRevokedOnDocumentStore } from "../utils";
import { isRevokedByOcspResponder, isRevokedByOcspResponder2, isRevokedOnDocumentStore } from "../utils";
import { InvalidRevocationStatus, RevocationStatus, ValidRevocationStatus } from "../revocation.types";
import {
DidSignedIssuanceStatus,
Expand Down Expand Up @@ -98,11 +98,17 @@ const verifyV2 = async (
"REVOCATION_LOCATION_MISSING"
);
case v2.RevocationType.OcspResponder:
if (typeof revocationItem.location === "string") {
return isRevokedByOcspResponder({
certificateId: documentData.id as string,
location: revocationItem.location,
});
const { location } = revocationItem;
if (typeof location === "string") {
return isRevokedByOcspResponder2({
merkleRoot,
targetHash,
proofs,
location,
}).catch(() =>
// FIXME: Omit this catch fallback after removing support for old OCSP responders
isRevokedByOcspResponder({ certificateId: documentData.id as string, location })
);
}
throw new CodedError(
"missing revocation location for an issuer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,41 @@ export type DidSignedIssuanceStatusArray = Static<typeof DidSignedIssuanceStatus
*/

export const ValidOcspReasonCode = Number.withConstraint((n) => n >= 0 && n <= 10 && n != 7);
/**
* @deprecated Replaced by `ValidOcspResponse2`
*
* This type guard is retained for backwards compatibility with older OCSP responders
* that are already in the wild. Do consider removing support for the old OCSP responders
* in the next major version of oa-verify.
*
* OCSP Responder (new response format): https://github.com/Open-Attestation/ocsp-responder#checking-document-status
*/
export const ValidOcspResponse = Record({
certificateStatus: OcspResponderRevocationStatus,
});
export const ValidOcspResponse2 = Record({
revoked: Literal(false),
documentHash: String,
});

/**
* @deprecated Replaced by `ValidOcspResponseRevoked2`
*
* This type guard is retained for backwards compatibility with older OCSP responders
* that are already in the wild. Do consider removing support for the old OCSP responders
* in the next major version of oa-verify.
*
* OCSP Responder (new response format): https://github.com/Open-Attestation/ocsp-responder#checking-document-status
*/
export const ValidOcspResponseRevoked = Record({
reasonCode: ValidOcspReasonCode,
certificateStatus: OcspResponderRevocationStatus,
});
export const ValidOcspResponseRevoked2 = Record({
revoked: Literal(true),
documentHash: String,
reasonCode: ValidOcspReasonCode,
});

/**
* Data for v2 Fragments
Expand Down
Loading

0 comments on commit fd5d5f0

Please sign in to comment.