3939import com .ibm .as400 .access .AS400SecurityException ;
4040import com .ibm .as400 .access .SecureAS400 ;
4141import java .beans .PropertyVetoException ;
42+ import java .io .FileInputStream ;
4243import java .io .IOException ;
44+ import java .security .KeyStore ;
45+ import java .security .cert .Certificate ;
46+ import java .security .cert .CertificateFactory ;
4347import java .util .logging .Level ;
4448import 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}
0 commit comments