feat(http): SslContextBuilder supports system/trust-all/trust-paths modes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package com.cameleer.server.app.http;
|
||||
|
||||
import com.cameleer.server.core.http.OutboundHttpProperties;
|
||||
import com.cameleer.server.core.http.OutboundHttpRequestContext;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class SslContextBuilder {
|
||||
|
||||
public SSLContext build(OutboundHttpProperties systemProps, OutboundHttpRequestContext ctx) throws Exception {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
|
||||
if (systemProps.trustAll() || ctx.trustMode() == com.cameleer.server.core.http.TrustMode.TRUST_ALL) {
|
||||
sslContext.init(null, new TrustManager[]{new TrustAllManager()}, null);
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
List<X509Certificate> extraCerts = new ArrayList<>();
|
||||
List<String> paths = new ArrayList<>(systemProps.trustedCaPemPaths());
|
||||
if (ctx.trustMode() == com.cameleer.server.core.http.TrustMode.TRUST_PATHS) {
|
||||
paths.addAll(ctx.trustedCaPemPaths());
|
||||
}
|
||||
for (String p : paths) {
|
||||
extraCerts.addAll(loadPem(p));
|
||||
}
|
||||
|
||||
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
ks.load(null, null);
|
||||
|
||||
// Load JDK default trust roots
|
||||
TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
defaultTmf.init((KeyStore) null);
|
||||
int i = 0;
|
||||
for (TrustManager tm : defaultTmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager x509tm) {
|
||||
for (X509Certificate cert : x509tm.getAcceptedIssuers()) {
|
||||
ks.setCertificateEntry("default-" + (i++), cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add configured extras
|
||||
for (X509Certificate cert : extraCerts) {
|
||||
ks.setCertificateEntry("extra-" + (i++), cert);
|
||||
}
|
||||
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ks);
|
||||
sslContext.init(null, tmf.getTrustManagers(), null);
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
private Collection<? extends X509Certificate> loadPem(String path) throws IOException, java.security.cert.CertificateException {
|
||||
Path p = Path.of(path);
|
||||
if (!Files.exists(p)) {
|
||||
throw new IllegalArgumentException("CA file not found: " + path);
|
||||
}
|
||||
try (var in = Files.newInputStream(p)) {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<X509Certificate> certs = (Collection<X509Certificate>) cf.generateCertificates(in);
|
||||
return certs;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TrustAllManager implements X509TrustManager {
|
||||
@Override public void checkClientTrusted(X509Certificate[] chain, String authType) {}
|
||||
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) {}
|
||||
@Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.cameleer.server.app.http;
|
||||
|
||||
import com.cameleer.server.core.http.OutboundHttpProperties;
|
||||
import com.cameleer.server.core.http.OutboundHttpRequestContext;
|
||||
import com.cameleer.server.core.http.TrustMode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class SslContextBuilderTest {
|
||||
private final OutboundHttpProperties systemProps =
|
||||
new OutboundHttpProperties(false, List.of(), Duration.ofMillis(2000), Duration.ofMillis(5000),
|
||||
null, null, null);
|
||||
private final SslContextBuilder builder = new SslContextBuilder();
|
||||
|
||||
@Test
|
||||
void systemDefaultUsesJdkTrustStore() throws Exception {
|
||||
SSLContext ctx = builder.build(systemProps, OutboundHttpRequestContext.systemDefault());
|
||||
assertThat(ctx).isNotNull();
|
||||
assertThat(ctx.getProtocol()).isEqualTo("TLS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void trustAllSkipsValidation() throws Exception {
|
||||
SSLContext ctx = builder.build(systemProps,
|
||||
new OutboundHttpRequestContext(TrustMode.TRUST_ALL, List.of(), null, null));
|
||||
assertThat(ctx).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void trustPathsLoadsPemFile() throws Exception {
|
||||
Path pem = Path.of("src/test/resources/test-ca.pem");
|
||||
assertThat(pem).exists();
|
||||
SSLContext ctx = builder.build(systemProps,
|
||||
new OutboundHttpRequestContext(TrustMode.TRUST_PATHS, List.of(pem.toString()), null, null));
|
||||
assertThat(ctx).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void trustPathsMissingFileThrows() {
|
||||
assertThatThrownBy(() -> builder.build(systemProps,
|
||||
new OutboundHttpRequestContext(TrustMode.TRUST_PATHS, List.of("/no/such/file.pem"), null, null)))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("CA file not found");
|
||||
}
|
||||
}
|
||||
19
cameleer-server-app/src/test/resources/test-ca.pem
Normal file
19
cameleer-server-app/src/test/resources/test-ca.pem
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDFzCCAf+gAwIBAgIUawEUnI7oAYTMsqYYJUBqGJhM8LMwDQYJKoZIhvcNAQEL
|
||||
BQAwGzEZMBcGA1UEAwwQY2FtZWxlZXItdGVzdC1jYTAeFw0yNjA0MTkxMzUzMTNa
|
||||
Fw0zNjA0MTYxMzUzMTNaMBsxGTAXBgNVBAMMEGNhbWVsZWVyLXRlc3QtY2EwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCalBl0hjPSXYZjFLImDT5awayX
|
||||
nPmX7/TyduX2xSksLnGBfD1YcLYhIaOU3Bp6o+Wld8+nzNjwgMUcKdv1mWqDL8t0
|
||||
nlf0JGAntNEQWGwxFUGSLcmzgVf8/d1s0mpZ1/mS6RDzoQ8i8rNq5mYPXkWFAvgD
|
||||
G1FQpaBs71VC7vVcEgnJ4kK/8cNcQ/nvaI+T/Tk+sciu57XuMoa8AoJV/Oz/hdkc
|
||||
fIoYeUpFp36pYn71yFyZ1N+1xCTi1ruBmMekYWKNaNY6/edk2z3iTFgv4Dk4QS1O
|
||||
6GEdgUi9RQIgQwyYltQK2z+wLA8e982JK5HllsLYU0AcY6fvg+JlhEEpCsdnAgMB
|
||||
AAGjUzBRMB0GA1UdDgQWBBQYRTifnA5YLxiqjIYMDcZLgDrtUDAfBgNVHSMEGDAW
|
||||
gBQYRTifnA5YLxiqjIYMDcZLgDrtUDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQBpNfv6I9dsUflmslv6el5VvDOyZ3GWQIE/Zy0p+i0NYjeWMWtX
|
||||
rFPw1mnfdhlyFMpNkP8ddyDaSoqnnFsC4pXqXibrey0NDYX7zujyVIaXixf6koU9
|
||||
2/vNIb4evnJyVT9CcophDrobkR4NwU9SrMO7KAecb/U6shrfgmjIanUnCsfGdwsP
|
||||
f85DQDbOd9UhnMfMB43I8tjn2Po155npmwGK7J4hZpWTUj/rbNt3fmy2mx6rqdkp
|
||||
p61nLY3ba61bnl7QQ7VW7nhaIC7t5sO6NFDdv06MG8Cr5kcvoJDPe93iNeVT8iLp
|
||||
Rxs85FvIVYIt58jMvr+RSEfTD8fIvXl0uRDy
|
||||
-----END CERTIFICATE-----
|
||||
Reference in New Issue
Block a user