Skip to content

Commit 79ec6a4

Browse files
committed
LDEV-5571 Custom truststore for SSL certs
1 parent 6fb24d1 commit 79ec6a4

17 files changed

Lines changed: 1029 additions & 132 deletions

File tree

core/src/main/cfml/context/admin/services.certificates.cfm

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
<cflocation url="#request.self#" addtoken="no">
77
</cfif>
88

9-
10-
11-
<!---
9+
<!---
1210
Defaults --->
1311
<cfparam name="url.action2" default="list">
1412
<cfparam name="form.mainAction" default="none">
@@ -24,34 +22,33 @@ Defaults --->
2422
<cfset _port=session.certPort>
2523

2624
<cfscript>
27-
LuceeTrustStore = false;
28-
if ((server.system.properties["lucee.use.lucee.SSL.TrustStore"]?: false)
29-
|| (server.system.environment["lucee_use_lucee_SSL_TrustStore"]?: false)){
30-
LuceeTrustStore = true;
31-
};
32-
25+
customCaCertsEnabled = !(server.system.properties["lucee.ssl.customcacerts.enabled"]?: "true").equalsIgnoreCase("false");
3326
</cfscript>
3427

35-
<cfif !LuceeTrustStore>
28+
<cfif !customCaCertsEnabled>
3629
<p>
37-
<b>As Lucee is currently using the JVM TrustStore/cacerts file, this functionality isn't available.</b>
30+
<b>Custom CA certificates are disabled.</b>
3831
<br><br>
39-
Set the following System or Environment variables to enable: <code>lucee.use.lucee.SSL.TrustStore = true;</code>
32+
Set the following System or Environment variable to enable: <code>lucee.ssl.customcacerts.enabled=true</code>
4033
</p>
4134
</cfif>
4235

4336
<cftry>
4437
<cfswitch expression="#form.mainAction#">
45-
<!--- UPDATE --->
46-
38+
<!--- INSTALL --->
4739
<cfcase value="#stText.services.certificate.install#">
48-
<cfadmin
40+
<cfadmin
4941
type="#request.adminType#"
5042
password="#session["password"&request.adminType]#"
5143
action="updatesslcertificate" host="#form.host#" port="#form.port#">
52-
53-
54-
</cfcase>
44+
</cfcase>
45+
<!--- REMOVE --->
46+
<cfcase value="Remove">
47+
<cfadmin
48+
type="#request.adminType#"
49+
password="#session["password"&request.adminType]#"
50+
action="removesslcertificate" alias="#form.alias#">
51+
</cfcase>
5552
</cfswitch>
5653
<cfcatch>
5754
<cfset error.message=cfcatch.message>
@@ -61,13 +58,13 @@ Defaults --->
6158
</cftry>
6259

6360

64-
<!---
61+
<!---
6562
Redirtect to entry --->
6663
<cfif cgi.request_method EQ "POST" and error.message EQ "">
6764
<cflocation url="#request.self#?action=#url.action#" addtoken="no">
6865
</cfif>
6966

70-
<!---
67+
<!---
7168
Error Output --->
7269
<cfset printError(error)>
7370
<cfoutput>
@@ -104,9 +101,55 @@ Error Output --->
104101
</table>
105102
</cfformClassic>
106103

