001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.iso;
020
021import org.jpos.core.Configurable;
022import org.jpos.core.Configuration;
023import org.jpos.core.ConfigurationException;
024import org.jpos.util.SimpleLogSource;
025
026import javax.naming.InvalidNameException;
027import javax.naming.ldap.LdapName;
028import javax.naming.ldap.Rdn;
029import javax.net.ssl.*;
030import javax.security.auth.x500.X500Principal;
031import java.io.File;
032import java.io.FileInputStream;
033import java.io.IOException;
034import java.net.InetAddress;
035import java.net.ServerSocket;
036import java.net.Socket;
037import java.net.UnknownHostException;
038import java.security.cert.Certificate;
039import java.security.cert.CertificateException;
040import java.security.cert.X509Certificate;
041import java.security.GeneralSecurityException;
042import java.security.KeyStore;
043import java.security.SecureRandom;
044
045/**
046 * <code>SunJSSESocketFactory</code> is used by BaseChannel and ISOServer
047 * in order to provide hooks for SSL implementations.
048 *
049 * @version $Revision$ $Date$
050 * @author  Bharavi Gade
051 * @author Alwyn Schoeman
052 * @since   1.3.3
053 */
054public class GenericSSLSocketFactory 
055        extends SimpleLogSource 
056        implements ISOServerSocketFactory,ISOClientSocketFactory, Configurable
057{ 
058    /** Default constructor; no instance state to initialise. */
059    public GenericSSLSocketFactory() {}
060
061    private SSLContext sslc=null;
062    private SSLServerSocketFactory  serverFactory=null;
063    private SSLSocketFactory socketFactory=null;
064
065    private String keyStore=null;
066    private String password=null;
067    private String keyPassword=null;
068    private String serverName;
069    private boolean clientAuthNeeded=false;
070    private boolean serverAuthNeeded=false;
071    private String[] enabledCipherSuites;
072    private String[] enabledProtocols;
073
074    private Configuration cfg;
075
076    /**
077     * Sets the path of the JKS key store used for the TLS handshake.
078     *
079     * @param keyStore filesystem path of the JKS key store
080     */
081    public void setKeyStore(String keyStore){
082        this.keyStore=keyStore;
083    }
084
085    /**
086     * Sets the key store password.
087     *
088     * @param password key store password
089     */
090    public void setPassword(String password){
091        this.password=password;
092    }
093
094    /**
095     * Sets the password protecting the private key entry.
096     *
097     * @param keyPassword password protecting the private key entry
098     */
099    public void setKeyPassword(String keyPassword){
100        this.keyPassword=keyPassword;
101    }
102
103    /**
104     * Sets the Common Name (CN) used to verify the peer certificate.
105     *
106     * @param serverName expected Common Name (CN) of the peer certificate
107     */
108    public void setServerName(String serverName){
109        this.serverName=serverName;
110    }
111
112    /**
113     * Toggles whether accepted sockets require TLS client authentication.
114     *
115     * @param clientAuthNeeded require TLS client authentication on accepted sockets
116     */
117    public void setClientAuthNeeded(boolean clientAuthNeeded){
118        this.clientAuthNeeded=clientAuthNeeded;
119    }
120
121    /**
122     * Toggles whether outbound sockets validate the server certificate chain.
123     *
124     * @param serverAuthNeeded validate the server certificate chain on outbound sockets
125     */
126    public void setServerAuthNeeded(boolean serverAuthNeeded){
127        this.serverAuthNeeded=serverAuthNeeded;
128    }
129
130    private TrustManager[] getTrustManagers(KeyStore ks)
131        throws GeneralSecurityException {
132        if (serverAuthNeeded) {
133            TrustManagerFactory tm = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
134            tm.init( ks ); 
135            return tm.getTrustManagers(); 
136        } else {
137            // Create a trust manager that does not validate certificate chains
138            return new TrustManager[]{
139                new X509TrustManager() {
140                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
141                        return new java.security.cert.X509Certificate[] {};
142                    }
143                    public void checkClientTrusted(
144                        java.security.cert.X509Certificate[] certs, String authType) {
145                    }
146                    public void checkServerTrusted(
147                        java.security.cert.X509Certificate[] certs, String authType) {
148                    }
149                }
150            };
151        }
152    }
153
154    /**
155     * Create a SSLSocket Context
156     * @return the SSLContext
157     * @returns null if exception occurrs
158     */
159    private SSLContext getSSLContext() throws ISOException {
160        if(password==null)  password=getPassword();
161        if(keyPassword ==null)  keyPassword=getKeyPassword();
162        if(keyStore==null || keyStore.length()==0) {
163            keyStore=System.getProperty("user.home")+File.separator+".keystore";
164        }
165
166        try{
167            KeyStore ks = KeyStore.getInstance( "JKS" );
168            FileInputStream fis = new FileInputStream (new File (keyStore));
169            ks.load(fis,password.toCharArray());
170            fis.close();
171            KeyManagerFactory km = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
172            km.init( ks, keyPassword.toCharArray() );
173            KeyManager[] kma = km.getKeyManagers();
174            TrustManager[] tma = getTrustManagers( ks );
175            SSLContext sslc = SSLContext.getInstance( "TLS" );
176            sslc.init( kma, tma, new SecureRandom() );
177            return sslc;
178        } catch(Exception e) {
179            throw new ISOException (e);
180        } finally { 
181            password=null;
182            keyPassword=null;
183        }
184    }
185
186    /**
187     * Create a socket factory
188     * @return the socket factory
189     * @exception ISOException if an error occurs during server socket
190     * creation
191     */
192    protected SSLServerSocketFactory createServerSocketFactory() 
193        throws ISOException
194    {
195        if(sslc==null) sslc=getSSLContext();
196        return sslc.getServerSocketFactory();
197    }
198        
199    /**
200     * Create a socket factory
201     * @return the socket factory
202     * @exception ISOException if an error occurs during server socket
203     * creation
204     */
205    protected SSLSocketFactory createSocketFactory() 
206        throws ISOException
207    {
208        if(sslc==null) sslc=getSSLContext();
209        return sslc.getSocketFactory();
210    }
211    
212    /**
213     * Create a server socket on the specified port (port 0 indicates
214     * an anonymous port).
215     * @param  port the port number
216     * @return the server socket on the specified port
217     * @exception IOException should an I/O error occurs during 
218     * @exception ISOException should an error occurs during 
219     * creation
220     */
221    public ServerSocket createServerSocket(int port) 
222        throws IOException, ISOException
223    {
224        if(serverFactory==null) serverFactory=createServerSocketFactory();
225        ServerSocket socket = serverFactory.createServerSocket(port);
226        SSLServerSocket serverSocket = (SSLServerSocket) socket;
227        serverSocket.setNeedClientAuth(clientAuthNeeded);
228        if (enabledCipherSuites != null && enabledCipherSuites.length > 0) {
229            serverSocket.setEnabledCipherSuites(enabledCipherSuites);
230        }
231        if (enabledProtocols != null && enabledProtocols.length > 0) {
232            serverSocket.setEnabledProtocols(enabledProtocols);
233        }
234        return socket;
235    }
236    
237    /**
238     * Create a client socket connected to the specified host and port.
239     * @param  host   the host name
240     * @param  port   the port number
241     * @return a socket connected to the specified host and port.
242     * @exception IOException if an I/O error occurs during socket creation
243     * @exception ISOException should any other error occurs
244     */
245    public Socket createSocket(String host, int port) 
246        throws IOException, ISOException
247    {
248        if(socketFactory==null) socketFactory=createSocketFactory();
249        SSLSocket s = (SSLSocket) socketFactory.createSocket(host,port);
250        verifyHostname(s);
251        return s;
252    }
253
254    /**
255     * Verify that serverName and CN equals.
256     *
257     * <pre>
258     * Origin:      jakarta-commons/httpclient
259     * File:        StrictSSLProtocolSocketFactory.java
260     * Revision:    1.5
261     * License:     Apache-2.0
262     * </pre>
263     *
264     * @param socket a SSLSocket value
265     * @exception SSLPeerUnverifiedException  If there are problems obtaining
266     * the server certificates from the SSL session, or the server host name 
267     * does not match with the "Common Name" in the server certificates 
268     * SubjectDN.
269     * @exception UnknownHostException  If we are not able to resolve
270     * the SSL sessions returned server host name. 
271     */
272    private void verifyHostname(SSLSocket socket)
273        throws SSLPeerUnverifiedException, UnknownHostException
274    {
275        if (!serverAuthNeeded) {
276            return; 
277        }
278
279        SSLSession session = socket.getSession();
280
281        if (serverName==null || serverName.length()==0) {
282            serverName = session.getPeerHost();
283            try {
284                InetAddress addr = InetAddress.getByName(serverName);
285            } catch (UnknownHostException uhe) {
286                throw new UnknownHostException("Could not resolve SSL " +
287                                               "server name " + serverName);
288            }
289        }
290
291
292        Certificate[] certs = session.getPeerCertificates();
293        if (certs==null || certs.length==0)
294            throw new SSLPeerUnverifiedException("No server certificates found");
295
296        if (!(certs[0] instanceof X509Certificate cert))
297            throw new SSLPeerUnverifiedException("Server certificate is not X.509");
298
299        String cn = getCN(cert);
300        if (!serverName.equalsIgnoreCase(cn)) {
301            throw new SSLPeerUnverifiedException("Invalid SSL server name. "+
302                    "Expected '" + serverName +
303                    "', got '" + cn + "'");
304        }
305    }
306
307    /**
308     * Extracts the Common Name (CN) from an X.509 certificate's subject DN
309     * using the standard {@link X500Principal} and {@link LdapName} APIs,
310     * which correctly handle quoting and escaping per RFC 2253.
311     *
312     * @param cert  an X.509 certificate
313     * @return the value of the "Common Name" field, or null if not present.
314     */
315    private String getCN(X509Certificate cert) throws SSLPeerUnverifiedException {
316        try {
317            X500Principal principal = cert.getSubjectX500Principal();
318            LdapName ln = new LdapName(principal.getName(X500Principal.RFC2253));
319            for (Rdn rdn : ln.getRdns()) {
320                if ("CN".equalsIgnoreCase(rdn.getType())) {
321                    return rdn.getValue().toString();
322                }
323            }
324            return null;
325        } catch (InvalidNameException e) {
326            throw new SSLPeerUnverifiedException("Invalid subject DN: " + e.getMessage());
327        }
328    }
329
330    /**
331     * Returns the path of the configured JKS key store.
332     *
333     * @return filesystem path of the JKS key store
334     */
335    public String getKeyStore() {
336        return keyStore;
337    }
338
339    /**
340     * Hook returning the key store password.
341     * Subclasses are expected to override this to source the password
342     * from a secret manager rather than a system property.
343     *
344     * @return key store password
345     */
346    protected String getPassword() {
347        return System.getProperty("jpos.ssl.storepass", "password");
348    }
349
350    /**
351     * Hook returning the private-key entry password.
352     * Subclasses are expected to override this to source the password
353     * from a secret manager rather than a system property.
354     *
355     * @return private-key entry password
356     */
357    protected String getKeyPassword() {
358        return System.getProperty("jpos.ssl.keypass", "password");
359    }
360
361    /**
362     * Returns the configured peer certificate Common Name.
363     *
364     * @return expected Common Name (CN) of the peer certificate
365     */
366    public String getServerName() {
367        return serverName;
368    }
369
370    /**
371     * Returns whether accepted sockets require TLS client authentication.
372     *
373     * @return {@code true} when accepted sockets require TLS client authentication
374     */
375    public boolean getClientAuthNeeded() {
376        return clientAuthNeeded;
377    }
378
379    /**
380     * Returns whether outbound sockets validate the server certificate chain.
381     *
382     * @return {@code true} when outbound sockets validate the server certificate chain
383     */
384    public boolean getServerAuthNeeded() {
385        return serverAuthNeeded;
386    }
387
388    /**
389     * Sets the explicit list of TLS cipher suites enabled on created sockets.
390     *
391     * @param enabledCipherSuites cipher suites to enable on created sockets;
392     *                            {@code null} or empty leaves provider defaults in place
393     */
394    public void setEnabledCipherSuites(String[] enabledCipherSuites) {
395        this.enabledCipherSuites = enabledCipherSuites;
396    }
397
398    /**
399     * Returns the explicit list of TLS cipher suites enabled on created sockets.
400     *
401     * @return cipher suites enabled on created sockets, or {@code null} when provider defaults apply
402     */
403    public String[] getEnabledCipherSuites() {
404        return enabledCipherSuites;
405    }
406
407
408    public void setConfiguration(Configuration cfg) throws ConfigurationException {
409        this.cfg = cfg;
410        keyStore = cfg.get("keystore");
411        clientAuthNeeded = cfg.getBoolean("clientauth");
412        serverAuthNeeded = cfg.getBoolean("serverauth");
413        serverName = cfg.get("servername");
414        password = cfg.get("storepassword", null);
415        keyPassword = cfg.get("keypassword", null);
416        enabledCipherSuites = cfg.getAll("addEnabledCipherSuite");
417        enabledProtocols = cfg.getAll("addEnabledProtocol");
418    }
419    /**
420     * Returns the configuration applied via {@link #setConfiguration(Configuration)}.
421     *
422     * @return active configuration, or {@code null} if not yet configured
423     */
424    public Configuration getConfiguration() {
425        return cfg;
426    }
427}