--- phase: 04-security plan: 03 type: execute wave: 2 depends_on: ["04-01"] files_modified: - cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SseConnectionManager.java - cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SsePayloadSigner.java - cameleer-server-app/src/test/java/com/cameleer/server/app/security/SseSigningIT.java - cameleer-server-app/src/test/java/com/cameleer/server/app/agent/SsePayloadSignerTest.java autonomous: true requirements: - SECU-04 must_haves: truths: - "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" artifacts: - path: "cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SsePayloadSigner.java" provides: "Component that signs SSE command payloads before delivery" - path: "cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SseConnectionManager.java" provides: "Updated onCommandReady with signing before sendEvent" key_links: - from: "SseConnectionManager.onCommandReady" to: "SsePayloadSigner.signPayload" via: "Signs payload before SSE delivery" pattern: "ssePayloadSigner\\.signPayload" - from: "SsePayloadSigner" to: "Ed25519SigningService.sign" via: "Delegates signing to Ed25519 service" pattern: "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. @C:/Users/Hendrik/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/Hendrik/.claude/get-shit-done/templates/summary.md @.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 @cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SseConnectionManager.java From core/security/Ed25519SigningService.java: ```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: ```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: ```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 cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SsePayloadSigner.java, cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SseConnectionManager.java, cameleer-server-app/src/test/java/com/cameleer/server/app/agent/SsePayloadSignerTest.java, cameleer-server-app/src/test/java/com/cameleer/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=` - 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/cameleer-server && mvn test -pl cameleer-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. - 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) After completion, create `.planning/phases/04-security/04-03-SUMMARY.md`