From 174ee87aef7779fc6095b9f75b978583f595f704 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Thu, 3 Apr 2025 23:59:14 +0530 Subject: [PATCH 01/29] chore: create basic log generation mechanism --- infrastructure/w3id/package.json | 1 + infrastructure/w3id/src/logs/log-manager.ts | 27 +++++++++++++++++++++ infrastructure/w3id/src/logs/log.types.ts | 8 ++++++ infrastructure/w3id/src/logs/store.ts | 0 infrastructure/w3id/src/utils/uuid.ts | 6 ++--- pnpm-lock.yaml | 9 +++++++ 6 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 infrastructure/w3id/src/logs/log-manager.ts create mode 100644 infrastructure/w3id/src/logs/log.types.ts delete mode 100644 infrastructure/w3id/src/logs/store.ts diff --git a/infrastructure/w3id/package.json b/infrastructure/w3id/package.json index 8c415e512..d45fa4f10 100644 --- a/infrastructure/w3id/package.json +++ b/infrastructure/w3id/package.json @@ -19,6 +19,7 @@ "author": "", "license": "ISC", "dependencies": { + "canonicalize": "^2.1.0", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts new file mode 100644 index 000000000..48e5a3240 --- /dev/null +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -0,0 +1,27 @@ +import { LogEvent } from "./log.types"; +import { StorageSpec } from "./storage/storage-spec"; + +/** + * Class to generate historic event logs for all historic events for an Identifier + * starting with generating it's first log entry + */ + +// TODO: Create a specification link inside our docs for how generation of identifier works + +export class IDLogManager { + private logsRepository: StorageSpec; + + constructor(logsRepository: StorageSpec) { + this.logsRepository = logsRepository; + } + + private async appendEntry(entries: LogEvent[]) {} + + private async createGenesisEntry() {} + + async createLogEntry() { + const entries = await this.logsRepository.findMany({}); + if (entries.length > 0) return this.appendEntry(entries); + return this.createGenesisEntry(); + } +} diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts new file mode 100644 index 000000000..9ad7e2b1c --- /dev/null +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -0,0 +1,8 @@ +export type LogEvent = { + versionId: string; + versionTime: Date; + updateKeys: string[]; + nextKeyHashes: string[]; + method: `w3id:v${number}.${number}.${number}`; + uuid: string; +}; diff --git a/infrastructure/w3id/src/logs/store.ts b/infrastructure/w3id/src/logs/store.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/infrastructure/w3id/src/utils/uuid.ts b/infrastructure/w3id/src/utils/uuid.ts index 1ffc1ae12..ecb953b33 100644 --- a/infrastructure/w3id/src/utils/uuid.ts +++ b/infrastructure/w3id/src/utils/uuid.ts @@ -10,8 +10,8 @@ import { v4 as uuidv4, v5 as uuidv5 } from "uuid"; */ export function generateUuid( - entropy: string, - namespace: string = uuidv4(), + entropy: string, + namespace: string = uuidv4(), ): string { - return uuidv5(entropy, namespace); + return uuidv5(entropy, namespace); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d9d6bf3e..7e0029d75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,6 +146,9 @@ importers: infrastructure/w3id: dependencies: + canonicalize: + specifier: ^2.1.0 + version: 2.1.0 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -1497,6 +1500,10 @@ packages: caniuse-lite@1.0.30001706: resolution: {integrity: sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==} + canonicalize@2.1.0: + resolution: {integrity: sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==} + hasBin: true + chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} @@ -4832,6 +4839,8 @@ snapshots: caniuse-lite@1.0.30001706: {} + canonicalize@2.1.0: {} + chai@5.2.0: dependencies: assertion-error: 2.0.1 From 749bf5af6c1fea556dd5c5002426349003aa05e8 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Fri, 4 Apr 2025 00:07:49 +0530 Subject: [PATCH 02/29] chore: add hashing utility function --- infrastructure/w3id/src/logs/log-manager.ts | 2 +- infrastructure/w3id/src/utils/hash.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 infrastructure/w3id/src/utils/hash.ts diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 48e5a3240..249a4fc12 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -19,7 +19,7 @@ export class IDLogManager { private async createGenesisEntry() {} - async createLogEntry() { + async createLogEvent() { const entries = await this.logsRepository.findMany({}); if (entries.length > 0) return this.appendEntry(entries); return this.createGenesisEntry(); diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts new file mode 100644 index 000000000..4bff8eff8 --- /dev/null +++ b/infrastructure/w3id/src/utils/hash.ts @@ -0,0 +1,13 @@ +import canonicalize from "canonicalize"; + +export async function hashOject(object: Record) { + const canonical = canonicalize(object); + const buffer = new TextEncoder().encode(canonical); + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); + + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + return hashHex; +} From 9443996e7aad196ce9f92218820edc174d11dfc8 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Fri, 4 Apr 2025 11:16:00 +0530 Subject: [PATCH 03/29] chore: rotation event --- infrastructure/w3id/src/logs/log.types.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index 9ad7e2b1c..ef7052e55 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -6,3 +6,13 @@ export type LogEvent = { method: `w3id:v${number}.${number}.${number}`; uuid: string; }; + +export enum LogEvents { + Rotation, +} + +export type RotationLogEntry = { + type: LogEvents.Rotation; + nextKeyHashes: string[]; + updateKeys: string[]; +}; From 06c17145a3354ec86128edc0e7926a9a2187691f Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 8 Apr 2025 21:37:59 +0530 Subject: [PATCH 04/29] feat: genesis entry --- infrastructure/w3id/src/logs/log-manager.ts | 35 +++++++++++++++++---- infrastructure/w3id/src/logs/log.types.ts | 21 +++++++++++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 249a4fc12..405958cdd 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,4 +1,10 @@ -import { LogEvent } from "./log.types"; +import { + CreateLogEventOptions, + GenesisLogOptions, + LogEvent, + LogEvents, + RotationLogOptions, +} from "./log.types"; import { StorageSpec } from "./storage/storage-spec"; /** @@ -15,13 +21,30 @@ export class IDLogManager { this.logsRepository = logsRepository; } - private async appendEntry(entries: LogEvent[]) {} + private async appendEntry( + entries: LogEvent[], + options: RotationLogOptions, + ) {} - private async createGenesisEntry() {} + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextPubKey, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: [nextPubKey], + // TODO: integrate this shit with the actual version of the package.json + method: `w3id:v0.0.0`, + }; + await this.logsRepository.create(logEvent); + return logEvent; + } - async createLogEvent() { + async createLogEvent(options: CreateLogEventOptions) { const entries = await this.logsRepository.findMany({}); - if (entries.length > 0) return this.appendEntry(entries); - return this.createGenesisEntry(); + if (options.type === LogEvents.Genesis) + return this.createGenesisEntry(options); + return this.appendEntry(entries, options); } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index ef7052e55..7850f7a97 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,18 +1,33 @@ export type LogEvent = { + id: string; versionId: string; versionTime: Date; updateKeys: string[]; nextKeyHashes: string[]; - method: `w3id:v${number}.${number}.${number}`; - uuid: string; + method: `w3id:v${string}`; }; export enum LogEvents { Rotation, + Genesis, } -export type RotationLogEntry = { +export type RotationLogOptions = { type: LogEvents.Rotation; nextKeyHashes: string[]; updateKeys: string[]; }; + +export type GenesisLogOptions = { + type: LogEvents.Genesis; + nextPubKey: string; + id: string; + signer: Signer; +}; + +export type Signer = { + sign: (buffer: Uint8Array) => Promise | Uint8Array; + pubKey: string; +}; + +export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; From 69045763876b6874c38b5f103032a66a3a62bba7 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 8 Apr 2025 21:41:40 +0530 Subject: [PATCH 05/29] feat: generalize hash function --- infrastructure/w3id/src/logs/log-manager.ts | 4 +++- infrastructure/w3id/src/utils/hash.ts | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 405958cdd..7f1819da6 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -24,7 +24,9 @@ export class IDLogManager { private async appendEntry( entries: LogEvent[], options: RotationLogOptions, - ) {} + ) { + const latestEntry = entries[entries.length - 1]; + } private async createGenesisEntry(options: GenesisLogOptions) { const { id, nextPubKey, signer } = options; diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts index 4bff8eff8..aa3c297c5 100644 --- a/infrastructure/w3id/src/utils/hash.ts +++ b/infrastructure/w3id/src/utils/hash.ts @@ -1,13 +1,26 @@ import canonicalize from "canonicalize"; -export async function hashOject(object: Record) { - const canonical = canonicalize(object); - const buffer = new TextEncoder().encode(canonical); - const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); +export async function hash( + input: string | Record, +): Promise { + let dataToHash: string; + + if (typeof input === "string") { + dataToHash = input; + } else { + const canonical = canonicalize(input); + if (!canonical) { + throw new Error("Failed to canonicalize object"); + } + dataToHash = canonical; + } + const buffer = new TextEncoder().encode(dataToHash); + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); + return hashHex; } From c213fe08eba2752cadadb8c17ae378f009dd58a7 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 8 Apr 2025 21:45:24 +0530 Subject: [PATCH 06/29] feat: append entry --- infrastructure/w3id/src/logs/log-manager.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 7f1819da6..2bbd24d43 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -6,6 +6,7 @@ import { RotationLogOptions, } from "./log.types"; import { StorageSpec } from "./storage/storage-spec"; +import { hash } from "../utils/hash"; /** * Class to generate historic event logs for all historic events for an Identifier @@ -26,6 +27,20 @@ export class IDLogManager { options: RotationLogOptions, ) { const latestEntry = entries[entries.length - 1]; + const logHash = hash(latestEntry); + const index = Number(latestEntry.id.split("-")[0]) + 1; + + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: options.updateKeys, + nextKeyHashes: options.nextKeyHashes, + // TODO: integrate this shit with the actual version of the package.json + method: `w3id:v0.0.0`, + }; + + await this.logsRepository.create(logEvent); } private async createGenesisEntry(options: GenesisLogOptions) { From db76535819c8b686aadb6077e9a3e5aae771d19c Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 8 Apr 2025 22:43:07 +0530 Subject: [PATCH 07/29] chore: basic tests --- infrastructure/w3id/package.json | 1 + infrastructure/w3id/src/logs/log.types.ts | 2 +- infrastructure/w3id/src/utils/codec.ts | 16 +++++ infrastructure/w3id/tests/logs/log.test.ts | 73 ++++++++++++++++++++++ pnpm-lock.yaml | 8 +++ 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 infrastructure/w3id/src/utils/codec.ts create mode 100644 infrastructure/w3id/tests/logs/log.test.ts diff --git a/infrastructure/w3id/package.json b/infrastructure/w3id/package.json index d45fa4f10..5943dc5ca 100644 --- a/infrastructure/w3id/package.json +++ b/infrastructure/w3id/package.json @@ -20,6 +20,7 @@ "license": "ISC", "dependencies": { "canonicalize": "^2.1.0", + "tweetnacl": "^1.0.3", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index 7850f7a97..17acb0989 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -26,7 +26,7 @@ export type GenesisLogOptions = { }; export type Signer = { - sign: (buffer: Uint8Array) => Promise | Uint8Array; + sign: (string: string) => Promise | string; pubKey: string; }; diff --git a/infrastructure/w3id/src/utils/codec.ts b/infrastructure/w3id/src/utils/codec.ts new file mode 100644 index 000000000..fa7d9c2c3 --- /dev/null +++ b/infrastructure/w3id/src/utils/codec.ts @@ -0,0 +1,16 @@ +export function uint8ArrayToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +export function hexToUint8Array(hex: string): Uint8Array { + if (hex.length % 2 !== 0) { + throw new Error("Hex string must have an even length"); + } + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); + } + return bytes; +} diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts new file mode 100644 index 000000000..0ef92dbed --- /dev/null +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -0,0 +1,73 @@ +import { StorageSpec } from "../../src/logs/storage/storage-spec.ts"; +import { LogEvent, LogEvents, Signer } from "../../src/logs/log.types.ts"; +import { IDLogManager } from "../../src/logs/log-manager"; +import { generateUuid } from "../../src/utils/uuid"; +import { describe, it, test } from "vitest"; +import { hash } from "../../src/utils/hash"; +import nacl from "tweetnacl"; +import { uint8ArrayToHex, hexToUint8Array } from "../../src/utils/codec"; + +class InMemoryStorage + implements StorageSpec +{ + private data: K[] = []; + + public static build(): StorageSpec< + T, + K + > { + return new InMemoryStorage(); + } + + public async create(body: T): Promise { + const entry = body as unknown as K; + this.data.push(entry); + return entry; + } + + public async findOne(options: Partial): Promise { + const result = this.data.find((item) => + Object.entries(options).every( + ([key, value]) => item[key as keyof K] === value, + ), + ); + + if (!result) throw new Error("Not found"); + return result; + } + + public async findMany(options: Partial): Promise { + return this.data.filter((item) => + Object.entries(options).every( + ([key, value]) => item[key as keyof K] === value, + ), + ); + } +} +const logManager = new IDLogManager(InMemoryStorage.build()); +const w3id = `@${generateUuid("asdfa")}`; + +const keyPair = nacl.sign.keyPair(); + +const publicKey = uint8ArrayToHex(keyPair.publicKey); +const signer: Signer = { + pubKey: publicKey, + sign: (str: string) => { + const buffer = hexToUint8Array(str); + const signature = nacl.sign(buffer, keyPair.secretKey); + return uint8ArrayToHex(signature); + }, +}; + +describe("LogManager", async () => { + test("Create Genesis Entry", async () => { + const nextKeyHash = await hash("asdf"); + const logEvent = await logManager.createLogEvent({ + id: w3id, + type: LogEvents.Genesis, + nextPubKey: nextKeyHash, + signer, + }); + console.log(logEvent); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e0029d75..d21bc996e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: canonicalize: specifier: ^2.1.0 version: 2.1.0 + tweetnacl: + specifier: ^1.0.3 + version: 1.0.3 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -3134,6 +3137,9 @@ packages: tween-functions@1.2.0: resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -6661,6 +6667,8 @@ snapshots: tween-functions@1.2.0: {} + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 From 0a0d74777159cff3de88f4ec2995d55af568c9e3 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 11:41:18 +0530 Subject: [PATCH 08/29] chore: add tests for rotation --- infrastructure/w3id/package.json | 1 + infrastructure/w3id/src/logs/log-manager.ts | 24 ++++++-- infrastructure/w3id/src/logs/log.types.ts | 6 +- infrastructure/w3id/src/utils/codec.ts | 4 ++ infrastructure/w3id/tests/logs/log.test.ts | 68 ++++++++++++++++----- pnpm-lock.yaml | 8 +++ 6 files changed, 89 insertions(+), 22 deletions(-) diff --git a/infrastructure/w3id/package.json b/infrastructure/w3id/package.json index 5943dc5ca..a278bb85d 100644 --- a/infrastructure/w3id/package.json +++ b/infrastructure/w3id/package.json @@ -20,6 +20,7 @@ "license": "ISC", "dependencies": { "canonicalize": "^2.1.0", + "multiformats": "^13.3.2", "tweetnacl": "^1.0.3", "uuid": "^11.1.0" }, diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 2bbd24d43..fd48f900c 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -7,6 +7,7 @@ import { } from "./log.types"; import { StorageSpec } from "./storage/storage-spec"; import { hash } from "../utils/hash"; +import canonicalize from "canonicalize"; /** * Class to generate historic event logs for all historic events for an Identifier @@ -26,34 +27,47 @@ export class IDLogManager { entries: LogEvent[], options: RotationLogOptions, ) { + const { signer, nextKeyHashes, nextKeySigner } = options; const latestEntry = entries[entries.length - 1]; const logHash = hash(latestEntry); const index = Number(latestEntry.id.split("-")[0]) + 1; + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); + const logEvent: LogEvent = { id: latestEntry.id, versionTime: new Date(Date.now()), versionId: `${index}-${logHash}`, - updateKeys: options.updateKeys, - nextKeyHashes: options.nextKeyHashes, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, // TODO: integrate this shit with the actual version of the package.json method: `w3id:v0.0.0`, }; + const proof = await options.signer.sign( + canonicalize(logEvent) as string, + ); + logEvent.proof = proof; + await this.logsRepository.create(logEvent); + + return logEvent; } private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextPubKey, signer } = options; + const { id, nextKeyHashes, signer } = options; const logEvent: LogEvent = { id, - versionId: `0-${id}`, + versionId: `0-${id.split("@")[1]}`, versionTime: new Date(Date.now()), updateKeys: [signer.pubKey], - nextKeyHashes: [nextPubKey], + nextKeyHashes: nextKeyHashes, // TODO: integrate this shit with the actual version of the package.json method: `w3id:v0.0.0`, }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; await this.logsRepository.create(logEvent); return logEvent; } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index 17acb0989..ba08614c6 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -5,6 +5,7 @@ export type LogEvent = { updateKeys: string[]; nextKeyHashes: string[]; method: `w3id:v${string}`; + proof?: string; }; export enum LogEvents { @@ -15,12 +16,13 @@ export enum LogEvents { export type RotationLogOptions = { type: LogEvents.Rotation; nextKeyHashes: string[]; - updateKeys: string[]; + signer: Signer; + nextKeySigner: Signer; }; export type GenesisLogOptions = { type: LogEvents.Genesis; - nextPubKey: string; + nextKeyHashes: string[]; id: string; signer: Signer; }; diff --git a/infrastructure/w3id/src/utils/codec.ts b/infrastructure/w3id/src/utils/codec.ts index fa7d9c2c3..d496e1d33 100644 --- a/infrastructure/w3id/src/utils/codec.ts +++ b/infrastructure/w3id/src/utils/codec.ts @@ -14,3 +14,7 @@ export function hexToUint8Array(hex: string): Uint8Array { } return bytes; } + +export function stringToUint8Array(str: string): Uint8Array { + return new TextEncoder().encode(str); +} diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index 0ef92dbed..e102c8848 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -2,10 +2,11 @@ import { StorageSpec } from "../../src/logs/storage/storage-spec.ts"; import { LogEvent, LogEvents, Signer } from "../../src/logs/log.types.ts"; import { IDLogManager } from "../../src/logs/log-manager"; import { generateUuid } from "../../src/utils/uuid"; -import { describe, it, test } from "vitest"; +import { describe, expect, test, expectTypeOf } from "vitest"; import { hash } from "../../src/utils/hash"; import nacl from "tweetnacl"; -import { uint8ArrayToHex, hexToUint8Array } from "../../src/utils/codec"; +import { uint8ArrayToHex, stringToUint8Array } from "../../src/utils/codec"; +import { base58btc } from "multiformats/bases/base58"; class InMemoryStorage implements StorageSpec @@ -48,26 +49,63 @@ const logManager = new IDLogManager(InMemoryStorage.build()); const w3id = `@${generateUuid("asdfa")}`; const keyPair = nacl.sign.keyPair(); +let currNextKey = nacl.sign.keyPair(); -const publicKey = uint8ArrayToHex(keyPair.publicKey); -const signer: Signer = { - pubKey: publicKey, - sign: (str: string) => { - const buffer = hexToUint8Array(str); - const signature = nacl.sign(buffer, keyPair.secretKey); - return uint8ArrayToHex(signature); - }, -}; +function createSigner(keyPair: nacl.SignKeyPair): Signer { + const publicKey = uint8ArrayToHex(keyPair.publicKey); + const signer: Signer = { + pubKey: publicKey, + sign: (str: string) => { + const buffer = stringToUint8Array(str); + const signature = nacl.sign(buffer, keyPair.secretKey); + return base58btc.encode(signature); + }, + }; + return signer; +} describe("LogManager", async () => { - test("Create Genesis Entry", async () => { - const nextKeyHash = await hash("asdf"); + test("GenesisEvent: [Creates Entry]", async () => { + const nextKeyHash = await hash(uint8ArrayToHex(currNextKey.publicKey)); + const signer = createSigner(keyPair); const logEvent = await logManager.createLogEvent({ id: w3id, type: LogEvents.Genesis, - nextPubKey: nextKeyHash, + nextKeyHashes: [nextKeyHash], + signer, + }); + expectTypeOf(logEvent).toMatchObjectType(); + }); + + test("KeyRotation: [Error At Wrong Next Key]", async () => { + const nextKeyPair = nacl.sign.keyPair(); + const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); + + const signer = createSigner(nextKeyPair); + const nextKeySigner = createSigner(nextKeyPair); + const logEvent = logManager.createLogEvent({ + type: LogEvents.Rotation, + nextKeyHashes: [nextKeyHash], + signer, + nextKeySigner, + }); + + await expect(logEvent).rejects.toThrow(); + }); + + test("KeyRotation: [Creates Entry]", async () => { + const nextKeyPair = nacl.sign.keyPair(); + const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); + + const signer = createSigner(keyPair); + const nextKeySigner = createSigner(currNextKey); + const logEvent = await logManager.createLogEvent({ + type: LogEvents.Rotation, + nextKeyHashes: [nextKeyHash], signer, + nextKeySigner, }); - console.log(logEvent); + + expectTypeOf(logEvent).toMatchObjectType(); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d21bc996e..6ec4c7075 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: canonicalize: specifier: ^2.1.0 version: 2.1.0 + multiformats: + specifier: ^13.3.2 + version: 13.3.2 tweetnacl: specifier: ^1.0.3 version: 1.0.3 @@ -2514,6 +2517,9 @@ packages: typescript: optional: true + multiformats@13.3.2: + resolution: {integrity: sha512-qbB0CQDt3QKfiAzZ5ZYjLFOs+zW43vA4uyM8g27PeEuXZybUOFyjrVdP93HPBHMoglibwfkdVwbzfUq8qGcH6g==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -6020,6 +6026,8 @@ snapshots: - '@types/node' optional: true + multiformats@13.3.2: {} + mute-stream@2.0.0: {} nanoid@3.3.11: {} From b9eee010098a02d9692e0d0c9d4fdaf568689a11 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 13:42:02 +0530 Subject: [PATCH 09/29] feat: add malform throws --- infrastructure/w3id/src/logs/log-manager.ts | 85 ++++++++++++++++++--- infrastructure/w3id/src/utils/array.ts | 20 +++++ infrastructure/w3id/tests/logs/log.test.ts | 69 ++++++++++++++++- 3 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 infrastructure/w3id/src/utils/array.ts diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index fd48f900c..c57a9816f 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -4,10 +4,12 @@ import { LogEvent, LogEvents, RotationLogOptions, + VerifierCallback, } from "./log.types"; import { StorageSpec } from "./storage/storage-spec"; import { hash } from "../utils/hash"; import canonicalize from "canonicalize"; +import { isSubsetOf } from "../utils/array"; /** * Class to generate historic event logs for all historic events for an Identifier @@ -17,10 +19,72 @@ import canonicalize from "canonicalize"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - private logsRepository: StorageSpec; + repository: StorageSpec; - constructor(logsRepository: StorageSpec) { - this.logsRepository = logsRepository; + constructor(repository: StorageSpec) { + this.repository = repository; + } + + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex: number = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string; + + for (const e of log) { + let [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new Error("Malformed DID Log"); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new Error("Malformed chain"); + } + + currentNextKeyHashesSeen = e.nextKeyHashes; + await this.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 + ? lastUpdateKeysSeen + : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } + + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new Error(); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new Error("Invalid Proof"); } private async appendEntry( @@ -29,8 +93,8 @@ export class IDLogManager { ) { const { signer, nextKeyHashes, nextKeySigner } = options; const latestEntry = entries[entries.length - 1]; - const logHash = hash(latestEntry); - const index = Number(latestEntry.id.split("-")[0]) + 1; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; const currKeyHash = await hash(nextKeySigner.pubKey); if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); @@ -45,13 +109,10 @@ export class IDLogManager { method: `w3id:v0.0.0`, }; - const proof = await options.signer.sign( - canonicalize(logEvent) as string, - ); + const proof = await signer.sign(canonicalize(logEvent) as string); logEvent.proof = proof; - await this.logsRepository.create(logEvent); - + await this.repository.create(logEvent); return logEvent; } @@ -68,12 +129,12 @@ export class IDLogManager { }; const proof = await signer.sign(canonicalize(logEvent) as string); logEvent.proof = proof; - await this.logsRepository.create(logEvent); + await this.repository.create(logEvent); return logEvent; } async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.logsRepository.findMany({}); + const entries = await this.repository.findMany({}); if (options.type === LogEvents.Genesis) return this.createGenesisEntry(options); return this.appendEntry(entries, options); diff --git a/infrastructure/w3id/src/utils/array.ts b/infrastructure/w3id/src/utils/array.ts new file mode 100644 index 000000000..2762aac6d --- /dev/null +++ b/infrastructure/w3id/src/utils/array.ts @@ -0,0 +1,20 @@ +/** + * Utility function to check if A is subset of B + */ + +export function isSubsetOf(a: unknown[], b: unknown[]) { + const map = new Map(); + + for (const el of b) { + map.set(el, (map.get(el) || 0) + 1); + } + + for (const el of a) { + if (!map.has(el) || map.get(el) === 0) { + return false; + } + map.set(el, map.get(el) - 1); + } + + return true; +} diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index e102c8848..fc9583ee9 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -1,12 +1,22 @@ import { StorageSpec } from "../../src/logs/storage/storage-spec.ts"; -import { LogEvent, LogEvents, Signer } from "../../src/logs/log.types.ts"; +import { + LogEvent, + LogEvents, + Signer, + VerifierCallback, +} from "../../src/logs/log.types.ts"; import { IDLogManager } from "../../src/logs/log-manager"; import { generateUuid } from "../../src/utils/uuid"; import { describe, expect, test, expectTypeOf } from "vitest"; import { hash } from "../../src/utils/hash"; import nacl from "tweetnacl"; -import { uint8ArrayToHex, stringToUint8Array } from "../../src/utils/codec"; +import { + uint8ArrayToHex, + stringToUint8Array, + hexToUint8Array, +} from "../../src/utils/codec"; import { base58btc } from "multiformats/bases/base58"; +import falso from "@ngneat/falso"; class InMemoryStorage implements StorageSpec @@ -51,13 +61,30 @@ const w3id = `@${generateUuid("asdfa")}`; const keyPair = nacl.sign.keyPair(); let currNextKey = nacl.sign.keyPair(); +const verifierCallback: VerifierCallback = async ( + message: string, + signature: string, + pubKey: string, +) => { + const signatureBuffer = base58btc.decode(signature); + const messageBuffer = stringToUint8Array(message); + const publicKey = hexToUint8Array(pubKey); + const isValid = nacl.sign.detached.verify( + messageBuffer, + signatureBuffer, + publicKey, + ); + + return isValid; +}; + function createSigner(keyPair: nacl.SignKeyPair): Signer { const publicKey = uint8ArrayToHex(keyPair.publicKey); const signer: Signer = { pubKey: publicKey, sign: (str: string) => { const buffer = stringToUint8Array(str); - const signature = nacl.sign(buffer, keyPair.secretKey); + const signature = nacl.sign.detached(buffer, keyPair.secretKey); return base58btc.encode(signature); }, }; @@ -108,4 +135,40 @@ describe("LogManager", async () => { expectTypeOf(logEvent).toMatchObjectType(); }); + + test("Verification: [Verifies Correct Chain]", async () => { + const events = await logManager.repository.findMany({}); + const result = await IDLogManager.validateLogChain( + events, + verifierCallback, + ); + expect(result).toBe(true); + }); + + test("Verification: [Throws on Malformed Index Chain]", async () => { + const _events = await logManager.repository.findMany({}); + const events = JSON.parse(JSON.stringify(_events)); + events[1].versionId = `2-${falso.randUuid()}`; + const result = IDLogManager.validateLogChain(events, verifierCallback); + + await expect(result).rejects.toThrow(); + }); + + test("Verification: [Throws on Malformed Hash Chain]", async () => { + const _events = await logManager.repository.findMany({}); + const events = JSON.parse(JSON.stringify(_events)); + events[1].versionId = `1-${falso.randUuid()}`; + const result = IDLogManager.validateLogChain(events, verifierCallback); + + await expect(result).rejects.toThrow(); + }); + + test("Verification: [Throws on Wrong Signature]", async () => { + const _events = await logManager.repository.findMany({}); + const events = JSON.parse(JSON.stringify(_events)); + events[1].proof = `12312313`; + const result = IDLogManager.validateLogChain(events, verifierCallback); + + await expect(result).rejects.toThrow(); + }); }); From 54bbff4053224f8abb8be82f81b1de8a4220f099 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 13:47:28 +0530 Subject: [PATCH 10/29] chore: add the right errors --- infrastructure/w3id/src/errors/errors.ts | 5 +++++ infrastructure/w3id/src/logs/log-manager.ts | 11 ++++++++--- infrastructure/w3id/tests/logs/log.test.ts | 17 +++++++++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 infrastructure/w3id/src/errors/errors.ts diff --git a/infrastructure/w3id/src/errors/errors.ts b/infrastructure/w3id/src/errors/errors.ts new file mode 100644 index 000000000..aa0fe596d --- /dev/null +++ b/infrastructure/w3id/src/errors/errors.ts @@ -0,0 +1,5 @@ +export class MalformedIndexChainError extends Error {} + +export class MalformedHashChainError extends Error {} + +export class BadSignatureError extends Error {} diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index c57a9816f..36a38f525 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -10,6 +10,11 @@ import { StorageSpec } from "./storage/storage-spec"; import { hash } from "../utils/hash"; import canonicalize from "canonicalize"; import { isSubsetOf } from "../utils/array"; +import { + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, +} from "../errors/errors"; /** * Class to generate historic event logs for all historic events for an Identifier @@ -37,7 +42,7 @@ export class IDLogManager { for (const e of log) { let [_index, _hash] = e.versionId.split("-"); const index = Number(_index); - if (currIndex !== index) throw new Error("Malformed DID Log"); + if (currIndex !== index) throw new MalformedIndexChainError(); const hashedUpdateKeys = await Promise.all( e.updateKeys.map(async (k) => await hash(k)), ); @@ -47,7 +52,7 @@ export class IDLogManager { currentNextKeyHashesSeen, ); if (!updateKeysSeen || lastHash !== _hash) - throw new Error("Malformed chain"); + throw new MalformedHashChainError(); } currentNextKeyHashesSeen = e.nextKeyHashes; @@ -84,7 +89,7 @@ export class IDLogManager { ); if (signValidates) verified = true; } - if (!verified) throw new Error("Invalid Proof"); + if (!verified) throw new BadSignatureError(); } private async appendEntry( diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index fc9583ee9..134a2ac1d 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -17,6 +17,12 @@ import { } from "../../src/utils/codec"; import { base58btc } from "multiformats/bases/base58"; import falso from "@ngneat/falso"; +import { + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, +} from "../../src/errors/errors.ts"; +import { sign } from "node:crypto"; class InMemoryStorage implements StorageSpec @@ -151,7 +157,7 @@ describe("LogManager", async () => { events[1].versionId = `2-${falso.randUuid()}`; const result = IDLogManager.validateLogChain(events, verifierCallback); - await expect(result).rejects.toThrow(); + await expect(result).rejects.toThrow(MalformedIndexChainError); }); test("Verification: [Throws on Malformed Hash Chain]", async () => { @@ -160,15 +166,18 @@ describe("LogManager", async () => { events[1].versionId = `1-${falso.randUuid()}`; const result = IDLogManager.validateLogChain(events, verifierCallback); - await expect(result).rejects.toThrow(); + await expect(result).rejects.toThrow(MalformedHashChainError); }); test("Verification: [Throws on Wrong Signature]", async () => { const _events = await logManager.repository.findMany({}); const events = JSON.parse(JSON.stringify(_events)); - events[1].proof = `12312313`; + const newKeyPair = nacl.sign.keyPair(); + const signer = createSigner(newKeyPair); + delete events[1].proof; + events[1].proof = await signer.sign(events[1]); const result = IDLogManager.validateLogChain(events, verifierCallback); - await expect(result).rejects.toThrow(); + await expect(result).rejects.toThrow(BadSignatureError); }); }); From a6776e2891a29d8f12efa2aec3d4b43e976eeeb1 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 13:57:38 +0530 Subject: [PATCH 11/29] chore: fix CI stuff --- infrastructure/w3id/src/logs/log-manager.ts | 238 ++++++++++---------- infrastructure/w3id/src/logs/log.types.ts | 38 ++-- infrastructure/w3id/src/utils/array.ts | 22 +- infrastructure/w3id/src/utils/codec.ts | 24 +- infrastructure/w3id/src/utils/hash.ts | 36 +-- infrastructure/w3id/src/utils/uuid.ts | 6 +- 6 files changed, 179 insertions(+), 185 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 36a38f525..0597d56bf 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,20 +1,20 @@ -import { - CreateLogEventOptions, - GenesisLogOptions, - LogEvent, - LogEvents, - RotationLogOptions, - VerifierCallback, -} from "./log.types"; -import { StorageSpec } from "./storage/storage-spec"; -import { hash } from "../utils/hash"; import canonicalize from "canonicalize"; -import { isSubsetOf } from "../utils/array"; import { - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; +import { isSubsetOf } from "../utils/array"; +import { hash } from "../utils/hash"; +import { + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + LogEvents, + type RotationLogOptions, + type VerifierCallback, +} from "./log.types"; +import type { StorageSpec } from "./storage/storage-spec"; /** * Class to generate historic event logs for all historic events for an Identifier @@ -24,124 +24,118 @@ import { // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec) { + this.repository = repository; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex: number = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - let [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await this.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 - ? lastUpdateKeysSeen - : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new Error(); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new Error(); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry( - entries: LogEvent[], - options: RotationLogOptions, - ) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { signer, nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - // TODO: integrate this shit with the actual version of the package.json - method: `w3id:v0.0.0`, - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - // TODO: integrate this shit with the actual version of the package.json - method: `w3id:v0.0.0`, - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (options.type === LogEvents.Genesis) - return this.createGenesisEntry(options); - return this.appendEntry(entries, options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (options.type === LogEvents.Genesis) + return this.createGenesisEntry(options); + return this.appendEntry(entries, options); + } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index ba08614c6..eba983779 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,35 +1,35 @@ export type LogEvent = { - id: string; - versionId: string; - versionTime: Date; - updateKeys: string[]; - nextKeyHashes: string[]; - method: `w3id:v${string}`; - proof?: string; + id: string; + versionId: string; + versionTime: Date; + updateKeys: string[]; + nextKeyHashes: string[]; + method: `w3id:v${string}`; + proof?: string; }; export enum LogEvents { - Rotation, - Genesis, + Rotation = 0, + Genesis = 1, } export type RotationLogOptions = { - type: LogEvents.Rotation; - nextKeyHashes: string[]; - signer: Signer; - nextKeySigner: Signer; + type: LogEvents.Rotation; + nextKeyHashes: string[]; + signer: Signer; + nextKeySigner: Signer; }; export type GenesisLogOptions = { - type: LogEvents.Genesis; - nextKeyHashes: string[]; - id: string; - signer: Signer; + type: LogEvents.Genesis; + nextKeyHashes: string[]; + id: string; + signer: Signer; }; export type Signer = { - sign: (string: string) => Promise | string; - pubKey: string; + sign: (string: string) => Promise | string; + pubKey: string; }; export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; diff --git a/infrastructure/w3id/src/utils/array.ts b/infrastructure/w3id/src/utils/array.ts index 2762aac6d..6f3280ce4 100644 --- a/infrastructure/w3id/src/utils/array.ts +++ b/infrastructure/w3id/src/utils/array.ts @@ -3,18 +3,18 @@ */ export function isSubsetOf(a: unknown[], b: unknown[]) { - const map = new Map(); + const map = new Map(); - for (const el of b) { - map.set(el, (map.get(el) || 0) + 1); - } + for (const el of b) { + map.set(el, (map.get(el) || 0) + 1); + } - for (const el of a) { - if (!map.has(el) || map.get(el) === 0) { - return false; - } - map.set(el, map.get(el) - 1); - } + for (const el of a) { + if (!map.has(el) || map.get(el) === 0) { + return false; + } + map.set(el, map.get(el) - 1); + } - return true; + return true; } diff --git a/infrastructure/w3id/src/utils/codec.ts b/infrastructure/w3id/src/utils/codec.ts index d496e1d33..f05ce212e 100644 --- a/infrastructure/w3id/src/utils/codec.ts +++ b/infrastructure/w3id/src/utils/codec.ts @@ -1,20 +1,20 @@ export function uint8ArrayToHex(bytes: Uint8Array): string { - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); } export function hexToUint8Array(hex: string): Uint8Array { - if (hex.length % 2 !== 0) { - throw new Error("Hex string must have an even length"); - } - const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); - } - return bytes; + if (hex.length % 2 !== 0) { + throw new Error("Hex string must have an even length"); + } + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16); + } + return bytes; } export function stringToUint8Array(str: string): Uint8Array { - return new TextEncoder().encode(str); + return new TextEncoder().encode(str); } diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts index aa3c297c5..4215eb1db 100644 --- a/infrastructure/w3id/src/utils/hash.ts +++ b/infrastructure/w3id/src/utils/hash.ts @@ -1,26 +1,26 @@ import canonicalize from "canonicalize"; export async function hash( - input: string | Record, + input: string | Record, ): Promise { - let dataToHash: string; + let dataToHash: string; - if (typeof input === "string") { - dataToHash = input; - } else { - const canonical = canonicalize(input); - if (!canonical) { - throw new Error("Failed to canonicalize object"); - } - dataToHash = canonical; - } + if (typeof input === "string") { + dataToHash = input; + } else { + const canonical = canonicalize(input); + if (!canonical) { + throw new Error("Failed to canonicalize object"); + } + dataToHash = canonical; + } - const buffer = new TextEncoder().encode(dataToHash); - const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); + const buffer = new TextEncoder().encode(dataToHash); + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); - return hashHex; + return hashHex; } diff --git a/infrastructure/w3id/src/utils/uuid.ts b/infrastructure/w3id/src/utils/uuid.ts index ecb953b33..1ffc1ae12 100644 --- a/infrastructure/w3id/src/utils/uuid.ts +++ b/infrastructure/w3id/src/utils/uuid.ts @@ -10,8 +10,8 @@ import { v4 as uuidv4, v5 as uuidv5 } from "uuid"; */ export function generateUuid( - entropy: string, - namespace: string = uuidv4(), + entropy: string, + namespace: string = uuidv4(), ): string { - return uuidv5(entropy, namespace); + return uuidv5(entropy, namespace); } From 3fc21b612755f97a6f2bcb2fcd81081261683bba Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 14:07:08 +0530 Subject: [PATCH 12/29] chore: add missing file --- infrastructure/w3id/src/logs/log.types.ts | 44 +++++++++++++---------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index eba983779..f273eca38 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,35 +1,41 @@ export type LogEvent = { - id: string; - versionId: string; - versionTime: Date; - updateKeys: string[]; - nextKeyHashes: string[]; - method: `w3id:v${string}`; - proof?: string; + id: string; + versionId: string; + versionTime: Date; + updateKeys: string[]; + nextKeyHashes: string[]; + method: `w3id:v${string}`; + proof?: string; }; export enum LogEvents { - Rotation = 0, - Genesis = 1, + Rotation, + Genesis, } export type RotationLogOptions = { - type: LogEvents.Rotation; - nextKeyHashes: string[]; - signer: Signer; - nextKeySigner: Signer; + type: LogEvents.Rotation; + nextKeyHashes: string[]; + signer: Signer; + nextKeySigner: Signer; }; +export type VerifierCallback = ( + message: string, + signature: string, + pubKey: string, +) => Promise; + export type GenesisLogOptions = { - type: LogEvents.Genesis; - nextKeyHashes: string[]; - id: string; - signer: Signer; + type: LogEvents.Genesis; + nextKeyHashes: string[]; + id: string; + signer: Signer; }; export type Signer = { - sign: (string: string) => Promise | string; - pubKey: string; + sign: (string: string) => Promise | string; + pubKey: string; }; export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; From 11af7715b64ab848b3e8f509240c725ae8687362 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 14:08:44 +0530 Subject: [PATCH 13/29] chore: fix event type enum --- infrastructure/w3id/src/logs/log-manager.ts | 227 ++++++++++---------- infrastructure/w3id/src/logs/log.types.ts | 6 +- infrastructure/w3id/tests/logs/log.test.ts | 9 +- 3 files changed, 123 insertions(+), 119 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 0597d56bf..c997e2c7b 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,18 +1,18 @@ import canonicalize from "canonicalize"; import { - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - LogEvents, - type RotationLogOptions, - type VerifierCallback, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + LogEventType, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -24,118 +24,123 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec) { + this.repository = repository; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 + ? lastUpdateKeysSeen + : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new Error(); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new Error(); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry( + entries: LogEvent[], + options: RotationLogOptions, + ) { + const { signer, nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (options.type === LogEvents.Genesis) - return this.createGenesisEntry(options); - return this.appendEntry(entries, options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (options.type === LogEventType.Genesis) + return this.createGenesisEntry(options); + return this.appendEntry(entries, options); + } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index f273eca38..d8f2924d7 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -8,13 +8,13 @@ export type LogEvent = { proof?: string; }; -export enum LogEvents { +export enum LogEventType { Rotation, Genesis, } export type RotationLogOptions = { - type: LogEvents.Rotation; + type: LogEventType.Rotation; nextKeyHashes: string[]; signer: Signer; nextKeySigner: Signer; @@ -27,7 +27,7 @@ export type VerifierCallback = ( ) => Promise; export type GenesisLogOptions = { - type: LogEvents.Genesis; + type: LogEventType.Genesis; nextKeyHashes: string[]; id: string; signer: Signer; diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index 134a2ac1d..3317106fe 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -1,7 +1,7 @@ import { StorageSpec } from "../../src/logs/storage/storage-spec.ts"; import { LogEvent, - LogEvents, + LogEventType, Signer, VerifierCallback, } from "../../src/logs/log.types.ts"; @@ -22,7 +22,6 @@ import { MalformedHashChainError, MalformedIndexChainError, } from "../../src/errors/errors.ts"; -import { sign } from "node:crypto"; class InMemoryStorage implements StorageSpec @@ -103,7 +102,7 @@ describe("LogManager", async () => { const signer = createSigner(keyPair); const logEvent = await logManager.createLogEvent({ id: w3id, - type: LogEvents.Genesis, + type: LogEventType.Genesis, nextKeyHashes: [nextKeyHash], signer, }); @@ -117,7 +116,7 @@ describe("LogManager", async () => { const signer = createSigner(nextKeyPair); const nextKeySigner = createSigner(nextKeyPair); const logEvent = logManager.createLogEvent({ - type: LogEvents.Rotation, + type: LogEventType.Rotation, nextKeyHashes: [nextKeyHash], signer, nextKeySigner, @@ -133,7 +132,7 @@ describe("LogManager", async () => { const signer = createSigner(keyPair); const nextKeySigner = createSigner(currNextKey); const logEvent = await logManager.createLogEvent({ - type: LogEvents.Rotation, + type: LogEventType.Rotation, nextKeyHashes: [nextKeyHash], signer, nextKeySigner, From 82bce445e0966e11610cbace940927be56c54db8 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 14:09:23 +0530 Subject: [PATCH 14/29] chore: format --- infrastructure/w3id/src/logs/log-manager.ts | 227 ++++++++++---------- infrastructure/w3id/src/logs/log.types.ts | 44 ++-- 2 files changed, 133 insertions(+), 138 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index c997e2c7b..dd2be2203 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,18 +1,18 @@ import canonicalize from "canonicalize"; import { - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - LogEventType, - type RotationLogOptions, - type VerifierCallback, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + LogEventType, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -24,123 +24,118 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec) { + this.repository = repository; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 - ? lastUpdateKeysSeen - : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new Error(); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new Error(); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry( - entries: LogEvent[], - options: RotationLogOptions, - ) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { signer, nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (options.type === LogEventType.Genesis) - return this.createGenesisEntry(options); - return this.appendEntry(entries, options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (options.type === LogEventType.Genesis) + return this.createGenesisEntry(options); + return this.appendEntry(entries, options); + } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index d8f2924d7..b46a25123 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,41 +1,41 @@ export type LogEvent = { - id: string; - versionId: string; - versionTime: Date; - updateKeys: string[]; - nextKeyHashes: string[]; - method: `w3id:v${string}`; - proof?: string; + id: string; + versionId: string; + versionTime: Date; + updateKeys: string[]; + nextKeyHashes: string[]; + method: `w3id:v${string}`; + proof?: string; }; export enum LogEventType { - Rotation, - Genesis, + Rotation, + Genesis, } export type RotationLogOptions = { - type: LogEventType.Rotation; - nextKeyHashes: string[]; - signer: Signer; - nextKeySigner: Signer; + type: LogEventType.Rotation; + nextKeyHashes: string[]; + signer: Signer; + nextKeySigner: Signer; }; export type VerifierCallback = ( - message: string, - signature: string, - pubKey: string, + message: string, + signature: string, + pubKey: string, ) => Promise; export type GenesisLogOptions = { - type: LogEventType.Genesis; - nextKeyHashes: string[]; - id: string; - signer: Signer; + type: LogEventType.Genesis; + nextKeyHashes: string[]; + id: string; + signer: Signer; }; export type Signer = { - sign: (string: string) => Promise | string; - pubKey: string; + sign: (string: string) => Promise | string; + pubKey: string; }; export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; From 9f7a500fcc5ed1bfae4d67eba9e659e527109bd9 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 14:11:52 +0530 Subject: [PATCH 15/29] feat: add proper error --- infrastructure/w3id/src/errors/errors.ts | 2 + infrastructure/w3id/src/logs/log-manager.ts | 229 ++++++++++---------- infrastructure/w3id/tests/logs/log.test.ts | 3 +- 3 files changed, 122 insertions(+), 112 deletions(-) diff --git a/infrastructure/w3id/src/errors/errors.ts b/infrastructure/w3id/src/errors/errors.ts index aa0fe596d..862c7ecaf 100644 --- a/infrastructure/w3id/src/errors/errors.ts +++ b/infrastructure/w3id/src/errors/errors.ts @@ -3,3 +3,5 @@ export class MalformedIndexChainError extends Error {} export class MalformedHashChainError extends Error {} export class BadSignatureError extends Error {} + +export class BadNextKeySpecifiedError extends Error {} diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index dd2be2203..e0e329083 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,18 +1,19 @@ import canonicalize from "canonicalize"; import { - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadNextKeySpecifiedError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - LogEventType, - type RotationLogOptions, - type VerifierCallback, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + LogEventType, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -24,118 +25,124 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec) { + this.repository = repository; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 + ? lastUpdateKeysSeen + : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new Error(); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new Error(); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry( + entries: LogEvent[], + options: RotationLogOptions, + ) { + const { signer, nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) throw new Error(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) + throw new BadNextKeySpecifiedError(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (options.type === LogEventType.Genesis) - return this.createGenesisEntry(options); - return this.appendEntry(entries, options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (options.type === LogEventType.Genesis) + return this.createGenesisEntry(options); + return this.appendEntry(entries, options); + } } diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index 3317106fe..6873e5ea0 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -18,6 +18,7 @@ import { import { base58btc } from "multiformats/bases/base58"; import falso from "@ngneat/falso"; import { + BadNextKeySpecifiedError, BadSignatureError, MalformedHashChainError, MalformedIndexChainError, @@ -122,7 +123,7 @@ describe("LogManager", async () => { nextKeySigner, }); - await expect(logEvent).rejects.toThrow(); + await expect(logEvent).rejects.toThrow(BadNextKeySpecifiedError); }); test("KeyRotation: [Creates Entry]", async () => { From 29a240e135be1992baf6a93ba450d31456d88edc Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 14:15:55 +0530 Subject: [PATCH 16/29] chore: format --- infrastructure/w3id/src/logs/log-manager.ts | 231 ++++++++++---------- 1 file changed, 113 insertions(+), 118 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index e0e329083..a439240fa 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,19 +1,19 @@ import canonicalize from "canonicalize"; import { - BadNextKeySpecifiedError, - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadNextKeySpecifiedError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - LogEventType, - type RotationLogOptions, - type VerifierCallback, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + LogEventType, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -25,124 +25,119 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec) { + this.repository = repository; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 - ? lastUpdateKeysSeen - : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new Error(); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new Error(); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry( - entries: LogEvent[], - options: RotationLogOptions, - ) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { signer, nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) - throw new BadNextKeySpecifiedError(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) + throw new BadNextKeySpecifiedError(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (options.type === LogEventType.Genesis) - return this.createGenesisEntry(options); - return this.appendEntry(entries, options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (options.type === LogEventType.Genesis) + return this.createGenesisEntry(options); + return this.appendEntry(entries, options); + } } From bdf3bd1449f4343ebe8565327b49b4d1237e246e Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 17:04:08 +0530 Subject: [PATCH 17/29] chore: remove eventtypes enum --- infrastructure/w3id/src/logs/log-manager.ts | 7 +++---- infrastructure/w3id/src/logs/log.types.ts | 17 +++++------------ infrastructure/w3id/tests/logs/log.test.ts | 5 +---- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index a439240fa..7269b1304 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -11,7 +11,6 @@ import { type CreateLogEventOptions, type GenesisLogOptions, type LogEvent, - LogEventType, type RotationLogOptions, type VerifierCallback, } from "./log.types"; @@ -136,8 +135,8 @@ export class IDLogManager { async createLogEvent(options: CreateLogEventOptions) { const entries = await this.repository.findMany({}); - if (options.type === LogEventType.Genesis) - return this.createGenesisEntry(options); - return this.appendEntry(entries, options); + if (entries.length > 0) + return this.appendEntry(entries, options as RotationLogOptions); + return this.createGenesisEntry(options as GenesisLogOptions); } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index b46a25123..1e1461680 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -8,26 +8,19 @@ export type LogEvent = { proof?: string; }; -export enum LogEventType { - Rotation, - Genesis, -} +export type VerifierCallback = ( + message: string, + signature: string, + pubKey: string, +) => Promise; export type RotationLogOptions = { - type: LogEventType.Rotation; nextKeyHashes: string[]; signer: Signer; nextKeySigner: Signer; }; -export type VerifierCallback = ( - message: string, - signature: string, - pubKey: string, -) => Promise; - export type GenesisLogOptions = { - type: LogEventType.Genesis; nextKeyHashes: string[]; id: string; signer: Signer; diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index 6873e5ea0..d4b87baf3 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -1,7 +1,6 @@ import { StorageSpec } from "../../src/logs/storage/storage-spec.ts"; import { LogEvent, - LogEventType, Signer, VerifierCallback, } from "../../src/logs/log.types.ts"; @@ -103,7 +102,6 @@ describe("LogManager", async () => { const signer = createSigner(keyPair); const logEvent = await logManager.createLogEvent({ id: w3id, - type: LogEventType.Genesis, nextKeyHashes: [nextKeyHash], signer, }); @@ -117,7 +115,6 @@ describe("LogManager", async () => { const signer = createSigner(nextKeyPair); const nextKeySigner = createSigner(nextKeyPair); const logEvent = logManager.createLogEvent({ - type: LogEventType.Rotation, nextKeyHashes: [nextKeyHash], signer, nextKeySigner, @@ -133,7 +130,6 @@ describe("LogManager", async () => { const signer = createSigner(keyPair); const nextKeySigner = createSigner(currNextKey); const logEvent = await logManager.createLogEvent({ - type: LogEventType.Rotation, nextKeyHashes: [nextKeyHash], signer, nextKeySigner, @@ -174,6 +170,7 @@ describe("LogManager", async () => { const events = JSON.parse(JSON.stringify(_events)); const newKeyPair = nacl.sign.keyPair(); const signer = createSigner(newKeyPair); + delete events[1].proof; events[1].proof = await signer.sign(events[1]); const result = IDLogManager.validateLogChain(events, verifierCallback); From 11453fe7ebfb32aa895c69ebdc066813b8d59e60 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 18:01:50 +0530 Subject: [PATCH 18/29] chore: add new error for bad options --- infrastructure/w3id/src/errors/errors.ts | 2 ++ infrastructure/w3id/src/logs/log-manager.ts | 12 +++++++++--- infrastructure/w3id/src/logs/log.types.ts | 19 +++++++++++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/infrastructure/w3id/src/errors/errors.ts b/infrastructure/w3id/src/errors/errors.ts index 862c7ecaf..b87e583d3 100644 --- a/infrastructure/w3id/src/errors/errors.ts +++ b/infrastructure/w3id/src/errors/errors.ts @@ -5,3 +5,5 @@ export class MalformedHashChainError extends Error {} export class BadSignatureError extends Error {} export class BadNextKeySpecifiedError extends Error {} + +export class BadOptionsSpecifiedError extends Error {} diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 7269b1304..e410a3567 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,6 +1,7 @@ import canonicalize from "canonicalize"; import { BadNextKeySpecifiedError, + BadOptionsSpecifiedError, BadSignatureError, MalformedHashChainError, MalformedIndexChainError, @@ -8,6 +9,8 @@ import { import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { + isGenesisOptions, + isRotationOptions, type CreateLogEventOptions, type GenesisLogOptions, type LogEvent, @@ -135,8 +138,11 @@ export class IDLogManager { async createLogEvent(options: CreateLogEventOptions) { const entries = await this.repository.findMany({}); - if (entries.length > 0) - return this.appendEntry(entries, options as RotationLogOptions); - return this.createGenesisEntry(options as GenesisLogOptions); + if (entries.length > 0) { + if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError(); + return this.appendEntry(entries, options); + } + if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); + return this.createGenesisEntry(options); } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index 1e1461680..fb141101d 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -14,6 +14,11 @@ export type VerifierCallback = ( pubKey: string, ) => Promise; +export type Signer = { + sign: (string: string) => Promise | string; + pubKey: string; +}; + export type RotationLogOptions = { nextKeyHashes: string[]; signer: Signer; @@ -26,9 +31,15 @@ export type GenesisLogOptions = { signer: Signer; }; -export type Signer = { - sign: (string: string) => Promise | string; - pubKey: string; -}; +export function isGenesisOptions( + options: CreateLogEventOptions, +): options is GenesisLogOptions { + return "id" in options; +} +export function isRotationOptions( + options: CreateLogEventOptions, +): options is RotationLogOptions { + return "nextKeySigner" in options; +} export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; From 504240344dc9d2b25c2210b305c83c738b091df9 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 9 Apr 2025 18:09:44 +0530 Subject: [PATCH 19/29] chore: add options tests --- infrastructure/w3id/tests/logs/log.test.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index d4b87baf3..90db3bbce 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -18,6 +18,7 @@ import { base58btc } from "multiformats/bases/base58"; import falso from "@ngneat/falso"; import { BadNextKeySpecifiedError, + BadOptionsSpecifiedError, BadSignatureError, MalformedHashChainError, MalformedIndexChainError, @@ -97,6 +98,17 @@ function createSigner(keyPair: nacl.SignKeyPair): Signer { } describe("LogManager", async () => { + test("GenesisEvent: [Throw at Bad Options]", async () => { + const nextKeyHash = await hash(uint8ArrayToHex(currNextKey.publicKey)); + const signer = createSigner(keyPair); + const logEvent = logManager.createLogEvent({ + nextKeySigner: signer, + nextKeyHashes: [nextKeyHash], + signer, + }); + await expect(logEvent).rejects.toThrow(BadOptionsSpecifiedError); + }); + test("GenesisEvent: [Creates Entry]", async () => { const nextKeyHash = await hash(uint8ArrayToHex(currNextKey.publicKey)); const signer = createSigner(keyPair); @@ -108,6 +120,20 @@ describe("LogManager", async () => { expectTypeOf(logEvent).toMatchObjectType(); }); + test("KeyRotation: [Throw At Bad Options]", async () => { + const nextKeyPair = nacl.sign.keyPair(); + const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); + + const signer = createSigner(nextKeyPair); + const logEvent = logManager.createLogEvent({ + nextKeyHashes: [nextKeyHash], + signer, + id: `@{falso.randUuid()}`, + }); + + await expect(logEvent).rejects.toThrow(BadOptionsSpecifiedError); + }); + test("KeyRotation: [Error At Wrong Next Key]", async () => { const nextKeyPair = nacl.sign.keyPair(); const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); From 9c3543098c7d14303460a0c283c98070167b9ff2 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 20:58:58 +0530 Subject: [PATCH 20/29] feat: add codec tests --- infrastructure/w3id/tests/utils/codec.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 infrastructure/w3id/tests/utils/codec.test.ts diff --git a/infrastructure/w3id/tests/utils/codec.test.ts b/infrastructure/w3id/tests/utils/codec.test.ts new file mode 100644 index 000000000..cc7f21390 --- /dev/null +++ b/infrastructure/w3id/tests/utils/codec.test.ts @@ -0,0 +1,34 @@ +import { + uint8ArrayToHex, + hexToUint8Array, + stringToUint8Array, +} from "../../src/utils/codec"; +import falso from "@ngneat/falso"; +import { describe, test, expect } from "vitest"; + +describe("Codec", () => { + test("uint8ArrayToHex", () => { + const input = new Uint8Array([1, 2, 3, 4]); + const expected = "01020304"; + expect(uint8ArrayToHex(input)).toBe(expected); + }); + + test("hexToUint8Array", () => { + const input = "01020304"; + const expected = new Uint8Array([1, 2, 3, 4]); + expect(hexToUint8Array(input)).toEqual(expected); + }); + + test("hexToUint8Array (Odd Length)", () => { + const input = "010203045"; + expect(() => hexToUint8Array(input)).toThrow( + "Hex string must have an even length", + ); + }); + + test("stringToUint8Array", () => { + const input = "hello"; + const expected = new Uint8Array([104, 101, 108, 108, 111]); + expect(stringToUint8Array(input)).toEqual(expected); + }); +}); From d1de58a305e44bd6f3aadbb6e4639d20f82f4497 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:04:45 +0530 Subject: [PATCH 21/29] fix: err handling && jsdoc --- infrastructure/w3id/src/logs/log-manager.ts | 236 ++++++++++---------- infrastructure/w3id/src/utils/array.ts | 30 ++- 2 files changed, 137 insertions(+), 129 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index e410a3567..9c9fc0f91 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,21 +1,21 @@ import canonicalize from "canonicalize"; import { - BadNextKeySpecifiedError, - BadOptionsSpecifiedError, - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadNextKeySpecifiedError, + BadOptionsSpecifiedError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - isGenesisOptions, - isRotationOptions, - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - type RotationLogOptions, - type VerifierCallback, + isGenesisOptions, + isRotationOptions, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -27,122 +27,122 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec) { + this.repository = repository; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new Error(); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new BadSignatureError("No proof found in the log event."); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { signer, nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) - throw new BadNextKeySpecifiedError(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) + throw new BadNextKeySpecifiedError(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (entries.length > 0) { - if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError(); - return this.appendEntry(entries, options); - } - if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); - return this.createGenesisEntry(options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (entries.length > 0) { + if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError(); + return this.appendEntry(entries, options); + } + if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); + return this.createGenesisEntry(options); + } } diff --git a/infrastructure/w3id/src/utils/array.ts b/infrastructure/w3id/src/utils/array.ts index 6f3280ce4..3c49f3cbb 100644 --- a/infrastructure/w3id/src/utils/array.ts +++ b/infrastructure/w3id/src/utils/array.ts @@ -1,20 +1,28 @@ /** * Utility function to check if A is subset of B + * + * @param a Array to check if it's a subset + * @param b Array to check against + * @returns true if every element in 'a' is present in 'b' with at least the same frequency + * @example + * isSubsetOf([1, 2], [1, 2, 3]) // returns true + * isSubsetOf([1, 1, 2], [1, 2, 3]) // returns false (not enough 1's in b) + * isSubsetOf([], [1, 2]) // returns true (empty set is a subset of any set) */ export function isSubsetOf(a: unknown[], b: unknown[]) { - const map = new Map(); + const map = new Map(); - for (const el of b) { - map.set(el, (map.get(el) || 0) + 1); - } + for (const el of b) { + map.set(el, (map.get(el) || 0) + 1); + } - for (const el of a) { - if (!map.has(el) || map.get(el) === 0) { - return false; - } - map.set(el, map.get(el) - 1); - } + for (const el of a) { + if (!map.has(el) || map.get(el) === 0) { + return false; + } + map.set(el, map.get(el) - 1); + } - return true; + return true; } From bc6cfe685233c70ed8bbacf029e5ebde2ee731c9 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:05:25 +0530 Subject: [PATCH 22/29] fix: run format --- infrastructure/w3id/src/logs/log-manager.ts | 236 ++++++++++---------- infrastructure/w3id/src/utils/array.ts | 22 +- 2 files changed, 129 insertions(+), 129 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 9c9fc0f91..b69e3dc20 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,21 +1,21 @@ import canonicalize from "canonicalize"; import { - BadNextKeySpecifiedError, - BadOptionsSpecifiedError, - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadNextKeySpecifiedError, + BadOptionsSpecifiedError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - isGenesisOptions, - isRotationOptions, - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - type RotationLogOptions, - type VerifierCallback, + isGenesisOptions, + isRotationOptions, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -27,122 +27,122 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec) { + this.repository = repository; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new BadSignatureError("No proof found in the log event."); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new BadSignatureError("No proof found in the log event."); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { signer, nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) - throw new BadNextKeySpecifiedError(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) + throw new BadNextKeySpecifiedError(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes, signer } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (entries.length > 0) { - if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError(); - return this.appendEntry(entries, options); - } - if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); - return this.createGenesisEntry(options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (entries.length > 0) { + if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError(); + return this.appendEntry(entries, options); + } + if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); + return this.createGenesisEntry(options); + } } diff --git a/infrastructure/w3id/src/utils/array.ts b/infrastructure/w3id/src/utils/array.ts index 3c49f3cbb..19224fec4 100644 --- a/infrastructure/w3id/src/utils/array.ts +++ b/infrastructure/w3id/src/utils/array.ts @@ -11,18 +11,18 @@ */ export function isSubsetOf(a: unknown[], b: unknown[]) { - const map = new Map(); + const map = new Map(); - for (const el of b) { - map.set(el, (map.get(el) || 0) + 1); - } + for (const el of b) { + map.set(el, (map.get(el) || 0) + 1); + } - for (const el of a) { - if (!map.has(el) || map.get(el) === 0) { - return false; - } - map.set(el, map.get(el) - 1); - } + for (const el of a) { + if (!map.has(el) || map.get(el) === 0) { + return false; + } + map.set(el, map.get(el) - 1); + } - return true; + return true; } From af977ed5bb028d01dd36b322ec01c19889b139e0 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:06:14 +0530 Subject: [PATCH 23/29] fix: remove unused import --- infrastructure/w3id/tests/utils/codec.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/infrastructure/w3id/tests/utils/codec.test.ts b/infrastructure/w3id/tests/utils/codec.test.ts index cc7f21390..b0bd388b5 100644 --- a/infrastructure/w3id/tests/utils/codec.test.ts +++ b/infrastructure/w3id/tests/utils/codec.test.ts @@ -3,7 +3,6 @@ import { hexToUint8Array, stringToUint8Array, } from "../../src/utils/codec"; -import falso from "@ngneat/falso"; import { describe, test, expect } from "vitest"; describe("Codec", () => { From 2b6d2d0d1c8b5bb5ed10be83ebeed109c3a65d24 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:17:43 +0530 Subject: [PATCH 24/29] fix: improve default error messages --- infrastructure/w3id/src/errors/errors.ts | 35 ++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/infrastructure/w3id/src/errors/errors.ts b/infrastructure/w3id/src/errors/errors.ts index b87e583d3..6ae2b81d0 100644 --- a/infrastructure/w3id/src/errors/errors.ts +++ b/infrastructure/w3id/src/errors/errors.ts @@ -1,9 +1,34 @@ -export class MalformedIndexChainError extends Error {} +export class MalformedIndexChainError extends Error { + constructor(message: string = "Malformed index chain detected") { + super(message); + this.name = "MalformedIndexChainError"; + } +} -export class MalformedHashChainError extends Error {} +export class MalformedHashChainError extends Error { + constructor(message: string = "Malformed hash chain detected") { + super(message); + this.name = "MalformedHashChainError"; + } +} -export class BadSignatureError extends Error {} +export class BadSignatureError extends Error { + constructor(message: string = "Bad signature detected") { + super(message); + this.name = "BadSignatureError"; + } +} -export class BadNextKeySpecifiedError extends Error {} +export class BadNextKeySpecifiedError extends Error { + constructor(message: string = "Bad next key specified") { + super(message); + this.name = "BadNextKeySpecifiedError"; + } +} -export class BadOptionsSpecifiedError extends Error {} +export class BadOptionsSpecifiedError extends Error { + constructor(message: string = "Bad options specified") { + super(message); + this.name = "BadOptionsSpecifiedError"; + } +} From eb02c9ef9caad4e3f985a03c368f6141f5bdac24 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:23:10 +0530 Subject: [PATCH 25/29] fix: move redundant logic to function --- infrastructure/w3id/src/errors/errors.ts | 40 ++++++++++++------------ infrastructure/w3id/src/utils/hash.ts | 39 ++++++++++++----------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/infrastructure/w3id/src/errors/errors.ts b/infrastructure/w3id/src/errors/errors.ts index 6ae2b81d0..4a314b081 100644 --- a/infrastructure/w3id/src/errors/errors.ts +++ b/infrastructure/w3id/src/errors/errors.ts @@ -1,34 +1,34 @@ export class MalformedIndexChainError extends Error { - constructor(message: string = "Malformed index chain detected") { - super(message); - this.name = "MalformedIndexChainError"; - } + constructor(message: string = "Malformed index chain detected") { + super(message); + this.name = "MalformedIndexChainError"; + } } export class MalformedHashChainError extends Error { - constructor(message: string = "Malformed hash chain detected") { - super(message); - this.name = "MalformedHashChainError"; - } + constructor(message: string = "Malformed hash chain detected") { + super(message); + this.name = "MalformedHashChainError"; + } } export class BadSignatureError extends Error { - constructor(message: string = "Bad signature detected") { - super(message); - this.name = "BadSignatureError"; - } + constructor(message: string = "Bad signature detected") { + super(message); + this.name = "BadSignatureError"; + } } export class BadNextKeySpecifiedError extends Error { - constructor(message: string = "Bad next key specified") { - super(message); - this.name = "BadNextKeySpecifiedError"; - } + constructor(message: string = "Bad next key specified") { + super(message); + this.name = "BadNextKeySpecifiedError"; + } } export class BadOptionsSpecifiedError extends Error { - constructor(message: string = "Bad options specified") { - super(message); - this.name = "BadOptionsSpecifiedError"; - } + constructor(message: string = "Bad options specified") { + super(message); + this.name = "BadOptionsSpecifiedError"; + } } diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts index 4215eb1db..db6834ebb 100644 --- a/infrastructure/w3id/src/utils/hash.ts +++ b/infrastructure/w3id/src/utils/hash.ts @@ -1,26 +1,29 @@ import canonicalize from "canonicalize"; +import { uint8ArrayToHex } from "./codec"; export async function hash( - input: string | Record, + input: string | Record, ): Promise { - let dataToHash: string; + let dataToHash: string; - if (typeof input === "string") { - dataToHash = input; - } else { - const canonical = canonicalize(input); - if (!canonical) { - throw new Error("Failed to canonicalize object"); - } - dataToHash = canonical; - } + if (typeof input === "string") { + dataToHash = input; + } else { + const canonical = canonicalize(input); + if (!canonical) { + throw new Error( + `Failed to canonicalize object: ${JSON.stringify(input).substring(0, 100)}...`, + ); + } + dataToHash = canonical; + } - const buffer = new TextEncoder().encode(dataToHash); - const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); + const buffer = new TextEncoder().encode(dataToHash); + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); + // const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = uint8ArrayToHex( + Array.from(new Uint8Array(hashBuffer)) as unknown as Uint8Array, + ); - return hashHex; + return hashHex; } From 123fa494398362ca0eb90b94edda11b2657c1cf1 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:23:36 +0530 Subject: [PATCH 26/29] fix: run format --- infrastructure/w3id/src/utils/hash.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts index db6834ebb..edef3efb0 100644 --- a/infrastructure/w3id/src/utils/hash.ts +++ b/infrastructure/w3id/src/utils/hash.ts @@ -20,7 +20,6 @@ export async function hash( const buffer = new TextEncoder().encode(dataToHash); const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); - // const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = uint8ArrayToHex( Array.from(new Uint8Array(hashBuffer)) as unknown as Uint8Array, ); From 4f9ab1236e9a8bf2b4282a034b12fc519a839152 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:25:08 +0530 Subject: [PATCH 27/29] fix: type shadow --- infrastructure/w3id/src/logs/log.types.ts | 2 +- infrastructure/w3id/src/utils/hash.ts | 38 +++++++++++------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index fb141101d..2f03b3115 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -15,7 +15,7 @@ export type VerifierCallback = ( ) => Promise; export type Signer = { - sign: (string: string) => Promise | string; + sign: (message: string) => Promise | string; pubKey: string; }; diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts index edef3efb0..40036aed8 100644 --- a/infrastructure/w3id/src/utils/hash.ts +++ b/infrastructure/w3id/src/utils/hash.ts @@ -2,27 +2,27 @@ import canonicalize from "canonicalize"; import { uint8ArrayToHex } from "./codec"; export async function hash( - input: string | Record, + input: string | Record, ): Promise { - let dataToHash: string; + let dataToHash: string; - if (typeof input === "string") { - dataToHash = input; - } else { - const canonical = canonicalize(input); - if (!canonical) { - throw new Error( - `Failed to canonicalize object: ${JSON.stringify(input).substring(0, 100)}...`, - ); - } - dataToHash = canonical; - } + if (typeof input === "string") { + dataToHash = input; + } else { + const canonical = canonicalize(input); + if (!canonical) { + throw new Error( + `Failed to canonicalize object: ${JSON.stringify(input).substring(0, 100)}...`, + ); + } + dataToHash = canonical; + } - const buffer = new TextEncoder().encode(dataToHash); - const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); - const hashHex = uint8ArrayToHex( - Array.from(new Uint8Array(hashBuffer)) as unknown as Uint8Array, - ); + const buffer = new TextEncoder().encode(dataToHash); + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); + const hashHex = uint8ArrayToHex( + Array.from(new Uint8Array(hashBuffer)) as unknown as Uint8Array, + ); - return hashHex; + return hashHex; } From 34ce0acb357283efde237024e27f409766923456 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:27:55 +0530 Subject: [PATCH 28/29] fix: useless conversion/cast --- infrastructure/w3id/src/utils/hash.ts | 36 +++++++++++++-------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts index 40036aed8..1f859748e 100644 --- a/infrastructure/w3id/src/utils/hash.ts +++ b/infrastructure/w3id/src/utils/hash.ts @@ -2,27 +2,25 @@ import canonicalize from "canonicalize"; import { uint8ArrayToHex } from "./codec"; export async function hash( - input: string | Record, + input: string | Record, ): Promise { - let dataToHash: string; + let dataToHash: string; - if (typeof input === "string") { - dataToHash = input; - } else { - const canonical = canonicalize(input); - if (!canonical) { - throw new Error( - `Failed to canonicalize object: ${JSON.stringify(input).substring(0, 100)}...`, - ); - } - dataToHash = canonical; - } + if (typeof input === "string") { + dataToHash = input; + } else { + const canonical = canonicalize(input); + if (!canonical) { + throw new Error( + `Failed to canonicalize object: ${JSON.stringify(input).substring(0, 100)}...`, + ); + } + dataToHash = canonical; + } - const buffer = new TextEncoder().encode(dataToHash); - const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); - const hashHex = uint8ArrayToHex( - Array.from(new Uint8Array(hashBuffer)) as unknown as Uint8Array, - ); + const buffer = new TextEncoder().encode(dataToHash); + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); + const hashHex = uint8ArrayToHex(new Uint8Array(hashBuffer)); - return hashHex; + return hashHex; } From c9c6d5e439444a49c045fdecc28678406d75d787 Mon Sep 17 00:00:00 2001 From: Soham Jaiswal Date: Wed, 9 Apr 2025 21:28:13 +0530 Subject: [PATCH 29/29] fix: run format --- infrastructure/w3id/src/utils/hash.ts | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/infrastructure/w3id/src/utils/hash.ts b/infrastructure/w3id/src/utils/hash.ts index 1f859748e..d7e8c8525 100644 --- a/infrastructure/w3id/src/utils/hash.ts +++ b/infrastructure/w3id/src/utils/hash.ts @@ -2,25 +2,25 @@ import canonicalize from "canonicalize"; import { uint8ArrayToHex } from "./codec"; export async function hash( - input: string | Record, + input: string | Record, ): Promise { - let dataToHash: string; + let dataToHash: string; - if (typeof input === "string") { - dataToHash = input; - } else { - const canonical = canonicalize(input); - if (!canonical) { - throw new Error( - `Failed to canonicalize object: ${JSON.stringify(input).substring(0, 100)}...`, - ); - } - dataToHash = canonical; - } + if (typeof input === "string") { + dataToHash = input; + } else { + const canonical = canonicalize(input); + if (!canonical) { + throw new Error( + `Failed to canonicalize object: ${JSON.stringify(input).substring(0, 100)}...`, + ); + } + dataToHash = canonical; + } - const buffer = new TextEncoder().encode(dataToHash); - const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); - const hashHex = uint8ArrayToHex(new Uint8Array(hashBuffer)); + const buffer = new TextEncoder().encode(dataToHash); + const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); + const hashHex = uint8ArrayToHex(new Uint8Array(hashBuffer)); - return hashHex; + return hashHex; }