From 4b6a39d87f4fdb706a59e87d87ef371bdc63549f Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 21 Apr 2025 14:44:36 +0530 Subject: [PATCH 1/7] feat: alloc according to entropy and namespace --- infrastructure/evault-core/package.json | 71 ++-- infrastructure/evault-core/src/evault.ts | 83 +++- infrastructure/evault-core/src/http/server.ts | 145 +++++++ infrastructure/evault-core/src/http/types.ts | 18 + .../evault-provisioner/package.json | 3 +- .../evault-provisioner/src/index.ts | 29 +- infrastructure/w3id/src/index.ts | 235 +++++------ infrastructure/w3id/src/logs/log-manager.ts | 364 ++++++++++-------- infrastructure/w3id/src/logs/log.types.ts | 70 ++-- infrastructure/w3id/tests/utils/crypto.ts | 24 +- pnpm-lock.yaml | 89 +++++ 11 files changed, 753 insertions(+), 378 deletions(-) create mode 100644 infrastructure/evault-core/src/http/server.ts create mode 100644 infrastructure/evault-core/src/http/types.ts diff --git a/infrastructure/evault-core/package.json b/infrastructure/evault-core/package.json index 5563f462d..580695563 100644 --- a/infrastructure/evault-core/package.json +++ b/infrastructure/evault-core/package.json @@ -1,36 +1,39 @@ { - "name": "evault-core", - "version": "0.1.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "vitest --config vitest.config.ts", - "build": "tsc", - "dev": "node --watch --import tsx src/evault.ts", - "start": "node ./dist/evault.js" - }, - "packageManager": "pnpm@10.6.5", - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@types/json-schema": "^7.0.15", - "@types/node": "^22.13.10", - "dotenv": "^16.5.0", - "testcontainers": "^10.24.2", - "tsx": "^4.19.3", - "typescript": "^5.8.3", - "uuid": "^11.1.0", - "vitest": "^3.0.9" - }, - "dependencies": { - "@testcontainers/neo4j": "^10.24.2", - "graphql": "^16.10.0", - "graphql-type-json": "^0.3.2", - "graphql-voyager": "^2.1.0", - "graphql-yoga": "^5.13.4", - "json-schema": "^0.4.0", - "neo4j-driver": "^5.28.1", - "w3id": "workspace:*" - } + "name": "evault-core", + "version": "0.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "vitest --config vitest.config.ts", + "build": "tsc", + "dev": "node --watch --import tsx src/evault.ts", + "start": "node ./dist/evault.js" + }, + "packageManager": "pnpm@10.6.5", + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/json-schema": "^7.0.15", + "@types/node": "^22.13.10", + "dotenv": "^16.5.0", + "testcontainers": "^10.24.2", + "tsx": "^4.19.3", + "typescript": "^5.8.3", + "uuid": "^11.1.0", + "vitest": "^3.0.9" + }, + "dependencies": { + "@testcontainers/neo4j": "^10.24.2", + "@fastify/swagger": "^8.14.0", + "@fastify/swagger-ui": "^3.0.0", + "fastify": "^4.26.2", + "graphql": "^16.10.0", + "graphql-type-json": "^0.3.2", + "graphql-voyager": "^2.1.0", + "graphql-yoga": "^5.13.4", + "json-schema": "^0.4.0", + "neo4j-driver": "^5.28.1", + "w3id": "workspace:*" + } } diff --git a/infrastructure/evault-core/src/evault.ts b/infrastructure/evault-core/src/evault.ts index eb07f175a..815deda4b 100644 --- a/infrastructure/evault-core/src/evault.ts +++ b/infrastructure/evault-core/src/evault.ts @@ -1,6 +1,13 @@ -import { Server } from "http"; import { DbService } from "./db/db.service"; import { GraphQLServer } from "./protocol/graphql-server"; +import { registerHttpRoutes } from "./http/server"; +import fastify, { + FastifyInstance, + FastifyRequest, + FastifyReply, +} from "fastify"; +import { renderVoyagerPage } from "graphql-voyager/middleware"; +import { createYoga } from "graphql-yoga"; import dotenv from "dotenv"; import path from "path"; import neo4j from "neo4j-driver"; @@ -8,7 +15,8 @@ import neo4j from "neo4j-driver"; dotenv.config({ path: path.resolve(__dirname, "../../../.env") }); class EVault { - server: Server; + server: FastifyInstance; + graphqlServer: GraphQLServer; constructor() { const uri = process.env.NEO4J_URI || "bolt://localhost:7687"; @@ -27,18 +35,73 @@ class EVault { const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)); const dbService = new DbService(driver); - const gqlServer = new GraphQLServer(dbService); - this.server = gqlServer.server as Server; + this.graphqlServer = new GraphQLServer(dbService); + + // Create Fastify server + this.server = fastify({ + logger: true, + }); } - start() { - const port = process.env.NOMAD_PORT_http || process.env.PORT || 4000; - this.server.listen(Number(port), "0.0.0.0", () => { - console.log(`GraphQL Server started on http://0.0.0.0:${port}`); - console.log(`Voyager started on http://0.0.0.0:${port}`); + async initialize() { + // Register HTTP routes + await registerHttpRoutes(this.server); + + // Create Yoga instance with the schema from GraphQLServer + const yoga = createYoga({ + schema: this.graphqlServer.getSchema(), + graphiql: true, + }); + + // Mount GraphQL endpoint + this.server.route({ + url: "/graphql", + method: ["GET", "POST", "OPTIONS"], + handler: async (req: FastifyRequest, reply: FastifyReply) => { + const response = await yoga.handleNodeRequest( + req.raw, + reply.raw, + ); + response.headers.forEach((value, key) => { + reply.header(key, value); + }); + reply.status(response.status); + reply.send(response.body); + return reply; + }, }); + + // Mount Voyager endpoint + this.server.get( + "/voyager", + (req: FastifyRequest, reply: FastifyReply) => { + reply.type("text/html").send( + renderVoyagerPage({ + endpointUrl: "/graphql", + }), + ); + }, + ); + } + + async start() { + await this.initialize(); + + const port = process.env.NOMAD_PORT_http || process.env.PORT || 4000; + + await this.server.listen({ port: Number(port), host: "0.0.0.0" }); + console.log(`Server started on http://0.0.0.0:${port}`); + console.log( + `GraphQL endpoint available at http://0.0.0.0:${port}/graphql`, + ); + console.log( + `GraphQL Voyager available at http://0.0.0.0:${port}/voyager`, + ); + console.log( + `API Documentation available at http://0.0.0.0:${port}/documentation`, + ); } } const evault = new EVault(); -evault.start(); +evault.start().catch(console.error); diff --git a/infrastructure/evault-core/src/http/server.ts b/infrastructure/evault-core/src/http/server.ts new file mode 100644 index 000000000..92d6d624f --- /dev/null +++ b/infrastructure/evault-core/src/http/server.ts @@ -0,0 +1,145 @@ +import fastify, { FastifyInstance } from "fastify"; +import swagger from "@fastify/swagger"; +import swaggerUi from "@fastify/swagger-ui"; +import { W3ID } from "w3id"; +import { + WatcherSignatureRequest, + WatcherRequest, + TypedRequest, + TypedReply, +} from "./types"; + +export async function registerHttpRoutes( + server: FastifyInstance +): Promise { + // Register Swagger + await server.register(swagger, { + swagger: { + info: { + title: "eVault Core API", + description: "API documentation for eVault Core HTTP endpoints", + version: "1.0.0", + }, + tags: [ + { name: "identity", description: "Identity related endpoints" }, + { + name: "watchers", + description: "Watcher signature related endpoints", + }, + ], + }, + }); + + await server.register(swaggerUi, { + routePrefix: "/docs", + }); + + // Whois endpoint + server.get( + "/whois", + { + schema: { + tags: ["identity"], + description: "Get W3ID response with logs", + response: { + 200: { + type: "object", + properties: { + w3id: { type: "object" }, + logs: { + type: "array", + items: { type: "object" }, + }, + }, + }, + }, + }, + }, + async (request: TypedRequest<{}>, reply: TypedReply) => { + // TODO: Implement actual W3ID verification and log retrieval + const w3id = new W3ID({} as any); // TODO: Add proper W3ID initialization + return { + w3id: w3id, + logs: [], // TODO: Implement log retrieval + }; + } + ); + + // Watchers signature endpoint + server.post<{ Body: WatcherSignatureRequest }>( + "/watchers/sign", + { + schema: { + tags: ["watchers"], + description: "Post a signature for a specific log entry", + body: { + type: "object", + required: ["w3id", "signature", "logEntryId"], + properties: { + w3id: { type: "string" }, + signature: { type: "string" }, + logEntryId: { type: "string" }, + }, + }, + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + message: { type: "string" }, + }, + }, + }, + }, + }, + async ( + request: TypedRequest, + reply: TypedReply + ) => { + const { w3id, signature, logEntryId } = request.body; + // TODO: Implement signature verification and storage + return { + success: true, + message: "Signature stored successfully", + }; + } + ); + + // Watchers request endpoint + server.post<{ Body: WatcherRequest }>( + "/watchers/request", + { + schema: { + tags: ["watchers"], + description: "Request signature for a log entry", + body: { + type: "object", + required: ["w3id", "logEntryId"], + properties: { + w3id: { type: "string" }, + logEntryId: { type: "string" }, + }, + }, + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + message: { type: "string" }, + requestId: { type: "string" }, + }, + }, + }, + }, + }, + async (request: TypedRequest, reply: TypedReply) => { + const { w3id, logEntryId } = request.body; + // TODO: Implement signature request logic + return { + success: true, + message: "Signature request created", + requestId: "req_" + Date.now(), + }; + } + ); +} diff --git a/infrastructure/evault-core/src/http/types.ts b/infrastructure/evault-core/src/http/types.ts new file mode 100644 index 000000000..cdc4e7c8c --- /dev/null +++ b/infrastructure/evault-core/src/http/types.ts @@ -0,0 +1,18 @@ +import { FastifyReply, FastifyRequest } from "fastify"; + +export interface WatcherSignatureRequest { + w3id: string; + signature: string; + logEntryId: string; +} + +export interface WatcherRequest { + w3id: string; + logEntryId: string; +} + +export type TypedRequest = FastifyRequest<{ + Body: T; +}>; + +export type TypedReply = FastifyReply; diff --git a/infrastructure/evault-provisioner/package.json b/infrastructure/evault-provisioner/package.json index 0414cdf09..d0f86ee0f 100644 --- a/infrastructure/evault-provisioner/package.json +++ b/infrastructure/evault-provisioner/package.json @@ -11,9 +11,10 @@ "test": "vitest" }, "dependencies": { - "express": "^4.18.2", "axios": "^1.6.7", "dotenv": "^16.4.5", + "express": "^4.18.2", + "jose": "^5.2.2", "w3id": "workspace:*" }, "devDependencies": { diff --git a/infrastructure/evault-provisioner/src/index.ts b/infrastructure/evault-provisioner/src/index.ts index 955128474..11857cb85 100644 --- a/infrastructure/evault-provisioner/src/index.ts +++ b/infrastructure/evault-provisioner/src/index.ts @@ -4,6 +4,7 @@ import { generateNomadJob } from "./templates/evault.nomad.js"; import dotenv from "dotenv"; import { subscribeToAlloc } from "./listeners/alloc.js"; import { W3IDBuilder } from "w3id"; +import * as jose from "jose"; dotenv.config(); @@ -13,7 +14,8 @@ const port = process.env.PORT || 3000; app.use(express.json()); interface ProvisionRequest { - w3id: string; + registryEntropy: string; + namespace: string; } interface ProvisionResponse { @@ -38,17 +40,34 @@ app.post( try { // TODO: change this to take namespace from the payload, and signed entropy // JWT so that we can verify both parts of the UUID come from know source - const { w3id } = req.body; + const { registryEntropy, namespace } = req.body; - if (!w3id) { + if (!registryEntropy || !namespace) { return res.status(400).json({ success: false, - error: "tenantId is required", - message: "Missing required field: tenantId", + error: "registryEntropy and namespace are required", + message: + "Missing required fields: registryEntropy, namespace", }); } + const jwksResponse = await axios.get( + `http://localhost:4321/.well-known/jwks.json`, + ); + console.log(jwksResponse.data); + + const JWKS = jose.createLocalJWKSet(jwksResponse.data); + + const { payload } = await jose.jwtVerify(registryEntropy, JWKS); + + console.log(payload); const evaultId = await new W3IDBuilder().withGlobal(true).build(); + const userId = await new W3IDBuilder() + .withNamespace(namespace) + .withGlobal(true) + .build(); + + const w3id = userId.id; const jobJSON = generateNomadJob(w3id, evaultId.id); const jobName = `evault-${w3id}`; diff --git a/infrastructure/w3id/src/index.ts b/infrastructure/w3id/src/index.ts index 050b81ea9..0de776afe 100644 --- a/infrastructure/w3id/src/index.ts +++ b/infrastructure/w3id/src/index.ts @@ -7,134 +7,137 @@ import { generateRandomAlphaNum } from "./utils/rand"; import { generateUuid } from "./utils/uuid"; export class W3ID { - constructor( - public id: string, - public logs?: IDLogManager, - ) {} + constructor( + public id: string, + public logs?: IDLogManager, + ) {} - /** - * Signs a JWT with the W3ID's signer - * @param payload - The JWT payload - * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) - * @returns The signed JWT - */ - public async signJWT( - payload: JWTPayload, - header?: JWTHeader, - ): Promise { - if (!this.logs?.signer) { - throw new Error("W3ID must have a signer to sign JWTs"); - } - return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); - } + /** + * Signs a JWT with the W3ID's signer + * @param payload - The JWT payload + * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) + * @returns The signed JWT + */ + public async signJWT( + payload: JWTPayload, + header?: JWTHeader, + ): Promise { + if (!this.logs?.signer) { + throw new Error("W3ID must have a signer to sign JWTs"); + } + return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); + } } export class W3IDBuilder { - private signer?: Signer; - private repository?: StorageSpec; - private entropy?: string; - private namespace?: string; - private nextKeyHash?: string; - private global?: boolean = false; + private signer?: Signer; + private repository?: StorageSpec; + private entropy?: string; + private namespace?: string; + private nextKeyHash?: string; + private global?: boolean = false; - /** - * Specify entropy to create the identity with - * - * @param {string} str - */ - public withEntropy(str: string): W3IDBuilder { - this.entropy = str; - return this; - } + /** + * Specify entropy to create the identity with + * + * @param {string} str + */ + public withEntropy(str: string): W3IDBuilder { + this.entropy = str; + return this; + } - /** - * Specify namespace to use to generate the UUIDv5 - * - * @param {string} uuid - */ - public withNamespace(uuid: string): W3IDBuilder { - this.namespace = uuid; - return this; - } + /** + * Specify namespace to use to generate the UUIDv5 + * + * @param {string} uuid + */ + public withNamespace(uuid: string): W3IDBuilder { + this.namespace = uuid; + return this; + } - /** - * Specify whether to create a global identifier or a local identifer - * - * According to the project specification there are supposed to be 2 main types of - * W3ID's ones which are tied to more permanent entities - * - * A global identifer is expected to live at the registry and starts with an \`@\` - * - * @param {boolean} isGlobal - */ - public withGlobal(isGlobal: boolean): W3IDBuilder { - this.global = isGlobal; - return this; - } + /** + * Specify whether to create a global identifier or a local identifer + * + * According to the project specification there are supposed to be 2 main types of + * W3ID's ones which are tied to more permanent entities + * + * A global identifer is expected to live at the registry and starts with an \`@\` + * + * @param {boolean} isGlobal + */ + public withGlobal(isGlobal: boolean): W3IDBuilder { + this.global = isGlobal; + return this; + } - /** - * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a - * repository in which the logs would be stored - * - * @param {StorageSpec} storage - */ - public withRepository(storage: StorageSpec): W3IDBuilder { - this.repository = storage; - return this; - } + /** + * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a + * repository in which the logs would be stored + * + * @param {StorageSpec} storage + */ + public withRepository( + storage: StorageSpec, + ): W3IDBuilder { + this.repository = storage; + return this; + } - /** - * Attach a keypair to the W3ID, a key attached W3ID would also need a repository - * to be added. - * - * @param {Signer} signer - */ - public withSigner(signer: Signer): W3IDBuilder { - this.signer = signer; - return this; - } + /** + * Attach a keypair to the W3ID, a key attached W3ID would also need a repository + * to be added. + * + * @param {Signer} signer + */ + public withSigner(signer: Signer): W3IDBuilder { + this.signer = signer; + return this; + } - /** - * Specify the SHA256 hash of the next key which will sign the next log entry after - * rotation of keys - * - * @param {string} hash - */ - public withNextKeyHash(hash: string): W3IDBuilder { - this.nextKeyHash = hash; - return this; - } + /** + * Specify the SHA256 hash of the next key which will sign the next log entry after + * rotation of keys + * + * @param {string} hash + */ + public withNextKeyHash(hash: string): W3IDBuilder { + this.nextKeyHash = hash; + return this; + } - /** - * Build the W3ID with provided builder options - * - * @returns Promise - */ - public async build(): Promise { - this.entropy = this.entropy ?? generateRandomAlphaNum(); - this.namespace = this.namespace ?? uuidv4(); - const id = `${ - this.global ? "@" : "" - }${generateUuid(this.entropy, this.namespace)}`; - if (!this.signer) { - return new W3ID(id); - } - if (!this.repository) - throw new Error( - "Repository is required, pass with `withRepository` method", - ); + /** + * Build the W3ID with provided builder options + * + * @returns Promise + */ + public async build(): Promise { + this.entropy = this.entropy ?? generateRandomAlphaNum(); + this.namespace = this.namespace ?? uuidv4(); + const id = `${ + this.global ? "@" : "" + }${generateUuid(this.entropy, this.namespace)}`; + if (!this.signer) { + return new W3ID(id); + } + if (!this.repository) + throw new Error( + "Repository is required, pass with `withRepository` method", + ); - if (!this.nextKeyHash) - throw new Error( - "NextKeyHash is required pass with `withNextKeyHash` method", - ); - const logs = new IDLogManager(this.repository, this.signer); - await logs.createLogEvent({ - id, - nextKeyHashes: [this.nextKeyHash], - }); - return new W3ID(id, logs); - } + if (!this.nextKeyHash) + throw new Error( + "NextKeyHash is required pass with `withNextKeyHash` method", + ); + const logs = new IDLogManager(this.repository, this.signer); + await logs.createLogEvent({ + id, + nextKeyHashes: [this.nextKeyHash], + }); + return new W3ID(id, logs); + } } export * from "./utils/jwt"; +export * from "./logs/storage/storage-spec"; diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 1c5c2b759..99a64c4df 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,22 +1,22 @@ 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 { - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - type RotationLogOptions, - type Signer, - type VerifierCallback, - isGenesisOptions, - isRotationOptions, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + type RotationLogOptions, + type Signer, + type VerifierCallback, + isGenesisOptions, + isRotationOptions, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -28,161 +28,185 @@ 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; - signer: Signer; - - constructor(repository: StorageSpec, signer: Signer) { - this.repository = repository; - this.signer = signer; - } - - /** - * Validate a chain of W3ID logs - * - * @param {LogEvent[]} log - * @param {VerifierCallback} verifyCallback - * @returns {Promise} - */ - - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ): Promise { - 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(); - } - - 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; - } - - /** - * Validate cryptographic signature on a single LogEvent - * - * @param {LogEvent} e - * @param {string[]} currentUpdateKeys - * @param {VerifierCallback} verifyCallback - * @returns {Promise} - */ - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ): Promise { - 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(); - } - - /** - * Append a new log entry for a W3ID - * - * @param {LogEvent[]} entries - * @param {RotationLogOptions} options - * @returns Promise - */ - private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { - const { 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 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 this.signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - - await this.repository.create(logEvent); - this.signer = nextKeySigner; - return logEvent; - } - - /** - * Create genesis entry for a W3ID log - * - * @param {GenesisLogOptions} options - * @returns Promise - */ - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes } = options; - const idTag = id.includes("@") ? id.split("@")[1] : id; - const logEvent: LogEvent = { - id, - versionId: `0-${idTag}`, - versionTime: new Date(Date.now()), - updateKeys: [this.signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await this.signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } - - /** - * Create a log event and save it to the repository - * - * @param {CreateLogEventOptions} options - * @returns Promise - */ - async createLogEvent(options: CreateLogEventOptions): Promise { - 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); - } + repository: StorageSpec; + signer: Signer; + + constructor(repository: StorageSpec, signer: Signer) { + this.repository = repository; + this.signer = signer; + } + + /** + * Validate a chain of W3ID logs + * + * @param {LogEvent[]} log + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ): Promise { + 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(); + } + + 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; + } + + /** + * Validate cryptographic signature on a single LogEvent + * + * @param {LogEvent} e + * @param {string[]} currentUpdateKeys + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ): Promise { + const proofs = e.proofs; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proofs; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proofs) + throw new BadSignatureError("No proof found in the log event."); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proofs, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } + + /** + * Append a new log entry for a W3ID + * + * @param {LogEvent[]} entries + * @param {RotationLogOptions} options + * @returns Promise + */ + private async appendEntry( + entries: LogEvent[], + options: RotationLogOptions, + ) { + const { 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 logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + + const signature = await this.signer.sign( + canonicalize(logEvent) as string, + ); + logEvent.proofs = [ + { + kid: `${logEvent.id}#0`, + alg: this.signer.alg, + signature, + }, + ]; + + await this.repository.create(logEvent); + this.signer = nextKeySigner; + return logEvent; + } + + /** + * Create genesis entry for a W3ID log + * + * @param {GenesisLogOptions} options + * @returns Promise + */ + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes } = options; + const idTag = id.includes("@") ? id.split("@")[1] : id; + const logEvent: LogEvent = { + id, + versionId: `0-${idTag}`, + versionTime: new Date(Date.now()), + updateKeys: [this.signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const signature = await this.signer.sign( + canonicalize(logEvent) as string, + ); + logEvent.proofs = [ + { + kid: `${id}#0`, + alg: this.signer.alg, + signature, + }, + ]; + + await this.repository.create(logEvent); + return logEvent; + } + + /** + * Create a log event and save it to the repository + * + * @param {CreateLogEventOptions} options + * @returns Promise + */ + async createLogEvent(options: CreateLogEventOptions): Promise { + 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/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index 3f5b871fc..a40f3eb49 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,61 +1,67 @@ +export type Proof = { + kid: string; + signature: string; + alg: string; +}; + 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}`; + proofs?: Proof[]; }; export type VerifierCallback = ( - message: string, - signature: string, - pubKey: string, + message: string, + proofs: Proof[], + pubKey: string, ) => Promise; export type JWTHeader = { - alg: string; - typ: "JWT"; - kid?: string; + alg: string; + typ: "JWT"; + kid?: string; }; export type JWTPayload = { - [key: string]: unknown; - iat?: number; - exp?: number; - nbf?: number; - iss?: string; - sub?: string; - aud?: string; - jti?: string; + [key: string]: unknown; + iat?: number; + exp?: number; + nbf?: number; + iss?: string; + sub?: string; + aud?: string; + jti?: string; }; export type Signer = { - sign: (message: string) => Promise | string; - pubKey: string; - alg: string; + sign: (message: string) => Promise | string; + pubKey: string; + alg: string; }; export type RotationLogOptions = { - nextKeyHashes: string[]; - nextKeySigner: Signer; + nextKeyHashes: string[]; + nextKeySigner: Signer; }; export type GenesisLogOptions = { - nextKeyHashes: string[]; - id: string; + nextKeyHashes: string[]; + id: string; }; export function isGenesisOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is GenesisLogOptions { - return "id" in options; + return "id" in options; } export function isRotationOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is RotationLogOptions { - return "nextKeySigner" in options; + return "nextKeySigner" in options; } export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; diff --git a/infrastructure/w3id/tests/utils/crypto.ts b/infrastructure/w3id/tests/utils/crypto.ts index bd4f249b1..9a84413a8 100644 --- a/infrastructure/w3id/tests/utils/crypto.ts +++ b/infrastructure/w3id/tests/utils/crypto.ts @@ -1,5 +1,5 @@ import { base58btc } from "multiformats/bases/base58"; -import { Signer, VerifierCallback } from "../../src/logs/log.types"; +import { Proof, Signer, VerifierCallback } from "../../src/logs/log.types"; import { hexToUint8Array, stringToUint8Array, @@ -9,17 +9,21 @@ import nacl from "tweetnacl"; export const verifierCallback: VerifierCallback = async ( message: string, - signature: string, + proofs: Proof[], pubKey: string, ) => { - const signatureBuffer = base58btc.decode(signature); - const messageBuffer = stringToUint8Array(message); - const publicKey = hexToUint8Array(pubKey); - const isValid = nacl.sign.detached.verify( - messageBuffer, - signatureBuffer, - publicKey, - ); + let isValid = true; + for (const proof of proofs) { + const signatureBuffer = base58btc.decode(proof.signature); + const messageBuffer = stringToUint8Array(message); + const publicKey = hexToUint8Array(pubKey); + const valid = nacl.sign.detached.verify( + messageBuffer, + signatureBuffer, + publicKey, + ); + if (!valid) isValid = false; + } return isValid; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 096c52c77..5c7ff545b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,18 @@ importers: infrastructure/evault-core: dependencies: + '@fastify/swagger': + specifier: ^8.14.0 + version: 8.15.0 + '@fastify/swagger-ui': + specifier: ^3.0.0 + version: 3.1.0 '@testcontainers/neo4j': specifier: ^10.24.2 version: 10.24.2 + fastify: + specifier: ^4.26.2 + version: 4.29.0 graphql: specifier: ^16.10.0 version: 16.10.0 @@ -205,6 +214,9 @@ importers: express: specifier: ^4.18.2 version: 4.21.2 + jose: + specifier: ^5.2.2 + version: 5.10.0 w3id: specifier: workspace:* version: link:../w3id @@ -984,6 +996,10 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/accept-negotiator@1.1.0': + resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} + engines: {node: '>=14'} + '@fastify/ajv-compiler@3.6.0': resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} @@ -1006,6 +1022,18 @@ packages: '@fastify/merge-json-schemas@0.1.1': resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + '@fastify/send@2.1.0': + resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} + + '@fastify/static@7.0.4': + resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} + + '@fastify/swagger-ui@3.1.0': + resolution: {integrity: sha512-68jm6k8VzvHXkEBT4Dakm/kkzUlPO4POIi0agWJSWxsYichPBqzjo+IpfqPl4pSJR1zCToQhEOo+cv+yJL2qew==} + + '@fastify/swagger@8.15.0': + resolution: {integrity: sha512-zy+HEEKFqPMS2sFUsQU5X0MHplhKJvWeohBwTCkBAJA/GDYGLGUWQaETEhptiqxK7Hs0fQB9B4MDb3pbwIiCwA==} + '@floating-ui/core@1.6.9': resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} @@ -4023,6 +4051,10 @@ packages: json-schema-ref-resolver@1.0.1: resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-resolver@2.0.0: + resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==} + engines: {node: '>=10'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4263,6 +4295,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -4461,6 +4498,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -6437,6 +6477,8 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 + '@fastify/accept-negotiator@1.1.0': {} + '@fastify/ajv-compiler@3.6.0': dependencies: ajv: 8.17.1 @@ -6465,6 +6507,41 @@ snapshots: dependencies: fast-deep-equal: 3.1.3 + '@fastify/send@2.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.0 + mime: 3.0.0 + + '@fastify/static@7.0.4': + dependencies: + '@fastify/accept-negotiator': 1.1.0 + '@fastify/send': 2.1.0 + content-disposition: 0.5.4 + fastify-plugin: 4.5.1 + fastq: 1.19.1 + glob: 10.4.5 + + '@fastify/swagger-ui@3.1.0': + dependencies: + '@fastify/static': 7.0.4 + fastify-plugin: 4.5.1 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.7.1 + + '@fastify/swagger@8.15.0': + dependencies: + fastify-plugin: 4.5.1 + json-schema-resolver: 2.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.7.1 + transitivePeerDependencies: + - supports-color + '@floating-ui/core@1.6.9': dependencies: '@floating-ui/utils': 0.2.9 @@ -10319,6 +10396,14 @@ snapshots: dependencies: fast-deep-equal: 3.1.3 + json-schema-resolver@2.0.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + rfdc: 1.4.1 + uri-js: 4.4.1 + transitivePeerDependencies: + - supports-color + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -10518,6 +10603,8 @@ snapshots: mime@1.6.0: {} + mime@3.0.0: {} + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -10775,6 +10862,8 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openapi-types@12.1.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 From da302fbf8650d6d3a99ab706a5a6a92bc605b5f4 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 22 Apr 2025 00:48:11 +0530 Subject: [PATCH 2/7] chore: move exports --- infrastructure/evault-core/src/w3id/w3id.ts | 16 ++ .../src/templates/evault.nomad.ts | 1 + infrastructure/w3id/src/index.ts | 145 +--------------- infrastructure/w3id/src/w3id.ts | 158 ++++++++++++++++++ 4 files changed, 179 insertions(+), 141 deletions(-) create mode 100644 infrastructure/evault-core/src/w3id/w3id.ts create mode 100644 infrastructure/w3id/src/w3id.ts diff --git a/infrastructure/evault-core/src/w3id/w3id.ts b/infrastructure/evault-core/src/w3id/w3id.ts new file mode 100644 index 000000000..1e4bfab76 --- /dev/null +++ b/infrastructure/evault-core/src/w3id/w3id.ts @@ -0,0 +1,16 @@ +import { W3ID as W3IDClass, W3IDBuilder } from "w3id"; + +export class W3ID { + private static instance: W3IDClass; + + private constructor() {} + + static async get(options?: { id: string }) { + if (W3ID.instance) return W3ID.instance; + if (!options) + throw new Error( + "No instance of W3ID exists yet, please create it by passing options", + ); + W3ID.instance = await new W3IDBuilder().build(); + } +} diff --git a/infrastructure/evault-provisioner/src/templates/evault.nomad.ts b/infrastructure/evault-provisioner/src/templates/evault.nomad.ts index e36e03e35..126757a16 100644 --- a/infrastructure/evault-provisioner/src/templates/evault.nomad.ts +++ b/infrastructure/evault-provisioner/src/templates/evault.nomad.ts @@ -78,6 +78,7 @@ export function generateNomadJob(w3id: string, eVaultId: string) { NEO4J_USER: neo4jUser, NEO4J_PASSWORD: neo4jPassword, PORT: "${NOMAD_PORT_http}", + W3ID: w3id, }, Resources: { CPU: 300, diff --git a/infrastructure/w3id/src/index.ts b/infrastructure/w3id/src/index.ts index 0de776afe..2c4850aa9 100644 --- a/infrastructure/w3id/src/index.ts +++ b/infrastructure/w3id/src/index.ts @@ -1,143 +1,6 @@ -import { v4 as uuidv4 } from "uuid"; -import { IDLogManager } from "./logs/log-manager"; -import type { JWTHeader, JWTPayload, LogEvent, Signer } from "./logs/log.types"; -import type { StorageSpec } from "./logs/storage/storage-spec"; -import { signJWT } from "./utils/jwt"; -import { generateRandomAlphaNum } from "./utils/rand"; -import { generateUuid } from "./utils/uuid"; - -export class W3ID { - constructor( - public id: string, - public logs?: IDLogManager, - ) {} - - /** - * Signs a JWT with the W3ID's signer - * @param payload - The JWT payload - * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) - * @returns The signed JWT - */ - public async signJWT( - payload: JWTPayload, - header?: JWTHeader, - ): Promise { - if (!this.logs?.signer) { - throw new Error("W3ID must have a signer to sign JWTs"); - } - return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); - } -} - -export class W3IDBuilder { - private signer?: Signer; - private repository?: StorageSpec; - private entropy?: string; - private namespace?: string; - private nextKeyHash?: string; - private global?: boolean = false; - - /** - * Specify entropy to create the identity with - * - * @param {string} str - */ - public withEntropy(str: string): W3IDBuilder { - this.entropy = str; - return this; - } - - /** - * Specify namespace to use to generate the UUIDv5 - * - * @param {string} uuid - */ - public withNamespace(uuid: string): W3IDBuilder { - this.namespace = uuid; - return this; - } - - /** - * Specify whether to create a global identifier or a local identifer - * - * According to the project specification there are supposed to be 2 main types of - * W3ID's ones which are tied to more permanent entities - * - * A global identifer is expected to live at the registry and starts with an \`@\` - * - * @param {boolean} isGlobal - */ - public withGlobal(isGlobal: boolean): W3IDBuilder { - this.global = isGlobal; - return this; - } - - /** - * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a - * repository in which the logs would be stored - * - * @param {StorageSpec} storage - */ - public withRepository( - storage: StorageSpec, - ): W3IDBuilder { - this.repository = storage; - return this; - } - - /** - * Attach a keypair to the W3ID, a key attached W3ID would also need a repository - * to be added. - * - * @param {Signer} signer - */ - public withSigner(signer: Signer): W3IDBuilder { - this.signer = signer; - return this; - } - - /** - * Specify the SHA256 hash of the next key which will sign the next log entry after - * rotation of keys - * - * @param {string} hash - */ - public withNextKeyHash(hash: string): W3IDBuilder { - this.nextKeyHash = hash; - return this; - } - - /** - * Build the W3ID with provided builder options - * - * @returns Promise - */ - public async build(): Promise { - this.entropy = this.entropy ?? generateRandomAlphaNum(); - this.namespace = this.namespace ?? uuidv4(); - const id = `${ - this.global ? "@" : "" - }${generateUuid(this.entropy, this.namespace)}`; - if (!this.signer) { - return new W3ID(id); - } - if (!this.repository) - throw new Error( - "Repository is required, pass with `withRepository` method", - ); - - if (!this.nextKeyHash) - throw new Error( - "NextKeyHash is required pass with `withNextKeyHash` method", - ); - const logs = new IDLogManager(this.repository, this.signer); - await logs.createLogEvent({ - id, - nextKeyHashes: [this.nextKeyHash], - }); - return new W3ID(id, logs); - } -} - +export * from "./w3id"; export * from "./utils/jwt"; export * from "./logs/storage/storage-spec"; +export * from "./logs/log.types"; +export * from "./logs/log-manager"; +export * from "./utils/hash"; diff --git a/infrastructure/w3id/src/w3id.ts b/infrastructure/w3id/src/w3id.ts new file mode 100644 index 000000000..9352ecc57 --- /dev/null +++ b/infrastructure/w3id/src/w3id.ts @@ -0,0 +1,158 @@ +import { v4 as uuidv4 } from "uuid"; +import { IDLogManager } from "./logs/log-manager"; +import type { JWTHeader, JWTPayload, LogEvent, Signer } from "./logs/log.types"; +import type { StorageSpec } from "./logs/storage/storage-spec"; +import { signJWT } from "./utils/jwt"; +import { generateRandomAlphaNum } from "./utils/rand"; +import { generateUuid } from "./utils/uuid"; + +export class W3ID { + constructor( + public id: string, + public logs?: IDLogManager, + ) {} + + /** + * Signs a JWT with the W3ID's signer + * @param payload - The JWT payload + * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) + * @returns The signed JWT + */ + public async signJWT( + payload: JWTPayload, + header?: JWTHeader, + ): Promise { + if (!this.logs?.signer) { + throw new Error("W3ID must have a signer to sign JWTs"); + } + return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); + } +} + +export class W3IDBuilder { + private signer?: Signer; + private repository?: StorageSpec; + private entropy?: string; + private namespace?: string; + private nextKeyHash?: string; + private global?: boolean = false; + private id?: string; + + /** + * Specify entropy to create the identity with + * + * @param {string} str + */ + public withEntropy(str: string): W3IDBuilder { + this.entropy = str; + return this; + } + + /** + * Specify namespace to use to generate the UUIDv5 + * + * @param {string} uuid + */ + public withNamespace(uuid: string): W3IDBuilder { + this.namespace = uuid; + return this; + } + + /** + * Specify whether to create a global identifier or a local identifer + * + * According to the project specification there are supposed to be 2 main types of + * W3ID's ones which are tied to more permanent entities + * + * A global identifer is expected to live at the registry and starts with an \`@\` + * + * @param {boolean} isGlobal + */ + public withGlobal(isGlobal: boolean): W3IDBuilder { + this.global = isGlobal; + return this; + } + + /** + * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a + * repository in which the logs would be stored + * + * @param {StorageSpec} storage + */ + public withRepository( + storage: StorageSpec, + ): W3IDBuilder { + this.repository = storage; + return this; + } + + /** + * Pre-specify a UUID to use as the W3ID + * + * @param {string} id + */ + public withId(id: string): W3IDBuilder { + this.id = id; + return this; + } + + /** + * Attach a keypair to the W3ID, a key attached W3ID would also need a repository + * to be added. + * + * @param {Signer} signer + */ + public withSigner(signer: Signer): W3IDBuilder { + this.signer = signer; + return this; + } + + /** + * Specify the SHA256 hash of the next key which will sign the next log entry after + * rotation of keys + * + * @param {string} hash + */ + public withNextKeyHash(hash: string): W3IDBuilder { + this.nextKeyHash = hash; + return this; + } + + /** + * Build the W3ID with provided builder options + * + * @returns Promise + */ + public async build(): Promise { + if ((this.id && this.namespace) || (this.id && this.entropy)) + throw new Error( + "Namespace and Entropy can't be specified when using pre-defined ID", + ); + this.entropy = this.entropy ?? generateRandomAlphaNum(); + this.namespace = this.namespace ?? uuidv4(); + this.id = + this.id && this.id?.includes("@") ? this.id.split("@")[1] : this.id; + const id = `${ + this.global ? "@" : "" + }${this.id ?? generateUuid(this.entropy, this.namespace)}`; + if (!this.signer) { + return new W3ID(id); + } + if (!this.repository) + throw new Error( + "Repository is required, pass with `withRepository` method", + ); +, + if (!this.nextKeyHash) + throw new Error( + "NextKeyHash is required pass with `withNextKeyHash` method", + ); + const logs = new IDLogManager(this.repository, this.signer); + await logs.createLogEvent({ + id, + nextKeyHashes: [this.nextKeyHash], + }); + return new W3ID(id, logs); + } +} + From 838ffc2f1616f91f4fe07027ac839f7605a5cb03 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 22 Apr 2025 15:20:15 +0530 Subject: [PATCH 3/7] chore: docs --- "\\" | 132 ++++++++ infrastructure/evault-core/README.md | 182 +++++++++++ .../evault-core/docs/w3id-integration.md | 303 ++++++++++++++++++ .../evault-provisioner/src/index.ts | 4 +- .../src/templates/evault.nomad.ts | 39 ++- infrastructure/w3id/README.md | 132 +++++++- 6 files changed, 780 insertions(+), 12 deletions(-) create mode 100644 "\\" create mode 100644 infrastructure/evault-core/README.md create mode 100644 infrastructure/evault-core/docs/w3id-integration.md diff --git "a/\\" "b/\\" new file mode 100644 index 000000000..34cc6b975 --- /dev/null +++ "b/\\" @@ -0,0 +1,132 @@ +export function generatePassword(length = 16): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + const charsLength = chars.length; + const randomValues = new Uint32Array(length); + + crypto.getRandomValues(randomValues); + + for (let i = 0; i < length; i++) { + result += chars.charAt(randomValues[i] % charsLength); + } + + return result; +} + +export function generateNomadJob(w3id: string, eVaultId: string) { + const neo4jUser = "neo4j"; + const neo4jPassword = generatePassword(24); + + return { + Job: { + ID: `evault-${w3id}`, + Name: `evault-${w3id}`, + Type: "service", + Datacenters: ["dc1"], + TaskGroups: [ + { + Name: "evault", + Networks: [ + { + Mode: "bridge", + DynamicPorts: [ + { + Label: "http", + }, + ], + }, + ], + , +"Volumes": { + "evault-store": { + "Type": "csi", + "Source": "evault-store-abc123", + "ReadOnly": false, + "AccessMode": "single-node-writer", + "AttachmentMode": "file-system", + "Sticky": true + }, + "neo4j-data": { + "Type": "csi", + "Source": "neo4j-data-abc123", + "ReadOnly": false, + "AccessMode": "single-node-writer", + "AttachmentMode": "file-system", + "Sticky": true + } +} + Services: [ + { + Name: `evault`, + PortLabel: "http", + Tags: ["internal"], + Meta: { + whois: w3id, + id: eVaultId, + }, + }, + ], + Tasks: [ + { + Name: "neo4j", + Driver: "docker", + Config: { + image: "neo4j:5.15", + ports: [], + volume_mounts: [ + { + Volume: "neo4j-data", + Destination: "/data", + ReadOnly: false, + }, + ], + }, + Env: { + NEO4J_AUTH: `${neo4jUser}/${neo4jPassword}`, + "dbms.connector.bolt.listen_address": + "0.0.0.0:7687", + }, + Resources: { + CPU: 300, + MemoryMB: 2048, + }, + }, + { + Name: "evault", + Driver: "docker", + Config: { + image: "merulauvo/evault:latest", + ports: ["http"], + volume_mounts: [ + { + Volume: "evault-store", + Destination: "/evault/data", + ReadOnly: false, + }, + ], + }, + Env: { + NEO4J_URI: "bolt://localhost:7687", + NEO4J_USER: neo4jUser, + NEO4J_PASSWORD: neo4jPassword, + PORT: "${NOMAD_PORT_http}", + W3ID: w3id, + }, + Resources: { + CPU: 300, + MemoryMB: 512, + }, + DependsOn: [ + { + Name: "neo4j", + Condition: "running", + }, + ], + }, + ], + }, + ], + }, + }; +} diff --git a/infrastructure/evault-core/README.md b/infrastructure/evault-core/README.md new file mode 100644 index 000000000..64da9eade --- /dev/null +++ b/infrastructure/evault-core/README.md @@ -0,0 +1,182 @@ +# eVault Core + +eVault is a secure, distributed data storage and access system designed for the MetaState ecosystem. It provides a robust framework for storing, managing, and accessing structured data with fine-grained access control and GraphQL-based querying capabilities. + +## Overview + +eVault is a core component of the MetaState infrastructure that enables: + +- Secure storage of structured data +- Fine-grained access control using W3ID +- GraphQL-based data querying and manipulation +- Distributed data management +- Integration with the MetaState ecosystem + +## Architecture + +### Core Components + +1. **GraphQL Server** + + - Provides a flexible API for data operations + - Supports complex queries and mutations + - Includes built-in documentation and visualization tools + +2. **Access Control System** + + - W3ID-based authentication + - Fine-grained access control lists (ACL) + - Secure token-based authentication + +3. **Data Storage** + + - Neo4j-based storage backend + - Structured data model with envelopes + - Support for multiple data types and ontologies + +4. **HTTP Server** + - Fastify-based web server + - RESTful endpoints for basic operations + - GraphQL endpoint for advanced operations + +### Data Model + +The eVault system uses a hierarchical data model: + +- **MetaEnvelope**: Top-level container for related data + + - Contains multiple Envelopes + - Has an associated ontology + - Includes access control information + +- **Envelope**: Individual data container + - Contains structured data + - Has a specific value type + - Linked to a MetaEnvelope + +## Features + +### 1. Data Management + +- Store and retrieve structured data +- Update and delete data with version control +- Search and filter data by ontology and content + +### 2. Access Control + +- W3ID-based authentication +- Fine-grained access control lists +- Secure token-based operations + +### 3. Query Capabilities + +- GraphQL-based querying +- Complex search operations +- Real-time data access + +### 4. Integration + +- Seamless integration with W3ID +- Support for multiple data formats +- Extensible architecture + +## API Documentation + +### GraphQL Operations + +#### Queries + +- `getMetaEnvelopeById`: Retrieve a specific MetaEnvelope +- `findMetaEnvelopesByOntology`: Find envelopes by ontology +- `searchMetaEnvelopes`: Search envelopes by content +- `getAllEnvelopes`: List all available envelopes + +#### Mutations + +- `storeMetaEnvelope`: Create a new MetaEnvelope +- `deleteMetaEnvelope`: Remove a MetaEnvelope +- `updateEnvelopeValue`: Update envelope content + +### HTTP Endpoints + +- `/graphql`: GraphQL API endpoint +- `/voyager`: GraphQL schema visualization +- `/documentation`: API documentation + +## Getting Started + +### Prerequisites + +- Node.js +- Neo4j database +- W3ID system + +### Installation + +1. Clone the repository +2. Install dependencies: + ```bash + npm install + ``` +3. Configure environment variables: + ``` + NEO4J_URI=bolt://localhost:7687 + NEO4J_USER=neo4j + NEO4J_PASSWORD=your_password + PORT=4000 + ``` +4. Start the server: + ```bash + npm start + ``` + +## Security Considerations + +- All operations require W3ID authentication +- Access control is enforced at both API and database levels +- Data is encrypted in transit and at rest +- Regular security audits and updates + +## Integration Guide + +### W3ID Integration + +eVault uses W3ID for authentication and access control: + +1. Obtain a W3ID token +2. Include token in Authorization header +3. Access eVault resources based on permissions + +### Data Storage + +1. Define data ontology +2. Create MetaEnvelope with appropriate ACL +3. Store and manage data through the API + +## Development + +### Testing + +```bash +npm test +``` + +### Documentation + +- API documentation available at `/documentation` +- GraphQL schema visualization at `/voyager` +- Example queries in `src/protocol/examples` + +## Contributing + +1. Fork the repository +2. Create feature branch +3. Submit pull request + +## License + +[License information] + +## Support + +[Support information] diff --git a/infrastructure/evault-core/docs/w3id-integration.md b/infrastructure/evault-core/docs/w3id-integration.md new file mode 100644 index 000000000..0543afe6d --- /dev/null +++ b/infrastructure/evault-core/docs/w3id-integration.md @@ -0,0 +1,303 @@ +# W3ID Integration Documentation + +## Overview + +The eVault Core system integrates with W3ID (Web3 Identity) to provide decentralized identity verification and signature capabilities. This document outlines the technical implementation and functional aspects of the W3ID integration. + +## Technical Architecture + +### Components + +1. **W3ID Client** + + - Uses the `w3id` package for identity verification + - Handles JWT token validation and signature verification + - Manages identity claims and verification status + +2. **HTTP Endpoints** + + - Fastify-based REST API + - Swagger documentation available at `/docs` + - GraphQL integration for complex queries + +3. **Signature System** + - Decentralized signature verification + - Log-based signature tracking + - Multi-party signature support + +## API Endpoints + +### Identity Verification + +#### GET /whois + +Returns W3ID identity information and associated logs. + +**Request:** + +```http +GET /whois +Authorization: Bearer +``` + +**Response:** + +```json +{ + "w3id": { + "did": "did:example:123", + "verificationStatus": "verified", + "claims": { + "name": "John Doe", + "email": "john@example.com" + } + }, + "logs": [ + { + "timestamp": "2024-03-20T12:00:00Z", + "action": "identity_verification", + "status": "success" + } + ] +} +``` + +### Signature Management + +#### POST /watchers/sign + +Submit a signature for a specific log entry. + +**Request:** + +```http +POST /watchers/sign +Authorization: Bearer +Content-Type: application/json + +{ + "w3id": "did:example:123", + "signature": "0x1234...", + "logEntryId": "log_123" +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Signature stored successfully" +} +``` + +#### POST /watchers/request + +Request a signature for a log entry. + +**Request:** + +```http +POST /watchers/request +Authorization: Bearer +Content-Type: application/json + +{ + "w3id": "did:example:123", + "logEntryId": "log_123" +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Signature request created", + "requestId": "req_1234567890" +} +``` + +## Functional Documentation + +### Identity Verification Flow + +1. **Initial Verification** + + - User presents W3ID JWT token + - System validates token and extracts identity claims + - Identity status is logged in the system + +2. **Signature Request Process** + + - User requests signature for a log entry + - System verifies user's identity and permissions + - Signature request is created and tracked + +3. **Signature Submission** + - User submits signature for requested log entry + - System validates signature against W3ID + - Signature is recorded in the log + +### Security Considerations + +1. **Token Validation** + + - All endpoints require valid W3ID JWT tokens + - Token expiration is enforced + - Token claims are verified against system requirements + +2. **Signature Security** + + - Signatures are cryptographically verified + - Each signature is tied to a specific identity + - Signature requests are tracked and validated + +3. **Log Integrity** + - All actions are logged with timestamps + - Log entries are immutable once signed + - Multi-party verification is supported + +## Integration Guide + +### Prerequisites + +1. W3ID JWT token generation +2. Access to the eVault Core system +3. Proper permissions for signature operations + +### Implementation Steps + +1. **Identity Setup** + + ```typescript + import { W3ID } from "w3id"; + + const w3id = new W3ID({ + // Configuration options + }); + ``` + +2. **Token Generation** + + ```typescript + const token = await w3id.generateToken({ + claims: { + // Identity claims + }, + }); + ``` + +3. **API Integration** + ```typescript + // Example API call with W3ID token + const response = await fetch("/whois", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + ``` + +## Error Handling + +### Common Error Responses + +1. **Invalid Token** + + ```json + { + "error": "invalid_token", + "message": "Invalid or expired W3ID token" + } + ``` + +2. **Invalid Signature** + + ```json + { + "error": "invalid_signature", + "message": "Signature verification failed" + } + ``` + +3. **Permission Denied** + ```json + { + "error": "permission_denied", + "message": "Insufficient permissions for operation" + } + ``` + +## Monitoring and Logging + +### Log Structure + +```typescript +interface LogEntry { + timestamp: string; + action: + | "identity_verification" + | "signature_request" + | "signature_submission"; + status: "success" | "failure"; + details: { + w3id: string; + logEntryId?: string; + signature?: string; + error?: string; + }; +} +``` + +### Monitoring Endpoints + +1. **Identity Status** + + - Track verification attempts + - Monitor token usage + - Audit identity changes + +2. **Signature Tracking** + - Monitor signature requests + - Track signature submissions + - Audit signature verification + +## Best Practices + +1. **Token Management** + + - Rotate tokens regularly + - Use appropriate token scopes + - Implement proper token storage + +2. **Signature Handling** + + - Validate signatures immediately + - Maintain signature audit trail + - Implement proper error handling + +3. **Security** + - Use HTTPS for all communications + - Implement rate limiting + - Monitor for suspicious activity + +## Troubleshooting + +### Common Issues + +1. **Token Validation Failures** + + - Check token expiration + - Verify token claims + - Ensure proper token format + +2. **Signature Verification Issues** + + - Verify signature format + - Check identity permissions + - Validate log entry existence + +3. **API Integration Problems** + - Verify endpoint URLs + - Check request headers + - Validate response format diff --git a/infrastructure/evault-provisioner/src/index.ts b/infrastructure/evault-provisioner/src/index.ts index 11857cb85..dd8a08fdf 100644 --- a/infrastructure/evault-provisioner/src/index.ts +++ b/infrastructure/evault-provisioner/src/index.ts @@ -53,17 +53,15 @@ app.post( const jwksResponse = await axios.get( `http://localhost:4321/.well-known/jwks.json`, ); - console.log(jwksResponse.data); const JWKS = jose.createLocalJWKSet(jwksResponse.data); const { payload } = await jose.jwtVerify(registryEntropy, JWKS); - console.log(payload); - const evaultId = await new W3IDBuilder().withGlobal(true).build(); const userId = await new W3IDBuilder() .withNamespace(namespace) + .withEntropy(payload.entropy as string) .withGlobal(true) .build(); diff --git a/infrastructure/evault-provisioner/src/templates/evault.nomad.ts b/infrastructure/evault-provisioner/src/templates/evault.nomad.ts index 126757a16..de13f1fc5 100644 --- a/infrastructure/evault-provisioner/src/templates/evault.nomad.ts +++ b/infrastructure/evault-provisioner/src/templates/evault.nomad.ts @@ -37,6 +37,24 @@ export function generateNomadJob(w3id: string, eVaultId: string) { ], }, ], + Volumes: { + "evault-store": { + Type: "csi", + Source: `evault-store-${w3id}`, + ReadOnly: false, + AccessMode: "single-node-writer", + AttachmentMode: "file-system", + Sticky: true, + }, + "neo4j-data": { + Type: "csi", + Source: `neo4j-data-${w3id}`, + ReadOnly: false, + AccessMode: "single-node-writer", + AttachmentMode: "file-system", + Sticky: true, + }, + }, Services: [ { Name: `evault`, @@ -55,6 +73,13 @@ export function generateNomadJob(w3id: string, eVaultId: string) { Config: { image: "neo4j:5.15", ports: [], + volume_mounts: [ + { + Volume: "neo4j-data", + Destination: "/data", + ReadOnly: false, + }, + ], }, Env: { NEO4J_AUTH: `${neo4jUser}/${neo4jPassword}`, @@ -72,6 +97,13 @@ export function generateNomadJob(w3id: string, eVaultId: string) { Config: { image: "merulauvo/evault:latest", ports: ["http"], + volume_mounts: [ + { + Volume: "evault-store", + Destination: "/evault/data", + ReadOnly: false, + }, + ], }, Env: { NEO4J_URI: "bolt://localhost:7687", @@ -84,7 +116,12 @@ export function generateNomadJob(w3id: string, eVaultId: string) { CPU: 300, MemoryMB: 512, }, - DependsOn: ["neo4j"], + DependsOn: [ + { + Name: "neo4j", + Condition: "running", + }, + ], }, ], }, diff --git a/infrastructure/w3id/README.md b/infrastructure/w3id/README.md index 723c219cd..198d816ea 100644 --- a/infrastructure/w3id/README.md +++ b/infrastructure/w3id/README.md @@ -1,8 +1,124 @@ -# W3ID +# W3ID - Web 3 Identity System -The metastate ecosystem has the only 1 type of identifiers, W3ID (did:w3id protocol, defined below) for all types of entities, e.g.. +W3ID is a robust identity system designed for the MetaState ecosystem, providing persistent, globally unique identifiers for various entities. This system ensures secure identity management with support for key rotation, friend-based recovery, and seamless eVault migration. -![MetaState W3ID Relations](../../images/w3id-relations.png) +## Key Features + +### 1. Universal Identity Management + +- **Single Identifier System**: W3ID serves as the primary identifier for all entities in the MetaState ecosystem +- **Global and Local IDs**: Supports both global identifiers (starting with '@') and local identifiers +- **UUID-based**: Utilizes UUID v4/v5 for guaranteed uniqueness and persistence + +### 2. Security and Recovery + +- **Key Rotation Support**: Enables secure key rotation without changing the identity +- **Friend-Based Recovery**: Allows trusted friends to verify identity for recovery +- **Notary Integration**: Supports notary-based verification and recovery processes + +### 3. eVault Integration + +- **Persistent Storage**: Maintains identity across eVault migrations +- **Also-Known-As Records**: Tracks eVault migrations for seamless access +- **Device Management**: Supports device-specific identifiers + +### 4. Advanced Logging System + +- **Immutable Event Logging**: Maintains a tamper-proof record of all identity-related events +- **Key Rotation Tracking**: Logs all key changes and rotations +- **JWT Support**: Built-in JWT signing and verification capabilities +- **Storage Agnostic**: Supports various storage backends through the StorageSpec interface + +## Technical Implementation + +### W3ID Format + +The W3ID follows a simple yet powerful format: + +- Global IDs: `@` (e.g., `@e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a`) +- Local IDs: `@/` (e.g., `@e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a/f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7`) + +### Logging Mechanism + +The W3ID system implements a sophisticated logging mechanism through the `IDLogManager` class: + +1. **Event Logging** + + - All identity-related events are logged in an immutable format + - Each log entry is cryptographically signed + - Supports key rotation events and identity updates + +2. **Storage Integration** + + - Flexible storage backend support + - Implements the `StorageSpec` interface for custom storage solutions + - Ensures data persistence and availability + +3. **Security Features** + - JWT signing and verification + - Key rotation tracking + - Next key hash verification + +## Usage Examples + +### Creating a W3ID + +```typescript +const w3id = await new W3IDBuilder() + .withGlobal(true) + .withRepository(storage) + .withSigner(signer) + .withNextKeyHash(nextKeyHash) + .build(); +``` + +### Signing a JWT + +```typescript +const jwt = await w3id.signJWT({ + sub: "user123", + exp: Date.now() + 3600, +}); +``` + +## Technical Requirements + +- Globally persistent and unique identifiers +- Namespace range > 10^22 +- Support for key rotation +- Loose binding to physical documents +- UUID v4/v5 compliance + +## Additional Features + +### Friend-Based Recovery + +- Trust list management +- Notary integration +- Multi-party verification + +### eVault Migration + +- Also-known-as record tracking +- Seamless data access +- Migration history maintenance + +## Implementation Details + +The W3ID system is implemented in TypeScript and provides: + +- Strong typing for all operations +- Builder pattern for W3ID creation +- Flexible storage integration +- Comprehensive logging system + +## Security Considerations + +- Keys are loosely bound to identifiers +- Support for key rotation +- Cryptographic signing of all operations +- Immutable event logging +- Friend-based recovery mechanisms ## Where is it used @@ -39,12 +155,12 @@ UUID range is 2^122 or 15 orders larger than expected amount of IDs (10^22) ther ### Example: `@e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a` -If a local ID is needed, it is added after “/”, also as UUID range e.g.: +If a local ID is needed, it is added after "/", also as UUID range e.g.: `@e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a/f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7` -which means “the object `f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7` at the eVault `e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a`, +which means "the object `f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7` at the eVault `e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a`, where `e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a` could be: either the exact URL of the eVault, or -the URL of its “controller”, the owner, then such URL should be resolved to the current eVault which this person (or group) controls +the URL of its "controller", the owner, then such URL should be resolved to the current eVault which this person (or group) controls ## W3ID Key binding @@ -54,13 +170,13 @@ The Identifier would be loosely bound to a set of keys, meaning an identifier is #### Friend Based Recovery -2-3 Friends may verify the identity of a person to recover lost metastate ID on the eVault. This would be based on a trust list which a person can create while they do still have access to their keys. This trust list by default would have a list of all notaries in the ecosystem but the user may modify it and add people who always need to approve the action of changing keys at their end as well. +2-3 Friends may verify the identity of a person to recover lost metastate ID on the eVault. This would be based on a trust list which a person can create while they do still have access to their keys. This trust list by default would have a list of all notaries in the ecosystem but the user may modify it and add people who always need to approve the action of changing keys at their end as well. So for example a user "Jack" says I trust the notary but I also want "Bob" to approve the action each time a notary tries to change the keys which are controlled by Jack. #### Migration of eVault -In the implementation it must be ensured that the file is still accessible regardless of the eVault it is stored in. This can be done via recording also-known-as records in the register for an eVault each time someone migrates their eVault. For example if a user migrates evault `e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a` to `f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7`, the register will store an entry pointing all requests to `e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a` to be redirected to `f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7` +In the implementation it must be ensured that the file is still accessible regardless of the eVault it is stored in. This can be done via recording also-known-as records in the register for an eVault each time someone migrates their eVault. For example if a user migrates evault `e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a` to `f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7`, the register will store an entry pointing all requests to `e4d909c2-5d2f-4a7d-9473-b34b6c0f1a5a` to be redirected to `f2a6743e-8d5b-43bc-a9f0-1c7a3b9e90d7` ## W3ID Document Binding From 0605ea29431a1474eea304a5e27239280624b80d Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 22 Apr 2025 17:21:38 +0530 Subject: [PATCH 4/7] feat: `whois` endpoint --- "\\" | 132 ------- evault.docker-compose.yml | 6 + infrastructure/evault-core/package.json | 4 +- infrastructure/evault-core/src/evault.ts | 169 ++++---- infrastructure/evault-core/src/http/server.ts | 40 +- .../evault-core/src/secrets/secrets-store.ts | 126 ++++++ infrastructure/evault-core/src/utils/codec.ts | 86 ++++ .../evault-core/src/utils/signer.ts | 44 +++ .../evault-core/src/w3id/log-service.ts | 42 ++ .../evault-core/src/w3id/log-storage.ts | 98 +++++ infrastructure/evault-core/src/w3id/w3id.ts | 74 +++- .../evault-core/tests/log-storage.spec.ts | 85 ++++ infrastructure/evault-core/tsconfig.json | 43 +- infrastructure/w3id/src/logs/log-manager.ts | 370 +++++++++--------- infrastructure/w3id/src/w3id.ts | 267 +++++++------ pnpm-lock.yaml | 6 + 16 files changed, 1008 insertions(+), 584 deletions(-) delete mode 100644 "\\" create mode 100644 infrastructure/evault-core/src/secrets/secrets-store.ts create mode 100644 infrastructure/evault-core/src/utils/codec.ts create mode 100644 infrastructure/evault-core/src/utils/signer.ts create mode 100644 infrastructure/evault-core/src/w3id/log-service.ts create mode 100644 infrastructure/evault-core/src/w3id/log-storage.ts create mode 100644 infrastructure/evault-core/tests/log-storage.spec.ts diff --git "a/\\" "b/\\" deleted file mode 100644 index 34cc6b975..000000000 --- "a/\\" +++ /dev/null @@ -1,132 +0,0 @@ -export function generatePassword(length = 16): string { - const chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let result = ""; - const charsLength = chars.length; - const randomValues = new Uint32Array(length); - - crypto.getRandomValues(randomValues); - - for (let i = 0; i < length; i++) { - result += chars.charAt(randomValues[i] % charsLength); - } - - return result; -} - -export function generateNomadJob(w3id: string, eVaultId: string) { - const neo4jUser = "neo4j"; - const neo4jPassword = generatePassword(24); - - return { - Job: { - ID: `evault-${w3id}`, - Name: `evault-${w3id}`, - Type: "service", - Datacenters: ["dc1"], - TaskGroups: [ - { - Name: "evault", - Networks: [ - { - Mode: "bridge", - DynamicPorts: [ - { - Label: "http", - }, - ], - }, - ], - , -"Volumes": { - "evault-store": { - "Type": "csi", - "Source": "evault-store-abc123", - "ReadOnly": false, - "AccessMode": "single-node-writer", - "AttachmentMode": "file-system", - "Sticky": true - }, - "neo4j-data": { - "Type": "csi", - "Source": "neo4j-data-abc123", - "ReadOnly": false, - "AccessMode": "single-node-writer", - "AttachmentMode": "file-system", - "Sticky": true - } -} - Services: [ - { - Name: `evault`, - PortLabel: "http", - Tags: ["internal"], - Meta: { - whois: w3id, - id: eVaultId, - }, - }, - ], - Tasks: [ - { - Name: "neo4j", - Driver: "docker", - Config: { - image: "neo4j:5.15", - ports: [], - volume_mounts: [ - { - Volume: "neo4j-data", - Destination: "/data", - ReadOnly: false, - }, - ], - }, - Env: { - NEO4J_AUTH: `${neo4jUser}/${neo4jPassword}`, - "dbms.connector.bolt.listen_address": - "0.0.0.0:7687", - }, - Resources: { - CPU: 300, - MemoryMB: 2048, - }, - }, - { - Name: "evault", - Driver: "docker", - Config: { - image: "merulauvo/evault:latest", - ports: ["http"], - volume_mounts: [ - { - Volume: "evault-store", - Destination: "/evault/data", - ReadOnly: false, - }, - ], - }, - Env: { - NEO4J_URI: "bolt://localhost:7687", - NEO4J_USER: neo4jUser, - NEO4J_PASSWORD: neo4jPassword, - PORT: "${NOMAD_PORT_http}", - W3ID: w3id, - }, - Resources: { - CPU: 300, - MemoryMB: 512, - }, - DependsOn: [ - { - Name: "neo4j", - Condition: "running", - }, - ], - }, - ], - }, - ], - }, - }; -} diff --git a/evault.docker-compose.yml b/evault.docker-compose.yml index 2228175be..adc8b9c55 100644 --- a/evault.docker-compose.yml +++ b/evault.docker-compose.yml @@ -11,6 +11,11 @@ services: - NEO4J_URI=${NEO4J_URI} - NEO4J_USER=${NEO4J_USER} - NEO4J_PASSWORD=${NEO4J_PASSWORD} + - SECRETS_STORE_PATH=/app/secrets/secrets.json + - ENCRYPTION_PASSWORD=${ENCRYPTION_PASSWORD} + - W3ID=${W3ID} + volumes: + - secrets:/app/secrets networks: - graphnet depends_on: @@ -44,6 +49,7 @@ services: volumes: neo4j_data: neo4j_logs: + secrets: networks: graphnet: diff --git a/infrastructure/evault-core/package.json b/infrastructure/evault-core/package.json index 580695563..15ff1abbf 100644 --- a/infrastructure/evault-core/package.json +++ b/infrastructure/evault-core/package.json @@ -24,16 +24,18 @@ "vitest": "^3.0.9" }, "dependencies": { - "@testcontainers/neo4j": "^10.24.2", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^3.0.0", + "@testcontainers/neo4j": "^10.24.2", "fastify": "^4.26.2", "graphql": "^16.10.0", "graphql-type-json": "^0.3.2", "graphql-voyager": "^2.1.0", "graphql-yoga": "^5.13.4", "json-schema": "^0.4.0", + "multiformats": "^13.3.2", "neo4j-driver": "^5.28.1", + "tweetnacl": "^1.0.3", "w3id": "workspace:*" } } diff --git a/infrastructure/evault-core/src/evault.ts b/infrastructure/evault-core/src/evault.ts index 815deda4b..402f0d1bc 100644 --- a/infrastructure/evault-core/src/evault.ts +++ b/infrastructure/evault-core/src/evault.ts @@ -1,106 +1,109 @@ import { DbService } from "./db/db.service"; +import { LogService } from "./w3id/log-service"; import { GraphQLServer } from "./protocol/graphql-server"; import { registerHttpRoutes } from "./http/server"; import fastify, { - FastifyInstance, - FastifyRequest, - FastifyReply, + FastifyInstance, + FastifyRequest, + FastifyReply, } from "fastify"; import { renderVoyagerPage } from "graphql-voyager/middleware"; import { createYoga } from "graphql-yoga"; import dotenv from "dotenv"; import path from "path"; -import neo4j from "neo4j-driver"; +import neo4j, { Driver } from "neo4j-driver"; +import { W3ID } from "./w3id/w3id"; dotenv.config({ path: path.resolve(__dirname, "../../../.env") }); class EVault { - server: FastifyInstance; - graphqlServer: GraphQLServer; - - constructor() { - const uri = process.env.NEO4J_URI || "bolt://localhost:7687"; - const user = process.env.NEO4J_USER || "neo4j"; - const password = process.env.NEO4J_PASSWORD || "neo4j"; - - if ( - !process.env.NEO4J_URI || - !process.env.NEO4J_USER || - !process.env.NEO4J_PASSWORD - ) { - console.warn( - "Using default Neo4j connection parameters. Set NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD environment variables for custom configuration.", - ); - } - - const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)); - const dbService = new DbService(driver); - this.graphqlServer = new GraphQLServer(dbService); - - // Create Fastify server - this.server = fastify({ - logger: true, - }); + server: FastifyInstance; + graphqlServer: GraphQLServer; + logService: LogService; + driver: Driver; + + constructor() { + const uri = process.env.NEO4J_URI || "bolt://localhost:7687"; + const user = process.env.NEO4J_USER || "neo4j"; + const password = process.env.NEO4J_PASSWORD || "neo4j"; + + if ( + !process.env.NEO4J_URI || + !process.env.NEO4J_USER || + !process.env.NEO4J_PASSWORD + ) { + console.warn( + "Using default Neo4j connection parameters. Set NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD environment variables for custom configuration." + ); } - async initialize() { - // Register HTTP routes - await registerHttpRoutes(this.server); + this.driver = neo4j.driver(uri, neo4j.auth.basic(user, password)); - // Create Yoga instance with the schema from GraphQLServer - const yoga = createYoga({ - schema: this.graphqlServer.getSchema(), - graphiql: true, - }); + const dbService = new DbService(this.driver); + this.logService = new LogService(this.driver); + this.graphqlServer = new GraphQLServer(dbService); + + this.server = fastify({ + logger: true, + }); + } + + async initialize() { + await registerHttpRoutes(this.server); + + const w3id = await W3ID.get({ + id: process.env.W3ID as string, + driver: this.driver, + password: process.env.ENCRYPTION_PASSWORD, + }); - // Mount GraphQL endpoint - this.server.route({ - url: "/graphql", - method: ["GET", "POST", "OPTIONS"], - handler: async (req: FastifyRequest, reply: FastifyReply) => { - const response = await yoga.handleNodeRequest( - req.raw, - reply.raw, - ); - response.headers.forEach((value, key) => { - reply.header(key, value); - }); - reply.status(response.status); - reply.send(response.body); - return reply; - }, + const yoga = createYoga({ + schema: this.graphqlServer.getSchema(), + graphiql: true, + }); + // change + + this.server.route({ + url: "/graphql", + method: ["GET", "POST", "OPTIONS"], + handler: async (req: FastifyRequest, reply: FastifyReply) => { + const response = await yoga.fetch(req.url, { + method: req.method, + headers: req.headers, + body: req.method === "POST" ? req.body : undefined, + }); + reply.status(response.status); + const headers: Record = {}; + response.headers.forEach((value, key) => { + headers[key] = value; }); + reply.headers(headers); + reply.send(response.body); + return reply; + }, + }); - // Mount Voyager endpoint - this.server.get( - "/voyager", - (req: FastifyRequest, reply: FastifyReply) => { - reply.type("text/html").send( - renderVoyagerPage({ - endpointUrl: "/graphql", - }), - ); - }, - ); - } + // Mount Voyager endpoint + this.server.get("/voyager", (req: FastifyRequest, reply: FastifyReply) => { + reply.type("text/html").send( + renderVoyagerPage({ + endpointUrl: "/graphql", + }) + ); + }); + } - async start() { - await this.initialize(); - - const port = process.env.NOMAD_PORT_http || process.env.PORT || 4000; - - await this.server.listen({ port: Number(port), host: "0.0.0.0" }); - console.log(`Server started on http://0.0.0.0:${port}`); - console.log( - `GraphQL endpoint available at http://0.0.0.0:${port}/graphql`, - ); - console.log( - `GraphQL Voyager available at http://0.0.0.0:${port}/voyager`, - ); - console.log( - `API Documentation available at http://0.0.0.0:${port}/documentation`, - ); - } + async start() { + await this.initialize(); + + const port = process.env.NOMAD_PORT_http || process.env.PORT || 4000; + + await this.server.listen({ port: Number(port), host: "0.0.0.0" }); + console.log(`Server started on http://0.0.0.0:${port}`); + console.log(`GraphQL endpoint available at http://0.0.0.0:${port}/graphql`); + console.log(`GraphQL Voyager available at http://0.0.0.0:${port}/voyager`); + console.log(`API Documentation available at http://0.0.0.0:${port}/docs`); + } } const evault = new EVault(); diff --git a/infrastructure/evault-core/src/http/server.ts b/infrastructure/evault-core/src/http/server.ts index 92d6d624f..082262d40 100644 --- a/infrastructure/evault-core/src/http/server.ts +++ b/infrastructure/evault-core/src/http/server.ts @@ -1,7 +1,8 @@ import fastify, { FastifyInstance } from "fastify"; import swagger from "@fastify/swagger"; import swaggerUi from "@fastify/swagger-ui"; -import { W3ID } from "w3id"; +import { W3ID } from "../w3id/w3id"; +import { LogEvent } from "w3id"; import { WatcherSignatureRequest, WatcherRequest, @@ -45,10 +46,31 @@ export async function registerHttpRoutes( 200: { type: "object", properties: { - w3id: { type: "object" }, + w3id: { type: "string" }, logs: { type: "array", - items: { type: "object" }, + items: { + type: "object", + properties: { + id: { type: "string" }, + versionId: { type: "string" }, + versionTime: { type: "string", format: "date-time" }, + updateKeys: { type: "array", items: { type: "string" } }, + nextKeyHashes: { type: "array", items: { type: "string" } }, + method: { type: "string" }, + proofs: { + type: "array", + items: { + type: "object", + properties: { + signature: { type: "string" }, + alg: { type: "string" }, + kid: { type: "string" }, + }, + }, + }, + }, + }, }, }, }, @@ -56,12 +78,14 @@ export async function registerHttpRoutes( }, }, async (request: TypedRequest<{}>, reply: TypedReply) => { - // TODO: Implement actual W3ID verification and log retrieval - const w3id = new W3ID({} as any); // TODO: Add proper W3ID initialization - return { - w3id: w3id, - logs: [], // TODO: Implement log retrieval + const w3id = await W3ID.get(); + const logs = (await w3id.logs?.repository.findMany({})) as LogEvent[]; + const result = { + w3id: w3id.id, + logs: logs, }; + console.log(result); + return result; } ); diff --git a/infrastructure/evault-core/src/secrets/secrets-store.ts b/infrastructure/evault-core/src/secrets/secrets-store.ts new file mode 100644 index 000000000..62ba86611 --- /dev/null +++ b/infrastructure/evault-core/src/secrets/secrets-store.ts @@ -0,0 +1,126 @@ +import { + createCipheriv, + createDecipheriv, + randomBytes, + pbkdf2Sync, +} from "crypto"; +import fs from "fs/promises"; +import path from "path"; +import { hexToUint8Array, uint8ArrayToHex } from "../utils/codec"; + +const ALGORITHM = "aes-256-gcm"; +const IV_LENGTH = 16; +const SALT_LENGTH = 32; +const TAG_LENGTH = 16; +const ITERATIONS = 100000; +const KEY_LENGTH = 32; + +interface StoredSeed { + encrypted: string; + iv: string; + salt: string; + nextKeyHash: string; +} + +export class SecretsStore { + private storePath: string; + private password: string; + + constructor(storePath: string, password: string) { + this.storePath = storePath; + this.password = password; + } + + private deriveKey(salt: Buffer): Buffer { + return pbkdf2Sync(this.password, salt, ITERATIONS, KEY_LENGTH, "sha256"); + } + + private async ensureStoreExists(): Promise { + try { + await fs.access(this.storePath); + } catch { + await fs.mkdir(path.dirname(this.storePath), { recursive: true }); + await fs.writeFile(this.storePath, JSON.stringify({})); + } + } + + private async readStore(): Promise> { + await this.ensureStoreExists(); + const content = await fs.readFile(this.storePath, "utf-8"); + return JSON.parse(content); + } + + private async writeStore(store: Record): Promise { + await fs.writeFile(this.storePath, JSON.stringify(store, null, 2)); + } + + private encrypt(data: Buffer): { + encrypted: string; + iv: string; + salt: string; + } { + const iv = randomBytes(IV_LENGTH); + const salt = randomBytes(SALT_LENGTH); + const key = this.deriveKey(salt); + const cipher = createCipheriv(ALGORITHM, key, iv); + const encrypted = Buffer.concat([ + cipher.update(data), + cipher.final(), + cipher.getAuthTag(), + ]); + return { + encrypted: uint8ArrayToHex(encrypted), + iv: uint8ArrayToHex(iv), + salt: uint8ArrayToHex(salt), + }; + } + + private decrypt(encrypted: string, iv: string, salt: string): Buffer { + const key = this.deriveKey(Buffer.from(hexToUint8Array(salt))); + const decipher = createDecipheriv( + ALGORITHM, + key, + Buffer.from(hexToUint8Array(iv)) + ); + const encryptedBuffer = Buffer.from(hexToUint8Array(encrypted)); + const tag = encryptedBuffer.slice(-TAG_LENGTH); + const data = encryptedBuffer.slice(0, -TAG_LENGTH); + decipher.setAuthTag(tag); + return Buffer.concat([decipher.update(data), decipher.final()]); + } + + public async storeSeed( + keyId: string, + seed: Uint8Array, + nextKeyHash: string + ): Promise { + const store = await this.readStore(); + const { encrypted, iv, salt } = this.encrypt(Buffer.from(seed)); + const storedSeed: StoredSeed = { encrypted, iv, salt, nextKeyHash }; + store[keyId] = JSON.stringify(storedSeed); + await this.writeStore(store); + } + + public async getSeed( + keyId: string + ): Promise<{ seed: Uint8Array; nextKeyHash: string }> { + const store = await this.readStore(); + const data: StoredSeed = JSON.parse(store[keyId]); + if (!data) throw new Error(`No seed found for key ${keyId}`); + return { + seed: this.decrypt(data.encrypted, data.iv, data.salt), + nextKeyHash: data.nextKeyHash, + }; + } + + public async deleteSeed(keyId: string): Promise { + const store = await this.readStore(); + delete store[keyId]; + await this.writeStore(store); + } + + public async listSeeds(): Promise { + const store = await this.readStore(); + return Object.keys(store); + } +} diff --git a/infrastructure/evault-core/src/utils/codec.ts b/infrastructure/evault-core/src/utils/codec.ts new file mode 100644 index 000000000..92fd8a916 --- /dev/null +++ b/infrastructure/evault-core/src/utils/codec.ts @@ -0,0 +1,86 @@ +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] = Number.parseInt(hex.slice(i, i + 2), 16); + } + return bytes; +} + +export function stringToUint8Array(str: string): Uint8Array { + return new TextEncoder().encode(str); +} + +const BASE58_ALPHABET = + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +function base58btcEncode(buffer: Uint8Array): string { + let num = BigInt("0x" + Buffer.from(buffer).toString("hex")); + const base = BigInt(58); + let encoded = ""; + + while (num > 0) { + const remainder = num % base; + num = num / base; + encoded = BASE58_ALPHABET[Number(remainder)] + encoded; + } + + // Handle leading zero bytes + for (const byte of buffer) { + if (byte === 0) { + encoded = BASE58_ALPHABET[0] + encoded; + } else { + break; + } + } + + return encoded; +} + +function base58btcDecode(str: string): Uint8Array { + const base = BigInt(58); + let num = BigInt(0); + + for (const char of str) { + const index = BASE58_ALPHABET.indexOf(char); + if (index === -1) throw new Error(`Invalid Base58 character "${char}"`); + num = num * base + BigInt(index); + } + + let hex = num.toString(16); + if (hex.length % 2) hex = "0" + hex; + let decoded = Uint8Array.from(Buffer.from(hex, "hex")); + + // Handle leading Base58 zeroes ('1' = 0x00) + let leadingZeros = 0; + for (const c of str) { + if (c === BASE58_ALPHABET[0]) { + leadingZeros++; + } else { + break; + } + } + + const result = new Uint8Array(leadingZeros + decoded.length); + result.set(decoded, leadingZeros); + return result; +} + +export function base58btcMultibaseEncode(data: Uint8Array): string { + return "z" + base58btcEncode(data); // 'z' = multibase prefix for base58btc +} + +export function base58btcMultibaseDecode(multibaseStr: string): Uint8Array { + if (!multibaseStr.startsWith("z")) { + throw new Error("Only multibase Base58BTC ('z' prefix) is supported"); + } + return base58btcDecode(multibaseStr.slice(1)); +} diff --git a/infrastructure/evault-core/src/utils/signer.ts b/infrastructure/evault-core/src/utils/signer.ts new file mode 100644 index 000000000..f58d2e651 --- /dev/null +++ b/infrastructure/evault-core/src/utils/signer.ts @@ -0,0 +1,44 @@ +import { Proof, Signer, VerifierCallback } from "w3id"; +import { + base58btcMultibaseDecode, + base58btcMultibaseEncode, + hexToUint8Array, + stringToUint8Array, + uint8ArrayToHex, +} from "./codec"; +import nacl from "tweetnacl"; + +export const verifierCallback: VerifierCallback = async ( + message: string, + proofs: Proof[], + pubKey: string +) => { + let isValid = true; + for (const proof of proofs) { + const signatureBuffer = base58btcMultibaseDecode(proof.signature); + const messageBuffer = stringToUint8Array(message); + const publicKey = hexToUint8Array(pubKey); + const valid = nacl.sign.detached.verify( + messageBuffer, + signatureBuffer, + publicKey + ); + if (!valid) isValid = false; + } + + return isValid; +}; + +export function createSigner(keyPair: nacl.SignKeyPair): Signer { + const publicKey = uint8ArrayToHex(keyPair.publicKey); + const signer: Signer = { + alg: "ed25519", + pubKey: publicKey, + sign: (str: string) => { + const buffer = stringToUint8Array(str); + const signature = nacl.sign.detached(buffer, keyPair.secretKey); + return base58btcMultibaseEncode(signature); + }, + }; + return signer; +} diff --git a/infrastructure/evault-core/src/w3id/log-service.ts b/infrastructure/evault-core/src/w3id/log-service.ts new file mode 100644 index 000000000..a27ac10f6 --- /dev/null +++ b/infrastructure/evault-core/src/w3id/log-service.ts @@ -0,0 +1,42 @@ +import { Driver } from "neo4j-driver"; +import { Neo4jLogStorage } from "./log-storage"; +import { LogEvent, StorageSpec } from "w3id"; + +/** + * Service for managing log events in Neo4j + */ +export class LogService implements StorageSpec { + private logStorage: Neo4jLogStorage; + + constructor(driver: Driver) { + this.logStorage = new Neo4jLogStorage(driver); + } + + /** + * Get the log storage instance + */ + public getLogStorage(): Neo4jLogStorage { + return this.logStorage; + } + + /** + * Create a new log event + */ + public async create(body: LogEvent): Promise { + return this.logStorage.create(body); + } + + /** + * Find a log event by ID + */ + public async findOne(options: Partial): Promise { + return this.logStorage.findOne(options); + } + + /** + * Find log events by options + */ + public async findMany(options: Partial): Promise { + return this.logStorage.findMany(options); + } +} diff --git a/infrastructure/evault-core/src/w3id/log-storage.ts b/infrastructure/evault-core/src/w3id/log-storage.ts new file mode 100644 index 000000000..af852d7cc --- /dev/null +++ b/infrastructure/evault-core/src/w3id/log-storage.ts @@ -0,0 +1,98 @@ +import { Driver } from "neo4j-driver"; +import { StorageSpec } from "w3id"; +import { LogEvent } from "w3id"; + +/** + * Neo4j storage adapter for logs implementing the StorageSpec interface + * + * Note: We store proofs as a JSON string because Neo4j has limitations on property types: + * - Neo4j only supports primitive types and arrays of primitives as property values + * - Complex objects like Maps or nested objects must be serialized + * - See: https://neo4j.com/docs/cypher-manual/current/values-and-types/ + * - See: https://neo4j.com/docs/cypher-manual/current/syntax/values/#cypher-values + */ +export class Neo4jLogStorage implements StorageSpec { + constructor(private driver: Driver) {} + + private mapToLogEvent(properties: any): LogEvent { + return { + id: properties.id, + versionId: properties.versionId, + versionTime: new Date(properties.versionTime), + updateKeys: properties.updateKeys, + nextKeyHashes: properties.nextKeyHashes, + method: properties.method, + proofs: properties.proofs ? JSON.parse(properties.proofs) : [], + }; + } + + public async create(body: LogEvent): Promise { + const session = this.driver.session(); + try { + const result = await session.run( + ` + CREATE (l:LogEvent { + id: $id, + versionId: $versionId, + versionTime: $versionTime, + updateKeys: $updateKeys, + nextKeyHashes: $nextKeyHashes, + method: $method, + proofs: $proofs + }) + RETURN l + `, + { + id: body.id, + versionId: body.versionId, + versionTime: body.versionTime.toISOString(), + updateKeys: body.updateKeys, + nextKeyHashes: body.nextKeyHashes, + method: body.method, + proofs: JSON.stringify(body.proofs || []), + } + ); + return this.mapToLogEvent(result.records[0].get("l").properties); + } finally { + await session.close(); + } + } + + public async findOne(options: Partial): Promise { + const session = this.driver.session(); + try { + const result = await session.run( + ` + MATCH (l:LogEvent) + WHERE l.id = $id + RETURN l + `, + { id: options.id } + ); + if (result.records.length === 0) { + throw new Error(`No log event found with id ${options.id}`); + } + return this.mapToLogEvent(result.records[0].get("l").properties); + } finally { + await session.close(); + } + } + + public async findMany(options: Partial): Promise { + const session = this.driver.session(); + try { + const result = await session.run( + ` + MATCH (l:LogEvent) + RETURN l + ` + ); + const mapped = result.records.map((record) => + this.mapToLogEvent(record.get("l").properties) + ); + return mapped; + } finally { + await session.close(); + } + } +} diff --git a/infrastructure/evault-core/src/w3id/w3id.ts b/infrastructure/evault-core/src/w3id/w3id.ts index 1e4bfab76..6ea8ea743 100644 --- a/infrastructure/evault-core/src/w3id/w3id.ts +++ b/infrastructure/evault-core/src/w3id/w3id.ts @@ -1,16 +1,70 @@ -import { W3ID as W3IDClass, W3IDBuilder } from "w3id"; +import { W3ID as W3IDClass, W3IDBuilder, hash } from "w3id"; +import { LogService } from "./log-service"; +import { Driver } from "neo4j-driver"; +import nacl from "tweetnacl"; +import { createSigner } from "../utils/signer"; +import { SecretsStore } from "../secrets/secrets-store"; +import { uint8ArrayToHex } from "../utils/codec"; export class W3ID { - private static instance: W3IDClass; + private static instance: W3IDClass; + private static secretsStore: SecretsStore; - private constructor() {} + private constructor() {} - static async get(options?: { id: string }) { - if (W3ID.instance) return W3ID.instance; - if (!options) - throw new Error( - "No instance of W3ID exists yet, please create it by passing options", - ); - W3ID.instance = await new W3IDBuilder().build(); + static async get(options?: { + id: string; + driver: Driver; + password?: string; + }) { + if (W3ID.instance) return W3ID.instance; + if (!options) + throw new Error( + "No instance of W3ID exists yet, please create it by passing options" + ); + + // Initialize secrets store if not already done + if (!W3ID.secretsStore) { + if (!options.password) { + throw new Error("Password is required for secrets store"); + } + W3ID.secretsStore = new SecretsStore( + process.env.SECRETS_STORE_PATH!, + options.password + ); } + + const repository = new LogService(options.driver); + const keyId = `w3id-${options.id}`; + + try { + // Try to get existing seed + const { seed, nextKeyHash } = await W3ID.secretsStore.getSeed(keyId); + const keyPair = nacl.sign.keyPair.fromSeed(seed); + W3ID.instance = await new W3IDBuilder() + .withId(options.id) + .withRepository(repository) + .withGlobal(true) + .withSigner(createSigner(keyPair)) + .withNextKeyHash(nextKeyHash) + .build(); + } catch { + // If no seed exists, create new one + const keyPair = nacl.sign.keyPair(); + const nextKeyPair = nacl.sign.keyPair(); + const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); + + // Store the seed + await W3ID.secretsStore.storeSeed(keyId, keyPair.secretKey, nextKeyHash); + + W3ID.instance = await new W3IDBuilder() + .withId(options.id) + .withRepository(repository) + .withSigner(createSigner(keyPair)) + .withNextKeyHash(nextKeyHash) + .build(); + } + + return W3ID.instance; + } } diff --git a/infrastructure/evault-core/tests/log-storage.spec.ts b/infrastructure/evault-core/tests/log-storage.spec.ts new file mode 100644 index 000000000..03746d8fc --- /dev/null +++ b/infrastructure/evault-core/tests/log-storage.spec.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import neo4j, { Driver } from "neo4j-driver"; +import { Neo4jContainer } from "@testcontainers/neo4j"; +import { Neo4jLogStorage } from "../src/w3id/log-storage"; +import { LogEvent } from "w3id"; + +describe("Neo4jLogStorage", () => { + let container; + let driver: Driver; + let storage: Neo4jLogStorage; + + beforeAll(async () => { + container = await new Neo4jContainer("neo4j:5.15").start(); + const uri = `bolt://localhost:${container.getMappedPort(7687)}`; + driver = neo4j.driver( + uri, + neo4j.auth.basic(container.getUsername(), container.getPassword()) + ); + storage = await Neo4jLogStorage.build(driver); + }); + + afterAll(async () => { + await driver.close(); + await container.stop(); + }); + + it("should create and retrieve a log event", async () => { + const logEvent: LogEvent = { + id: "test-id", + versionId: "0-test", + versionTime: new Date(), + updateKeys: ["key1", "key2"], + nextKeyHashes: ["hash1", "hash2"], + method: "w3id:v0.0.0", + }; + + const created = await storage.create(logEvent); + expect(created.id).toBe(logEvent.id); + expect(created.versionId).toBe(logEvent.versionId); + expect(created.updateKeys).toEqual(logEvent.updateKeys); + expect(created.nextKeyHashes).toEqual(logEvent.nextKeyHashes); + expect(created.method).toBe(logEvent.method); + + const retrieved = await storage.findOne({ id: logEvent.id }); + expect(retrieved.id).toBe(logEvent.id); + expect(retrieved.versionId).toBe(logEvent.versionId); + expect(retrieved.updateKeys).toEqual(logEvent.updateKeys); + expect(retrieved.nextKeyHashes).toEqual(logEvent.nextKeyHashes); + expect(retrieved.method).toBe(logEvent.method); + }); + + it("should find multiple log events", async () => { + const logEvent1: LogEvent = { + id: "test-id-1", + versionId: "0-test-1", + versionTime: new Date(), + updateKeys: ["key1"], + nextKeyHashes: ["hash1"], + method: "w3id:v0.0.0", + }; + + const logEvent2: LogEvent = { + id: "test-id-2", + versionId: "0-test-2", + versionTime: new Date(), + updateKeys: ["key2"], + nextKeyHashes: ["hash2"], + method: "w3id:v0.0.0", + }; + + await storage.create(logEvent1); + await storage.create(logEvent2); + + const events = await storage.findMany({ method: "w3id:v0.0.0" }); + expect(events.length).toBeGreaterThanOrEqual(2); + expect(events.some((e) => e.id === logEvent1.id)).toBe(true); + expect(events.some((e) => e.id === logEvent2.id)).toBe(true); + }); + + it("should throw error when log event not found", async () => { + await expect(storage.findOne({ id: "non-existent-id" })).rejects.toThrow( + "Log event not found" + ); + }); +}); diff --git a/infrastructure/evault-core/tsconfig.json b/infrastructure/evault-core/tsconfig.json index e2a38c9f4..826c63920 100644 --- a/infrastructure/evault-core/tsconfig.json +++ b/infrastructure/evault-core/tsconfig.json @@ -1,27 +1,18 @@ { - "compilerOptions": { - "target": "ES2021", - "module": "CommonJS", - "lib": [ - "ESNext", - "DOM" - ], - "declaration": true, - "declarationDir": "./dist/types", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "Node", - "skipLibCheck": true - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "*/**/*.spec.ts" - ] -} + "compilerOptions": { + "target": "ES2017", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "declaration": true, + "declarationDir": "./dist/types", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "Node", + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 99a64c4df..c3120cabd 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,22 +1,22 @@ 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 { - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - type RotationLogOptions, - type Signer, - type VerifierCallback, - isGenesisOptions, - isRotationOptions, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + type RotationLogOptions, + type Signer, + type VerifierCallback, + isGenesisOptions, + isRotationOptions, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -28,185 +28,175 @@ 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; - signer: Signer; - - constructor(repository: StorageSpec, signer: Signer) { - this.repository = repository; - this.signer = signer; - } - - /** - * Validate a chain of W3ID logs - * - * @param {LogEvent[]} log - * @param {VerifierCallback} verifyCallback - * @returns {Promise} - */ - - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ): Promise { - 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(); - } - - 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; - } - - /** - * Validate cryptographic signature on a single LogEvent - * - * @param {LogEvent} e - * @param {string[]} currentUpdateKeys - * @param {VerifierCallback} verifyCallback - * @returns {Promise} - */ - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ): Promise { - const proofs = e.proofs; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proofs; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proofs) - throw new BadSignatureError("No proof found in the log event."); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proofs, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } - - /** - * Append a new log entry for a W3ID - * - * @param {LogEvent[]} entries - * @param {RotationLogOptions} options - * @returns Promise - */ - private async appendEntry( - entries: LogEvent[], - options: RotationLogOptions, - ) { - const { 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 logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - - const signature = await this.signer.sign( - canonicalize(logEvent) as string, + repository: StorageSpec; + signer: Signer; + + constructor(repository: StorageSpec, signer: Signer) { + this.repository = repository; + this.signer = signer; + } + + /** + * Validate a chain of W3ID logs + * + * @param {LogEvent[]} log + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback + ): Promise { + 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 ); - logEvent.proofs = [ - { - kid: `${logEvent.id}#0`, - alg: this.signer.alg, - signature, - }, - ]; - - await this.repository.create(logEvent); - this.signer = nextKeySigner; - return logEvent; + 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); } - - /** - * Create genesis entry for a W3ID log - * - * @param {GenesisLogOptions} options - * @returns Promise - */ - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes } = options; - const idTag = id.includes("@") ? id.split("@")[1] : id; - const logEvent: LogEvent = { - id, - versionId: `0-${idTag}`, - versionTime: new Date(Date.now()), - updateKeys: [this.signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const signature = await this.signer.sign( - canonicalize(logEvent) as string, - ); - logEvent.proofs = [ - { - kid: `${id}#0`, - alg: this.signer.alg, - signature, - }, - ]; - - await this.repository.create(logEvent); - return logEvent; + return true; + } + + /** + * Validate cryptographic signature on a single LogEvent + * + * @param {LogEvent} e + * @param {string[]} currentUpdateKeys + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback + ): Promise { + const proofs = e.proofs; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proofs; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proofs) + throw new BadSignatureError("No proof found in the log event."); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proofs, + key + ); + if (signValidates) verified = true; } - - /** - * Create a log event and save it to the repository - * - * @param {CreateLogEventOptions} options - * @returns Promise - */ - async createLogEvent(options: CreateLogEventOptions): Promise { - 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); + if (!verified) throw new BadSignatureError(); + } + + /** + * Append a new log entry for a W3ID + * + * @param {LogEvent[]} entries + * @param {RotationLogOptions} options + * @returns Promise + */ + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { 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 logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + + const signature = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proofs = [ + { + kid: `${logEvent.id}#0`, + alg: this.signer.alg, + signature, + }, + ]; + + await this.repository.create(logEvent); + this.signer = nextKeySigner; + return logEvent; + } + + /** + * Create genesis entry for a W3ID log + * + * @param {GenesisLogOptions} options + * @returns Promise + */ + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes } = options; + const idTag = id.includes("@") ? id.split("@")[1] : id; + const logEvent: LogEvent = { + id, + versionId: `0-${idTag}`, + versionTime: new Date(Date.now()), + updateKeys: [this.signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const signature = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proofs = [ + { + kid: `${id}#0`, + alg: this.signer.alg, + signature, + }, + ]; + + await this.repository.create(logEvent); + return logEvent; + } + + /** + * Create a log event and save it to the repository + * + * @param {CreateLogEventOptions} options + * @returns Promise + */ + async createLogEvent(options: CreateLogEventOptions): Promise { + 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/w3id.ts b/infrastructure/w3id/src/w3id.ts index 9352ecc57..c4b8608fb 100644 --- a/infrastructure/w3id/src/w3id.ts +++ b/infrastructure/w3id/src/w3id.ts @@ -7,152 +7,151 @@ import { generateRandomAlphaNum } from "./utils/rand"; import { generateUuid } from "./utils/uuid"; export class W3ID { - constructor( - public id: string, - public logs?: IDLogManager, - ) {} + constructor( + public id: string, + public logs?: IDLogManager + ) {} - /** - * Signs a JWT with the W3ID's signer - * @param payload - The JWT payload - * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) - * @returns The signed JWT - */ - public async signJWT( - payload: JWTPayload, - header?: JWTHeader, - ): Promise { - if (!this.logs?.signer) { - throw new Error("W3ID must have a signer to sign JWTs"); - } - return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); + /** + * Signs a JWT with the W3ID's signer + * @param payload - The JWT payload + * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) + * @returns The signed JWT + */ + public async signJWT( + payload: JWTPayload, + header?: JWTHeader + ): Promise { + if (!this.logs?.signer) { + throw new Error("W3ID must have a signer to sign JWTs"); } + return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); + } } export class W3IDBuilder { - private signer?: Signer; - private repository?: StorageSpec; - private entropy?: string; - private namespace?: string; - private nextKeyHash?: string; - private global?: boolean = false; - private id?: string; + private signer?: Signer; + private repository?: StorageSpec; + private entropy?: string; + private namespace?: string; + private nextKeyHash?: string; + private global?: boolean = false; + private id?: string; - /** - * Specify entropy to create the identity with - * - * @param {string} str - */ - public withEntropy(str: string): W3IDBuilder { - this.entropy = str; - return this; - } + /** + * Specify entropy to create the identity with + * + * @param {string} str + */ + public withEntropy(str: string): W3IDBuilder { + this.entropy = str; + return this; + } - /** - * Specify namespace to use to generate the UUIDv5 - * - * @param {string} uuid - */ - public withNamespace(uuid: string): W3IDBuilder { - this.namespace = uuid; - return this; - } + /** + * Specify namespace to use to generate the UUIDv5 + * + * @param {string} uuid + */ + public withNamespace(uuid: string): W3IDBuilder { + this.namespace = uuid; + return this; + } - /** - * Specify whether to create a global identifier or a local identifer - * - * According to the project specification there are supposed to be 2 main types of - * W3ID's ones which are tied to more permanent entities - * - * A global identifer is expected to live at the registry and starts with an \`@\` - * - * @param {boolean} isGlobal - */ - public withGlobal(isGlobal: boolean): W3IDBuilder { - this.global = isGlobal; - return this; - } + /** + * Specify whether to create a global identifier or a local identifer + * + * According to the project specification there are supposed to be 2 main types of + * W3ID's ones which are tied to more permanent entities + * + * A global identifer is expected to live at the registry and starts with an \`@\` + * + * @param {boolean} isGlobal + */ + public withGlobal(isGlobal: boolean): W3IDBuilder { + this.global = isGlobal; + return this; + } - /** - * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a - * repository in which the logs would be stored - * - * @param {StorageSpec} storage - */ - public withRepository( - storage: StorageSpec, - ): W3IDBuilder { - this.repository = storage; - return this; - } + /** + * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a + * repository in which the logs would be stored + * + * @param {StorageSpec} storage + */ + public withRepository(storage: StorageSpec): W3IDBuilder { + this.repository = storage; + return this; + } - /** - * Pre-specify a UUID to use as the W3ID - * - * @param {string} id - */ - public withId(id: string): W3IDBuilder { - this.id = id; - return this; - } + /** + * Pre-specify a UUID to use as the W3ID + * + * @param {string} id + */ + public withId(id: string): W3IDBuilder { + this.id = id; + return this; + } - /** - * Attach a keypair to the W3ID, a key attached W3ID would also need a repository - * to be added. - * - * @param {Signer} signer - */ - public withSigner(signer: Signer): W3IDBuilder { - this.signer = signer; - return this; - } + /** + * Attach a keypair to the W3ID, a key attached W3ID would also need a repository + * to be added. + * + * @param {Signer} signer + */ + public withSigner(signer: Signer): W3IDBuilder { + this.signer = signer; + return this; + } - /** - * Specify the SHA256 hash of the next key which will sign the next log entry after - * rotation of keys - * - * @param {string} hash - */ - public withNextKeyHash(hash: string): W3IDBuilder { - this.nextKeyHash = hash; - return this; - } + /** + * Specify the SHA256 hash of the next key which will sign the next log entry after + * rotation of keys + * + * @param {string} hash + */ + public withNextKeyHash(hash: string): W3IDBuilder { + this.nextKeyHash = hash; + return this; + } - /** - * Build the W3ID with provided builder options - * - * @returns Promise - */ - public async build(): Promise { - if ((this.id && this.namespace) || (this.id && this.entropy)) - throw new Error( - "Namespace and Entropy can't be specified when using pre-defined ID", - ); - this.entropy = this.entropy ?? generateRandomAlphaNum(); - this.namespace = this.namespace ?? uuidv4(); - this.id = - this.id && this.id?.includes("@") ? this.id.split("@")[1] : this.id; - const id = `${ - this.global ? "@" : "" - }${this.id ?? generateUuid(this.entropy, this.namespace)}`; - if (!this.signer) { - return new W3ID(id); - } - if (!this.repository) - throw new Error( - "Repository is required, pass with `withRepository` method", - ); -, - if (!this.nextKeyHash) - throw new Error( - "NextKeyHash is required pass with `withNextKeyHash` method", - ); - const logs = new IDLogManager(this.repository, this.signer); - await logs.createLogEvent({ - id, - nextKeyHashes: [this.nextKeyHash], - }); - return new W3ID(id, logs); + /** + * Build the W3ID with provided builder options + * + * @returns Promise + */ + public async build(): Promise { + if ((this.id && this.namespace) || (this.id && this.entropy)) + throw new Error( + "Namespace and Entropy can't be specified when using pre-defined ID" + ); + this.entropy = this.entropy ?? generateRandomAlphaNum(); + this.namespace = this.namespace ?? uuidv4(); + this.id = + this.id && this.id?.includes("@") ? this.id.split("@")[1] : this.id; + const id = `${ + this.global ? "@" : "" + }${this.id ?? generateUuid(this.entropy, this.namespace)}`; + if (!this.signer) { + return new W3ID(id); } -} + if (!this.repository) + throw new Error( + "Repository is required, pass with `withRepository` method" + ); + if (!this.nextKeyHash) + throw new Error( + "NextKeyHash is required pass with `withNextKeyHash` method" + ); + const logs = new IDLogManager(this.repository, this.signer); + const currentLogs = await logs.repository.findMany({}); + if (currentLogs.length > 0) return new W3ID(id, logs); + await logs.createLogEvent({ + id, + nextKeyHashes: [this.nextKeyHash], + }); + return new W3ID(id, logs); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c7ff545b..6357a3beb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,9 +171,15 @@ importers: json-schema: specifier: ^0.4.0 version: 0.4.0 + multiformats: + specifier: ^13.3.2 + version: 13.3.2 neo4j-driver: specifier: ^5.28.1 version: 5.28.1 + tweetnacl: + specifier: ^1.0.3 + version: 1.0.3 w3id: specifier: workspace:* version: link:../w3id From f047287a2f7a11dbd7f1ec7d78e4fefb28809a7b Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 22 Apr 2025 18:03:25 +0530 Subject: [PATCH 5/7] feat: watcher endpoints --- infrastructure/evault-core/package.json | 1 + infrastructure/evault-core/src/http/server.ts | 136 +++++++++++++++--- platforms/registry/src/consul.ts | 5 +- pnpm-lock.yaml | 3 + 4 files changed, 122 insertions(+), 23 deletions(-) diff --git a/infrastructure/evault-core/package.json b/infrastructure/evault-core/package.json index 15ff1abbf..acdc7f38d 100644 --- a/infrastructure/evault-core/package.json +++ b/infrastructure/evault-core/package.json @@ -27,6 +27,7 @@ "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^3.0.0", "@testcontainers/neo4j": "^10.24.2", + "axios": "^1.6.7", "fastify": "^4.26.2", "graphql": "^16.10.0", "graphql-type-json": "^0.3.2", diff --git a/infrastructure/evault-core/src/http/server.ts b/infrastructure/evault-core/src/http/server.ts index 082262d40..ea1d01d83 100644 --- a/infrastructure/evault-core/src/http/server.ts +++ b/infrastructure/evault-core/src/http/server.ts @@ -3,12 +3,19 @@ import swagger from "@fastify/swagger"; import swaggerUi from "@fastify/swagger-ui"; import { W3ID } from "../w3id/w3id"; import { LogEvent } from "w3id"; -import { - WatcherSignatureRequest, - WatcherRequest, - TypedRequest, - TypedReply, -} from "./types"; +import axios from "axios"; +import { WatcherRequest, TypedRequest, TypedReply } from "./types"; +import { verifierCallback } from "../utils/signer"; + +interface WatcherSignatureRequest { + w3id: string; + logEntryId: string; + proof: { + signature: string; + alg: string; + kid: string; + }; +} export async function registerHttpRoutes( server: FastifyInstance @@ -98,11 +105,19 @@ export async function registerHttpRoutes( description: "Post a signature for a specific log entry", body: { type: "object", - required: ["w3id", "signature", "logEntryId"], + required: ["w3id", "logEntryId", "proof"], properties: { w3id: { type: "string" }, - signature: { type: "string" }, logEntryId: { type: "string" }, + proof: { + type: "object", + required: ["signature", "alg", "kid"], + properties: { + signature: { type: "string" }, + alg: { type: "string" }, + kid: { type: "string" }, + }, + }, }, }, response: { @@ -120,12 +135,51 @@ export async function registerHttpRoutes( request: TypedRequest, reply: TypedReply ) => { - const { w3id, signature, logEntryId } = request.body; - // TODO: Implement signature verification and storage - return { - success: true, - message: "Signature stored successfully", - }; + const { w3id, logEntryId, proof } = request.body; + + try { + const currentW3ID = await W3ID.get(); + if (!currentW3ID.logs) { + throw new Error("W3ID must have logs enabled"); + } + + const logEvent = await currentW3ID.logs.repository.findOne({ + versionId: logEntryId, + }); + if (!logEvent) { + throw new Error(`Log event not found with id ${logEntryId}`); + } + + const isValid = await verifierCallback( + logEntryId, + [proof], + proof.kid.split("#")[0] + ); + if (!isValid) { + throw new Error("Invalid signature"); + } + + const updatedLogEvent: LogEvent = { + ...logEvent, + proofs: [...(logEvent.proofs || []), proof], + }; + + await currentW3ID.logs.repository.create(updatedLogEvent); + + return { + success: true, + message: "Signature stored successfully", + }; + } catch (error) { + console.error("Error storing signature:", error); + return { + success: false, + message: + error instanceof Error + ? error.message + : "Failed to store signature", + }; + } } ); @@ -158,12 +212,54 @@ export async function registerHttpRoutes( }, async (request: TypedRequest, reply: TypedReply) => { const { w3id, logEntryId } = request.body; - // TODO: Implement signature request logic - return { - success: true, - message: "Signature request created", - requestId: "req_" + Date.now(), - }; + + try { + // Resolve the W3ID to get its request endpoint + const registryResponse = await axios.get( + `http://localhost:4321/resolve?w3id=${w3id}` + ); + const { requestWatcherSignature } = registryResponse.data; + + // Get the current W3ID instance + const currentW3ID = await W3ID.get(); + if (!currentW3ID.logs) { + throw new Error("W3ID must have logs enabled"); + } + + // Find the log event + const logEvent = await currentW3ID.logs.repository.findOne({ + versionId: logEntryId, + }); + if (!logEvent) { + throw new Error(`Log event not found with id ${logEntryId}`); + } + + // Request signature from the watcher + const response = await axios.post(requestWatcherSignature, { + w3id: currentW3ID.id, + logEntryId, + signature: await currentW3ID.signJWT({ + sub: logEntryId, + exp: Date.now() + 3600 * 1000, // 1 hour expiry + }), + }); + + return { + success: true, + message: "Signature request created", + requestId: response.data.requestId, + }; + } catch (error) { + console.error("Error requesting signature:", error); + return { + success: false, + message: + error instanceof Error + ? error.message + : "Failed to request signature", + requestId: "", + }; + } } ); } diff --git a/platforms/registry/src/consul.ts b/platforms/registry/src/consul.ts index cbf6d8082..c35d125d1 100644 --- a/platforms/registry/src/consul.ts +++ b/platforms/registry/src/consul.ts @@ -19,10 +19,9 @@ export async function resolveService(w3id: string) { const address = `http://${services[0].ServiceAddress}:${services[0].ServicePort}`; return { graphql: `${address}/graphql`, - voyager: `${address}/voyager`, whois: `${address}/whois`, - logs: `${address}/logs`, - requestWatcherSignature: `${address}/request-signature`, + requestWatcherSignature: `${address}/watchers/request`, + wathcerSignEndpoint: `${address}/watchers/request`, }; } return null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6357a3beb..4150a3625 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,9 @@ importers: '@testcontainers/neo4j': specifier: ^10.24.2 version: 10.24.2 + axios: + specifier: ^1.6.7 + version: 1.8.4 fastify: specifier: ^4.26.2 version: 4.29.0 From d82a4fadd212965846f815dd7d33248754b0a999 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 22 Apr 2025 18:14:39 +0530 Subject: [PATCH 6/7] chore: fix format and lint --- infrastructure/w3id/src/logs/log-manager.ts | 368 ++++++++++---------- infrastructure/w3id/src/logs/log.types.ts | 70 ++-- infrastructure/w3id/src/w3id.ts | 267 +++++++------- 3 files changed, 352 insertions(+), 353 deletions(-) diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index c3120cabd..d7cf2f6b4 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,22 +1,22 @@ 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 { - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - type RotationLogOptions, - type Signer, - type VerifierCallback, - isGenesisOptions, - isRotationOptions, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + type RotationLogOptions, + type Signer, + type VerifierCallback, + isGenesisOptions, + isRotationOptions, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -28,175 +28,175 @@ 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; - signer: Signer; - - constructor(repository: StorageSpec, signer: Signer) { - this.repository = repository; - this.signer = signer; - } - - /** - * Validate a chain of W3ID logs - * - * @param {LogEvent[]} log - * @param {VerifierCallback} verifyCallback - * @returns {Promise} - */ - - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback - ): Promise { - 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(); - } - - 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; - } - - /** - * Validate cryptographic signature on a single LogEvent - * - * @param {LogEvent} e - * @param {string[]} currentUpdateKeys - * @param {VerifierCallback} verifyCallback - * @returns {Promise} - */ - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback - ): Promise { - const proofs = e.proofs; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proofs; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proofs) - throw new BadSignatureError("No proof found in the log event."); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proofs, - key - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } - - /** - * Append a new log entry for a W3ID - * - * @param {LogEvent[]} entries - * @param {RotationLogOptions} options - * @returns Promise - */ - private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { - const { 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 logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - - const signature = await this.signer.sign(canonicalize(logEvent) as string); - logEvent.proofs = [ - { - kid: `${logEvent.id}#0`, - alg: this.signer.alg, - signature, - }, - ]; - - await this.repository.create(logEvent); - this.signer = nextKeySigner; - return logEvent; - } - - /** - * Create genesis entry for a W3ID log - * - * @param {GenesisLogOptions} options - * @returns Promise - */ - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes } = options; - const idTag = id.includes("@") ? id.split("@")[1] : id; - const logEvent: LogEvent = { - id, - versionId: `0-${idTag}`, - versionTime: new Date(Date.now()), - updateKeys: [this.signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const signature = await this.signer.sign(canonicalize(logEvent) as string); - logEvent.proofs = [ - { - kid: `${id}#0`, - alg: this.signer.alg, - signature, - }, - ]; - - await this.repository.create(logEvent); - return logEvent; - } - - /** - * Create a log event and save it to the repository - * - * @param {CreateLogEventOptions} options - * @returns Promise - */ - async createLogEvent(options: CreateLogEventOptions): Promise { - 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); - } + repository: StorageSpec; + signer: Signer; + + constructor(repository: StorageSpec, signer: Signer) { + this.repository = repository; + this.signer = signer; + } + + /** + * Validate a chain of W3ID logs + * + * @param {LogEvent[]} log + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ): Promise { + 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(); + } + + 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; + } + + /** + * Validate cryptographic signature on a single LogEvent + * + * @param {LogEvent} e + * @param {string[]} currentUpdateKeys + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ): Promise { + const proofs = e.proofs; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proofs; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proofs) + throw new BadSignatureError("No proof found in the log event."); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proofs, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } + + /** + * Append a new log entry for a W3ID + * + * @param {LogEvent[]} entries + * @param {RotationLogOptions} options + * @returns Promise + */ + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { 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 logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + + const signature = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proofs = [ + { + kid: `${logEvent.id}#0`, + alg: this.signer.alg, + signature, + }, + ]; + + await this.repository.create(logEvent); + this.signer = nextKeySigner; + return logEvent; + } + + /** + * Create genesis entry for a W3ID log + * + * @param {GenesisLogOptions} options + * @returns Promise + */ + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes } = options; + const idTag = id.includes("@") ? id.split("@")[1] : id; + const logEvent: LogEvent = { + id, + versionId: `0-${idTag}`, + versionTime: new Date(Date.now()), + updateKeys: [this.signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const signature = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proofs = [ + { + kid: `${id}#0`, + alg: this.signer.alg, + signature, + }, + ]; + + await this.repository.create(logEvent); + return logEvent; + } + + /** + * Create a log event and save it to the repository + * + * @param {CreateLogEventOptions} options + * @returns Promise + */ + async createLogEvent(options: CreateLogEventOptions): Promise { + 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/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index a40f3eb49..0545ed625 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,67 +1,67 @@ export type Proof = { - kid: string; - signature: string; - alg: string; + kid: string; + signature: string; + alg: string; }; export type LogEvent = { - id: string; - versionId: string; - versionTime: Date; - updateKeys: string[]; - nextKeyHashes: string[]; - method: `w3id:v${string}`; - proofs?: Proof[]; + id: string; + versionId: string; + versionTime: Date; + updateKeys: string[]; + nextKeyHashes: string[]; + method: `w3id:v${string}`; + proofs?: Proof[]; }; export type VerifierCallback = ( - message: string, - proofs: Proof[], - pubKey: string, + message: string, + proofs: Proof[], + pubKey: string, ) => Promise; export type JWTHeader = { - alg: string; - typ: "JWT"; - kid?: string; + alg: string; + typ: "JWT"; + kid?: string; }; export type JWTPayload = { - [key: string]: unknown; - iat?: number; - exp?: number; - nbf?: number; - iss?: string; - sub?: string; - aud?: string; - jti?: string; + [key: string]: unknown; + iat?: number; + exp?: number; + nbf?: number; + iss?: string; + sub?: string; + aud?: string; + jti?: string; }; export type Signer = { - sign: (message: string) => Promise | string; - pubKey: string; - alg: string; + sign: (message: string) => Promise | string; + pubKey: string; + alg: string; }; export type RotationLogOptions = { - nextKeyHashes: string[]; - nextKeySigner: Signer; + nextKeyHashes: string[]; + nextKeySigner: Signer; }; export type GenesisLogOptions = { - nextKeyHashes: string[]; - id: string; + nextKeyHashes: string[]; + id: string; }; export function isGenesisOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is GenesisLogOptions { - return "id" in options; + return "id" in options; } export function isRotationOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is RotationLogOptions { - return "nextKeySigner" in options; + return "nextKeySigner" in options; } export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; diff --git a/infrastructure/w3id/src/w3id.ts b/infrastructure/w3id/src/w3id.ts index c4b8608fb..b2acb8460 100644 --- a/infrastructure/w3id/src/w3id.ts +++ b/infrastructure/w3id/src/w3id.ts @@ -7,151 +7,150 @@ import { generateRandomAlphaNum } from "./utils/rand"; import { generateUuid } from "./utils/uuid"; export class W3ID { - constructor( - public id: string, - public logs?: IDLogManager - ) {} + constructor( + public id: string, + public logs?: IDLogManager, + ) {} - /** - * Signs a JWT with the W3ID's signer - * @param payload - The JWT payload - * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) - * @returns The signed JWT - */ - public async signJWT( - payload: JWTPayload, - header?: JWTHeader - ): Promise { - if (!this.logs?.signer) { - throw new Error("W3ID must have a signer to sign JWTs"); - } - return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); - } + /** + * Signs a JWT with the W3ID's signer + * @param payload - The JWT payload + * @param header - Optional JWT header (defaults to using the signer's alg and W3ID's id as kid) + * @returns The signed JWT + */ + public async signJWT( + payload: JWTPayload, + header?: JWTHeader, + ): Promise { + if (!this.logs?.signer) { + throw new Error("W3ID must have a signer to sign JWTs"); + } + return signJWT(this.logs.signer, payload, `@${this.id}#0`, header); + } } export class W3IDBuilder { - private signer?: Signer; - private repository?: StorageSpec; - private entropy?: string; - private namespace?: string; - private nextKeyHash?: string; - private global?: boolean = false; - private id?: string; + private signer?: Signer; + private repository?: StorageSpec; + private entropy?: string; + private namespace?: string; + private nextKeyHash?: string; + private global?: boolean = false; + private id?: string; - /** - * Specify entropy to create the identity with - * - * @param {string} str - */ - public withEntropy(str: string): W3IDBuilder { - this.entropy = str; - return this; - } + /** + * Specify entropy to create the identity with + * + * @param {string} str + */ + public withEntropy(str: string): W3IDBuilder { + this.entropy = str; + return this; + } - /** - * Specify namespace to use to generate the UUIDv5 - * - * @param {string} uuid - */ - public withNamespace(uuid: string): W3IDBuilder { - this.namespace = uuid; - return this; - } + /** + * Specify namespace to use to generate the UUIDv5 + * + * @param {string} uuid + */ + public withNamespace(uuid: string): W3IDBuilder { + this.namespace = uuid; + return this; + } - /** - * Specify whether to create a global identifier or a local identifer - * - * According to the project specification there are supposed to be 2 main types of - * W3ID's ones which are tied to more permanent entities - * - * A global identifer is expected to live at the registry and starts with an \`@\` - * - * @param {boolean} isGlobal - */ - public withGlobal(isGlobal: boolean): W3IDBuilder { - this.global = isGlobal; - return this; - } + /** + * Specify whether to create a global identifier or a local identifer + * + * According to the project specification there are supposed to be 2 main types of + * W3ID's ones which are tied to more permanent entities + * + * A global identifer is expected to live at the registry and starts with an \`@\` + * + * @param {boolean} isGlobal + */ + public withGlobal(isGlobal: boolean): W3IDBuilder { + this.global = isGlobal; + return this; + } - /** - * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a - * repository in which the logs would be stored - * - * @param {StorageSpec} storage - */ - public withRepository(storage: StorageSpec): W3IDBuilder { - this.repository = storage; - return this; - } + /** + * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a + * repository in which the logs would be stored + * + * @param {StorageSpec} storage + */ + public withRepository(storage: StorageSpec): W3IDBuilder { + this.repository = storage; + return this; + } - /** - * Pre-specify a UUID to use as the W3ID - * - * @param {string} id - */ - public withId(id: string): W3IDBuilder { - this.id = id; - return this; - } + /** + * Pre-specify a UUID to use as the W3ID + * + * @param {string} id + */ + public withId(id: string): W3IDBuilder { + this.id = id; + return this; + } - /** - * Attach a keypair to the W3ID, a key attached W3ID would also need a repository - * to be added. - * - * @param {Signer} signer - */ - public withSigner(signer: Signer): W3IDBuilder { - this.signer = signer; - return this; - } + /** + * Attach a keypair to the W3ID, a key attached W3ID would also need a repository + * to be added. + * + * @param {Signer} signer + */ + public withSigner(signer: Signer): W3IDBuilder { + this.signer = signer; + return this; + } - /** - * Specify the SHA256 hash of the next key which will sign the next log entry after - * rotation of keys - * - * @param {string} hash - */ - public withNextKeyHash(hash: string): W3IDBuilder { - this.nextKeyHash = hash; - return this; - } + /** + * Specify the SHA256 hash of the next key which will sign the next log entry after + * rotation of keys + * + * @param {string} hash + */ + public withNextKeyHash(hash: string): W3IDBuilder { + this.nextKeyHash = hash; + return this; + } - /** - * Build the W3ID with provided builder options - * - * @returns Promise - */ - public async build(): Promise { - if ((this.id && this.namespace) || (this.id && this.entropy)) - throw new Error( - "Namespace and Entropy can't be specified when using pre-defined ID" - ); - this.entropy = this.entropy ?? generateRandomAlphaNum(); - this.namespace = this.namespace ?? uuidv4(); - this.id = - this.id && this.id?.includes("@") ? this.id.split("@")[1] : this.id; - const id = `${ - this.global ? "@" : "" - }${this.id ?? generateUuid(this.entropy, this.namespace)}`; - if (!this.signer) { - return new W3ID(id); - } - if (!this.repository) - throw new Error( - "Repository is required, pass with `withRepository` method" - ); - if (!this.nextKeyHash) - throw new Error( - "NextKeyHash is required pass with `withNextKeyHash` method" - ); - const logs = new IDLogManager(this.repository, this.signer); + /** + * Build the W3ID with provided builder options + * + * @returns Promise + */ + public async build(): Promise { + if ((this.id && this.namespace) || (this.id && this.entropy)) + throw new Error( + "Namespace and Entropy can't be specified when using pre-defined ID", + ); + this.entropy = this.entropy ?? generateRandomAlphaNum(); + this.namespace = this.namespace ?? uuidv4(); + this.id = this.id?.includes("@") ? this.id.split("@")[1] : this.id; + const id = `${ + this.global ? "@" : "" + }${this.id?.toString() ?? generateUuid(this.entropy, this.namespace)}`; + if (!this.signer) { + return new W3ID(id); + } + if (!this.repository) + throw new Error( + "Repository is required, pass with `withRepository` method", + ); + if (!this.nextKeyHash) + throw new Error( + "NextKeyHash is required pass with `withNextKeyHash` method", + ); + const logs = new IDLogManager(this.repository, this.signer); - const currentLogs = await logs.repository.findMany({}); - if (currentLogs.length > 0) return new W3ID(id, logs); - await logs.createLogEvent({ - id, - nextKeyHashes: [this.nextKeyHash], - }); - return new W3ID(id, logs); - } + const currentLogs = await logs.repository.findMany({}); + if (currentLogs.length > 0) return new W3ID(id, logs); + await logs.createLogEvent({ + id, + nextKeyHashes: [this.nextKeyHash], + }); + return new W3ID(id, logs); + } } From ac4ad7165a84a76d277767931cbbe195d4719d65 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 22 Apr 2025 18:20:23 +0530 Subject: [PATCH 7/7] chore: fix tests --- infrastructure/evault-core/tests/log-storage.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/evault-core/tests/log-storage.spec.ts b/infrastructure/evault-core/tests/log-storage.spec.ts index 03746d8fc..91abaed9d 100644 --- a/infrastructure/evault-core/tests/log-storage.spec.ts +++ b/infrastructure/evault-core/tests/log-storage.spec.ts @@ -16,7 +16,7 @@ describe("Neo4jLogStorage", () => { uri, neo4j.auth.basic(container.getUsername(), container.getPassword()) ); - storage = await Neo4jLogStorage.build(driver); + storage = new Neo4jLogStorage(driver); }); afterAll(async () => { @@ -79,7 +79,7 @@ describe("Neo4jLogStorage", () => { it("should throw error when log event not found", async () => { await expect(storage.findOne({ id: "non-existent-id" })).rejects.toThrow( - "Log event not found" + "No log event found with id non-existent-id" ); }); });