Skip to content

Commit

Permalink
feat: request verifiable presentation from user (#509) (#765)
Browse files Browse the repository at this point in the history
* fix: merge conflicts (#509)

* feat: create signing page for verifiable presentations (#509)

* feat: sign presentations with metamask (#509)

* test: add a test for usePresentVerifiableCredential (#509)

* fix: update type names (#509)

* test: add tests for new components and hooks (#509)

* fix: mark sdk functions with DEV (#509)

* fix: pr feedback (#509)

* fix: pr feedback (#509)

* fix: refactor verifiable presentation signing options into single page, use dropdown menu (#509)

* fix: fix tests from updated event names (#509)

* fix: update tests (#509)

* fix: update styling for verifiable presentation screen (#509)

* test: cleanup, fix test coverage (#509)
  • Loading branch information
AndrewCLu authored Sep 14, 2023
1 parent 87fa6a3 commit 1840b37
Show file tree
Hide file tree
Showing 32 changed files with 2,129 additions and 323 deletions.
15 changes: 15 additions & 0 deletions packages/app/src/background/contentScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ConnectedIdentityMetadata,
IRejectedRequest,
IMerkleProof,
IVerifiablePresentation,
} from "@cryptkeeperzk/types";

function injectScript() {
Expand Down Expand Up @@ -119,6 +120,20 @@ function injectScript() {
);
break;
}
case EventName.GENERATE_VERIFIABLE_PRESENTATION: {
window.postMessage(
{
target: "injected-injectedscript",
payload: [
null,
(action.payload as { verifiablePresentation: IVerifiablePresentation }).verifiablePresentation,
],
nonce: EventName.GENERATE_VERIFIABLE_PRESENTATION,
},
"*",
);
break;
}
default:
log.warn("unknown action in content script");
}
Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/background/cryptKeeper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const RPC_METHOD_ACCESS: Record<RPCAction, boolean> = {
[RPCAction.GENERATE_SEMAPHORE_PROOF]: true,
[RPCAction.GENERATE_RLN_PROOF]: true,
[RPCAction.ADD_VERIFIABLE_CREDENTIAL_REQUEST]: true,
[RPCAction.GENERATE_VERIFIABLE_PRESENTATION_REQUEST]: true,
[RPCAction.REVEAL_CONNECTED_IDENTITY_COMMITMENT_REQUEST]: true,
[RPCAction.JOIN_GROUP_REQUEST]: true,
[RPCAction.GENERATE_GROUP_MERKLE_PROOF]: true,
Expand Down Expand Up @@ -242,6 +243,26 @@ export default class CryptKeeperController {
this.lockService.ensure,
this.verifiableCredentialsService.deleteAllVerifiableCredentials,
);
this.handler.add(
RPCAction.GENERATE_VERIFIABLE_PRESENTATION,
this.lockService.ensure,
this.verifiableCredentialsService.generateVerifiablePresentation,
);
this.handler.add(
RPCAction.GENERATE_VERIFIABLE_PRESENTATION_WITH_CRYPTKEEPER,
this.lockService.ensure,
this.verifiableCredentialsService.generateVerifiablePresentationWithCryptkeeper,
);
this.handler.add(
RPCAction.GENERATE_VERIFIABLE_PRESENTATION_REQUEST,
this.lockService.ensure,
this.verifiableCredentialsService.generateVerifiablePresentationRequest,
);
this.handler.add(
RPCAction.REJECT_VERIFIABLE_PRESENTATION_REQUEST,
this.lockService.ensure,
this.verifiableCredentialsService.rejectVerifiablePresentationRequest,
);

// Injector
this.handler.add(RPCAction.CONNECT, this.injectorService.connect);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { IVerifiableCredential } from "@cryptkeeperzk/types";
/* eslint-disable @typescript-eslint/unbound-method */
import { EventName } from "@cryptkeeperzk/providers";
import browser from "webextension-polyfill";

import VerifiableCredentialsService from "@src/background/services/credentials";
import {
Expand All @@ -7,7 +9,14 @@ import {
serializeVerifiableCredential,
} from "@src/background/services/credentials/utils";
import SimpleStorage from "@src/background/services/storage";
import { ICryptkeeperVerifiableCredential } from "@src/types";
import pushMessage from "@src/util/pushMessage";

import type {
IVerifiablePresentation,
IVerifiableCredential,
IVerifiablePresentationRequest,
} from "@cryptkeeperzk/types";
import type { ICryptkeeperVerifiableCredential } from "@src/types";

jest.mock("@src/background/services/crypto", (): unknown => ({
...jest.requireActual("@src/background/services/crypto"),
Expand All @@ -19,6 +28,15 @@ jest.mock("@src/background/services/crypto", (): unknown => ({
})),
}));

const exampleSignature = "ck-signature";
jest.mock("@src/background/services/wallet", (): unknown => ({
getInstance: jest.fn(() => ({
signMessage: jest.fn(() => Promise.resolve(exampleSignature)),
})),
}));

jest.mock("@src/util/pushMessage");

jest.mock("@src/background/services/storage");

interface MockStorage {
Expand Down Expand Up @@ -86,9 +104,28 @@ describe("background/services/credentials", () => {
credentialsMap.set(exampleCredentialHashTwo, exampleCryptkeeperCredentialStringTwo);
const credentialsStorageString = JSON.stringify(Array.from(credentialsMap));

const exampleVerifiablePresentationRequest: IVerifiablePresentationRequest = {
request: "example request",
};
const exampleVerifiablePresentation: IVerifiablePresentation = {
context: ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiablePresentation"],
verifiableCredential: [exampleCredential],
};

const defaultTabs = [{ id: 1 }];

const defaultPopupTab = { id: 1, active: true, highlighted: true };

const verifiableCredentialsService = VerifiableCredentialsService.getInstance();

beforeEach(() => {
(browser.tabs.create as jest.Mock).mockResolvedValue(defaultPopupTab);

(browser.tabs.query as jest.Mock).mockResolvedValue(defaultTabs);

(browser.tabs.sendMessage as jest.Mock).mockRejectedValueOnce(false).mockResolvedValue(true);

(SimpleStorage as jest.Mock).mock.instances.forEach((instance: MockStorage) => {
instance.get.mockReturnValue(credentialsStorageString);
instance.set.mockReturnValue(undefined);
Expand All @@ -102,6 +139,106 @@ describe("background/services/credentials", () => {
instance.set.mockClear();
instance.clear.mockClear();
});

(pushMessage as jest.Mock).mockClear();

(browser.tabs.sendMessage as jest.Mock).mockClear();
});

describe("add and reject verifiable credential requests", () => {
test("should successfully create an add verifiable credential request", async () => {
await verifiableCredentialsService.addVerifiableCredentialRequest(exampleCredentialString);

expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true });

const defaultOptions = {
tabId: defaultPopupTab.id,
type: "popup",
focused: true,
width: 385,
height: 610,
};

expect(browser.windows.create).toBeCalledWith(defaultOptions);
});

test("should successfully reject a verifiable credential request", async () => {
await verifiableCredentialsService.rejectVerifiableCredentialRequest();

expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true });
expect(browser.tabs.sendMessage).toBeCalledWith(defaultTabs[0].id, {
type: EventName.USER_REJECT,
payload: { type: EventName.ADD_VERIFIABLE_CREDENTIAL },
});
});
});

describe("generate verifiable presentations", () => {
test("should successfully create a generate verifiable presentation request", async () => {
await verifiableCredentialsService.generateVerifiablePresentationRequest(exampleVerifiablePresentationRequest);

expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true });

const defaultOptions = {
tabId: defaultPopupTab.id,
type: "popup",
focused: true,
width: 385,
height: 610,
};

expect(browser.windows.create).toBeCalledWith(defaultOptions);
});

test("should successfully reject a verifiable presentation request", async () => {
await verifiableCredentialsService.rejectVerifiablePresentationRequest();

expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true });
expect(browser.tabs.sendMessage).toBeCalledWith(defaultTabs[0].id, {
type: EventName.USER_REJECT,
payload: { type: EventName.VERIFIABLE_PRESENTATION_REQUEST },
});
});

test("should successfully generate a verifiable presentation", async () => {
await verifiableCredentialsService.generateVerifiablePresentation(exampleVerifiablePresentation);

expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true });
expect(browser.tabs.sendMessage).toBeCalledWith(defaultTabs[0].id, {
type: EventName.GENERATE_VERIFIABLE_PRESENTATION,
payload: { verifiablePresentation: exampleVerifiablePresentation },
});
});

test("should successfully generate a verifiable presentation with cryptkeeper", async () => {
const exampleAddress = "0x123";
const ETHEREUM_SIGNATURE_SPECIFICATION_TYPE = "EthereumEip712Signature2021";
const VERIFIABLE_CREDENTIAL_PROOF_PURPOSE = "assertionMethod";

await verifiableCredentialsService.generateVerifiablePresentationWithCryptkeeper({
verifiablePresentation: exampleVerifiablePresentation,
address: exampleAddress,
});

const signedVerifiablePresentation = {
...exampleVerifiablePresentation,
proof: [
{
type: [ETHEREUM_SIGNATURE_SPECIFICATION_TYPE],
proofPurpose: VERIFIABLE_CREDENTIAL_PROOF_PURPOSE,
verificationMethod: exampleAddress,
created: new Date(),
proofValue: exampleSignature,
},
],
};

expect(browser.tabs.query).toBeCalledWith({ lastFocusedWindow: true });
expect(browser.tabs.sendMessage).toBeCalledWith(defaultTabs[0].id, {
type: EventName.GENERATE_VERIFIABLE_PRESENTATION,
payload: { verifiablePresentation: signedVerifiablePresentation },
});
});
});

describe("add and retrieve verifiable credentials", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
validateSerializedVerifiableCredential,
generateInitialMetadataForVerifiableCredential,
hashVerifiableCredential,
serializeVerifiablePresentation,
} from "../utils";

describe("util/serializeCryptkeeperVerifiableCredential", () => {
Expand Down Expand Up @@ -42,6 +43,41 @@ describe("util/serializeCryptkeeperVerifiableCredential", () => {
});
});

describe("util/serializeVerifiablePresentation", () => {
test("should serialize a verifiable presentation correctly", () => {
const rawCredential = {
context: ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
issuer: "did:ethr:0x123",
issuanceDate: new Date("2010-01-01T19:23:24Z"),
credentialSubject: {
id: "did:ethr:0x123",
claims: {
name: "John Doe",
},
},
};
const verifiablePresentation = {
context: ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiablePresentation"],
verifiableCredential: [rawCredential],
};

expect(serializeVerifiablePresentation(verifiablePresentation)).toStrictEqual(
stringify({ ...verifiablePresentation, verifiableCredential: [serializeVerifiableCredential(rawCredential)] }),
);
});

test("should serialize a verifiable presentation with no credentials correctly", () => {
const verifiablePresentation = {
context: ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiablePresentation"],
};

expect(serializeVerifiablePresentation(verifiablePresentation)).toStrictEqual(stringify(verifiablePresentation));
});
});

describe("util/deserializeVerifiableCredential", () => {
test("should deserialize a verifiable credential correctly", async () => {
const rawCredential = {
Expand Down
Loading

0 comments on commit 1840b37

Please sign in to comment.