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>
This commit is contained in:
186
.planning/phases/04-security/04-03-PLAN.md
Normal file
186
.planning/phases/04-security/04-03-PLAN.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
phase: 04-security
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["04-01"]
|
||||
files_modified:
|
||||
- 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
|
||||
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: "cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SsePayloadSigner.java"
|
||||
provides: "Component that signs SSE command payloads before delivery"
|
||||
- path: "cameleer3-server-app/src/main/java/com/cameleer3/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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<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>
|
||||
|
||||
<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
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 (will exist after execution): -->
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: SsePayloadSigner + signing integration in SseConnectionManager</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<behavior>
|
||||
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")
|
||||
</behavior>
|
||||
<action>
|
||||
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."
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest="SsePayloadSignerTest,SseSigningIT" -Dsurefire.reuseForks=false</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- 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)
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
mvn clean verify
|
||||
SsePayloadSigner unit tests pass. SseSigningIT integration test verifies end-to-end Ed25519 signing of SSE command events.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-security/04-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user