Skip to content

Conversation

@coodos
Copy link
Contributor

@coodos coodos commented Dec 9, 2025

Description of change

Issue Number

closes #465

Type of change

  • Breaking (any change that would cause existing functionality to not work as expected)
  • New (a change which implements a new feature

How the change has been tested

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for multiple public key binding certificates
    • Introduced key binding certificates feature for enhanced identity verification.
    • New registry endpoint for generating key binding certificates.
  • Database Updates

    • Automatic migration of existing public key data to JWT certificate format
  • Style

    • Minor UI layout and spacing adjustments in control panel.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

Walkthrough

This PR introduces JWT-based key binding certificates to secure public key exchange between subsystems. It converts the single publicKey field to a publicKeys array, adds certificate generation in the registry, and implements JWKS-based verification across evault-core, signature-validator, and eid-wallet controllers. A database migration handles existing data transformation.

Changes

Cohort / File(s) Summary
UI/Layout adjustments
infrastructure/control-panel/src/routes/+layout.svelte, infrastructure/control-panel/src/routes/actions/+page.svelte
Reordered CSS class tokens and repositioned toast notification; no behavioral changes.
Database schema and migration
infrastructure/evault-core/src/core/db/db.service.ts, infrastructure/evault-core/src/core/db/migrations/migrate-publickey-to-array.ts
Renamed getPublicKey()getPublicKeys() (returns string[]), setPublicKey()addPublicKey() (appends to array). Migration converts existing publicKey: string to publicKeys: string[].
eVault initialization and HTTP API
infrastructure/evault-core/src/index.ts, infrastructure/evault-core/src/core/http/server.ts
Integrated publicKey-to-array migration on startup. Updated /whois endpoint to return keyBindingCertificates (string[]) instead of publicKey. Updated PATCH /public-key to use addPublicKey and generate certificates if registry configured.
Key binding certificate generation
platforms/registry/src/jwt.ts, platforms/registry/src/index.ts
Added generateKeyBindingCertificate(ename, publicKey) function that creates ES256 JWTs. Added POST /key-binding-certificate endpoint (secret-protected) to generate certificates. Enhanced getJWK() to expose public JWK set from environment.
Signature verification
infrastructure/signature-validator/src/index.ts
Replaced single public key retrieval with getKeyBindingCertificates(). Implements JWKS-based JWT verification loop: fetch certificates, verify each JWT, match ename, extract and validate public key, verify signature; returns success or aggregated errors.
eID wallet key verification
infrastructure/eid-wallet/src/lib/global/controllers/evault.ts
Added robust pre-verification: extracts keyBindingCertificates from whois, retrieves current device key via KeyService, fetches registry JWKS, iterates certificates, verifies JWTs, matches ename and public key; marks key as saved if match found.
Package dependencies
infrastructure/eid-wallet/package.json, infrastructure/signature-validator/package.json, platforms/blabsy/package.json
Added jose@^5.2.0 to eid-wallet and signature-validator; fixed trailing newline in blabsy.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client as Client App
    participant EV as eVault Core
    participant Registry
    participant SigValidator as Signature Validator
    
    rect rgb(200, 220, 255)
    note over User,Registry: Key Binding Certificate Generation & Whois
    Client->>EV: GET /whois?ename=user@example.com
    EV->>EV: getPublicKeys(eName) → string[]
    alt publicKeys exist and registry configured
        EV->>Registry: GET /jwks
        Registry-->>EV: JWK set (public keys)
        EV->>Registry: POST /key-binding-certificate<br/>(ename, publicKey) for each key
        Registry->>Registry: generateKeyBindingCertificate<br/>(ES256 JWT sign)
        Registry-->>EV: token (JWT)
        EV->>EV: Collect keyBindingCertificates[]
    end
    EV-->>Client: { w3id, keyBindingCertificates[] }
    end
    
    rect rgb(220, 240, 220)
    note over Client,SigValidator: Signature Verification with Key Binding
    Client->>SigValidator: verify(signature, multikey, eName)
    SigValidator->>EV: GET /whois?ename=eName
    EV-->>SigValidator: { keyBindingCertificates[] }
    SigValidator->>Registry: GET /jwks
    Registry-->>SigValidator: JWK set
    SigValidator->>SigValidator: Loop keyBindingCertificates
    loop For each certificate JWT
        SigValidator->>SigValidator: Verify JWT signature (JWKS)
        SigValidator->>SigValidator: Check ename matches
        SigValidator->>SigValidator: Extract publicKey from JWT
        SigValidator->>SigValidator: Verify multikey signature<br/>with publicKey
        alt Match found
            SigValidator-->>Client: ✓ Valid (publicKey, eName)
        end
    end
    alt No match
        SigValidator-->>Client: ✗ Error (aggregated failures)
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Database migration correctness: Verify the migration handles edge cases (NULL values, duplicates, large datasets) and rollback strategy.
  • JWT verification implementation: Cross-check jose usage, JWKS parsing, and error handling paths in signature-validator and eid-wallet.
  • API contract changes: Validate /whois response shape change, certificate generation endpoint authorization, and backward compatibility considerations.
  • Error handling: Ensure per-certificate failures in loops don't mask critical issues; verify fallback and logging behavior across components.
  • Cross-component integration: Confirm registry endpoint is correctly consumed by both eVault and signature-validator; verify shared secret validation.

Possibly related PRs

  • Chore/evault pitstop refactor #395: Modifies overlapping public-key and key-binding flows in evault-core db/service, whois/PATCH handlers, and registry JWT logic—directly related to the multi-key architecture introduced here.
  • Feat/registry and evault provisioning #106: Introduces registry JWT/JWKS endpoints and jose integration that this PR builds upon for key binding certificate generation and verification.
  • Feat/emover #468: Modifies the same eid-wallet evault controller file where JWT/JWKS verification logic was added in this PR.

Suggested labels

evault-refactor, key-binding-certificates, multi-signature

Suggested reviewers

  • sosweetham
  • JulienAuvo
  • grv-saini-20

Poem

🐰 Keys once single, now they dance in arrays,
Certificates signed through JWKS's maze,
eVaults and registries in harmony aligned,
Trust chains verified, no MITM to find! 🔐

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 2 inconclusive)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements key binding certificate generation and verification across multiple components, replacing direct public key exchange. However, the implementation does not appear to include CA chain verification or root CA validation as required by issue #465. Implement CA chain verification and root CA validation against on-board root certificates as specified in issue #465, and provide audit documentation of the verification flow.
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description is incomplete. While the issue is referenced and type of change is partially indicated, the 'Type of change' section is truncated, and 'How the change has been tested' is missing content. Complete the 'Type of change' section (it appears cut off after 'New (a change which implements a new feature'), and provide detailed testing information in 'How the change has been tested'.
Out of Scope Changes check ❓ Inconclusive Most changes are in-scope refactoring to support key binding certificates. However, cosmetic UI changes in the control panel files appear unrelated to the core objective of addressing the security vulnerability described in issue #465. Clarify the purpose of cosmetic changes to the control panel UI components (+layout.svelte, +page.svelte) and their relationship to the key binding certificate security implementation.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing direct public key exchange with key binding certificates, which is the core objective of this PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/key-binding-certs-on-whois

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coodos coodos merged commit aad82bc into main Dec 9, 2025
6 of 7 checks passed
@coodos coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
infrastructure/evault-core/src/core/http/server.ts (1)

