Skip to content

Commit

Permalink
feat: implement OCSP Responder for OA v3 documents (Open-Attestation#245
Browse files Browse the repository at this point in the history
)

- Implement handling of `OCSP_RESPONDER` type of revocation for OA v3 documents
- Add tests for DID-issued OA v3 documents with OCSP_RESPONDER type of revocation
- Housekeeping of filenames for fixtures

BREAKING CHANGE: the old OCSP Responder implementation (where it checks by by document id instead of merkle root & intermediate hashes) is now deprecated

It is no longer possible to perform OCSP revocation by document id as mentioned in Open-Attestation#228


Fixes Open-Attestation#243
  • Loading branch information
HJunyuan authored Feb 6, 2023
1 parent 9638ba5 commit edeccf6
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 217 deletions.
20 changes: 0 additions & 20 deletions src/types/core.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { SignedWrappedDocument, v2, v3, WrappedDocument } from "@govtechsg/open-attestation";
import { Resolver } from "did-resolver";
import { providers } from "ethers";
import {
OcspResponderRevocationReason,
OcspResponderRevocationStatus,
} from "src/verifiers/documentStatus/revocation.types";
import { Reason } from "./error";

/**
Expand Down Expand Up @@ -120,19 +116,3 @@ export interface ProviderDetails {
url?: string;
apiKey?: string;
}

/**
* Specifies the parameters of the OCSP response
* @param {string} certificateStatus - status of the certificate {@link OcspResponderRevocationStatus}
*/
export interface OcspResponse {
certificateStatus: OcspResponderRevocationStatus;
}

/**
* Specifies the parameters of the OCSP response when document is revoked
* @param {number} reasonCode - code indicating reason for revocation {@link OcspResponderRevocationReason}
*/
export interface OcspResponseRevoked extends OcspResponse {
reasonCode: OcspResponderRevocationReason;
}
221 changes: 109 additions & 112 deletions src/verifiers/documentStatus/didSigned/didSignedDocumentStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import sampleDidSignedRevocationStoreButRevokedV3 from "../../../../test/fixture
import sampleDidSignedV3 from "../../../../test/fixtures/v3/did-signed.json";
import sampleDnsDidSignedRevocationStoreNotRevokedV3 from "../../../../test/fixtures/v3/dnsdid-revocation-store-signed-not-revoked.json";
import sampleDnsDidSignedRevocationStoreButRevokedV3 from "../../../../test/fixtures/v3/dnsdid-revocation-store-signed-revoked.json";
import sampleDidSignedOcspResponderV3 from "../../../../test/fixtures/v3/did-ocsp-revocation-signed.json";
import sampleDNSDidSignedV3 from "../../../../test/fixtures/v3/dnsdid-signed.json";
import sampleDocumentStoreWrappedV3 from "../../../../test/fixtures/v3/documentStore-wrapped.json";
import sampleTokenRegistryWrappedV3 from "../../../../test/fixtures/v3/tokenRegistry-wrapped.json";
Expand All @@ -25,15 +26,14 @@ import sampleDidSignedRevocationStoreNotRevokedV2 from "../../../../test/fixture
import sampleDidSignedRevocationStoreButRevokedV2 from "../../../../test/fixtures/v2/did-revocation-store-signed-revoked.json";
import sampleDnsDidSignedRevocationStoreNotRevokedV2 from "../../../../test/fixtures/v2/dnsdid-revocation-store-signed-not-revoked.json";
import sampleDnsDidSignedRevocationStoreButRevokedV2 from "../../../../test/fixtures/v2/dnsdid-revocation-store-signed-revoked.json";

import sampleDidSignedOcsp from "../../../../test/fixtures/v2/did-revocation-ocsp-signed.json";
import sampleDidSignedOcspResponderV2 from "../../../../test/fixtures/v2/did-ocsp-revocation-signed.json";

const didSignedRevocationStoreNotRevokedV2 = sampleDidSignedRevocationStoreNotRevokedV2 as SignedWrappedDocument<v2.OpenAttestationDocument>;
const didSignedRevocationStoreButRevokedV2 = sampleDidSignedRevocationStoreButRevokedV2 as SignedWrappedDocument<v2.OpenAttestationDocument>;
const didSignedRevocationStoreButNoLocationV2 = sampleDidSignedRevocationStoreButNoLocationV2 as SignedWrappedDocument<v2.OpenAttestationDocument>;
const dnsDidSignedRevocationStoreNotRevokedV2 = sampleDnsDidSignedRevocationStoreNotRevokedV2 as SignedWrappedDocument<v2.OpenAttestationDocument>;
const dnsDidSignedRevocationStoreButRevokedV2 = sampleDnsDidSignedRevocationStoreButRevokedV2 as SignedWrappedDocument<v2.OpenAttestationDocument>;
const didSignedOcsp = sampleDidSignedOcsp as SignedWrappedDocument<v2.OpenAttestationDocument>;
const didSignedOcspResponderV2 = sampleDidSignedOcspResponderV2 as SignedWrappedDocument<v2.OpenAttestationDocument>;

