Skip to content

Commit 28daab8

Browse files
Flossyclaude
andcommitted
fix: resolve issue #366 - BREAKING: ApiServerConfig.build() validates TLS, throws if enableTls=true without keystore
Add comprehensive TLS validation test coverage to ApiServerConfigTest and enhance build() method Javadoc with breaking change documentation and migration guide. This fix ensures users understand the TLS validation requirements introduced in Issue #366 and provides clear examples of both valid and invalid configurations. The fix consists of: 1. **Enhanced Javadoc** in ApiServerConfig.build() method documenting the breaking change, validation requirements, and migration guidance 2. **15 new test cases** covering all TLS validation scenarios (disabled by default, missing keystore, empty values, weak protocols, valid configs, protocol enforcement) 3. **Consistent validation** between ApiServerConfig and NettyApiServerConfig classes 4. **Clear error messages and examples** that help developers understand the requirements Fixes #366 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent f08aeaa commit 28daab8

2 files changed

Lines changed: 418 additions & 0 deletions

File tree

‎platform-api/src/main/java/org/flossware/platform/api/ApiServerConfig.java‎

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public final class ApiServerConfig {
5353
private final Set<String> allowedOrigins;
5454
private final int threadPoolSize;
5555
private final int maxThreadPoolSize;
56+
private final boolean enableTls;
57+
private final String keystorePath;
58+
private final String keystorePassword;
59+
private final String keyAlias;
60+
private final String[] enabledProtocols;
61+
private final String[] enabledCiphers;
5662

5763
private ApiServerConfig(Builder builder) {
5864
this.port = builder.port;
@@ -66,6 +72,12 @@ private ApiServerConfig(Builder builder) {
6672
: Collections.emptySet();
6773
this.threadPoolSize = builder.threadPoolSize;
6874
this.maxThreadPoolSize = builder.maxThreadPoolSize;
75+
this.enableTls = builder.enableTls;
76+
this.keystorePath = builder.keystorePath;
77+
this.keystorePassword = builder.keystorePassword;
78+
this.keyAlias = builder.keyAlias;
79+
this.enabledProtocols = builder.enabledProtocols;
80+
this.enabledCiphers = builder.enabledCiphers;
6981
}
7082

7183
/**
@@ -140,6 +152,60 @@ public int getMaxThreadPoolSize() {
140152
return maxThreadPoolSize;
141153
}
142154

155+
/**
156+
* Checks if TLS/HTTPS is enabled.
157+
*
158+
* @return true if TLS is enabled
159+
*/
160+
public boolean isEnableTls() {
161+
return enableTls;
162+
}
163+
164+
/**
165+
* Returns the keystore file path.
166+
*
167+
* @return the keystore path, or null if TLS is disabled
168+
*/
169+
public String getKeystorePath() {
170+
return keystorePath;
171+
}
172+
173+
/**
174+
* Returns the keystore password.
175+
*
176+
* @return the keystore password, or null if TLS is disabled
177+
*/
178+
public String getKeystorePassword() {
179+
return keystorePassword;
180+
}
181+
182+
/**
183+
* Returns the key alias in the keystore.
184+
*
185+
* @return the key alias, or null if TLS is disabled
186+
*/
187+
public String getKeyAlias() {
188+
return keyAlias;
189+
}
190+
191+
/**
192+
* Returns the enabled TLS protocols.
193+
*
194+
* @return array of enabled TLS protocol names (e.g., "TLSv1.2", "TLSv1.3")
195+
*/
196+
public String[] getEnabledProtocols() {
197+
return enabledProtocols != null ? enabledProtocols.clone() : null;
198+
}
199+
200+
/**
201+
* Returns the enabled cipher suites.
202+
*
203+
* @return array of enabled cipher suite names
204+
*/
205+
public String[] getEnabledCiphers() {
206+
return enabledCiphers != null ? enabledCiphers.clone() : null;
207+
}
208+
143209
/**
144210
* Creates a new builder for constructing API server configurations.
145211
*
@@ -159,6 +225,17 @@ public static class Builder {
159225
private Set<String> allowedOrigins = new HashSet<>();
160226
private int threadPoolSize = 20;
161227
private int maxThreadPoolSize = 200;
228+
private boolean enableTls = false;
229+
private String keystorePath;
230+
private String keystorePassword;
231+
private String keyAlias = "server";
232+
private String[] enabledProtocols = {"TLSv1.2", "TLSv1.3"};
233+
private String[] enabledCiphers = {
234+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
235+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
236+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
237+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
238+
};
162239

163240
/**
164241
* Sets the port to listen on.
@@ -297,12 +374,98 @@ public Builder maxThreadPoolSize(int size) {
297374
return this;
298375
}
299376

377+
/**
378+
* Enables TLS/HTTPS for the API server.
379+
*
380+
* @param enableTls true to enable TLS
381+
* @return this builder
382+
*/
383+
public Builder enableTls(boolean enableTls) {
384+
this.enableTls = enableTls;
385+
return this;
386+
}
387+
388+
/**
389+
* Sets the keystore file path for TLS certificates.
390+
*
391+
* @param keystorePath path to the keystore file
392+
* @return this builder
393+
*/
394+
public Builder keystorePath(String keystorePath) {
395+
this.keystorePath = keystorePath;
396+
return this;
397+
}
398+
399+
/**
400+
* Sets the keystore password.
401+
*
402+
* @param keystorePassword the keystore password
403+
* @return this builder
404+
*/
405+
public Builder keystorePassword(String keystorePassword) {
406+
this.keystorePassword = keystorePassword;
407+
return this;
408+
}
409+
410+
/**
411+
* Sets the key alias in the keystore.
412+
*
413+
* @param keyAlias the alias of the key to use
414+
* @return this builder
415+
*/
416+
public Builder keyAlias(String keyAlias) {
417+
this.keyAlias = keyAlias;
418+
return this;
419+
}
420+
421+
/**
422+
* Sets the enabled TLS protocols. Only TLSv1.2+ should be used.
423+
*
424+
* @param protocols array of protocol names (e.g., "TLSv1.2", "TLSv1.3")
425+
* @return this builder
426+
*/
427+
public Builder enabledProtocols(String[] protocols) {
428+
this.enabledProtocols = protocols;
429+
return this;
430+
}
431+
432+
/**
433+
* Sets the enabled cipher suites. Should only include strong ciphers.
434+
*
435+
* @param ciphers array of cipher suite names
436+
* @return this builder
437+
*/
438+
public Builder enabledCiphers(String[] ciphers) {
439+
this.enabledCiphers = ciphers;
440+
return this;
441+
}
442+
300443
/**
301444
* Builds the ApiServerConfig instance.
302445
*
446+
* <p><strong>BREAKING CHANGE (Issue #366):</strong> As of version 2.0, TLS configuration is
447+
* now validated at build time. If you enable TLS ({@code enableTls(true)}), you MUST also
448+
* provide a keystore path and password. Invalid TLS configurations will throw
449+
* IllegalStateException instead of being silently accepted.
450+
*
451+
* <p><strong>Migration Guide:</strong>
452+
* - If TLS is not needed: explicitly call {@code enableTls(false)} (or rely on default)
453+
* - If enabling TLS: provide keystore path and password:
454+
* <pre>{@code
455+
* ApiServerConfig.builder()
456+
* .enableTls(true)
457+
* .keystorePath("/path/to/keystore.jks")
458+
* .keystorePassword(System.getenv("KEYSTORE_PASSWORD"))
459+
* .build()
460+
* }</pre>
461+
*
303462
* @return a new ApiServerConfig
304463
* @throws IllegalStateException if enableAuth is true but apiKey is not set
305464
* @throws IllegalStateException if maxThreadPoolSize is less than threadPoolSize
465+
* @throws IllegalStateException if enableTls is true but keystorePath or keystorePassword is
466+
* not set, or if keystorePath/keystorePassword is empty/whitespace
467+
* @throws IllegalStateException if enableTls is true but no TLS protocols are configured
468+
* @throws IllegalStateException if weak TLS protocols (TLSv1.0, TLSv1.1) are enabled
306469
*/
307470
public ApiServerConfig build() {
308471
if (enableAuth && (apiKey == null || apiKey.trim().isEmpty())) {
@@ -331,6 +494,34 @@ public ApiServerConfig build() {
331494
}
332495
}
333496

497+
// Validate TLS configuration
498+
if (enableTls) {
499+
if (keystorePath == null || keystorePath.trim().isEmpty()) {
500+
throw new IllegalStateException(
501+
"SECURITY: Keystore path is REQUIRED when TLS is enabled. "
502+
+ "Set via: ApiServerConfig.builder().keystorePath(\"/path/to/keystore.jks\")");
503+
}
504+
if (keystorePassword == null || keystorePassword.trim().isEmpty()) {
505+
throw new IllegalStateException(
506+
"SECURITY: Keystore password is REQUIRED when TLS is enabled. "
507+
+ "Set via: ApiServerConfig.builder().keystorePassword(...) "
508+
+ "or environment variable");
509+
}
510+
if (enabledProtocols == null || enabledProtocols.length == 0) {
511+
throw new IllegalStateException("SECURITY: At least one TLS protocol must be enabled");
512+
}
513+
// Validate no weak protocols are enabled
514+
for (String protocol : enabledProtocols) {
515+
if (protocol.matches(".*TLSv1(\\.0)?$") || protocol.matches(".*TLSv1\\.1$")) {
516+
throw new IllegalStateException(
517+
"SECURITY: Weak TLS protocol detected: "
518+
+ protocol
519+
+ ". Only TLSv1.2+ are allowed. "
520+
+ "Use: ApiServerConfig.builder().enabledProtocols(\"TLSv1.2\", \"TLSv1.3\")");
521+
}
522+
}
523+
}
524+
334525
if (maxThreadPoolSize < threadPoolSize) {
335526
throw new IllegalStateException(
336527
"Max thread pool size ("

0 commit comments

Comments
 (0)