70-76: Critical: API schema doesn't match actual response structure.

The Swagger schema at lines 70-76 declares the response has a publicKey field of type string (nullable), but the actual response at line 155 returns keyBindingCertificates (an array of strings) instead. This breaks the API contract, will generate incorrect documentation, and will cause client validation failures.

Apply this diff to update the schema to match the actual response:

                 response: {
                         type: "object",
                         properties: {
                             w3id: { type: "string" },
-                            publicKey: { type: "string", nullable: true },
+                            keyBindingCertificates: { 
+                                type: "array",
+                                items: { type: "string" }
+                            },
                         },
                     },
♻️ Duplicate comments (1)
infrastructure/eid-wallet/package.json (1)

45-45: Verify the jose library version for security advisories.

Ensure that version ^5.2.0 of the jose library is current, secure, and free from known vulnerabilities.

This is the same concern as flagged for infrastructure/signature-validator/package.json. The verification script from that review applies here as well.

🧹 Nitpick comments (6)
infrastructure/eid-wallet/src/lib/global/controllers/evault.ts (5)

135-147: Duplicate getPublicKey call can be consolidated.

The same getPublicKey(KEY_ID, context) call is made here and again at lines 224-228. If the first call fails but we "continue to sync anyway," the second call will likely fail too.

Consider reusing currentPublicKey (if successfully retrieved) to avoid the redundant call:

