204 lines
13 KiB
Markdown
204 lines
13 KiB
Markdown
|
|
---
|
||
|
|
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>
|