Skip to content

Commit

Permalink
feat(identity): replacing the merkle root for a hash
Browse files Browse the repository at this point in the history
    - have replaced the merkle root calculation with a hash function
    - have dropped merkle libs
    - credentialSubject:
        - have changed the format of the id
        - provide is field now, and not appended to the id
  • Loading branch information
nutrina authored and gdixon committed May 30, 2022
1 parent b436572 commit c650edd
Show file tree
Hide file tree
Showing 14 changed files with 94 additions and 192 deletions.
6 changes: 0 additions & 6 deletions app/__mocks__/@dpopp/identity/src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
// mock everything that we're using in @dpopp/identity/src into an object and export it
const identity = {};

// pass a false proof
identity.generateMerkle = jest.fn(() => ({
proofs: ["proof"],
root: "merkleRoot",
}));
// always verifies
identity.verifyCredential = jest.fn(() => true);
identity.verifyMerkleProof = jest.fn(() => true);
identity.fetchVerifiableCredential = jest.fn(() => true);

// return full mock
Expand Down
2 changes: 1 addition & 1 deletion iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ There are a few options for adding the variable into the build process:
that variable in the `secrets` array.
- (If the value can be public) Hardcode the value in plaintext into the Github Actions script and feed it into the
Pulumi file as described above. Alternatively, hardcode the value into the Pulumi file directly. Also note that it can
be added to `environment` array in the `iam` container definition instead of `secrets`, since the value can be public.
be added to `environment` array in the `iam` container definition instead of `secrets`, since the value can be public.
5 changes: 3 additions & 2 deletions iam/__mocks__/@dpopp/identity/dist/commonjs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ identity.issueChallengeCredential = jest.fn(async (DIDKit, key, record) => ({
}));