-            // 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,
-                );
+            // Reuse currentPublicKey if already retrieved, otherwise fetch it
+            const publicKey = currentPublicKey ?? await (async () => {
+                try {
+                    return await this.#keyService.getPublicKey(KEY_ID, context);
+                } catch (error) {
+                    console.error(
+                        `Failed to get public key for ${KEY_ID} with context ${context}:`,
+                        error,
+                    );
+                    return undefined;
+                }
+            })();

Alternatively, simply reuse currentPublicKey directly since it's the same key:

-            let publicKey: string | undefined;
-            try {
-                publicKey = await this.#keyService.getPublicKey(
-                    KEY_ID,
-                    context,
-                );
-                console.log(
-                    `Public key retrieved: ${publicKey?.substring(0, 60)}...`,
-                );
-                console.log(`Public key (full): ${publicKey}`);
-            } catch (error) {
-                console.error(
-                    `Failed to get public key for ${KEY_ID} with context ${context}:`,
-                    error,
-                );
-                return;
-            }
+            const publicKey = currentPublicKey;
+            if (publicKey) {
+                console.log(
+                    `Public key retrieved: ${publicKey.substring(0, 60)}...`,
+                );
+                console.log(`Public key (full): ${publicKey}`);
+            }

158-167: Validate JWKS response structure before creating JWKSet.

If jwksResponse.data doesn't have the expected { keys: [...] } structure, createLocalJWKSet may throw or behave unexpectedly. Consider adding validation:

                         const jwksResponse = await axios.get(jwksUrl, {
                             timeout: 10000,
                         });
+                        if (!jwksResponse.data?.keys || !Array.isArray(jwksResponse.data.keys)) {
+                            console.warn("Invalid JWKS response structure");
+                            throw new Error("Invalid JWKS response");
+                        }
                         const JWKS = jose.createLocalJWKSet(jwksResponse.data);

182-195: Consider explicit validation of JWT payload fields.

The publicKey is extracted via type assertion without validation. While the comparison will safely fail if publicKey is undefined, explicit validation makes the intent clearer and helps catch malformed certificates:

                                 // Extract publicKey from JWT payload
-                                const extractedPublicKey =
-                                    payload.publicKey as string;
+                                const extractedPublicKey = payload.publicKey;
+                                if (typeof extractedPublicKey !== "string") {
+                                    console.warn("Certificate payload missing publicKey");
+                                    continue;
+                                }
                                 if (extractedPublicKey === currentPublicKey) {

205-212: Clarify security implications of error fallback.

When JWKS fetch or certificate verification fails, the code continues to sync the public key anyway. This is likely intentional for resilience (allowing sync even if registry is temporarily unavailable), but it's worth documenting:

             } catch (error) {
                 console.error(
                     "Error checking existing public keys:",
                     error,
                 );
-                // Continue to sync anyway
+                // Continue to sync anyway - verification failures shouldn't block
+                // new key registration. Server-side validation ensures security.
             }

If strict verification is required (per PR objectives about certificate chain validation), consider whether this fallback should instead abort the sync when keyBindingCertificates exist but verification fails.


229-232: Consider removing verbose public key logging for production.

Logging the full public key (line 232) is fine for debugging but adds noise in production. Consider using a debug flag or removing before release:

