Derive Ed25519 signing key from JWT secret instead of storing in DB #121

Closed
opened 2026-04-03 17:15:38 +02:00 by claude · 0 comments
Owner

Current State

The Ed25519 config signing key pair is persisted in the server_config PostgreSQL table as plaintext Base64 (commit 81f1339). This was added to fix agents rejecting commands after server restarts (the key was previously ephemeral).

Problem

The private signing key is stored unencrypted in the database. Anyone with DB read access can extract it and forge signed config payloads.

Proposed Fix

Derive the Ed25519 key deterministically from CAMELEER_JWT_SECRET using HKDF.

  • Use HKDF-SHA256 with the JWT secret as input key material and a fixed info string (e.g., "cameleer3-ed25519-signing") to derive a 32-byte seed
  • Use the seed to generate the Ed25519 key pair deterministically
  • Same JWT secret = same signing key across restarts — no DB storage needed
  • Key rotates naturally when the JWT secret rotates

Benefits

  • No private key material in the database
  • No new configuration — reuses existing CAMELEER_JWT_SECRET
  • Deterministic: same secret always produces the same key pair
  • Simpler code: remove server_config load/persist logic

Implementation

  • Replace Ed25519SigningServiceImpl(JdbcTemplate) constructor with Ed25519SigningServiceImpl(String jwtSecret)
  • Use javax.crypto.Mac with HMAC-SHA256 or a proper HKDF implementation to derive the 32-byte seed
  • JDK 17 doesn't have a public API for seed-based Ed25519 key generation — may need EdDSAParameterSpec or Bouncy Castle

Risk

  • JDK 17's standard Ed25519 API doesn't expose seed-based key construction. Need to verify that deterministic key derivation is possible without adding a dependency. If not, encrypting the DB-stored key with the JWT secret (AES-GCM) is a simpler fallback.
## Current State The Ed25519 config signing key pair is persisted in the `server_config` PostgreSQL table as plaintext Base64 (commit `81f1339`). This was added to fix agents rejecting commands after server restarts (the key was previously ephemeral). ## Problem The private signing key is stored **unencrypted** in the database. Anyone with DB read access can extract it and forge signed config payloads. ## Proposed Fix **Derive the Ed25519 key deterministically from `CAMELEER_JWT_SECRET` using HKDF.** - Use HKDF-SHA256 with the JWT secret as input key material and a fixed info string (e.g., `"cameleer3-ed25519-signing"`) to derive a 32-byte seed - Use the seed to generate the Ed25519 key pair deterministically - Same JWT secret = same signing key across restarts — no DB storage needed - Key rotates naturally when the JWT secret rotates ### Benefits - No private key material in the database - No new configuration — reuses existing `CAMELEER_JWT_SECRET` - Deterministic: same secret always produces the same key pair - Simpler code: remove `server_config` load/persist logic ### Implementation - Replace `Ed25519SigningServiceImpl(JdbcTemplate)` constructor with `Ed25519SigningServiceImpl(String jwtSecret)` - Use `javax.crypto.Mac` with HMAC-SHA256 or a proper HKDF implementation to derive the 32-byte seed - JDK 17 doesn't have a public API for seed-based Ed25519 key generation — may need `EdDSAParameterSpec` or Bouncy Castle ### Risk - JDK 17's standard Ed25519 API doesn't expose seed-based key construction. Need to verify that deterministic key derivation is possible without adding a dependency. If not, encrypting the DB-stored key with the JWT secret (AES-GCM) is a simpler fallback.
Sign in to join this conversation.