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:
@@ -76,10 +76,12 @@ Plans:
|
||||
2. Agents can refresh expired JWTs via the refresh endpoint without re-registering
|
||||
3. Server generates an Ed25519 keypair at startup, delivers the public key during registration, and all config-update and replay SSE payloads carry a valid Ed25519 signature
|
||||
4. Bootstrap token from CAMELEER_AUTH_TOKEN environment variable is required for initial agent registration
|
||||
**Plans**: TBD
|
||||
**Plans:** 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 04-01: JWT authentication filter, refresh flow, Ed25519 keypair generation and config signing, bootstrap token validation
|
||||
- [ ] 04-01-PLAN.md -- Security service foundation: JwtService, Ed25519SigningService, BootstrapTokenValidator, Maven deps, config
|
||||
- [ ] 04-02-PLAN.md -- Spring Security filter chain, JWT auth filter, registration/refresh integration, existing test adaptation
|
||||
- [ ] 04-03-PLAN.md -- Ed25519 signing of SSE command payloads (config-update, deep-trace, replay)
|
||||
|
||||
## Progress
|
||||
|
||||
@@ -92,4 +94,4 @@ Note: Phases 2 and 3 both depend only on Phase 1 and could execute in parallel.
|
||||
| 1. Ingestion Pipeline + API Foundation | 3/3 | Complete | 2026-03-11 |
|
||||
| 2. Transaction Search + Diagrams | 3/4 | Gap Closure | |
|
||||
| 3. Agent Registry + SSE Push | 2/2 | Complete | 2026-03-11 |
|
||||
| 4. Security | 0/1 | Not started | - |
|
||||
| 4. Security | 0/3 | Not started | - |
|
||||
|
||||
203
.planning/phases/04-security/04-01-PLAN.md
Normal file
203
.planning/phases/04-security/04-01-PLAN.md
Normal file
@@ -0,0 +1,203 @@
|
||||
---
|
||||
phase: 04-security
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- cameleer3-server-app/pom.xml
|
||||
- cameleer3-server-core/src/main/java/com/cameleer3/server/core/security/JwtService.java
|
||||
- cameleer3-server-core/src/main/java/com/cameleer3/server/core/security/Ed25519SigningService.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtServiceImpl.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/Ed25519SigningServiceImpl.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/BootstrapTokenValidator.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java
|
||||
- cameleer3-server-app/src/main/resources/application.yml
|
||||
- cameleer3-server-app/src/test/resources/application-test.yml
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtServiceTest.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/Ed25519SigningServiceTest.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenValidatorTest.java
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SECU-03
|
||||
- SECU-05
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Ed25519 keypair is generated at server startup and public key is available as Base64"
|
||||
- "JwtService can create access tokens (1h expiry) and refresh tokens (7d expiry) with agentId and group claims"
|
||||
- "JwtService can validate tokens and extract agentId, distinguishing access vs refresh type"
|
||||
- "BootstrapTokenValidator accepts CAMELEER_AUTH_TOKEN and optionally CAMELEER_AUTH_TOKEN_PREVIOUS using constant-time comparison"
|
||||
- "Server fails fast on startup if CAMELEER_AUTH_TOKEN is not set"
|
||||
artifacts:
|
||||
- path: "cameleer3-server-core/src/main/java/com/cameleer3/server/core/security/JwtService.java"
|
||||
provides: "JWT service interface with createAccessToken, createRefreshToken, validateAndExtractAgentId"
|
||||
- path: "cameleer3-server-core/src/main/java/com/cameleer3/server/core/security/Ed25519SigningService.java"
|
||||
provides: "Ed25519 signing interface with sign(payload) and getPublicKeyBase64()"
|
||||
- path: "cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtServiceImpl.java"
|
||||
provides: "Nimbus JOSE+JWT HMAC-SHA256 implementation"
|
||||
- path: "cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/Ed25519SigningServiceImpl.java"
|
||||
provides: "JDK 17 Ed25519 KeyPairGenerator implementation"
|
||||
- path: "cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/BootstrapTokenValidator.java"
|
||||
provides: "Constant-time bootstrap token validation with dual-token rotation"
|
||||
key_links:
|
||||
- from: "JwtServiceImpl"
|
||||
to: "Nimbus JOSE+JWT MACSigner/MACVerifier"
|
||||
via: "HMAC-SHA256 signing with ephemeral 256-bit secret"
|
||||
pattern: "MACSigner|MACVerifier|SignedJWT"
|
||||
- from: "Ed25519SigningServiceImpl"
|
||||
to: "JDK KeyPairGenerator/Signature"
|
||||
via: "Ed25519 algorithm from java.security"
|
||||
pattern: "KeyPairGenerator\\.getInstance.*Ed25519"
|
||||
- from: "BootstrapTokenValidator"
|
||||
to: "SecurityProperties"
|
||||
via: "reads token values from config properties"
|
||||
pattern: "MessageDigest\\.isEqual"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the security service foundation: interfaces in core module, implementations in app module, Maven dependencies, and configuration properties. This provides all cryptographic building blocks (JWT creation/validation, Ed25519 signing, bootstrap token validation) that the filter chain and endpoint integration plans depend on.
|
||||
|
||||
Purpose: Establishes the security primitives before they are wired into Spring Security and controllers.
|
||||
Output: Working JwtService, Ed25519SigningService, BootstrapTokenValidator with passing unit tests.
|
||||
</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-VALIDATION.md
|
||||
|
||||
@cameleer3-server-app/pom.xml
|
||||
@cameleer3-server-app/src/main/resources/application.yml
|
||||
@cameleer3-server-app/src/test/resources/application-test.yml
|
||||
@cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryConfig.java
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing patterns to follow: core module = interfaces/domain, app module = Spring implementations -->
|
||||
|
||||
From core/agent/AgentRegistryService.java:
|
||||
```java
|
||||
// Plain class in core module, wired as bean by app module config
|
||||
public class AgentRegistryService {
|
||||
public AgentInfo register(String id, String name, String group, ...);
|
||||
public AgentInfo findById(String id);
|
||||
}
|
||||
```
|
||||
|
||||
From app/config/AgentRegistryConfig.java:
|
||||
```java
|
||||
@ConfigurationProperties(prefix = "agent-registry")
|
||||
public class AgentRegistryConfig { ... }
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Core interfaces + app implementations + Maven deps</name>
|
||||
<files>
|
||||
cameleer3-server-app/pom.xml,
|
||||
cameleer3-server-core/src/main/java/com/cameleer3/server/core/security/JwtService.java,
|
||||
cameleer3-server-core/src/main/java/com/cameleer3/server/core/security/Ed25519SigningService.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtServiceImpl.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/Ed25519SigningServiceImpl.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/BootstrapTokenValidator.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java,
|
||||
cameleer3-server-app/src/main/resources/application.yml,
|
||||
cameleer3-server-app/src/test/resources/application-test.yml,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtServiceTest.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/Ed25519SigningServiceTest.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenValidatorTest.java
|
||||
</files>
|
||||
<behavior>
|
||||
JwtService tests:
|
||||
- createAccessToken(agentId, group) returns a signed JWT string with sub=agentId, claim "group"=group, claim "type"="access", expiry ~1h from now
|
||||
- createRefreshToken(agentId, group) returns a signed JWT string with sub=agentId, claim "type"="refresh", expiry ~7d from now
|
||||
- validateAndExtractAgentId(validAccessToken) returns the agentId
|
||||
- validateAndExtractAgentId(expiredToken) throws exception
|
||||
- validateAndExtractAgentId(refreshToken) throws exception (wrong type for access validation)
|
||||
- validateRefreshToken(validRefreshToken) returns the agentId
|
||||
- validateRefreshToken(accessToken) throws exception (wrong type)
|
||||
|
||||
Ed25519SigningService tests:
|
||||
- getPublicKeyBase64() returns non-null Base64 string
|
||||
- sign(payload) returns Base64 signature string
|
||||
- Signature verifies against public key using JDK Signature.getInstance("Ed25519")
|
||||
- Different payloads produce different signatures
|
||||
- Tampered payload fails verification
|
||||
|
||||
BootstrapTokenValidator tests:
|
||||
- validate(correctToken) returns true
|
||||
- validate(wrongToken) returns false
|
||||
- validate(previousToken) returns true when CAMELEER_AUTH_TOKEN_PREVIOUS is set
|
||||
- validate(null) returns false
|
||||
- Uses constant-time comparison (MessageDigest.isEqual)
|
||||
</behavior>
|
||||
<action>
|
||||
1. Add Maven dependencies to cameleer3-server-app/pom.xml:
|
||||
- `spring-boot-starter-security` (managed version)
|
||||
- `com.nimbusds:nimbus-jose-jwt:9.47` (explicit, may not be transitive without OAuth2 resource server)
|
||||
- `spring-security-test` scope test (managed version)
|
||||
|
||||
2. Create core module interfaces:
|
||||
- `JwtService` interface: `createAccessToken(String agentId, String group)`, `createRefreshToken(String agentId, String group)`, `validateAndExtractAgentId(String token)` (access only), `validateRefreshToken(String token)` (refresh only). Returns String tokens, throws `InvalidTokenException` (new checked or runtime exception in core).
|
||||
- `Ed25519SigningService` interface: `sign(String payload)` returns Base64 signature string, `getPublicKeyBase64()` returns Base64-encoded X.509 SubjectPublicKeyInfo DER public key.
|
||||
|
||||
3. Create app module implementations:
|
||||
- `SecurityProperties` as `@ConfigurationProperties(prefix = "security")` with fields: `accessTokenExpiryMs` (default 3600000), `refreshTokenExpiryMs` (default 604800000), `bootstrapToken` (from env CAMELEER_AUTH_TOKEN), `bootstrapTokenPrevious` (from env CAMELEER_AUTH_TOKEN_PREVIOUS, nullable).
|
||||
- `JwtServiceImpl`: Generate random 256-bit HMAC secret in constructor (`new SecureRandom().nextBytes(secret)`). Use Nimbus `MACSigner`/`MACVerifier` with `JWSAlgorithm.HS256`. Claims: `sub`=agentId, `group`=group, `type`="access"|"refresh", `iat`=now, `exp`=now+expiry. Validation checks: signature valid, not expired, correct `type` claim.
|
||||
- `Ed25519SigningServiceImpl`: Generate `KeyPair` via `KeyPairGenerator.getInstance("Ed25519")` in constructor. `sign()` uses `Signature.getInstance("Ed25519")`, `initSign(privateKey)`, returns Base64-encoded signature bytes. `getPublicKeyBase64()` returns `Base64.getEncoder().encodeToString(publicKey.getEncoded())`.
|
||||
- `BootstrapTokenValidator`: Constructor takes `SecurityProperties`. `validate(String provided)` returns boolean. Uses `MessageDigest.isEqual(provided.getBytes(UTF_8), expected.getBytes(UTF_8))`. If first token fails and previousToken is non-null, tries previousToken. Returns false for null/blank input.
|
||||
- `SecurityBeanConfig` as `@Configuration` with `@EnableConfigurationProperties(SecurityProperties.class)`. Creates beans for `JwtServiceImpl`, `Ed25519SigningServiceImpl`, `BootstrapTokenValidator`. Add `@PostConstruct` or `InitializingBean` validation: if `SecurityProperties.bootstrapToken` is null or blank, throw `IllegalStateException("CAMELEER_AUTH_TOKEN environment variable must be set")`.
|
||||
|
||||
4. Update application.yml: Add `security.access-token-expiry-ms: 3600000`, `security.refresh-token-expiry-ms: 604800000`. Map env vars: `security.bootstrap-token: ${CAMELEER_AUTH_TOKEN:}`, `security.bootstrap-token-previous: ${CAMELEER_AUTH_TOKEN_PREVIOUS:}`.
|
||||
|
||||
5. Update application-test.yml: Add `security.bootstrap-token: test-bootstrap-token`, `security.bootstrap-token-previous: old-bootstrap-token`. Also set `CAMELEER_AUTH_TOKEN: test-bootstrap-token` as an env override if needed.
|
||||
|
||||
6. IMPORTANT: Adding spring-boot-starter-security will break ALL existing tests immediately (401 on all endpoints). To prevent this during Plan 01 (before the security filter chain is configured in Plan 02), add a temporary test security config class `src/test/java/com/cameleer3/server/app/security/TestSecurityConfig.java` annotated `@TestConfiguration` that creates a `SecurityFilterChain` permitting all requests. This keeps existing tests green while security services are built. Plan 02 will replace this with real security config and update tests.
|
||||
|
||||
7. Write unit tests per the behavior spec above. Tests should NOT require Spring context -- construct implementations directly with test SecurityProperties.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest="JwtServiceTest,Ed25519SigningServiceTest,BootstrapTokenValidatorTest" -Dsurefire.reuseForks=false</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- JwtService creates and validates access/refresh JWTs with correct claims and expiry
|
||||
- Ed25519SigningService generates keypair, signs payloads, signatures verify with public key
|
||||
- BootstrapTokenValidator uses constant-time comparison, supports dual-token rotation
|
||||
- Server startup fails if CAMELEER_AUTH_TOKEN is not set (tested via SecurityBeanConfig @PostConstruct)
|
||||
- All existing tests still pass (TestSecurityConfig permits all requests temporarily)
|
||||
- Maven compiles with new dependencies
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
mvn clean verify
|
||||
All new unit tests pass. All existing integration tests still pass (no 401 regressions).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- JwtServiceImpl creates signed JWTs with correct HMAC-SHA256, validates them, and rejects expired/wrong-type tokens
|
||||
- Ed25519SigningServiceImpl generates ephemeral keypair, signs payloads with verifiable signatures
|
||||
- BootstrapTokenValidator performs constant-time comparison with dual-token support
|
||||
- SecurityProperties loaded from application.yml with env var mapping
|
||||
- Startup fails fast when CAMELEER_AUTH_TOKEN is missing
|
||||
- Existing test suite remains green via TestSecurityConfig permit-all
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-security/04-01-SUMMARY.md`
|
||||
</output>
|
||||
293
.planning/phases/04-security/04-02-PLAN.md
Normal file
293
.planning/phases/04-security/04-02-PLAN.md
Normal file
@@ -0,0 +1,293 @@
|
||||
---
|
||||
phase: 04-security
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["04-01"]
|
||||
files_modified:
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentSseController.java
|
||||
- cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SecurityFilterIT.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenIT.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java
|
||||
- cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/TestSecurityConfig.java
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SECU-01
|
||||
- SECU-02
|
||||
- SECU-05
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "All API endpoints except health, register, and docs reject requests without valid JWT"
|
||||
- "POST /register requires bootstrap token in Authorization header, returns JWT + refresh token + Ed25519 public key"
|
||||
- "POST /agents/{id}/refresh accepts refresh token and returns new access JWT"
|
||||
- "SSE endpoint accepts JWT via ?token= query parameter"
|
||||
- "Health endpoint and Swagger UI remain publicly accessible"
|
||||
artifacts:
|
||||
- path: "cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java"
|
||||
provides: "OncePerRequestFilter extracting JWT from header or query param"
|
||||
- path: "cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java"
|
||||
provides: "SecurityFilterChain with permitAll for public paths, authenticated for rest"
|
||||
- path: "cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java"
|
||||
provides: "Updated register endpoint with bootstrap token validation, JWT issuance, public key"
|
||||
key_links:
|
||||
- from: "JwtAuthenticationFilter"
|
||||
to: "JwtService.validateAndExtractAgentId"
|
||||
via: "Filter delegates JWT validation to service"
|
||||
pattern: "jwtService\\.validateAndExtractAgentId"
|
||||
- from: "SecurityConfig"
|
||||
to: "JwtAuthenticationFilter"
|
||||
via: "addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)"
|
||||
pattern: "addFilterBefore"
|
||||
- from: "AgentRegistrationController.register"
|
||||
to: "BootstrapTokenValidator.validate"
|
||||
via: "Validates bootstrap token before processing registration"
|
||||
pattern: "bootstrapTokenValidator\\.validate"
|
||||
- from: "AgentRegistrationController.register"
|
||||
to: "JwtService.createAccessToken + createRefreshToken"
|
||||
via: "Issues tokens in registration response"
|
||||
pattern: "jwtService\\.create(Access|Refresh)Token"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire Spring Security into the application: JWT authentication filter, SecurityFilterChain configuration, bootstrap token validation on registration, JWT issuance in registration response, refresh endpoint, and SSE query-parameter authentication. Update existing tests to work with security enabled.
|
||||
|
||||
Purpose: Protects all endpoints with JWT authentication while keeping public endpoints accessible and providing the full agent registration-to-authentication flow.
|
||||
Output: Working security filter chain with protected/public endpoints, registration returns JWT + public key, refresh flow works, all tests pass.
|
||||
</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-VALIDATION.md
|
||||
@.planning/phases/04-security/04-01-SUMMARY.md
|
||||
|
||||
@cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java
|
||||
@cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentSseController.java
|
||||
@cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java
|
||||
@cameleer3-server-app/src/test/java/com/cameleer3/server/app/AbstractClickHouseIT.java
|
||||
@cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 (will exist after execution): -->
|
||||
|
||||
From core/security/JwtService.java:
|
||||
```java
|
||||
public interface JwtService {
|
||||
String createAccessToken(String agentId, String group);
|
||||
String createRefreshToken(String agentId, String group);
|
||||
String validateAndExtractAgentId(String token); // access tokens only
|
||||
String validateRefreshToken(String token); // refresh tokens only
|
||||
}
|
||||
```
|
||||
|
||||
From core/security/Ed25519SigningService.java:
|
||||
```java
|
||||
public interface Ed25519SigningService {
|
||||
String sign(String payload);
|
||||
String getPublicKeyBase64();
|
||||
}
|
||||
```
|
||||
|
||||
From app/security/BootstrapTokenValidator.java:
|
||||
```java
|
||||
public class BootstrapTokenValidator {
|
||||
public boolean validate(String provided);
|
||||
}
|
||||
```
|
||||
|
||||
From app/security/SecurityProperties.java:
|
||||
```java
|
||||
@ConfigurationProperties(prefix = "security")
|
||||
public class SecurityProperties {
|
||||
long accessTokenExpiryMs; // default 3600000
|
||||
long refreshTokenExpiryMs; // default 604800000
|
||||
String bootstrapToken; // from CAMELEER_AUTH_TOKEN env
|
||||
String bootstrapTokenPrevious; // from CAMELEER_AUTH_TOKEN_PREVIOUS env, nullable
|
||||
}
|
||||
```
|
||||
|
||||
From core/agent/AgentRegistryService.java:
|
||||
```java
|
||||
public class AgentRegistryService {
|
||||
public AgentInfo register(String id, String name, String group, String version, List<String> routeIds, Map<String, Object> capabilities);
|
||||
public AgentInfo findById(String id);
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: SecurityFilterChain + JwtAuthenticationFilter + registration/refresh integration</name>
|
||||
<files>
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentSseController.java,
|
||||
cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java
|
||||
</files>
|
||||
<action>
|
||||
1. Create `JwtAuthenticationFilter extends OncePerRequestFilter` (NOT annotated @Component -- constructed in SecurityConfig to avoid double registration):
|
||||
- Constructor takes `JwtService` and `AgentRegistryService`
|
||||
- `doFilterInternal`: extract token via `extractToken(request)`, if token present: call `jwtService.validateAndExtractAgentId(token)`, verify agent exists via `agentRegistry.findById(agentId)`, if valid set `UsernamePasswordAuthenticationToken(agentId, null, List.of())` in `SecurityContextHolder`. If any exception, log debug and do NOT set auth (Spring Security rejects). Always call `chain.doFilter(request, response)`.
|
||||
- `extractToken(request)`: first check `Authorization` header for `Bearer ` prefix, then check `request.getParameter("token")` for SSE query param. Return null if neither.
|
||||
|
||||
2. Create `SecurityConfig` as `@Configuration @EnableWebSecurity`:
|
||||
- Single `@Bean SecurityFilterChain filterChain(HttpSecurity http, JwtService jwtService, AgentRegistryService registryService)`:
|
||||
- `csrf(AbstractHttpConfigurer::disable)` -- REST API, no browser forms
|
||||
- `sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))`
|
||||
- `authorizeHttpRequests`: permitAll for `/api/v1/health`, `/api/v1/agents/register`, `/api/v1/api-docs/**`, `/api/v1/swagger-ui/**`, `/swagger-ui/**`, `/v3/api-docs/**`, `/swagger-ui.html`. `anyRequest().authenticated()`.
|
||||
- `addFilterBefore(new JwtAuthenticationFilter(jwtService, registryService), UsernamePasswordAuthenticationFilter.class)`
|
||||
- Also disable default form login and httpBasic: `.formLogin(AbstractHttpConfigurer::disable).httpBasic(AbstractHttpConfigurer::disable)`
|
||||
|
||||
3. Update `AgentRegistrationController.register()`:
|
||||
- Add `BootstrapTokenValidator`, `JwtService`, `Ed25519SigningService` as constructor dependencies
|
||||
- Before processing registration body, extract bootstrap token from `Authorization: Bearer <token>` header (use `@RequestHeader("Authorization")` or extract from HttpServletRequest). If missing or invalid (`bootstrapTokenValidator.validate()` returns false), return `401 Unauthorized` with no detail body.
|
||||
- After successful registration, generate tokens: `jwtService.createAccessToken(agentId, group)` and `jwtService.createRefreshToken(agentId, group)`
|
||||
- Update response map: replace `"serverPublicKey", null` with `"serverPublicKey", ed25519SigningService.getPublicKeyBase64()`. Add `"accessToken"` and `"refreshToken"` fields.
|
||||
|
||||
4. Add a new refresh endpoint in `AgentRegistrationController` (or a new controller -- keep it in the same controller since it's agent auth flow):
|
||||
- `POST /api/v1/agents/{id}/refresh` with request body containing `{"refreshToken": "..."}`.
|
||||
- Validate refresh token via `jwtService.validateRefreshToken(token)`, extract agentId, verify it matches path `{id}`, verify agent exists.
|
||||
- Return new access token: `{"accessToken": "..."}`.
|
||||
- Return 401 for invalid/expired refresh token, 404 for unknown agent.
|
||||
- NOTE: This endpoint must be AUTHENTICATED (requires valid JWT OR the refresh token itself). Per the user decision, the refresh endpoint uses the refresh token for auth, so add `/api/v1/agents/*/refresh` to permitAll in SecurityConfig, and validate the refresh token in the controller itself.
|
||||
|
||||
5. Update `AgentSseController.events()`:
|
||||
- The SSE endpoint uses `?token=<jwt>` query parameter. The `JwtAuthenticationFilter` already handles this (extracts from query param). No changes needed to the controller itself -- Spring Security handles auth via the filter.
|
||||
- However, verify the SSE endpoint path `/api/v1/agents/{id}/events` is NOT in permitAll (it should require JWT auth).
|
||||
|
||||
6. Update `WebConfig` if needed: The `ProtocolVersionInterceptor` excluded paths should align with Spring Security public paths. The SSE events path is already excluded from protocol version check (Phase 3 decision). Verify no conflicts.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean compile -pl cameleer3-server-app</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- SecurityConfig creates stateless filter chain with correct public/protected path split
|
||||
- JwtAuthenticationFilter extracts JWT from header or query param, validates, sets SecurityContext
|
||||
- Registration endpoint requires bootstrap token, returns accessToken + refreshToken + serverPublicKey
|
||||
- Refresh endpoint issues new access token from valid refresh token
|
||||
- Application compiles with all security wiring
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Security integration tests + existing test adaptation</name>
|
||||
<files>
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/TestSecurityConfig.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SecurityFilterIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ExecutionControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/MetricsControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/BackpressureIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramRenderControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DetailControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/SearchControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/storage/DiagramLinkingIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/storage/IngestionSchemaIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/interceptor/ProtocolVersionIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/OpenApiIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ForwardCompatIT.java,
|
||||
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/HealthControllerIT.java
|
||||
</files>
|
||||
<action>
|
||||
1. Replace the Plan 01 temporary `TestSecurityConfig` (permit-all) with real security active in tests. Remove the permit-all override so tests run with actual security enforcement.
|
||||
|
||||
2. Create `TestSecurityHelper` utility class in test root:
|
||||
- Autowire `JwtService` and `AgentRegistryService`
|
||||
- `registerTestAgent(String agentId)`: calls `registryService.register(agentId, "test", "test-group", "1.0", List.of(), Map.of())` and returns `jwtService.createAccessToken(agentId, "test-group")`
|
||||
- `authHeaders(String jwt)`: returns HttpHeaders with `Authorization: Bearer <jwt>` and `X-Cameleer-Protocol-Version: 1` and `Content-Type: application/json`
|
||||
- `bootstrapHeaders()`: returns HttpHeaders with `Authorization: Bearer test-bootstrap-token` and `X-Cameleer-Protocol-Version: 1` and `Content-Type: application/json`
|
||||
- Make it a Spring `@Component` so it can be autowired in test classes
|
||||
|
||||
3. Update ALL existing IT classes (17 files) to use JWT authentication:
|
||||
- Autowire `TestSecurityHelper`
|
||||
- In `@BeforeEach` or at test start, call `helper.registerTestAgent("test-agent-<testclass>")` to get a JWT
|
||||
- Replace all `protocolHeaders()` calls with headers that include the JWT Bearer token
|
||||
- For HealthControllerIT and OpenApiIT: verify these still work WITHOUT JWT (they're public endpoints)
|
||||
- For AgentRegistrationControllerIT: update `registerAgent()` helper to use bootstrap token header, verify response now includes `accessToken`, `refreshToken`, `serverPublicKey` (non-null)
|
||||
|
||||
4. Create new security-specific integration tests:
|
||||
|
||||
`SecurityFilterIT` (extends AbstractClickHouseIT):
|
||||
- Test: GET /api/v1/agents without JWT returns 401 or 403
|
||||
- Test: GET /api/v1/agents with valid JWT returns 200
|
||||
- Test: GET /api/v1/health without JWT returns 200 (public)
|
||||
- Test: POST /api/v1/data/executions without JWT returns 401 or 403
|
||||
- Test: Request with expired JWT returns 401 or 403
|
||||
- Test: Request with malformed JWT returns 401 or 403
|
||||
|
||||
`BootstrapTokenIT` (extends AbstractClickHouseIT):
|
||||
- Test: POST /register without bootstrap token returns 401
|
||||
- Test: POST /register with wrong bootstrap token returns 401
|
||||
- Test: POST /register with correct bootstrap token returns 200 with tokens
|
||||
- Test: POST /register with previous bootstrap token returns 200 (dual-token rotation)
|
||||
|
||||
`RegistrationSecurityIT` (extends AbstractClickHouseIT):
|
||||
- Test: Registration response contains non-null `serverPublicKey` (Base64 string)
|
||||
- Test: Registration response contains `accessToken` and `refreshToken`
|
||||
- Test: Access token from registration can be used to access protected endpoints
|
||||
|
||||
`JwtRefreshIT` (extends AbstractClickHouseIT):
|
||||
- Test: POST /agents/{id}/refresh with valid refresh token returns new access token
|
||||
- Test: POST /agents/{id}/refresh with expired refresh token returns 401
|
||||
- Test: POST /agents/{id}/refresh with access token (wrong type) returns 401
|
||||
- Test: POST /agents/{id}/refresh with mismatched agent ID returns 401
|
||||
- Test: New access token from refresh can access protected endpoints
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- All 17 existing ITs pass with JWT authentication
|
||||
- SecurityFilterIT: protected endpoints reject unauthenticated requests, public endpoints remain open
|
||||
- BootstrapTokenIT: registration requires valid bootstrap token, supports dual-token rotation
|
||||
- RegistrationSecurityIT: registration returns public key + tokens
|
||||
- JwtRefreshIT: refresh flow issues new access tokens, rejects invalid refresh tokens
|
||||
- Full `mvn clean verify` is green
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
mvn clean verify
|
||||
All existing tests pass with JWT auth. New security ITs validate protected/public endpoint split, bootstrap token flow, registration security, and refresh flow.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Protected endpoints return 401/403 without JWT, 200 with valid JWT
|
||||
- Public endpoints (health, register, docs) remain accessible without JWT
|
||||
- Registration requires bootstrap token, returns accessToken + refreshToken + serverPublicKey
|
||||
- Refresh endpoint issues new access JWT from valid refresh token
|
||||
- SSE endpoint accepts JWT via query parameter
|
||||
- All 17 existing ITs adapted and passing
|
||||
- 4 new security ITs passing
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-security/04-02-SUMMARY.md`
|
||||
</output>
|
||||
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