-                console.log(
-                    `Public key retrieved: ${publicKey?.substring(0, 60)}...`,
-                );
-                console.log(`Public key (full): ${publicKey}`);
+                console.log(`Public key retrieved for sync: ${publicKey?.substring(0, 20)}...`);
infrastructure/evault-core/src/core/http/server.ts (1)

111-151: Consider improving observability for certificate generation failures.

The code generates key binding certificates for each public key, with per-key error handling that logs errors and continues. However, if the registry is unavailable or all certificate generations fail, the response will contain an empty keyBindingCertificates array with no indication to the caller that something went wrong.

Consider one of the following approaches:

  1. Include a certificateGenerationErrors field in the response to indicate partial failures
  2. Add metrics/telemetry to track certificate generation success/failure rates
  3. Return a warning in the response when certificates couldn't be generated but publicKeys exist

This would help diagnose issues where signature verification fails due to missing certificates.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90e811d and 4889e7b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • infrastructure/control-panel/src/routes/+layout.svelte (1 hunks)
  • infrastructure/control-panel/src/routes/actions/+page.svelte (2 hunks)
  • infrastructure/eid-wallet/package.json (1 hunks)
  • infrastructure/eid-wallet/src/lib/global/controllers/evault.ts (2 hunks)
  • infrastructure/evault-core/src/core/db/db.service.ts (2 hunks)
  • infrastructure/evault-core/src/core/db/migrations/migrate-publickey-to-array.ts (1 hunks)
  • infrastructure/evault-core/src/core/http/server.ts (2 hunks)
  • infrastructure/evault-core/src/index.ts (1 hunks)
  • infrastructure/signature-validator/package.json (1 hunks)
  • infrastructure/signature-validator/src/index.ts (5 hunks)
  • platforms/blabsy/package.json (1 hunks)
  • platforms/registry/src/index.ts (2 hunks)
  • platforms/registry/src/jwt.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
infrastructure/eid-wallet/src/lib/global/controllers/evault.ts (3)
infrastructure/eid-wallet/src/lib/global/controllers/user.ts (2)
  • isFake (88-98)
  • isFake (100-113)
infrastructure/eid-wallet/src/env.d.ts (1)
  • PUBLIC_REGISTRY_URL (6-6)
platforms/pictique/src/env.d.ts (1)
  • PUBLIC_REGISTRY_URL (7-7)
infrastructure/evault-core/src/index.ts (1)
infrastructure/evault-core/src/core/db/migrations/migrate-publickey-to-array.ts (1)
  • migratePublicKeyToArray (13-47)
platforms/registry/src/jwt.ts (1)
infrastructure/evault-core/generate-test-token.js (2)
  • token (39-50)
  • privateKey (36-36)
platforms/registry/src/index.ts (1)
platforms/registry/src/jwt.ts (1)
  • generateKeyBindingCertificate (70-81)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: test
🔇 Additional comments (22)
infrastructure/control-panel/src/routes/+layout.svelte (2)

18-18: LGTM!

The single-line expression is clean and preserves the original logic correctly.


22-22: LGTM!

Class token reordering has no functional impact.

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

70-75: LGTM!

Class reordering and text formatting changes are cosmetic with no functional impact.


91-94: LGTM!

Class reordering and text wrapping are cosmetic adjustments.


101-101: LGTM!

Position class reordering has no impact on the rendered output.

platforms/blabsy/package.json (2)

64-64: Trailing newline fix.

The trailing newline addition is a non-functional formatting adjustment.


1-64: This review comment is incorrect.

The blabsy package is a Next.js frontend application that does not implement JWT-based key binding certificates or ePassports. JWT/jose functionality is implemented in infrastructure/evault-core and infrastructure/signature-validator, both of which already declare the jose dependency (^5.2.2 and ^5.2.0 respectively). No changes to platforms/blabsy/package.json are required for this feature.

Likely an incorrect or invalid review comment.

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

9-9: LGTM!

The jose library is a solid choice for JWT/JWKS operations, providing secure and spec-compliant implementations.


