Skip to content

Commit aad82bc

Browse files
authored
feat: key binding certs instead of direct public keys (#568)
1 parent 90e811d commit aad82bc

File tree

14 files changed

+381
-106
lines changed

14 files changed

+381
-106
lines changed

infrastructure/control-panel/src/routes/+layout.svelte

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@
1515
{ label: 'Actions', href: '/actions' }
1616
];
1717
18-
const isActive = (href: string) =>
19-
href === '/' ? pageUrl === '/' : pageUrl.startsWith(href);
18+
const isActive = (href: string) => (href === '/' ? pageUrl === '/' : pageUrl.startsWith(href));
2019
</script>
2120

2221
<main class="flex min-h-screen bg-gray-50">
23-
<aside class="w-64 border-black-100 border-r bg-white px-8 py-10">
22+
<aside class="border-black-100 w-64 border-r bg-white px-8 py-10">
2423
<h4 class="text-lg font-semibold text-black">Navigation</h4>
2524
<nav class="mt-6 flex flex-col gap-2">
2625
{#each navLinks as link}

infrastructure/control-panel/src/routes/actions/+page.svelte

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@
6767
</script>
6868

6969
<section class="max-w-3xl space-y-6">
70-
<div class="rounded-2xl border border-black-100 bg-white px-8 py-6 shadow-sm">
70+
<div class="border-black-100 rounded-2xl border bg-white px-8 py-6 shadow-sm">
7171
<h2 class="text-2xl font-semibold text-black">Actions</h2>
72-
<p class="mt-2 text-black-700">
73-
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the control panel.
72+
<p class="text-black-700 mt-2">
73+
Manual actions for orchestrating DreamSync. Trigger matchmaking directly from the
74+
control panel.
7475
</p>
7576

7677
<div class="mt-6 space-y-4">
@@ -87,16 +88,17 @@
8788
Trigger DreamSync
8889
</ButtonAction>
8990
</div>
90-
<p class="text-sm text-black-500">
91-
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process to finish.
91+
<p class="text-black-500 text-sm">
92+
Note: Matchmaking can take up to 4 minutes to complete. Please wait for the process
93+
to finish.
9294
</p>
9395
</div>
9496
</div>
9597
</section>
9698

9799
{#if toast}
98100
<div
99-
class={`fixed right-6 top-24 z-50 rounded-lg border px-4 py-3 shadow-lg ${
101+
class={`fixed top-24 right-6 z-50 rounded-lg border px-4 py-3 shadow-lg ${
100102
toast.variant === 'success'
101103
? 'border-green-200 bg-green-100 text-green-800'
102104
: 'border-danger-200 bg-danger-100 text-danger-500'
@@ -105,4 +107,3 @@
105107
<p class="font-semibold">{toast.message}</p>
106108
</div>
107109
{/if}
108-

infrastructure/eid-wallet/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"graphql-request": "^6.1.0",
4343
"html5-qrcode": "^2.3.8",
4444
"import": "^0.0.6",
45+
"jose": "^5.2.0",
4546
"svelte-loading-spinners": "^0.3.6",
4647
"svelte-qrcode": "^1.0.1",
4748
"tailwind-merge": "^3.0.2",

infrastructure/eid-wallet/src/lib/global/controllers/evault.ts

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import type { Store } from "@tauri-apps/plugin-store";
77
import axios from "axios";
88
import { GraphQLClient } from "graphql-request";
9+
import * as jose from "jose";
910
import NotificationService from "../../services/NotificationService";
1011
import type { KeyService } from "./key";
1112
import type { UserController } from "./user";
@@ -122,22 +123,94 @@ export class VaultController {
122123
},
123124
});
124125

125-
const existingPublicKey = whoisResponse.data?.publicKey;
126-
if (existingPublicKey) {
127-
// Public key already exists, mark as saved
128-
localStorage.setItem(`publicKeySaved_${eName}`, "true");
129-
console.log(`Public key already exists for ${eName}`);
130-
return;
131-
}
126+
// Get key binding certificates array from whois response
127+
const keyBindingCertificates =
128+
whoisResponse.data?.keyBindingCertificates;
132129

133-
// Get public key using the exact same logic as onboarding/verification flow
134-
// KEY_ID is always "default", context depends on whether user is pre-verification
130+
// Get current device's public key to check if it already exists
135131
const KEY_ID = "default";
136-
137-
// Determine context: check if user is pre-verification (fake/demo user)
138132
const isFake = await this.#userController.isFake;
139133
const context = isFake ? "pre-verification" : "onboarding";
140134

135+
let currentPublicKey: string | undefined;
136+
try {
137+
currentPublicKey = await this.#keyService.getPublicKey(
138+
KEY_ID,
139+
context,
140+
);
141+
} catch (error) {
142+
console.error(
143+
"Failed to get current public key for comparison:",
144+
error,
145+
);
146+
// Continue to sync anyway
147+
}
148+
149+
// If we have certificates and current key, check if it already exists
150+
if (
151+
keyBindingCertificates &&
152+
Array.isArray(keyBindingCertificates) &&
153+
keyBindingCertificates.length > 0 &&
154+
currentPublicKey
155+
) {
156+
try {
157+
// Get registry JWKS for JWT verification
158+
const registryUrl = PUBLIC_REGISTRY_URL;
159+
if (registryUrl) {
160+
const jwksUrl = new URL(
161+
"/.well-known/jwks.json",
162+
registryUrl,
163+
).toString();
164+
const jwksResponse = await axios.get(jwksUrl, {
165+
timeout: 10000,
166+
});
167+
const JWKS = jose.createLocalJWKSet(jwksResponse.data);
168+
169+
// Extract public keys from certificates and check if current key exists
170+
for (const jwt of keyBindingCertificates) {
171+
try {
172+
const { payload } = await jose.jwtVerify(
173+
jwt,
174+
JWKS,
175+
);
176+
177+
// Verify ename matches
178+
if (payload.ename !== eName) {
179+
continue;
180+
}
181+
182+
// Extract publicKey from JWT payload
183+
const extractedPublicKey =
184+
payload.publicKey as string;
185+
if (extractedPublicKey === currentPublicKey) {
186+
// Current device's key already exists, mark as saved
187+
localStorage.setItem(
188+
`publicKeySaved_${eName}`,
189+
"true",
190+
);
191+
console.log(
192+
`Public key already exists for ${eName}`,
193+
);
194+
return;
195+
}
196+
} catch (error) {
197+
// JWT verification failed, try next certificate
198+
console.warn(
199+
"Failed to verify key binding certificate:",
200+
error,
201+
);
202+
}
203+
}
204+
}
205+
} catch (error) {
206+
console.error(
207+
"Error checking existing public keys:",
208+
error,
209+
);
210+
// Continue to sync anyway
211+
}
212+
}
213+
141214
console.log("=".repeat(70));
142215
console.log("🔄 [VaultController] syncPublicKey called");
143216
console.log("=".repeat(70));

infrastructure/evault-core/src/core/db/db.service.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -676,43 +676,53 @@ export class DbService {
676676
}
677677

678678
/**
679-
* Gets the public key for a given eName.
679+
* Gets all public keys for a given eName.
680680
* @param eName - The eName identifier
681-
* @returns The public key string, or null if not found
681+
* @returns Array of public key strings, or empty array if not found
682682
*/
683-
async getPublicKey(eName: string): Promise<string | null> {
683+
async getPublicKeys(eName: string): Promise<string[]> {
684684
if (!eName) {
685-
throw new Error("eName is required for getting public key");
685+
throw new Error("eName is required for getting public keys");
686686
}
687687

688688
const result = await this.runQueryInternal(
689-
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
689+
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
690690
{ eName },
691691
);
692692

693693
if (!result.records[0]) {
694-
return null;
694+
return [];
695695
}
696696

697-
return result.records[0].get("publicKey") || null;
697+
const publicKeys = result.records[0].get("publicKeys");
698+
// Handle null/undefined and ensure we return an array
699+
if (!publicKeys || !Array.isArray(publicKeys)) {
700+
return [];
701+
}
702+
703+
return publicKeys;
698704
}
699705

700706
/**
701-
* Sets or updates the public key for a given eName.
707+
* Adds a public key to the array for a given eName (appends, avoids duplicates).
702708
* @param eName - The eName identifier
703-
* @param publicKey - The public key to store
709+
* @param publicKey - The public key to add
704710
*/
705-
async setPublicKey(eName: string, publicKey: string): Promise<void> {
711+
async addPublicKey(eName: string, publicKey: string): Promise<void> {
706712
if (!eName) {
707-
throw new Error("eName is required for setting public key");
713+
throw new Error("eName is required for adding public key");
708714
}
709715
if (!publicKey) {
710716
throw new Error("publicKey is required");
711717
}
712718

719+
// Use MERGE to create User if doesn't exist, then append publicKey if not already in array
713720
await this.runQueryInternal(
714721
`MERGE (u:User { eName: $eName })
715-
SET u.publicKey = $publicKey`,
722+
ON CREATE SET u.publicKeys = []
723+
WITH u
724+
WHERE NOT $publicKey IN u.publicKeys
725+
SET u.publicKeys = u.publicKeys + $publicKey`,
716726
{ eName, publicKey },
717727
);
718728
}
@@ -803,26 +813,26 @@ export class DbService {
803813
}
804814
}
805815

806-
// Copy User node with public key if it exists
816+
// Copy User node with public keys if it exists
807817
try {
808818
const userResult = await this.runQueryInternal(
809-
`MATCH (u:User { eName: $eName }) RETURN u.publicKey AS publicKey`,
819+
`MATCH (u:User { eName: $eName }) RETURN u.publicKeys AS publicKeys`,
810820
{ eName },
811821
);
812822

813823
if (userResult.records.length > 0) {
814-
const publicKey = userResult.records[0].get("publicKey");
815-
if (publicKey) {
824+
const publicKeys = userResult.records[0].get("publicKeys");
825+
if (publicKeys && Array.isArray(publicKeys) && publicKeys.length > 0) {
816826
console.log(
817-
`[MIGRATION] Copying User node with public key for eName: ${eName}`,
827+
`[MIGRATION] Copying User node with public keys for eName: ${eName}`,
818828
);
819829
await targetDbService.runQuery(
820830
`MERGE (u:User { eName: $eName })
821-
SET u.publicKey = $publicKey`,
822-
{ eName, publicKey },
831+
SET u.publicKeys = $publicKeys`,
832+
{ eName, publicKeys },
823833
);
824834
console.log(
825-
`[MIGRATION] User node with public key copied successfully`,
835+
`[MIGRATION] User node with public keys copied successfully`,
826836
);
827837
}
828838
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Neo4j Migration: Convert User.publicKey (string) to User.publicKeys (array)
3+
*
4+
* This migration converts the single publicKey property to an array of publicKeys
5+
* to support multiple device installs per user.
6+
*
7+
* For existing users with publicKey: converts to [publicKey]
8+
* For users without publicKey: initializes as []
9+
*/
10+
11+
import { Driver } from "neo4j-driver";
12+
13+
export async function migratePublicKeyToArray(driver: Driver): Promise<void> {
14+
const session = driver.session();
15+
try {
16+
// First, convert existing publicKey (string) to publicKeys (array)
17+
// For users with publicKey, set publicKeys = [publicKey] and remove publicKey
18+
const result = await session.run(
19+
`MATCH (u:User)
20+
WHERE u.publicKey IS NOT NULL AND u.publicKeys IS NULL
21+
SET u.publicKeys = [u.publicKey]
22+
REMOVE u.publicKey
23+
RETURN count(u) as converted`
24+
);
25+
26+
const converted = result.records[0]?.get("converted") || 0;
27+
console.log(`Converted ${converted} User nodes from publicKey to publicKeys array`);
28+
29+
// For users without publicKey, initialize publicKeys as empty array
30+
const result2 = await session.run(
31+
`MATCH (u:User)
32+
WHERE u.publicKey IS NULL AND u.publicKeys IS NULL
33+
SET u.publicKeys = []
34+
RETURN count(u) as initialized`
35+
);
36+
37+
const initialized = result2.records[0]?.get("initialized") || 0;
38+
console.log(`Initialized ${initialized} User nodes with empty publicKeys array`);
39+
40+
console.log("Migration completed: publicKey -> publicKeys array");
41+
} catch (error) {
42+
console.error("Error migrating publicKey to publicKeys array:", error);
43+
throw error;
44+
} finally {
45+
await session.close();
46+
}
47+
}
48+

0 commit comments

Comments
 (0)