Skip to content

Commit 6add18c

Browse files
committed
LDEV-5571 Custom truststore for SSL certs
1 parent 120710c commit 6add18c

16 files changed

Lines changed: 872 additions & 108 deletions

File tree

core/src/main/java/lucee/commons/net/http/HTTPDownloader.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ public static InputStream get( URL url, String username, String password, long c
191191

192192
try {
193193
// Get configured HttpClientBuilder (with connection pooling, true = use pooling)
194-
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder( true, null, null, "true" );
194+
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder( true, null, null, null, null, true, "true" );
195195

196196
// Create HTTP GET request
197197
HttpGet request = new HttpGet( url.toString() );
@@ -240,7 +240,7 @@ public static HTTPResponse head( URL url, long connectTimeout, long readTimeout,
240240

241241
try {
242242
// Get configured HttpClientBuilder (with connection pooling, true = use pooling)
243-
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder( true, null, null, "true" );
243+
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder( true, null, null, null, null, true, "true" );
244244

245245
// Create HTTP HEAD request
246246
HttpHead request = new HttpHead( url.toString() );
@@ -283,7 +283,7 @@ public static boolean exists( URL url ) {
283283
public static boolean exists( URL url, long connectTimeout, long readTimeout ) {
284284
try {
285285
// Get configured HttpClientBuilder (with connection pooling, true = use pooling)
286-
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder( true, null, null, "true" );
286+
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder( true, null, null, null, null, true, "true" );
287287

288288
// Create HTTP HEAD request
289289
HttpHead request = new HttpHead( url.toString() );

core/src/main/java/lucee/commons/net/http/httpclient/HTTPEngine4Impl.java

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,13 @@
1818
**/
1919
package lucee.commons.net.http.httpclient;
2020

21-
import java.io.File;
22-
import java.io.FileInputStream;
2321
import java.io.IOException;
2422
import java.io.InputStream;
2523
import java.lang.reflect.Field;
2624
import java.net.URL;
25+
import java.nio.file.Path;
26+
import java.nio.file.Paths;
2727
import java.security.GeneralSecurityException;
28-
import java.security.KeyManagementException;
29-
import java.security.KeyStore;
30-
import java.security.KeyStoreException;
31-
import java.security.NoSuchAlgorithmException;
32-
import java.security.UnrecoverableKeyException;
33-
import java.security.cert.CertificateException;
3428
import java.util.ArrayList;
3529
import java.util.Collection;
3630
import java.util.Iterator;
@@ -41,7 +35,7 @@
4135
import java.util.concurrent.TimeUnit;
4236
import java.util.concurrent.atomic.AtomicBoolean;
4337

44-
import javax.net.ssl.KeyManagerFactory;
38+
import javax.net.ssl.HostnameVerifier;
4539
import javax.net.ssl.SSLContext;
4640

4741
import org.apache.http.Header;
@@ -73,6 +67,7 @@
7367
import org.apache.http.conn.HttpClientConnectionManager;
7468
import org.apache.http.conn.socket.ConnectionSocketFactory;
7569
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
70+
import org.apache.http.conn.ssl.NoopHostnameVerifier;
7671
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
7772
import org.apache.http.entity.ByteArrayEntity;
7873
import org.apache.http.entity.ContentType;
@@ -109,6 +104,7 @@
109104
import lucee.runtime.PageContextImpl;
110105
import lucee.runtime.engine.ThreadLocalPageContext;
111106
import lucee.runtime.net.http.ReqRspUtil;
107+
import lucee.runtime.net.http.SSLUtil;
112108
import lucee.runtime.net.http.sni.DefaultHostnameVerifierImpl;
113109
import lucee.runtime.net.http.sni.DefaultHttpClientConnectionOperatorImpl;
114110
import lucee.runtime.net.http.sni.SSLConnectionSocketFactoryImpl;
@@ -271,9 +267,9 @@ private static Header toHeader(lucee.commons.net.http.Header header) {
271267
return new HeaderImpl(header.getName(), header.getValue());
272268
}
273269

274-
public static HttpClientBuilder getHttpClientBuilder(boolean pooling, String clientCert, String clientCertPassword, String redirect) throws GeneralSecurityException, IOException {
275-
String key = clientCert + ":" + clientCertPassword;
276-
Registry<ConnectionSocketFactory> reg = StringUtil.isEmpty(clientCert, true) ? createRegistry() : createRegistry(clientCert, clientCertPassword);
270+
public static HttpClientBuilder getHttpClientBuilder(boolean pooling, String clientCert, String clientCertPassword, String trustStore, String trustStorePassword, boolean sslVerify, String redirect) throws GeneralSecurityException {
271+
String key = clientCert + ":" + clientCertPassword + ":" + trustStore + ":" + trustStorePassword + ":" + sslVerify;
272+
Registry<ConnectionSocketFactory> reg = createRegistry( clientCert, clientCertPassword, trustStore, trustStorePassword, sslVerify );
277273

278274
if (!pooling) {
279275
HttpClientBuilder builder = HttpClients.custom();
@@ -328,31 +324,43 @@ public static void setTimeout(HttpClientBuilder builder, TimeSpan timeout) {
328324
builder.setDefaultRequestConfig(rcBuilder.build());
329325
}
330326

331-
private static Registry<ConnectionSocketFactory> createRegistry() throws GeneralSecurityException {
332-
SSLContext sslcontext = SSLContext.getInstance("TLS");
333-
sslcontext.init(null, null, new java.security.SecureRandom());
334-
SSLConnectionSocketFactory defaultsslsf = new SSLConnectionSocketFactoryImpl(sslcontext, new DefaultHostnameVerifierImpl());
335-
/* Register connection handlers */
336-
return RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", defaultsslsf).build();
327+
private static Registry<ConnectionSocketFactory> createRegistry( String clientCert, String clientCertPassword, String trustStore, String trustStorePassword, boolean sslVerify ) throws GeneralSecurityException {
328+
SSLContext sslContext;
329+
HostnameVerifier hostnameVerifier;
337330

338-
}
331+
try {
332+
Path clientCertPath = StringUtil.isEmpty( clientCert, true ) ? null : Paths.get( clientCert );
333+
char[] clientPassword = clientCertPassword != null ? clientCertPassword.toCharArray() : null;
334+
335+
if ( !sslVerify ) {
336+
// Disable all SSL verification (like curl -k)
337+
sslContext = SSLUtil.createUnsafeSSLContext( clientCertPath, clientPassword );
338+
hostnameVerifier = NoopHostnameVerifier.INSTANCE;
339+
}
340+
else if ( !StringUtil.isEmpty( trustStore, true ) ) {
341+
// Use custom trust store
342+
Path trustStorePath = Paths.get( trustStore );
343+
char[] trustPassword = trustStorePassword != null ? trustStorePassword.toCharArray() : "changeit".toCharArray();
344+
List<SSLUtil.TrustStoreConfig> additionalTrustStores = new ArrayList<>();
345+
additionalTrustStores.add( new SSLUtil.TrustStoreConfig( trustStorePath, trustPassword ) );
346+
sslContext = SSLUtil.createSSLContext( clientCertPath, clientPassword, additionalTrustStores );
347+
hostnameVerifier = new DefaultHostnameVerifierImpl();
348+
}
349+
else {
350+
// Standard mode with JVM + custom-cacerts
351+
sslContext = SSLUtil.createSSLContext( clientCertPath, clientPassword );
352+
hostnameVerifier = new DefaultHostnameVerifierImpl();
353+
}
354+
}
355+
catch ( IOException e ) {
356+
throw new GeneralSecurityException( "Failed to create SSL context", e );
357+
}
339358

340-
private static Registry<ConnectionSocketFactory> createRegistry(String clientCert, String clientCertPassword)
341-
throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
342-
// Currently, clientCert force usePool to being ignored
343-
if (clientCertPassword == null) clientCertPassword = "";
344-
// Load the client cert
345-
File ksFile = new File(clientCert);
346-
KeyStore clientStore = KeyStore.getInstance("PKCS12");
347-
clientStore.load(new FileInputStream(ksFile), clientCertPassword.toCharArray());
348-
// Prepare the keys
349-
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
350-
kmf.init(clientStore, clientCertPassword.toCharArray());
351-
SSLContext sslcontext = SSLContext.getInstance("TLS");
352-
// Configure the socket factory
353-
sslcontext.init(kmf.getKeyManagers(), null, new java.security.SecureRandom());
354-
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactoryImpl(sslcontext, new DefaultHostnameVerifierImpl());
355-
return RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslsf).build();
359+
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactoryImpl( sslContext, hostnameVerifier );
360+
return RegistryBuilder.<ConnectionSocketFactory>create()
361+
.register( "http", PlainConnectionSocketFactory.getSocketFactory() )
362+
.register( "https", sslsf )
363+
.build();
356364
}
357365

358366
public static void releaseConnectionManager() {
@@ -392,7 +400,7 @@ private static HTTPResponse invoke(URL url, HttpUriRequest request, String usern
392400
CloseableHttpClient client;
393401
proxy = ProxyDataImpl.validate(proxy, url.getHost());
394402

395-
HttpClientBuilder builder = getHttpClientBuilder(pooling, null, null, String.valueOf(redirect));
403+
HttpClientBuilder builder = getHttpClientBuilder(pooling, null, null, null, null, true, String.valueOf(redirect));
396404

397405
HttpHost hh = new HttpHost(url.getHost(), url.getPort());
398406
setHeader(request, headers);

core/src/main/java/lucee/runtime/config/ConfigFactoryImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.net.MalformedURLException;
2828
import java.net.URL;
2929
import java.nio.charset.Charset;
30+
import java.nio.file.Paths;
3031
import java.security.NoSuchAlgorithmException;
3132
import java.sql.SQLException;
3233
import java.util.ArrayList;
@@ -158,6 +159,7 @@
158159
import lucee.runtime.monitor.RequestMonitorProImpl;
159160
import lucee.runtime.monitor.RequestMonitorWrap;
160161
import lucee.runtime.net.http.ReqRspUtil;
162+
import lucee.runtime.net.http.SSLUtil;
161163
import lucee.runtime.net.mail.Server;
162164
import lucee.runtime.net.mail.ServerImpl;
163165
import lucee.runtime.net.proxy.ProxyData;
@@ -328,6 +330,7 @@ public static ConfigServerImpl newInstanceServer(CFMLEngineImpl engine, Map<Stri
328330
config.setRoot(root);
329331
// admin mode
330332
load(config, root, false, doNew, essentialOnly);
333+
SSLUtil.init( Paths.get( config.getConfigDir().getAbsolutePath(), "security" ) );
331334

332335
if (!essentialOnly) {
333336
createContextFiles(configDir, config, doNew);

core/src/main/java/lucee/runtime/functions/other/SSLCertificateInstall.java

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,46 @@
1818
**/
1919
package lucee.runtime.functions.other;
2020

21-
import lucee.commons.io.SystemUtil;
21+
import lucee.commons.io.res.Resource;
22+
import lucee.commons.net.http.httpclient.HTTPEngine4Impl;
2223
import lucee.runtime.PageContext;
23-
import lucee.runtime.exp.ApplicationException;
2424
import lucee.runtime.exp.PageException;
2525
import lucee.runtime.ext.function.Function;
26-
import lucee.commons.io.res.Resource;
2726
import lucee.runtime.net.http.CertificateInstaller;
2827
import lucee.runtime.op.Caster;
2928

3029
public final class SSLCertificateInstall implements Function {
3130

3231
private static final long serialVersionUID = -831759073098524176L;
3332

34-
public static String call(PageContext pc, String host) throws PageException {
35-
return call(pc, host, 443);
33+
public static String call( PageContext pc, String host ) throws PageException {
34+
return call( pc, host, 443 );
3635
}
3736

38-
public static String call(PageContext pc, String host, Number port) throws PageException {
39-
return call(pc, host, port, null, null);
37+
public static String call( PageContext pc, String host, Number port ) throws PageException {
38+
return call( pc, host, port, null, null );
4039
}
4140

42-
public static String call(PageContext pc, String host, Number port, Object cacerts) throws PageException {
43-
return call(pc, host, port, cacerts, null);
41+
public static String call( PageContext pc, String host, Number port, Object cacerts ) throws PageException {
42+
return call( pc, host, port, cacerts, null );
4443
}
4544

46-
public static String call(PageContext pc, String host, Number port, Object cacerts, String password) throws PageException {
47-
CertificateInstaller installer;
48-
Resource _cacerts;
45+
public static String call( PageContext pc, String host, Number port, Object cacerts, String password ) throws PageException {
4946
try {
50-
if (cacerts == null) {
51-
if (!SystemUtil.getSystemPropOrEnvVar("lucee.use.lucee.SSL.TrustStore", "").equalsIgnoreCase("true"))
52-
throw new ApplicationException("Using JVM cacerts, set lucee.use.lucee.SSL.TrustStore=true to enable"); // LDEV-917
53-
_cacerts = pc.getConfig().getSecurityDirectory();
54-
} else {
55-
_cacerts = Caster.toResource(pc, cacerts, true);
47+
if ( cacerts == null ) {
48+
CertificateInstaller.installToCustomCaCerts( host, Caster.toIntValue( port ) );
5649
}
57-
if (password == null) installer = new CertificateInstaller(_cacerts, host, Caster.toIntValue(port)); // use default password changeit
58-
else installer = new CertificateInstaller(_cacerts, host, Caster.toIntValue(port), password.toCharArray());
59-
installer.installAll(true);
60-
} catch (Exception e){
61-
throw Caster.toPageException(e);
50+
else {
51+
Resource keystore = Caster.toResource( pc, cacerts, true );
52+
CertificateInstaller installer = password == null
53+
? new CertificateInstaller( keystore, host, Caster.toIntValue( port ) )
54+
: new CertificateInstaller( keystore, host, Caster.toIntValue( port ), password.toCharArray() );
55+
installer.installAll( true );
56+
}
57+
HTTPEngine4Impl.releaseConnectionManager();
58+
}
59+
catch ( Exception e ) {
60+
throw Caster.toPageException( e );
6261
}
6362
return "";
6463
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package lucee.runtime.functions.other;
2+
3+
import java.io.OutputStream;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
import java.security.KeyStore;
7+
8+
import lucee.commons.net.http.httpclient.HTTPEngine4Impl;
9+
import lucee.runtime.PageContext;
10+
import lucee.runtime.exp.ApplicationException;
11+
import lucee.runtime.exp.PageException;
12+
import lucee.runtime.ext.function.Function;
13+
import lucee.runtime.net.http.SSLUtil;
14+
import lucee.runtime.op.Caster;
15+
16+
public final class SSLCertificateRemove implements Function {
17+
18+
private static final long serialVersionUID = 1L;
19+
private static final char[] DEFAULT_PASSWORD = "changeit".toCharArray();
20+
21+
public static boolean call( PageContext pc, String alias ) throws PageException {
22+
if ( !SSLUtil.isCustomCaCertsEnabled() ) {
23+
throw new ApplicationException( "custom-cacerts is disabled. Set lucee.ssl.customcacerts.enabled=true to enable." );
24+
}
25+
26+
Path customCaCertsPath = SSLUtil.getCustomCaCertsPath();
27+
if ( customCaCertsPath == null || !Files.exists( customCaCertsPath ) ) {
28+
throw new ApplicationException( "custom-cacerts keystore does not exist." );
29+
}
30+
31+
try {
32+
KeyStore ks = SSLUtil.loadKeyStore( customCaCertsPath, DEFAULT_PASSWORD );
33+
34+
if ( !ks.containsAlias( alias ) ) {
35+
throw new ApplicationException( "Certificate with alias [" + alias + "] not found in custom-cacerts." );
36+
}
37+
38+
ks.deleteEntry( alias );
39+
40+
try ( OutputStream os = Files.newOutputStream( customCaCertsPath ) ) {
41+
ks.store( os, DEFAULT_PASSWORD );
42+
}
43+
44+
// Invalidate connection managers so new connections use updated trust material
45+
HTTPEngine4Impl.releaseConnectionManager();
46+
47+
return true;
48+
}
49+
catch ( ApplicationException ae ) {
50+
throw ae;
51+
}
52+
catch ( Exception e ) {
53+
throw Caster.toPageException( e );
54+
}
55+
}
56+
57+
}

0 commit comments

Comments
 (0)