Files
cameleer-server/.planning/phases/04-security/04-03-PLAN.md
hsiegeln b7c35037e6 docs(04-security): create phase plan
3 plans in 2 waves covering all 5 SECU requirements:
- Plan 01 (W1): Security service foundation (JWT, Ed25519, bootstrap token)
- Plan 02 (W2): Spring Security filter chain, endpoint protection, test adaptation
- Plan 03 (W2): SSE payload signing with Ed25519

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 19:51:22 +01:00

9.3 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-security 03 execute 2
04-01
cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java
cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SsePayloadSigner.java
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java
cameleer3-server-app/src/test/java/com/cameleer3/server/app/agent/SsePayloadSignerTest.java
true
SECU-04
truths artifacts key_links
All config-update, deep-trace, and replay SSE events carry a valid Ed25519 signature in the data JSON
Signature is computed over the payload JSON without the signature field, then added as a 'signature' field
Agent can verify the signature using the public key received at registration
path provides
cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SsePayloadSigner.java Component that signs SSE command payloads before delivery
path provides
cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java Updated onCommandReady with signing before sendEvent
from to via pattern
SseConnectionManager.onCommandReady SsePayloadSigner.signPayload Signs payload before SSE delivery ssePayloadSigner.signPayload
from to via pattern
SsePayloadSigner Ed25519SigningService.sign Delegates signing to Ed25519 service ed25519SigningService.sign
Add Ed25519 signature to all SSE command payloads (config-update, deep-trace, replay) before delivery. The signature is computed over the data JSON and included as a `signature` field in the event data, enabling agents to verify payload integrity using the server's public key.

Purpose: Ensures all pushed configuration and commands are integrity-protected, so agents can trust the payloads they receive. Output: All SSE command events carry verifiable Ed25519 signatures.

<execution_context> @C:/Users/Hendrik/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/Hendrik/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-security/04-CONTEXT.md @.planning/phases/04-security/04-RESEARCH.md @.planning/phases/04-security/04-01-SUMMARY.md

@cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java

From core/security/Ed25519SigningService.java:

public interface Ed25519SigningService {
    String sign(String payload);        // Returns Base64-encoded signature
    String getPublicKeyBase64();        // Returns Base64-encoded X.509 public key
}

From app/agent/SseConnectionManager.java:

@Component
public class SseConnectionManager implements AgentEventListener {
    // Key method to modify:
    public void onCommandReady(String agentId, AgentCommand command) {
        String eventType = command.type().name().toLowerCase().replace('_', '-');
        boolean sent = sendEvent(agentId, command.id(), eventType, command.payload());
        // command.payload() is a String (JSON)
    }

    public boolean sendEvent(String agentId, String eventId, String eventType, Object data) {
        // data is sent via SseEmitter.event().data(data, MediaType.APPLICATION_JSON)
    }
}

From core/agent/AgentCommand.java:

public record AgentCommand(String id, CommandType type, String payload, String agentId, Instant createdAt, CommandStatus status) {
    // payload is a JSON string
}
Task 1: SsePayloadSigner + signing integration in SseConnectionManager cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SsePayloadSigner.java, cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java, cameleer3-server-app/src/test/java/com/cameleer3/server/app/agent/SsePayloadSignerTest.java, cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SseSigningIT.java SsePayloadSigner unit tests: - signPayload(jsonString) returns a new JSON string containing all original fields plus a "signature" field - The "signature" field is a Base64-encoded Ed25519 signature - The signature is computed over the ORIGINAL JSON string (without signature field) - Signature verifies against the public key from Ed25519SigningService - null or empty payload returns the payload unchanged (defensive)
SseSigningIT integration test:
- Register an agent (with bootstrap token), get public key from response
- Open SSE connection (with JWT query param)
- Send a config-update command to the agent
- Receive the SSE event and verify it contains a "signature" field
- Verify the signature against the public key using JDK Ed25519 Signature.getInstance("Ed25519")
1. Create `SsePayloadSigner` as a `@Component`: - Constructor takes `Ed25519SigningService` and `ObjectMapper` - `signPayload(String jsonPayload)` method: a. The payload JSON string IS the data to sign (sign the exact string) b. Compute signature: `ed25519SigningService.sign(jsonPayload)` returns Base64 signature c. Parse the JSON payload, add `"signature": signatureBase64` field, serialize back d. Return the signed JSON string - Handle edge cases: if payload is null or empty, return as-is with a log warning
2. Update `SseConnectionManager`:
   - Add `SsePayloadSigner` as a constructor dependency
   - In `onCommandReady()`, sign the payload before sending:
     ```java
     String signedPayload = ssePayloadSigner.signPayload(command.payload());
     boolean sent = sendEvent(agentId, command.id(), eventType, signedPayload);
     ```
   - The `sendEvent` method already sends `data` as `MediaType.APPLICATION_JSON`. Since `signedPayload` is already a JSON string, the SseEmitter will serialize it. IMPORTANT: Since the payload is already a JSON string and SseEmitter will try to JSON-serialize it (wrapping in quotes), we need to send it as a pre-serialized value. Change `sendEvent` to use `.data(signedPayload)` without MediaType for signed payloads, OR parse it to a JsonNode/Map first so Jackson serializes it correctly. The cleanest approach: parse the signed JSON string into a `JsonNode` via `objectMapper.readTree(signedPayload)` and pass that as the data object -- Jackson will serialize the tree correctly.

3. Write `SsePayloadSignerTest` (unit test, no Spring context):
   - Create a real `Ed25519SigningServiceImpl` and `ObjectMapper` for testing
   - Test cases per behavior spec above
   - Verify signature by using JDK `Signature.getInstance("Ed25519")` with the public key

4. Write `SseSigningIT` (extends AbstractClickHouseIT):
   - Register agent using bootstrap token (from application-test.yml)
   - Extract `serverPublicKey` from registration response
   - Get JWT from registration response
   - Open SSE connection via `java.net.http.HttpClient` async API (same pattern as AgentSseControllerIT) with `?token=<jwt>`
   - Use the agent command endpoint to push a config-update command to the agent
   - Read the SSE event from the stream
   - Parse the event data JSON, extract the `signature` field
   - Reconstruct the unsigned payload (remove signature field, serialize)
   - Verify signature using `Signature.getInstance("Ed25519")` with the public key decoded from Base64
   - NOTE: This test depends on Plan 02's bootstrap token and JWT auth being in place. If Plan 03 executes before Plan 02, the test will need the TestSecurityHelper or a different auth approach. Since both are Wave 2 but independent, document this: "If Plan 02 is not yet complete, use TestSecurityHelper from Plan 01's temporary permit-all config."
cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest="SsePayloadSignerTest,SseSigningIT" -Dsurefire.reuseForks=false - SsePayloadSigner signs JSON payloads with Ed25519 and adds signature field - SseConnectionManager signs all command payloads before SSE delivery - Unit tests verify signature roundtrip (sign + verify with public key) - Integration test verifies end-to-end: command sent -> SSE event received with valid signature - Existing SSE tests still pass (ping events are not signed, only command events) mvn clean verify SsePayloadSigner unit tests pass. SseSigningIT integration test verifies end-to-end Ed25519 signing of SSE command events.

<success_criteria>

  • All SSE command events (config-update, deep-trace, replay) include a "signature" field
  • Signature verifies against the server's Ed25519 public key
  • Signature is computed over the payload JSON without the signature field
  • Ping keepalive events are NOT signed (they are SSE comments, not data events)
  • Existing SSE functionality unchanged (connection, ping, delivery tracking) </success_criteria>
After completion, create `.planning/phases/04-security/04-03-SUMMARY.md`