From 198b7b5a374744e5646bb2cd48c53c374fff2518 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 25 Nov 2025 01:24:47 +0530 Subject: [PATCH 1/7] chore: key sync on login --- .../src/lib/global/controllers/evault.ts | 2 +- .../src/routes/(auth)/login/+page.svelte | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index 8df56e1f..447f1498 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -81,7 +81,7 @@ export class VaultController { * Sync public key to eVault core * Checks if public key was already saved, calls /whois, and PATCH if needed */ - private async syncPublicKey(eName: string): Promise { + async syncPublicKey(eName: string): Promise { try { // Check if we've already saved the public key const savedKey = localStorage.getItem(`publicKeySaved_${eName}`); diff --git a/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte index 6bb33e99..d0c3dfeb 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte @@ -98,6 +98,14 @@ onMount(async () => { } // For other errors, continue to app - non-blocking } + + // Sync public key to eVault core + try { + await globalState.vaultController.syncPublicKey(vault.ename); + } catch (error) { + console.error("Error syncing public key:", error); + // Continue to app even if sync fails - non-blocking + } } } catch (error) { console.error("Error during eVault health check:", error); @@ -170,6 +178,14 @@ onMount(async () => { } // For other errors, continue to app - non-blocking } + + // Sync public key to eVault core + try { + await globalState.vaultController.syncPublicKey(vault.ename); + } catch (error) { + console.error("Error syncing public key:", error); + // Continue to app even if sync fails - non-blocking + } } } catch (error) { console.error("Error during eVault health check:", error); From d1037517ae2a9a48052a4f3c45f8eba7d813387f Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 25 Nov 2025 01:27:36 +0530 Subject: [PATCH 2/7] chore: loosen CORS for evault-core --- infrastructure/evault-core/package.json | 1 + infrastructure/evault-core/src/index.ts | 9 +++++++++ pnpm-lock.yaml | 3 +++ 3 files changed, 13 insertions(+) diff --git a/infrastructure/evault-core/package.json b/infrastructure/evault-core/package.json index 94da22be..6342e050 100644 --- a/infrastructure/evault-core/package.json +++ b/infrastructure/evault-core/package.json @@ -16,6 +16,7 @@ "migration:revert": "npm run typeorm migration:revert -- -d dist/config/database.js" }, "dependencies": { + "@fastify/cors": "^8.5.0", "@fastify/formbody": "^8.0.2", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^3.0.0", diff --git a/infrastructure/evault-core/src/index.ts b/infrastructure/evault-core/src/index.ts index 259d388f..96d8313f 100644 --- a/infrastructure/evault-core/src/index.ts +++ b/infrastructure/evault-core/src/index.ts @@ -21,6 +21,7 @@ import fastify, { FastifyRequest, FastifyReply, } from "fastify"; +import fastifyCors from "@fastify/cors"; import { renderVoyagerPage } from "graphql-voyager/middleware"; import { connectWithRetry } from "./core/db/retry-neo4j"; import neo4j, { Driver } from "neo4j-driver"; @@ -116,6 +117,14 @@ const initializeEVault = async (provisioningServiceInstance?: ProvisioningServic logger: true, }); + // Register CORS plugin with relaxed settings + await fastifyServer.register(fastifyCors, { + origin: true, // Allow all origins + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "X-ENAME"], + credentials: true, + }); + // Register HTTP routes with provisioning service if available await registerHttpRoutes(fastifyServer, evaultInstance, provisioningServiceInstance, dbService); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eace876c..63dff12c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -365,6 +365,9 @@ importers: infrastructure/evault-core: dependencies: + '@fastify/cors': + specifier: ^8.5.0 + version: 8.5.0 '@fastify/formbody': specifier: ^8.0.2 version: 8.0.2 From 157a68512622f62da682529cac202fda95795b75 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 25 Nov 2025 01:32:10 +0530 Subject: [PATCH 3/7] fix: default key ID --- .../src/lib/global/controllers/evault.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index 447f1498..83f225aa 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -118,10 +118,25 @@ export class VaultController { return; } - // Get public key from KeyService - const publicKey = await this.#keyService.getPublicKey(eName, "signing"); + // Get public key using the exact same logic as onboarding/verification flow + // KEY_ID is always "default", context depends on whether user is pre-verification + const KEY_ID = "default"; + + // Determine context: check if user is pre-verification (fake/demo user) + const isFake = await this.#userController.isFake; + const context = isFake ? "pre-verification" : "onboarding"; + + // Get public key using the same method as getApplicationPublicKey() in onboarding/verify + let publicKey: string | undefined; + try { + publicKey = await this.#keyService.getPublicKey(KEY_ID, context); + } catch (error) { + console.error(`Failed to get public key for ${KEY_ID} with context ${context}:`, error); + return; + } + if (!publicKey) { - console.warn(`No public key found for ${eName}, cannot sync`); + console.warn(`No public key found for ${KEY_ID} with context ${context}, cannot sync`); return; } From dc95e1cda89f8e9ab094072e5631a2cbef43c6f0 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 25 Nov 2025 01:41:56 +0530 Subject: [PATCH 4/7] chore: EID_WALLET_TOKEN .env variable --- infrastructure/eid-wallet/src/env.d.ts | 1 + .../src/lib/global/controllers/evault.ts | 16 +++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/infrastructure/eid-wallet/src/env.d.ts b/infrastructure/eid-wallet/src/env.d.ts index 103b229d..a394bb47 100644 --- a/infrastructure/eid-wallet/src/env.d.ts +++ b/infrastructure/eid-wallet/src/env.d.ts @@ -5,4 +5,5 @@ declare namespace App {} declare module "$env/static/public" { export const PUBLIC_REGISTRY_URL: string; export const PUBLIC_PROVISIONER_URL: string; + export const EID_WALLET_TOKEN: string; } diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index 83f225aa..7b1fa9ad 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -1,4 +1,4 @@ -import { PUBLIC_REGISTRY_URL, PUBLIC_PROVISIONER_URL } from "$env/static/public"; +import { PUBLIC_REGISTRY_URL, PUBLIC_PROVISIONER_URL, EID_WALLET_TOKEN } from "$env/static/public"; import type { Store } from "@tauri-apps/plugin-store"; import axios from "axios"; import { GraphQLClient } from "graphql-request"; @@ -140,16 +140,10 @@ export class VaultController { return; } - // Get authentication token from registry - let authToken: string | null = null; - try { - const entropyResponse = await axios.get( - new URL("/entropy", PUBLIC_REGISTRY_URL).toString() - ); - authToken = entropyResponse.data?.token || null; - } catch (error) { - console.error("Failed to get auth token from registry:", error); - // Continue without token - server will reject if auth is required + // Get authentication token from environment variable + const authToken = EID_WALLET_TOKEN || null; + if (!authToken) { + console.warn("EID_WALLET_TOKEN not set, request may fail authentication"); } // Call PATCH /public-key to save the public key From b1ebf311c5e011253f8795c9d374ef52126c200e Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 25 Nov 2025 01:45:29 +0530 Subject: [PATCH 5/7] chore: apply changes for .env --- infrastructure/eid-wallet/src/env.d.ts | 2 +- .../src/lib/global/controllers/evault.ts | 6 +- .../evault-core/generate-test-token.js | 68 +++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 infrastructure/evault-core/generate-test-token.js diff --git a/infrastructure/eid-wallet/src/env.d.ts b/infrastructure/eid-wallet/src/env.d.ts index a394bb47..af2f5ae3 100644 --- a/infrastructure/eid-wallet/src/env.d.ts +++ b/infrastructure/eid-wallet/src/env.d.ts @@ -5,5 +5,5 @@ declare namespace App {} declare module "$env/static/public" { export const PUBLIC_REGISTRY_URL: string; export const PUBLIC_PROVISIONER_URL: string; - export const EID_WALLET_TOKEN: string; + export const PUBLIC_EID_WALLET_TOKEN: string; } diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index 7b1fa9ad..85aa4aa0 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -1,4 +1,4 @@ -import { PUBLIC_REGISTRY_URL, PUBLIC_PROVISIONER_URL, EID_WALLET_TOKEN } from "$env/static/public"; +import { PUBLIC_REGISTRY_URL, PUBLIC_PROVISIONER_URL, PUBLIC_EID_WALLET_TOKEN } from "$env/static/public"; import type { Store } from "@tauri-apps/plugin-store"; import axios from "axios"; import { GraphQLClient } from "graphql-request"; @@ -141,9 +141,9 @@ export class VaultController { } // Get authentication token from environment variable - const authToken = EID_WALLET_TOKEN || null; + const authToken = PUBLIC_EID_WALLET_TOKEN || null; if (!authToken) { - console.warn("EID_WALLET_TOKEN not set, request may fail authentication"); + console.warn("PUBLIC_EID_WALLET_TOKEN not set, request may fail authentication"); } // Call PATCH /public-key to save the public key diff --git a/infrastructure/evault-core/generate-test-token.js b/infrastructure/evault-core/generate-test-token.js new file mode 100644 index 00000000..3d9e1254 --- /dev/null +++ b/infrastructure/evault-core/generate-test-token.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +/** + * Script to generate a test JWT token for evault-core authentication + * + * Usage: + * REGISTRY_ENTROPY_KEY_JWK='{"kty":"EC",...}' node generate-test-token.js + * + * Or set the environment variable in a .env file + * + * You can also use tsx to run it: + * REGISTRY_ENTROPY_KEY_JWK='...' npx tsx generate-test-token.js + */ + +const { importJWK, SignJWT } = require("jose"); +const dotenv = require("dotenv"); +const path = require("path"); + +// Load .env file if it exists (try both root and current directory) +dotenv.config({ path: path.resolve(__dirname, "../../../.env") }); +dotenv.config({ path: path.resolve(__dirname, ".env") }); + +async function generateTestToken() { + const jwkString = '{"kty":"EC","use":"sig","alg":"ES256","kid":"entropy-key-1","crv":"P-256","x":"POWUVJwOulAW0gheTVUHF4nXUenMCg0jxhGKI8M1LLU","y":"Cb4GC8Tt0gW7zMr-DhsDJisGVNgWttwjnyQl1HyU7hg","d":"FWqvWBzoiZcD0hh4JAMdJ7foxFRGqyV2Ei_eWvr1Si4"}'; + + if (!jwkString) { + console.error("Error: REGISTRY_ENTROPY_KEY_JWK environment variable is required"); + console.error("\nUsage:"); + console.error(" REGISTRY_ENTROPY_KEY_JWK='' node generate-test-token.js"); + console.error("\nOr set it in your .env file"); + process.exit(1); + } + + try { + const jwk = JSON.parse(jwkString); + const privateKey = await importJWK(jwk, "ES256"); + + // Generate token valid for 1 year + const token = await new SignJWT({ + // Add any custom claims you want + platform: "eid-wallet", + purpose: "public-key-sync", + }) + .setProtectedHeader({ + alg: "ES256", + kid: jwk.kid || "entropy-key-1", + }) + .setIssuedAt() + .setExpirationTime("1y") // Valid for 1 year + .sign(privateKey); + + console.log("\nāœ… Generated JWT Token (valid for 1 year):\n"); + console.log(token); + console.log("\nšŸ“‹ Use this token in the Authorization header:"); + console.log(` Authorization: Bearer ${token}\n`); + + return token; + } catch (error) { + console.error("Error generating token:", error.message); + if (error.message.includes("JSON")) { + console.error("\nMake sure REGISTRY_ENTROPY_KEY_JWK is valid JSON"); + } + process.exit(1); + } +} + +generateTestToken(); + From 56aceacdbb953947edc1f9c0264b925d31156731 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 25 Nov 2025 02:01:43 +0530 Subject: [PATCH 6/7] feat: signature validator library --- .../src/lib/global/controllers/evault.ts | 46 +++- .../eid-wallet/src/lib/global/state.ts | 6 +- .../src/routes/(auth)/login/+page.svelte | 8 +- .../evault-core/src/core/http/server.ts | 42 ++- infrastructure/signature-validator/.gitignore | 5 + infrastructure/signature-validator/README.md | 67 +++++ .../signature-validator/package.json | 21 ++ .../signature-validator/src/index.ts | 242 ++++++++++++++++++ .../signature-validator/tsconfig.json | 18 ++ .../blabsy/src/components/common/seo.tsx | 6 +- .../blabsy/src/lib/context/theme-context.tsx | 4 +- 11 files changed, 430 insertions(+), 35 deletions(-) create mode 100644 infrastructure/signature-validator/.gitignore create mode 100644 infrastructure/signature-validator/README.md create mode 100644 infrastructure/signature-validator/package.json create mode 100644 infrastructure/signature-validator/src/index.ts create mode 100644 infrastructure/signature-validator/tsconfig.json diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index 85aa4aa0..917f6ab4 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -1,4 +1,8 @@ -import { PUBLIC_REGISTRY_URL, PUBLIC_PROVISIONER_URL, PUBLIC_EID_WALLET_TOKEN } from "$env/static/public"; +import { + PUBLIC_REGISTRY_URL, + PUBLIC_PROVISIONER_URL, + PUBLIC_EID_WALLET_TOKEN, +} from "$env/static/public"; import type { Store } from "@tauri-apps/plugin-store"; import axios from "axios"; import { GraphQLClient } from "graphql-request"; @@ -52,7 +56,11 @@ export class VaultController { #profileCreationStatus: "idle" | "loading" | "success" | "failed" = "idle"; #notificationService: NotificationService; - constructor(store: Store, userController: UserController, keyService?: KeyService) { + constructor( + store: Store, + userController: UserController, + keyService?: KeyService, + ) { this.#store = store; this.#userController = userController; this.#keyService = keyService || null; @@ -86,12 +94,16 @@ export class VaultController { // Check if we've already saved the public key const savedKey = localStorage.getItem(`publicKeySaved_${eName}`); if (savedKey === "true") { - console.log(`Public key already saved for ${eName}, skipping sync`); + console.log( + `Public key already saved for ${eName}, skipping sync`, + ); return; } if (!this.#keyService) { - console.warn("KeyService not available, cannot sync public key"); + console.warn( + "KeyService not available, cannot sync public key", + ); return; } @@ -121,7 +133,7 @@ export class VaultController { // Get public key using the exact same logic as onboarding/verification flow // KEY_ID is always "default", context depends on whether user is pre-verification const KEY_ID = "default"; - + // Determine context: check if user is pre-verification (fake/demo user) const isFake = await this.#userController.isFake; const context = isFake ? "pre-verification" : "onboarding"; @@ -129,21 +141,31 @@ export class VaultController { // Get public key using the same method as getApplicationPublicKey() in onboarding/verify let publicKey: string | undefined; try { - publicKey = await this.#keyService.getPublicKey(KEY_ID, context); + publicKey = await this.#keyService.getPublicKey( + KEY_ID, + context, + ); } catch (error) { - console.error(`Failed to get public key for ${KEY_ID} with context ${context}:`, error); + console.error( + `Failed to get public key for ${KEY_ID} with context ${context}:`, + error, + ); return; } if (!publicKey) { - console.warn(`No public key found for ${KEY_ID} with context ${context}, cannot sync`); + console.warn( + `No public key found for ${KEY_ID} with context ${context}, cannot sync`, + ); return; } // Get authentication token from environment variable const authToken = PUBLIC_EID_WALLET_TOKEN || null; if (!authToken) { - console.warn("PUBLIC_EID_WALLET_TOKEN not set, request may fail authentication"); + console.warn( + "PUBLIC_EID_WALLET_TOKEN not set, request may fail authentication", + ); } // Call PATCH /public-key to save the public key @@ -157,11 +179,7 @@ export class VaultController { headers["Authorization"] = `Bearer ${authToken}`; } - await axios.patch( - patchUrl, - { publicKey }, - { headers } - ); + await axios.patch(patchUrl, { publicKey }, { headers }); // Mark as saved localStorage.setItem(`publicKeySaved_${eName}`, "true"); diff --git a/infrastructure/eid-wallet/src/lib/global/state.ts b/infrastructure/eid-wallet/src/lib/global/state.ts index 5a1fc67d..cbe10731 100644 --- a/infrastructure/eid-wallet/src/lib/global/state.ts +++ b/infrastructure/eid-wallet/src/lib/global/state.ts @@ -35,7 +35,11 @@ export class GlobalState { this.securityController = new SecurityController(store); this.userController = new UserController(store); this.keyService = keyService; - this.vaultController = new VaultController(store, this.userController, keyService); + this.vaultController = new VaultController( + store, + this.userController, + keyService, + ); this.notificationService = NotificationService.getInstance(); } diff --git a/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte index d0c3dfeb..40b09162 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte @@ -101,7 +101,9 @@ onMount(async () => { // Sync public key to eVault core try { - await globalState.vaultController.syncPublicKey(vault.ename); + await globalState.vaultController.syncPublicKey( + vault.ename, + ); } catch (error) { console.error("Error syncing public key:", error); // Continue to app even if sync fails - non-blocking @@ -181,7 +183,9 @@ onMount(async () => { // Sync public key to eVault core try { - await globalState.vaultController.syncPublicKey(vault.ename); + await globalState.vaultController.syncPublicKey( + vault.ename, + ); } catch (error) { console.error("Error syncing public key:", error); // Continue to app even if sync fails - non-blocking diff --git a/infrastructure/evault-core/src/core/http/server.ts b/infrastructure/evault-core/src/core/http/server.ts index c45bd7b3..898901de 100644 --- a/infrastructure/evault-core/src/core/http/server.ts +++ b/infrastructure/evault-core/src/core/http/server.ts @@ -286,30 +286,50 @@ export async function registerHttpRoutes( // Helper function to validate JWT token async function validateToken(authHeader: string | null): Promise { if (!authHeader || !authHeader.startsWith("Bearer ")) { + console.error("Token validation: Missing or invalid Authorization header format"); return null; } const token = authHeader.substring(7); // Remove 'Bearer ' prefix try { - if (!process.env.REGISTRY_URL) { - console.error("REGISTRY_URL is not set"); + // Try REGISTRY_URL first, fallback to PUBLIC_REGISTRY_URL + const registryUrl = process.env.REGISTRY_URL || process.env.PUBLIC_REGISTRY_URL; + if (!registryUrl) { + console.error("Token validation: REGISTRY_URL or PUBLIC_REGISTRY_URL is not set"); return null; } - const jwksResponse = await axios.get( - new URL( - `/.well-known/jwks.json`, - process.env.REGISTRY_URL - ).toString() - ); + const jwksUrl = new URL(`/.well-known/jwks.json`, registryUrl).toString(); + console.log(`Token validation: Fetching JWKS from ${jwksUrl}`); + + const jwksResponse = await axios.get(jwksUrl, { + timeout: 5000, + }); + console.log(`Token validation: JWKS response keys count: ${jwksResponse.data?.keys?.length || 0}`); + const JWKS = jose.createLocalJWKSet(jwksResponse.data); + + // Decode token header to see what kid it's using + const decodedHeader = jose.decodeProtectedHeader(token); + console.log(`Token validation: Token header - alg: ${decodedHeader.alg}, kid: ${decodedHeader.kid}`); + const { payload } = await jose.jwtVerify(token, JWKS); - + + console.log(`Token validation: Token verified successfully, payload:`, payload); return payload; - } catch (error) { - console.error("Token validation failed:", error); + } catch (error: any) { + console.error("Token validation failed:", error.message || error); + if (error.code) { + console.error(`Token validation error code: ${error.code}`); + } + if (error.response) { + console.error(`Token validation HTTP error: ${error.response.status} - ${error.response.statusText}`); + } + if (error.cause) { + console.error(`Token validation error cause:`, error.cause); + } return null; } } diff --git a/infrastructure/signature-validator/.gitignore b/infrastructure/signature-validator/.gitignore new file mode 100644 index 00000000..d451c1bb --- /dev/null +++ b/infrastructure/signature-validator/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.log +.DS_Store + diff --git a/infrastructure/signature-validator/README.md b/infrastructure/signature-validator/README.md new file mode 100644 index 00000000..d66f7d6d --- /dev/null +++ b/infrastructure/signature-validator/README.md @@ -0,0 +1,67 @@ +# Signature Validator + +A TypeScript library for verifying signatures using public keys retrieved from eVault. + +## Installation + +```bash +npm install +npm run build +``` + +## Usage + +```typescript +import { verifySignature } from "signature-validator"; + +const result = await verifySignature({ + eName: "@user.w3id", + signature: "z...", // multibase encoded signature + payload: "message to verify", + registryBaseUrl: "https://registry.example.com" +}); + +if (result.valid) { + console.log("Signature is valid!"); +} else { + console.error("Signature invalid:", result.error); +} +``` + +## API + +### `verifySignature(options: VerifySignatureOptions): Promise` + +Verifies a signature by: +1. Resolving the eVault URL from the registry using the eName +2. Fetching the public key from the eVault `/whois` endpoint +3. Decoding the multibase-encoded public key +4. Verifying the signature using Web Crypto API + +#### Parameters + +- `eName`: The eName (W3ID) of the user +- `signature`: The signature to verify (multibase encoded string, supports 'z' prefix for base58btc or base64) +- `payload`: The payload that was signed (string) +- `registryBaseUrl`: Base URL of the registry service + +#### Returns + +- `valid`: Boolean indicating if the signature is valid +- `error`: Error message if verification failed +- `publicKey`: The public key that was used for verification + +## Public Key Format + +Public keys are expected to be in multibase format starting with 'z': +- `z` prefix indicates multibase encoding +- Supports base58btc (standard) or hex encoding + +Example: `z3059301306072a8648ce3d020106082a8648ce3d03010703420004a16b063e785d25945c44ae2e7a4cbd94c3316533427261244f696609d6afb848155b9016ad8d5c9ec59053b3b2cf2511af0c2414fc53d2abf96323bb1a031902` + +## Signature Format + +Signatures can be: +- Multibase encoded (starting with 'z' for base58btc) +- Base64 encoded (for software keys) + diff --git a/infrastructure/signature-validator/package.json b/infrastructure/signature-validator/package.json new file mode 100644 index 00000000..e9e0ca63 --- /dev/null +++ b/infrastructure/signature-validator/package.json @@ -0,0 +1,21 @@ +{ + "name": "signature-validator", + "version": "1.0.0", + "description": "Library for verifying signatures using public keys from eVault", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "vitest", + "dev": "tsc --watch" + }, + "dependencies": { + "axios": "^1.6.7", + "multiformats": "^13.3.2" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "typescript": "^5.3.3", + "vitest": "^1.6.1" + } +} \ No newline at end of file diff --git a/infrastructure/signature-validator/src/index.ts b/infrastructure/signature-validator/src/index.ts new file mode 100644 index 00000000..5690b0eb --- /dev/null +++ b/infrastructure/signature-validator/src/index.ts @@ -0,0 +1,242 @@ +import axios from "axios"; +import { base58btc } from "multiformats/bases/base58"; + +/** + * Options for signature verification + */ +export interface VerifySignatureOptions { + /** The eName (W3ID) of the user */ + eName: string; + /** The signature to verify (multibase encoded string) */ + signature: string; + /** The payload that was signed (string) */ + payload: string; + /** Base URL of the registry service */ + registryBaseUrl: string; +} + +/** + * Result of signature verification + */ +export interface VerifySignatureResult { + /** Whether the signature is valid */ + valid: boolean; + /** Error message if verification failed */ + error?: string; + /** The public key that was used for verification */ + publicKey?: string; +} + +/** + * Decodes a multibase-encoded public key + * Supports 'z' prefix for base58btc or hex encoding + * Based on the format used in SoftwareKeyManager: 'z' + hex + */ +function decodeMultibasePublicKey(multibaseKey: string): Uint8Array { + if (!multibaseKey.startsWith("z")) { + throw new Error("Public key must start with 'z' multibase prefix"); + } + + const encoded = multibaseKey.slice(1); // Remove 'z' prefix + + // Try hex first (as used in SoftwareKeyManager: 'z' + hex) + // Check if it looks like hex (only contains 0-9, a-f, A-F) + if (/^[0-9a-fA-F]+$/.test(encoded)) { + try { + if (encoded.length % 2 !== 0) { + throw new Error("Hex string must have even length"); + } + const bytes = new Uint8Array(encoded.length / 2); + for (let i = 0; i < encoded.length; i += 2) { + bytes[i / 2] = Number.parseInt(encoded.slice(i, i + 2), 16); + } + return bytes; + } catch (hexError) { + // Fall through to try base58btc + } + } + + // Try base58btc (standard multibase 'z' prefix) + try { + return base58btc.decode(encoded); + } catch (error) { + throw new Error( + `Failed to decode multibase public key. Tried hex and base58btc. Error: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +/** + * Decodes a multibase-encoded signature + * Supports base58btc or base64 + */ +function decodeSignature(signature: string): Uint8Array { + // If it starts with 'z', it's multibase base58btc + if (signature.startsWith("z")) { + try { + return base58btc.decode(signature.slice(1)); + } catch (error) { + throw new Error(`Failed to decode multibase signature: ${error instanceof Error ? error.message : String(error)}`); + } + } + + // Otherwise, try base64 (software keys return base64) + try { + const binaryString = atob(signature); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } catch (error) { + throw new Error(`Failed to decode signature as base64: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Retrieves the public key for a given eName + * @param eName - The eName (W3ID) of the user + * @param registryBaseUrl - Base URL of the registry service + * @returns The public key in multibase format + */ +async function getPublicKey(eName: string, registryBaseUrl: string): Promise { + // Step 1: Resolve eVault URL from registry + const resolveUrl = new URL(`/resolve?w3id=${encodeURIComponent(eName)}`, registryBaseUrl).toString(); + const resolveResponse = await axios.get(resolveUrl, { + timeout: 10000, + }); + + if (!resolveResponse.data?.uri) { + throw new Error(`Failed to resolve eVault URL for eName: ${eName}`); + } + + const evaultUrl = resolveResponse.data.uri; + + // Step 2: Get public key from eVault /whois endpoint + const whoisUrl = new URL("/whois", evaultUrl).toString(); + const whoisResponse = await axios.get(whoisUrl, { + headers: { + "X-ENAME": eName, + }, + timeout: 10000, + }); + + const publicKey = whoisResponse.data?.publicKey; + if (!publicKey) { + throw new Error(`No public key found for eName: ${eName}`); + } + + return publicKey; +} + +/** + * Verifies a signature using a public key from eVault + * + * @param options - Verification options + * @returns Promise resolving to verification result + * + * @example + * ```ts + * const result = await verifySignature({ + * eName: "@user.w3id", + * signature: "z...", + * payload: "message to verify", + * registryBaseUrl: "https://registry.example.com" + * }); + * + * if (result.valid) { + * console.log("Signature is valid!"); + * } else { + * console.error("Signature invalid:", result.error); + * } + * ``` + */ +export async function verifySignature( + options: VerifySignatureOptions +): Promise { + try { + const { eName, signature, payload, registryBaseUrl } = options; + + if (!eName) { + return { + valid: false, + error: "eName is required", + }; + } + + if (!signature) { + return { + valid: false, + error: "signature is required", + }; + } + + if (!payload) { + return { + valid: false, + error: "payload is required", + }; + } + + if (!registryBaseUrl) { + return { + valid: false, + error: "registryBaseUrl is required", + }; + } + + // Get public key from eVault + const publicKeyMultibase = await getPublicKey(eName, registryBaseUrl); + + // Decode the public key + const publicKeyBytes = decodeMultibasePublicKey(publicKeyMultibase); + + // Import the public key for Web Crypto API + // The public key is in SPKI format (SubjectPublicKeyInfo) + // Create a new ArrayBuffer from the Uint8Array + const publicKeyBuffer = new Uint8Array(publicKeyBytes).buffer; + + const publicKey = await crypto.subtle.importKey( + "spki", + publicKeyBuffer, + { + name: "ECDSA", + namedCurve: "P-256", + }, + false, + ["verify"] + ); + + // Decode the signature + const signatureBytes = decodeSignature(signature); + + // Convert payload to ArrayBuffer + const payloadBuffer = new TextEncoder().encode(payload); + + // Create a new ArrayBuffer from the signature Uint8Array + const signatureBuffer = new Uint8Array(signatureBytes).buffer; + + // Verify the signature + const isValid = await crypto.subtle.verify( + { + name: "ECDSA", + hash: "SHA-256", + }, + publicKey, + signatureBuffer, + payloadBuffer + ); + + return { + valid: isValid, + publicKey: publicKeyMultibase, + error: isValid ? undefined : "Signature verification failed", + }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + diff --git a/infrastructure/signature-validator/tsconfig.json b/infrastructure/signature-validator/tsconfig.json new file mode 100644 index 00000000..23b61758 --- /dev/null +++ b/infrastructure/signature-validator/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": "src", + "sourceMap": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/platforms/blabsy/src/components/common/seo.tsx b/platforms/blabsy/src/components/common/seo.tsx index 71f3de5f..5e0ea509 100644 --- a/platforms/blabsy/src/components/common/seo.tsx +++ b/platforms/blabsy/src/components/common/seo.tsx @@ -8,11 +8,7 @@ type SEOProps = { description?: string; }; -export function SEO({ - title, - image, - description -}: SEOProps): JSX.Element { +export function SEO({ title, image, description }: SEOProps): JSX.Element { const { asPath } = useRouter(); return ( diff --git a/platforms/blabsy/src/lib/context/theme-context.tsx b/platforms/blabsy/src/lib/context/theme-context.tsx index 12e9885c..1e53926b 100644 --- a/platforms/blabsy/src/lib/context/theme-context.tsx +++ b/platforms/blabsy/src/lib/context/theme-context.tsx @@ -57,7 +57,7 @@ export function ThemeContextProvider({ const root = document.documentElement; // Always use dark theme const forcedTheme: Theme = 'dark'; - + // Always ensure dark class is present and never remove it root.classList.add('dark'); // Prevent any accidental removal @@ -95,7 +95,7 @@ export function ThemeContextProvider({ // Ensure dark class is always applied on mount and updates const root = document.documentElement; root.classList.add('dark'); - + return () => clearTimeout(timeoutId); }, [userId]); From e6921306a098eaa4f6f0e7d6fd74c374d29d1c7c Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Tue, 25 Nov 2025 02:07:46 +0530 Subject: [PATCH 7/7] chore: fix lockfile --- pnpm-lock.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63dff12c..3015fe36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -475,6 +475,25 @@ importers: specifier: ^1.6.1 version: 1.6.1(@types/node@20.16.11)(jsdom@19.0.0(bufferutil@4.0.9))(lightningcss@1.30.2)(sass@1.94.1) + infrastructure/signature-validator: + dependencies: + axios: + specifier: ^1.6.7 + version: 1.13.2 + multiformats: + specifier: ^13.3.2 + version: 13.4.1 + devDependencies: + '@types/node': + specifier: ^20.11.24 + version: 20.16.11 + typescript: + specifier: ^5.3.3 + version: 5.8.2 + vitest: + specifier: ^1.6.1 + version: 1.6.1(@types/node@20.16.11)(jsdom@19.0.0(bufferutil@4.0.9))(lightningcss@1.30.2)(sass@1.94.1) + infrastructure/w3id: dependencies: canonicalize: