Skip to content

Commit 49ed6e7

Browse files
committed
Merge branch 'custom_ssl'
2 parents 2abc25c + d8c7696 commit 49ed6e7

4 files changed

Lines changed: 12008 additions & 11828 deletions

File tree

CHANGES.TXT

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ All rights reserved. Open Source, see LICENSE
77
-----
88
* Security
99
* Bump mssql to 12.0.2.jre8
10+
* TLS
11+
* `as400` command now takes optional `-certpath full_path_to_cert_chain` merging
12+
provided cert or cert chain to keystore allowing self-signed certs
1013
* Workflow
1114
* remove github maven cache statement
1215

src/main/java/ublu/AS400Factory.java

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,17 @@
3939
import com.ibm.as400.access.AS400SecurityException;
4040
import com.ibm.as400.access.SecureAS400;
4141
import java.beans.PropertyVetoException;
42+
import java.io.FileInputStream;
4243
import java.io.IOException;
44+
import java.security.KeyStore;
45+
import java.security.cert.Certificate;
46+
import java.security.cert.CertificateFactory;
4347
import java.util.logging.Level;
4448
import java.util.logging.Logger;
49+
import javax.net.ssl.SSLContext;
50+
import javax.net.ssl.SSLSocketFactory;
51+
import javax.net.ssl.TrustManager;
52+
import javax.net.ssl.TrustManagerFactory;
4553

