Files
cameleer-server/.planning/phases/04-security/04-01-PLAN.md

204 lines
13 KiB
Markdown
Raw Normal View History

---
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>