const documentStoreWrapV3 = sampleDocumentStoreWrappedV3 as WrappedDocument<v3.OpenAttestationDocument>;
const tokenRegistryWrapV3 = sampleTokenRegistryWrappedV3 as WrappedDocument<v3.OpenAttestationDocument>;
Expand All @@ -44,6 +44,7 @@ const didSignedRevocationStoreButNoLocationV3 = sampleDidSignedRevocationStoreBu
const dnsDidSignedV3 = sampleDNSDidSignedV3 as SignedWrappedDocument<v3.OpenAttestationDocument>;
const dnsDidSignedRevocationStoreNotRevokedV3 = sampleDnsDidSignedRevocationStoreNotRevokedV3 as SignedWrappedDocument<v3.OpenAttestationDocument>;
const dnsDidSignedRevocationStoreButRevokedV3 = sampleDnsDidSignedRevocationStoreButRevokedV3 as SignedWrappedDocument<v3.OpenAttestationDocument>;
const didSignedOcspResponderV3 = sampleDidSignedOcspResponderV3 as SignedWrappedDocument<v3.OpenAttestationDocument>;

jest.mock("../../../did/resolver");

Expand Down Expand Up @@ -453,112 +454,7 @@ describe("verify", () => {
}
`);
});
it("should pass when DID document is signed and is not revoked by an OCSP v1", async () => {
whenPublicKeyResolvesSuccessfully();

const handlers = [
rest.get("https://ocsp.example.com/SGCNM21566327", (_, res, ctx) => {
return res(
ctx.json({
certificateId: "SGCNM21566327",
certificateStatus: "good",
})
);
}),
];

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 v1", async () => {
whenPublicKeyResolvesSuccessfully();

const handlers = [
rest.get("https://ocsp.example.com/SGCNM21566327", (_, res, ctx) => {
return res(
ctx.json({
certificateId: "SGCNM21566327",
certificateStatus: "revoked",
reasonCode: 4,
revocationDate: "2021-10-26T05:02:20.100Z",
thisUpdate: "2021-10-26T05:02:20.100Z",
})
);
}),
];

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": "SUPERSEDED",
},
"revoked": true,
},
],
},
"issuedOnAll": true,
"revokedOnAny": true,
},
"name": "OpenAttestationDidSignedDocumentStatus",
"reason": Object {
"code": 4,
"codeString": "SUPERSEDED",
"message": "SUPERSEDED",
},
"status": "INVALID",
"type": "DOCUMENT_STATUS",
}
`);

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

const handlers = [
Expand Down Expand Up @@ -589,7 +485,7 @@ describe("verify", () => {
const server: SetupServerApi = setupServer(...handlers);
server.listen();

const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcsp, options);
const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcspResponderV2, options);
expect(res).toMatchInlineSnapshot(`
Object {
"data": Object {
Expand Down Expand Up @@ -618,7 +514,7 @@ describe("verify", () => {

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

const handlers = [
Expand Down Expand Up @@ -650,7 +546,7 @@ describe("verify", () => {
const server: SetupServerApi = setupServer(...handlers);
server.listen();

const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcsp, options);
const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcspResponderV2, options);
expect(res).toMatchInlineSnapshot(`
Object {
"data": Object {
Expand Down Expand Up @@ -985,5 +881,106 @@ describe("verify", () => {
}
`);
});
it("should pass when DID document is signed and is not revoked by an OCSP", async () => {
whenPublicKeyResolvesSuccessfully("0x1245e5B64D785b25057f7438F715f4aA5D965733");

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

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

const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcspResponderV3, options);
expect(res).toMatchInlineSnapshot(`
Object {
"data": Object {
"details": Object {
"issuance": Object {
"did": "did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733",
"issued": true,
},
"revocation": 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", async () => {
whenPublicKeyResolvesSuccessfully("0x1245e5B64D785b25057f7438F715f4aA5D965733");

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

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

const res = await openAttestationDidSignedDocumentStatus.verify(didSignedOcspResponderV3, options);
expect(res).toMatchInlineSnapshot(`
Object {
"data": Object {
"details": Object {
"issuance": Object {
"did": "did:ethr:0x1245e5B64D785b25057f7438F715f4aA5D965733",
"issued": true,
},
"revocation": Object {
"address": "https://ocsp.example.com",
"reason": Object {
"code": 4,
"codeString": "SUPERSEDED",
"message": "Document 0x69e1a174ea67e1c3119639f713f8a7348bbda54fdce60903621398cc2fea4d40 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 0x69e1a174ea67e1c3119639f713f8a7348bbda54fdce60903621398cc2fea4d40 has been revoked under OCSP Responder: https://ocsp.example.com",
},
"status": "INVALID",
"type": "DOCUMENT_STATUS",
}
`);

server.close();
});
});
});
23 changes: 16 additions & 7 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, isRevokedByOcspResponder2, isRevokedOnDocumentStore } from "../utils";
import { isRevokedByOcspResponder, isRevokedOnDocumentStore } from "../utils";
import { InvalidRevocationStatus, RevocationStatus, ValidRevocationStatus } from "../revocation.types";
import {
DidSignedIssuanceStatus,
Expand Down Expand Up @@ -100,15 +100,12 @@ const verifyV2 = async (
case v2.RevocationType.OcspResponder:
const { location } = revocationItem;
if (typeof location === "string") {
return isRevokedByOcspResponder2({
return isRevokedByOcspResponder({
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 Expand Up @@ -263,7 +260,19 @@ const verifyV3 = async (
"REVOCATION_LOCATION_MISSING"
);
case v3.RevocationType.OcspResponder:
throw new Error("Ocsp revocation type not yet supported for v3");
if (typeof location === "string") {
return isRevokedByOcspResponder({
merkleRoot,
targetHash,
proofs,
location,
});
}
throw new CodedError(
"missing revocation location for an issuer",
OpenAttestationDidSignedDocumentStatusCode.REVOCATION_LOCATION_MISSING,
"REVOCATION_LOCATION_MISSING"
);
case v3.RevocationType.None:
return { revoked: false };
default:
Expand Down
Loading

0 comments on commit edeccf6

Please sign in to comment.