126-128: LGTM!

Clean extraction with optional chaining. The type is validated later with Array.isArray() before use.

platforms/registry/src/jwt.ts (1)

84-94: LGTM! Good security practice.

The implementation correctly returns the JWK in JWKS format while omitting the private key component. This is a secure approach for exposing the public verification key.

platforms/registry/src/index.ts (1)

6-6: LGTM!

The import of generateKeyBindingCertificate is correctly added alongside existing JWT functions.

infrastructure/evault-core/src/index.ts (1)

104-110: Migration failure handling is appropriate but requires monitoring.

The migration error is caught and logged as a warning without failing startup. This is a pragmatic approach for backward compatibility, but be aware that a failed migration could lead to inconsistent behavior if the database still has the old publicKey field structure while the new code expects publicKeys arrays.

Consider monitoring migration execution in production to ensure all instances complete successfully. You may want to add telemetry or alerting for migration failures.

infrastructure/evault-core/src/core/db/migrations/migrate-publickey-to-array.ts (1)

13-47: LGTM! Migration is idempotent and handles errors correctly.

The migration properly:

  • Checks for publicKeys IS NULL to ensure idempotency
  • Handles both existing publicKey values and NULL cases
  • Closes the session in a finally block
  • Logs progress and errors appropriately
infrastructure/evault-core/src/core/db/db.service.ts (2)

683-704: LGTM! Defensive handling of publicKeys array.

The method correctly:

  • Returns an empty array when the User node doesn't exist
  • Handles null/undefined and non-array values gracefully
  • Maintains consistent return type of string[]

816-842: LGTM! User node copy correctly handles publicKeys array.

The copy logic properly:

  • Checks that publicKeys is an array with length > 0
  • Copies the entire array to the target
  • Includes appropriate logging
infrastructure/signature-validator/src/index.ts (3)

111-144: LGTM! Certificate retrieval flow is well-structured.

The function correctly:

  • Resolves the eVault URL from the registry
  • Fetches key binding certificates from the /whois endpoint
  • Returns an empty array when certificates are missing (handled appropriately by caller)

212-227: LGTM! Efficient signature verification setup.

The code correctly:

  • Fetches the JWKS from the registry for JWT verification
  • Creates a local JWK set for validation
  • Decodes the signature once to avoid redundant work in the loop

228-294: LGTM! Certificate verification loop is thorough and secure.

The verification loop correctly:

  • Verifies each JWT signature against the JWKS
  • Validates that the ename in the JWT matches the expected ename
  • Extracts and decodes the public key from the verified JWT payload
  • Imports the key and verifies the signature
  • Returns immediately on first successful verification
  • Aggregates errors for debugging when all certificates fail

This implements the security requirements from issue #465 by verifying the certificate chain before trusting the public key.

infrastructure/evault-core/src/core/http/server.ts (3)

97-109: LGTM! Robust error handling for public key retrieval.

The code correctly:

  • Retrieves all public keys using the new getPublicKeys method
  • Handles errors gracefully by continuing with an empty array
  • Maintains consistent array type

153-159: LGTM! Response structure correctly returns key binding certificates.

The response properly returns the keyBindingCertificates array instead of a single public key, aligning with the PR's objective to use certificate-based key exchange.


492-506: LGTM! Correctly uses addPublicKey for append-only behavior.

The PATCH endpoint properly:

  • Uses addPublicKey to append rather than overwrite
  • Updates the success message to reflect "added" instead of "saved"
  • Maintains appropriate error messaging
infrastructure/signature-validator/package.json (1)

17-17: jose@^5.2.0 is secure with no known vulnerabilities.

Version 5.2.0 is free from known CVEs. Recent advisories (CVE-2021-29443, CVE-2024-28176) do not affect v5.x, and CVE-2023-25653 targets a different package (node-jose). The version constraint allows updates within the v5 range; latest stable v6.1.2 is available if you choose to upgrade, but no security action is required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Public keys are never exchanged between subsystems – only the ePassports (open key certificates) + verification

2 participants