Skip to content

Commit dd5c638

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

17 files changed

Lines changed: 1028 additions & 130 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: 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
}

0 commit comments

Comments
 (0)