104+
<!--- Installed certificates in custom-cacerts --->
105+
<cfif customCaCertsEnabled>
106+
<cftry>
107+
<cfadmin
108+
type="#request.adminType#"
109+
password="#session["password"&request.adminType]#"
110+
action="getallsslcertificate" returnvariable="installedCerts">
111+
112+
<h2>Installed Certificates</h2>
113+
<cfif installedCerts.recordcount>
114+
<table class="maintbl">
115+
<thead>
116+
<tr>
117+
<th>#stText.services.certificate.subject#</th>
118+
<th>#stText.services.certificate.issuer#</th>
119+
<th>Alias</th>
120+
<th></th>
121+
</tr>
122+
</thead>
123+
<tbody>
124+
<cfloop query="installedCerts">
125+
<tr>
126+
<td>#installedCerts.subject#</td>
127+
<td>#installedCerts.issuer#</td>
128+
<td>#installedCerts.alias#</td>
129+
<td>
130+
<form action="#request.self#?action=#url.action#" method="post" style="display:inline">
131+
<input type="hidden" name="alias" value="#installedCerts.alias#">
132+
<input type="hidden" name="mainAction" value="Remove">
133+
<input class="button small" type="submit" value="Remove" onclick="return confirm('Remove certificate #JSStringFormat(installedCerts.alias)#?')">
134+
</form>
135+
</td>
136+
</tr>
137+
</cfloop>
138+
</tbody>
139+
</table>
140+
<cfelse>
141+
<p>No certificates installed in custom-cacerts.</p>
142+
</cfif>
143+
<cfcatch>
144+
<div class="error">#cfcatch.message# #cfcatch.detail#</div>
145+
</cfcatch>
146+
</cftry>
147+
</cfif>
148+
149+
<!--- Preview certs from remote host --->
107150
<cfif len(_host) and len(_port)>
108151
<cftry>
109-
<cfadmin
152+
<cfadmin
110153
type="#request.adminType#"
111154
password="#session["password"&request.adminType]#"
112155
action="getsslcertificate" host="#_host#" port="#_port#" returnvariable="qry">
@@ -143,4 +186,4 @@ Error Output --->
143186
</cfcatch>
144187
</cftry>
145188
</cfif>
146-
</cfoutput>
189+
</cfoutput>

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private static CloseableHttpClient getSharedClient() throws GeneralSecurityExcep
6262
if (SHARED_CLIENT == null) {
6363
synchronized (CLIENT_LOCK) {
6464
if (SHARED_CLIENT == null) {
65-
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
65+
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, null, null, true, "true");
6666
SHARED_CLIENT = builder.build();
6767
}
6868
}
@@ -221,7 +221,7 @@ public static InputStream get(URL url, String username, String password, long co
221221

222222
// Handle proxy and credentials
223223
ProxyData proxy = getProxyData(url.getHost());
224-
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
224+
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, null, null, true, "true");
225225
HttpHost httpHost = new HttpHost(url.getHost(), url.getPort());
226226
HttpContext context = HTTPEngine4Impl.setCredentials(builder, httpHost, username, password, false);
227227
HTTPEngine4Impl.setProxy(url.getHost(), builder, request, proxy);
@@ -262,7 +262,7 @@ public static HTTPResponse head(URL url, long connectTimeout, long readTimeout,
262262

263263
try {
264264
// Get configured HttpClientBuilder (with connection pooling, true = use pooling)
265-
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
265+
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, null, null, true, "true");
266266

267267
// Create HTTP HEAD request
268268
HttpHead request = new HttpHead(url.toString());
@@ -305,7 +305,7 @@ public static boolean exists(URL url) {
305305
public static boolean exists(URL url, long connectTimeout, long readTimeout) {
306306
try {
307307
// Get configured HttpClientBuilder (with connection pooling, true = use pooling)
308-
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, "true");
308+
HttpClientBuilder builder = HTTPEngine4Impl.getHttpClientBuilder(true, null, null, null, null, true, "true");
309309

310310
// Create HTTP HEAD request
311311
HttpHead request = new HttpHead(url.toString());

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

Lines changed: 44 additions & 37 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;
@@ -110,6 +105,7 @@
110105
import lucee.runtime.PageContextImpl;
111106
import lucee.runtime.engine.ThreadLocalPageContext;
112107
import lucee.runtime.net.http.ReqRspUtil;
108+
import lucee.runtime.net.http.SSLUtil;
113109
import lucee.runtime.net.http.sni.DefaultHostnameVerifierImpl;
114110
import lucee.runtime.net.http.sni.DefaultHttpClientConnectionOperatorImpl;
115111
import lucee.runtime.net.http.sni.SSLConnectionSocketFactoryImpl;
@@ -272,10 +268,9 @@ private static Header toHeader(lucee.commons.net.http.Header header) {
272268
return new HeaderImpl(header.getName(), header.getValue());
273269
}
274270

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

280275
if (!pooling) {
281276
HttpClientBuilder builder = HttpClients.custom();
@@ -331,31 +326,43 @@ public static void setTimeout(HttpClientBuilder builder, TimeSpan timeout) {
331326
builder.setDefaultRequestConfig(rcBuilder.build());
332327
}
333328

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

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

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

361368
public static void releaseConnectionManager() {
@@ -399,7 +406,7 @@ private static HTTPResponse invoke(URL url, HttpUriRequest request, String usern
399406
CloseableHttpClient client;
400407
proxy = ProxyDataImpl.validate(proxy, url.getHost());
401408

402-
HttpClientBuilder builder = getHttpClientBuilder(pooling, null, null, String.valueOf(redirect));
409+
HttpClientBuilder builder = getHttpClientBuilder(pooling, null, null, null, null, true, String.valueOf(redirect));
403410

404411
HttpHost hh = new HttpHost(url.getHost(), url.getPort());
405412
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;
@@ -157,6 +158,7 @@
157158
import lucee.runtime.monitor.RequestMonitorProImpl;
158159
import lucee.runtime.monitor.RequestMonitorWrap;
159160
import lucee.runtime.net.http.ReqRspUtil;
161+
import lucee.runtime.net.http.SSLUtil;
160162
import lucee.runtime.net.mail.Server;
161163
import lucee.runtime.net.mail.ServerImpl;
162164
import lucee.runtime.net.proxy.ProxyData;
@@ -338,6 +340,7 @@ public static ConfigServerImpl newInstanceServer(CFMLEngineImpl engine, Map<Stri
338340
config.setRoot(root);
339341
// admin mode
340342
load(config, root, false, doNew, essentialOnly);
343+
SSLUtil.init( Paths.get( config.getConfigDir().getAbsolutePath(), "security" ) );
341344

342345
if (!essentialOnly) {
343346
createContextFiles(configDir, config, doNew);

0 commit comments

Comments
 (0)