// always returns dummy VC
identity.issueMerkleCredential = jest.fn(async (DIDKit, key, record) => ({
identity.issueHashedCredential = jest.fn(async (DIDKit, key, record) => ({
record: {
type: record.type,
address: record.address,
Expand All @@ -22,7 +22,8 @@ identity.issueMerkleCredential = jest.fn(async (DIDKit, key, record) => ({
credential: {
credentialSubject: {
id: `did:ethr:${record.address}#${record.type}`,
root: "0x0-merkleRoot",
hash: "0x0-and-the-rest-of-hash",
provider: "PROVIDER",
},
},
proofs: [],
Expand Down
1 change: 0 additions & 1 deletion iam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"express": "4",
"google-auth-library": "^7.14.1",
"luxon": "^2.4.0",
"merkle-tools": "^1.4.1",
"tslint": "^6.1.3",
"twitter-api-sdk": "1.0.6",
"typescript": "~4.6.3"
Expand Down
8 changes: 4 additions & 4 deletions iam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {

// ---- Generate & Verify methods
import * as DIDKit from "@dpopp/identity/dist/commonjs/didkit-node";
import { issueChallengeCredential, issueMerkleCredential, verifyCredential } from "@dpopp/identity/dist/commonjs";
import { issueChallengeCredential, issueHashedCredential, verifyCredential } from "@dpopp/identity/dist/commonjs";

// ---- Identity Provider Management
import { Providers } from "./utils/providers";
Expand Down Expand Up @@ -101,7 +101,7 @@ app.post("/api/v0.0.0/challenge", (req: Request, res: Response): void => {
const challenge = providers.getChallenge(payload);
// if the request is valid then proceed to generate a challenge credential
if (challenge && challenge.valid === true) {
// recreate the record to ensure the minimun number of leafs are present to produce a valid merkleTree
// recreate the record to ensure the minimun number of properties are present
const record: RequestPayload = {
// add fields to identify the bearer of the challenge
type: payload.type,
Expand Down Expand Up @@ -162,7 +162,7 @@ app.post("/api/v0.0.0/verify", (req: Request, res: Response): void => {
.then((verifiedPayload) => {
// check if the request is valid against the selected Identity Provider
if (verifiedPayload && verifiedPayload?.valid === true) {
// recreate the record to ensure the minimun number of leafs are present to produce a valid merkleTree
// recreate the record to ensure the minimun number of properties
const record: ProofRecord = {
// type and address will always be known and can be obtained from the resultant credential
type: payload.type,
Expand All @@ -174,7 +174,7 @@ app.post("/api/v0.0.0/verify", (req: Request, res: Response): void => {
};

// generate a VC for the given payload
return issueMerkleCredential(DIDKit, key, record)
return issueHashedCredential(DIDKit, key, record)
.then(({ credential }) => {
return res.json({
record,
Expand Down
61 changes: 38 additions & 23 deletions identity/__tests__/credentials.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
// ---- Test subject
import {
issueChallengeCredential,
issueMerkleCredential,
issueHashedCredential,
verifyCredential,
fetchChallengeCredential,
fetchVerifiableCredential,
objToSortedArray,
} from "../src/credentials";

// ---- base64 encoding
import * as base64 from "@ethersproject/base64";

// ---- crypto lib for hashing
import { createHash } from "crypto";

// ---- Mocked values and helpers
import {
MOCK_CHALLENGE_CREDENTIAL,
Expand All @@ -15,7 +22,6 @@ import {
clearAxiosMocks,
} from "../__mocks__/axios";
import * as mockDIDKit from "../__mocks__/didkit";
import * as mockMerkle from "../src/merkle";

// ---- Types
import axios from "axios";
Expand Down Expand Up @@ -120,15 +126,9 @@ describe("Fetch Credentials", function () {
});

describe("Generate Credentials", function () {
const MOCK_MERKLE_ROOT = "mockMerkleRoot";
const generateMerkleSpy = jest.spyOn(mockMerkle, "generateMerkle").mockReturnValue({
proofs: {},
root: MOCK_MERKLE_ROOT,
});

beforeEach(() => {
mockDIDKit.clearDidkitMocks();
generateMerkleSpy.mockClear();
});

it("can generate a challenge credential", async () => {
Expand All @@ -151,26 +151,41 @@ describe("Generate Credentials", function () {
expect(typeof credential.proof).toEqual("object");
});

it("can generate a merkle credential", async () => {
it("can convert an object to an sorted array for deterministic hashing", async () => {
const record = {
type: "Simple",
address: "0x0",
version: "Test-Case-1",
email: "[email protected]",
};

// details of this credential are created by issueMerkleCredential - but the proof is added by DIDKit (which is mocked)
const { credential } = await issueMerkleCredential(DIDKit, key, record);

expect(generateMerkleSpy).toHaveBeenCalled();
expect(generateMerkleSpy).toHaveBeenCalledWith(record);
// expect to have called issueCredential
expect(DIDKit.issueCredential).toHaveBeenCalled();
// expect the structure/details added by issueMerkleCredential to be correct
expect(credential.credentialSubject.id).toEqual(`did:ethr:${record.address}#${record.type}`);
expect(typeof credential.credentialSubject.root === "string").toEqual(true);
expect(credential.credentialSubject.root).toEqual(MOCK_MERKLE_ROOT);
expect(typeof credential.proof).toEqual("object");
});
expect(objToSortedArray(record)).toEqual([
["address", "0x0"],
["email", "[email protected]"],
["type", "Simple"],
["version", "Test-Case-1"],
]);
}),
it("can generate a credential containing hash", async () => {
const record = {
type: "Simple",
address: "0x0",
version: "Test-Case-1",
};

const expectedHash: string =
"v1.0.0:" + base64.encode(createHash("sha256").update(key).update(JSON.stringify(objToSortedArray(record))).digest());
// details of this credential are created by issueHashedCredential - but the proof is added by DIDKit (which is mocked)
const { credential } = await issueHashedCredential(DIDKit, key, record);
// expect to have called issueCredential
expect(DIDKit.issueCredential).toHaveBeenCalled();
// expect the structure/details added by issueHashedCredential to be correct
expect(credential.credentialSubject.id).toEqual(`did:pkh:eip155:1:${record.address}`);
expect(credential.credentialSubject.provider).toEqual(`${record.type}`);
expect(typeof credential.credentialSubject.hash).toEqual("string");
expect(credential.credentialSubject.hash).toEqual(expectedHash);
expect(typeof credential.proof).toEqual("object");
});
});

describe("Verify Credentials", function () {
Expand All @@ -186,7 +201,7 @@ describe("Verify Credentials", function () {
};

// we are creating this VC so that we know that we have a valid VC in this context to test against (never expired)
const { credential: credentialToVerify } = await issueMerkleCredential(DIDKit, key, record);
const { credential: credentialToVerify } = await issueHashedCredential(DIDKit, key, record);

// all verifications will pass as the DIDKit response is mocked
expect(await verifyCredential(DIDKit, credentialToVerify)).toEqual(true);
Expand Down
45 changes: 0 additions & 45 deletions identity/__tests__/merkle.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion identity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
},
"dependencies": {
"@dpopp/types": "0.0.1",
"@ethersproject/base64": "^5.6.1",
"@ethersproject/providers": "^5.6.2",
"@spruceid/didkit-wasm": "^0.2.1",
"@spruceid/didkit-wasm-node": "^0.2.1",
"axios": "^0.26.1",
"hash-js-merkle-tools": "^1.4.4",
"typescript": "~4.6.3"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion identity/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ Shared identity management tools for the @dPoPP repo.
Instructions:

- Ensure @dpopp/identity is included as a package dependency
- Import like so: `import { issueMerkleCredential, verifyCredential } from "@dpopp/identity"`
- Import like so: `import { issueHashedCredential, verifyCredential } from "@dpopp/identity"`
46 changes: 35 additions & 11 deletions identity/src/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// ---- Merkle methods
import { generateMerkle } from "./merkle";

// ---- Types
import {
DIDKitLib,
Expand All @@ -13,6 +10,12 @@ import {
VerifiableCredentialRecord,
} from "@dpopp/types";

// ---- crypto lib for hashing
import { createHash } from "crypto";

// ---- base64 encoding
import * as base64 from "@ethersproject/base64";

// ---- Node/Browser http req library
import axios from "axios";

Expand All @@ -24,6 +27,9 @@ const addSeconds = (date: Date, seconds: number): Date => {
return result;
};

// Keeping track of the hashing mechanism (algo + content)
const VERSION = "v1.0.0";

// Internal method to issue a verfiable credential
const _issueCredential = async (
DIDKit: DIDKitLib,
Expand Down Expand Up @@ -89,25 +95,43 @@ export const issueChallengeCredential = async (

export const THIRTY_DAYS_TO_SECONDS = 30 * 86400;

// Return a verifiable credential with embedded merkle data
export const issueMerkleCredential = async (
export const objToSortedArray = (obj: { [k: string]: string }): string[][] => {
const keys: string[] = Object.keys(obj).sort();
return keys.reduce((out: string[][], key: string) => {
out.push([key, obj[key]]);
return out;
}, [] as string[][]);
};

// Return a verifiable credential with embedded hash
export const issueHashedCredential = async (
DIDKit: DIDKitLib,
key: string,
record: ProofRecord
): Promise<IssuedCredential> => {
// generate a merkleTree for the provided evidence
const merkle = generateMerkle(record);
// Generate a hash like SHA256(IAM_PRIVATE_KEY+PII), where PII is the (deterministic) JSON representation
// of the PII object after transforming it to an array of the form [[key:string, value:string], ...]
// with the elemnts sorted by key
const hash = base64.encode(
createHash("sha256")
.update(key, "utf-8")
.update(JSON.stringify(objToSortedArray(record)))
.digest()
);

// generate a verifiableCredential
const credential = await _issueCredential(DIDKit, key, THIRTY_DAYS_TO_SECONDS, {
credentialSubject: {
"@context": [
{
root: "https://schema.org/Text",
hash: "https://schema.org/Text",
provider: "https://schema.org/Text",
},
],
id: `did:ethr:${record.address}#${record.type}`,
// record the root of the records merkleTree (this will allow the user verifiably share the PPI held within the record)
root: merkle.root,
// TODO: the :1: is presumably the chain id (in our case mainnet) ?
id: `did:pkh:eip155:1:${record.address}`,
provider: record.type,
hash: `${VERSION}:${hash}`,
},
});

Expand Down
6 changes: 1 addition & 5 deletions identity/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
// export merkle and credential tooling
export * from "./merkle";
// export credential tooling
export * from "./credentials";

// export typings from libs
export type { Proof } from "hash-js-merkle-tools";
Loading

0 comments on commit c650edd

Please sign in to comment.