4654
/**
4755
* A factory to produce correctly configured AS400 objects.
@@ -143,10 +151,44 @@ protected static AS400 newAS400(SIGNON_SECURITY_TYPE signon_security_type,
143151
String systemName,
144152
String userId,
145153
String password) {
154+
return newAS400(signon_security_type, signon_handler_type, systemName, userId, password, null);
155+
}
156+
157+
/**
158+
* Create a new AS400 object with the correct security type and signon
159+
* handler using our extenders to have the password at our disposal for use
160+
* with JTOpenLite classes. Optionally specify a certificate file for SSL.
161+
*
162+
* @param signon_security_type is this none or ssl?
163+
* @param signon_handler_type handler type for failed signons
164+
* @param systemName systemName
165+
* @param userId userId
166+
* @param password password
167+
* @param certificatePath path to certificate file (optional, for SSL only)
168+
* @return the new AS400 object
169+
*/
170+
protected static AS400 newAS400(SIGNON_SECURITY_TYPE signon_security_type,
171+
SIGNON_HANDLER_TYPE signon_handler_type,
172+
String systemName,
173+
String userId,
174+
String password,
175+
String certificatePath) {
176+
177+
// Configure custom certificate if provided and using SSL
178+
if (signon_security_type == SIGNON_SECURITY_TYPE.SSL && certificatePath != null && !certificatePath.isEmpty()) {
179+
try {
180+
configureCustomCertificate(certificatePath);
181+
} catch (Exception e) {
182+
Logger.getLogger(AS400Factory.class.getName())
183+
.log(Level.SEVERE, "Failed to configure certificate from " + certificatePath, e);
184+
}
185+
}
186+
146187
AS400 as400
147188
= signon_security_type == SIGNON_SECURITY_TYPE.NONE
148189
? new AS400(systemName, userId, password.toCharArray())
149190
: new SecureAS400(systemName, userId, password.toCharArray());
191+
150192
switch (signon_handler_type) {
151193
case CUSTOM:
152194
as400.setSignonHandler(new SignonHandler());
@@ -181,10 +223,49 @@ protected static AS400 newAS400(SIGNON_SECURITY_TYPE signon_security_type,
181223
String userId,
182224
char[] password,
183225
char[] additionalAuthenticationFactor) throws AS400SecurityException, IOException {
226+
return newAS400(signon_security_type, signon_handler_type, systemName, userId, password, additionalAuthenticationFactor, null);
227+
}
228+
229+
/**
230+
* Create a new AS400 object with the correct security type and signon
231+
* handler using our extenders to have the password at our disposal for use
232+
* with JTOpenLite classes. Optionally specify a certificate file for SSL.
233+
*
234+
* @param signon_security_type is this none or ssl?
235+
* @param signon_handler_type handler type for failed signons
236+
* @param systemName systemName
237+
* @param userId userId
238+
* @param password password
239+
* @param additionalAuthenticationFactor e.g., MFA TOTP
240+
* @param certificatePath path to certificate file (optional, for SSL only)
241+
* @throws AS400SecurityException
242+
* @throws IOException
243+
* @return the new AS400 object
244+
*/
245+
protected static AS400 newAS400(SIGNON_SECURITY_TYPE signon_security_type,
246+
SIGNON_HANDLER_TYPE signon_handler_type,
247+
String systemName,
248+
String userId,
249+
char[] password,
250+
char[] additionalAuthenticationFactor,
251+
String certificatePath) throws AS400SecurityException, IOException {
252+
253+
// Configure custom certificate if provided and using SSL
254+
if (signon_security_type == SIGNON_SECURITY_TYPE.SSL && certificatePath != null && !certificatePath.isEmpty()) {
255+
try {
256+
configureCustomCertificate(certificatePath);
257+
} catch (Exception e) {
258+
Logger.getLogger(AS400Factory.class.getName())
259+
.log(Level.SEVERE, "Failed to configure certificate from " + certificatePath, e);
260+
throw new IOException("Failed to configure certificate from " + certificatePath, e);
261+
}
262+
}
263+
184264
AS400 as400
185265
= signon_security_type == SIGNON_SECURITY_TYPE.NONE
186266
? new AS400(systemName, userId, password, additionalAuthenticationFactor)
187267
: new SecureAS400(systemName, userId, password, additionalAuthenticationFactor);
268+
188269
switch (signon_handler_type) {
189270
case CUSTOM:
190271
as400.setSignonHandler(new SignonHandler());
@@ -237,7 +318,30 @@ public static AS400 newAS400(Interpreter interpreter, String systemName, String
237318
*/
238319
public static AS400 newAS400(Interpreter interpreter, String systemName, String userid, String password, SIGNON_SECURITY_TYPE signon_security_type)
239320
throws PropertyVetoException {
240-
AS400 as400 = newAS400(signon_security_type, getSignonHandlerType(interpreter), systemName, userid, password);
321+
String certificatePath = interpreter.getProperty("ssl.certificate.path", null);
322+
AS400 as400 = newAS400(signon_security_type, getSignonHandlerType(interpreter), systemName, userid, password, certificatePath);
323+
return as400;
324+
}
325+
326+
/**
327+
* Create a new AS400 object with our custom signon handler and with the
328+
* system name, user id and password already set. Connections (when made)
329+
* will be via SSL if <code> signon_security_type </code> argument is set to
330+
* <code> SSL </code>. Optionally specify a certificate file path.
331+
*
332+
* @return the new AS400 object
333+
* @param interpreter the interpreter calling us
334+
* @param systemName name or dotted ip
335+
* @param userid d'oh
336+
* @param password d'oh
337+
* @param signon_security_type provides whether we want SSL
338+
* @param certificatePath path to certificate file (optional, for SSL only)
339+
* @throws java.beans.PropertyVetoException if server name or user id cannot
340+
* be set
341+
*/
342+
public static AS400 newAS400(Interpreter interpreter, String systemName, String userid, String password, SIGNON_SECURITY_TYPE signon_security_type, String certificatePath)
343+
throws PropertyVetoException {
344+
AS400 as400 = newAS400(signon_security_type, getSignonHandlerType(interpreter), systemName, userid, password, certificatePath);
241345
return as400;
242346
}
243347
/**
@@ -260,7 +364,33 @@ public static AS400 newAS400(Interpreter interpreter, String systemName, String
260364
*/
261365
public static AS400 newAS400(Interpreter interpreter, String systemName, String userid, char [] password, char[] additionalAuthenticationFactor, SIGNON_SECURITY_TYPE signon_security_type)
262366
throws PropertyVetoException, AS400SecurityException, IOException {
263-
AS400 as400 = newAS400(signon_security_type, getSignonHandlerType(interpreter), systemName, userid, password, additionalAuthenticationFactor);
367+
String certificatePath = interpreter.getProperty("ssl.certificate.path", null);
368+
AS400 as400 = newAS400(signon_security_type, getSignonHandlerType(interpreter), systemName, userid, password, additionalAuthenticationFactor, certificatePath);
369+
return as400;
370+
}
371+
372+
/**
373+
* Create a new AS400 object with our custom signon handler and with the
374+
* system name, user id and password already set. Connections (when made)
375+
* will be via SSL if <code> signon_security_type </code> argument is set to
376+
* <code> SSL </code>. Optionally specify a certificate file path.
377+
*
378+
* @return the new AS400 object
379+
* @param interpreter the interpreter calling us
380+
* @param systemName name or dotted ip addr
381+
* @param userid d'oh
382+
* @param password d'oh
383+
* @param additionalAuthenticationFactor e.g. TOTP for MFA
384+
* @param signon_security_type provides whether we want SSL
385+
* @param certificatePath path to certificate file (optional, for SSL only)
386+
* @throws java.beans.PropertyVetoException if server name or user id cannot
387+
* be set
388+
* @throws com.ibm.as400.access.AS400SecurityException
389+
* @throws java.io.IOException
390+
*/
391+
public static AS400 newAS400(Interpreter interpreter, String systemName, String userid, char [] password, char[] additionalAuthenticationFactor, SIGNON_SECURITY_TYPE signon_security_type, String certificatePath)
392+
throws PropertyVetoException, AS400SecurityException, IOException {
393+
AS400 as400 = newAS400(signon_security_type, getSignonHandlerType(interpreter), systemName, userid, password, additionalAuthenticationFactor, certificatePath);
264394
return as400;
265395
}
266396

@@ -346,4 +476,92 @@ public static Integer serviceNameToInteger(String serviceName) {
346476
}
347477
return serviceInteger;
348478
}
479+
480+
/**
481+
* Configure custom certificate(s) for SSL connections by merging with
482+
* the default truststore. This allows trusting both custom certificates
483+
* and all standard CA certificates.
484+
*
485+
* Supports:
486+
* - Self-signed server certificates
487+
* - Self-signed CA certificates
488+
* - Certificate chains (multiple certificates in one file)
489+
* - X.509 certificates in PEM or DER format
490+
*
491+
* @param certificatePath path to the certificate file (can contain multiple certs)
492+
* @throws Exception if certificate loading or configuration fails
493+
*/
494+
private static void configureCustomCertificate(String certificatePath) throws Exception {
495+
// Load all certificates from the file (supports certificate chains)
496+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
497+
java.util.Collection<? extends Certificate> customCerts;
498+
499+
try (FileInputStream fis = new FileInputStream(certificatePath)) {
500+
customCerts = cf.generateCertificates(fis);
501+
}
502+
503+
if (customCerts.isEmpty()) {
504+
throw new Exception("No certificates found in file: " + certificatePath);
505+
}
506+
507+
// Load the default truststore to preserve existing trusted certificates
508+
TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
509+
defaultTmf.init((KeyStore) null); // null loads the default truststore
510+
511+
// Get the default KeyStore and add our custom certificates to it
512+
KeyStore mergedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
513+
mergedKeyStore.load(null, null);
514+
515+
// Add all custom certificates (handles certificate chains)
516+
int certIndex = 0;
517+
for (Certificate cert : customCerts) {
518+
mergedKeyStore.setCertificateEntry("custom-cert-" + certIndex++, cert);
519+
}
520+
521+
Logger.getLogger(AS400Factory.class.getName())
522+
.log(Level.INFO, "Loaded {0} certificate(s) from {1}", new Object[]{customCerts.size(), certificatePath});
523+
524+
// Copy all certificates from the default truststore
525+
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
526+
String defaultTruststorePath = System.getProperty("javax.net.ssl.trustStore");
527+
if (defaultTruststorePath != null) {
528+
try (FileInputStream defaultFis = new FileInputStream(defaultTruststorePath)) {
529+
String password = System.getProperty("javax.net.ssl.trustStorePassword");
530+
defaultKeyStore.load(defaultFis, password != null ? password.toCharArray() : null);
531+
532+
// Copy all entries from default keystore
533+
java.util.Enumeration<String> aliases = defaultKeyStore.aliases();
534+
while (aliases.hasMoreElements()) {
535+
String alias = aliases.nextElement();
536+
if (defaultKeyStore.isCertificateEntry(alias)) {
537+
mergedKeyStore.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
538+
}
539+
}
540+
}
541+
} else {
542+
// If no custom truststore is set, use the JVM's default CA certificates
543+
// by initializing from the default TrustManager's certificates
544+
for (TrustManager tm : defaultTmf.getTrustManagers()) {
545+
if (tm instanceof javax.net.ssl.X509TrustManager) {
546+
javax.net.ssl.X509TrustManager x509tm = (javax.net.ssl.X509TrustManager) tm;
547+
int i = 0;
548+
for (java.security.cert.X509Certificate cert : x509tm.getAcceptedIssuers()) {
549+
mergedKeyStore.setCertificateEntry("default-ca-" + i++, cert);
550+
}
551+
}
552+
}
553+
}
554+
555+
// Create a TrustManager with the merged KeyStore
556+
TrustManagerFactory mergedTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
557+
mergedTmf.init(mergedKeyStore);
558+
559+
// Set the default SSLContext to use our merged TrustManager
560+
SSLContext sslContext = SSLContext.getInstance("TLS");
561+
sslContext.init(null, mergedTmf.getTrustManagers(), new java.security.SecureRandom());
562+
SSLContext.setDefault(sslContext);
563+
564+
Logger.getLogger(AS400Factory.class.getName())
565+
.log(Level.INFO, "Configured custom SSL certificates (merged with default truststore)");
566+
}
349567
}

src/main/java/ublu/command/CmdAS400.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class CmdAS400 extends Command {
5050

5151
{
5252
setNameAndDescription("as400",
53-
"/4? [-to @var] [--,-as400,-from ~@var] [-usessl] [-ssl ~@tf] [-nodefault] [-new,-instance | -alive | -alivesvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -connectsvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -connectedsvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -connected | -disconnect | -disconnectsvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -ping sysname ~@{[ALL|CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -local | -validate | -qsvcport ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -svcport ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} ~@portnum | -setaspgrp -@{aspgrp} ~@{curlib} ~@{liblist} | -svcportdefault | -proxy ~@{server[:portnum]} | -sockets ~@tf | -netsockets ~@tf | -vrm ] ~@{system} ~@{user} ~@{password} [~@{additionalAuthenticationFactor}] : instance, connect to, query connection, or disconnect from an as400 system");
53+
"/4? [-to @var] [--,-as400,-from ~@var] [-usessl] [-ssl ~@tf] [-certpath ~@full_path_to_cert_chain] [-nodefault] [-new,-instance | -alive | -alivesvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -connectsvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -connectedsvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -connected | -disconnect | -disconnectsvc ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -ping sysname ~@{[ALL|CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -local | -validate | -qsvcport ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} | -svcport ~@{[CENTRAL|COMMAND|DATABASE|DATAQUEUE|FILE|PRINT|RECORDACCESS|SIGNON]} ~@portnum | -setaspgrp -@{aspgrp} ~@{curlib} ~@{liblist} | -svcportdefault | -proxy ~@{server[:portnum]} | -sockets ~@tf | -netsockets ~@tf | -vrm ] ~@{system} ~@{user} ~@{password} [~@{additionalAuthenticationFactor}] : instance, connect to, query connection, or disconnect from an as400 system");
5454
}
5555

5656
/**
@@ -152,6 +152,7 @@ public ArgArray as400(ArgArray argArray) {
152152
String aspGroup = "";
153153
String curLib = "";
154154
String libList = "";
155+
String certPath = "";
155156
boolean defaultServicePorts = true;
156157
boolean useSSL = false;
157158
boolean useSockets = false;
@@ -232,6 +233,9 @@ public ArgArray as400(ArgArray argArray) {
232233
case "-ssl":
233234
useSSL = argArray.nextTupleOrPop().getValue().equals(true);
234235
break;
236+
case "-certpath":
237+
certPath = argArray.nextMaybeQuotationTuplePopString();
238+
break;
235239
case "-sockets":
236240
operation = OPERATIONS.USESOCKETS;
237241
useSockets = argArray.nextTupleOrPop().getValue().equals(true);
@@ -257,6 +261,9 @@ public ArgArray as400(ArgArray argArray) {
257261
setCommandResult(COMMANDRESULT.FAILURE);
258262
} else {
259263
Integer serviceInteger;
264+
if (certPath.length() > 0) {
265+
this.getInterpreter().setProperty("ssl.certificate.path", certPath);
266+
}
260267
switch (operation) {
261268
case INSTANCE:
262269
if (getAs400() != null) {

0 commit comments

Comments
 (0)