feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: key binding certs instead of direct public keys - #568

Merged
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois
Dec 9, 2025
Merged

feat: key binding certs instead of direct public keys#568
coodos merged 1 commit into
mainfrom
feat/key-binding-certs-on-whois

Conversation

@coodos

@coodoscoodos commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe 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⚠️ WarningDocstring 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❓ InconclusiveThe 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❓ InconclusiveMost 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 nameStatusExplanation
Title check✅ PassedThe 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 mainDec 9, 2025
6 of 7 checks passed
@coodos
coodos deleted the feat/key-binding-certs-on-whois branch December 9, 2025 17:42

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadplatforms/registry/src/jwt.ts
Sign up for freeto 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

1 participant

@coodos