TransportSecurity.java
/*
** Module : TransportSecurity.java
** Abstract : handles JSSE setup of a custom KeyManager and TrustManager
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 NVS 20050208 @19718 Created the file and completed the initial
** implementation. Certificates that should be
** located in the directory, are temporarily
** loaded from a file based truststore.
** 002 NVS 20050303 @20254 Added new method getTrustStore. Needed in
** SecurityManager class, authenticateClient
** method to verify the server's certificate in
** case DNS based name verification fails.
** Server side trust store is now built in
** SecurityCache class.
** 003 NVS 20050303 @24615 Implemented truly anonymous (no certificate)
** connections support with the server's
** certificate validation now optional.
** 004 GES 20061221 @31801 Refactoring and massive code cleanup. Removed
** dependencies on AuthUIHelper.
** 005 JJC 20090208 @41407 Added a special trust manager for the use
** in the admin applet exclusively -
** ZeroActionTrustManager.
** 006 LMR 20101209 TransportSecurity's constructor was modified
** to bypass all logic related to keystores and
** certificates if net/connection/secure is false
** in the BootstrapConfig (if not present it
** defaults to false).
** 007 GES 20111003 Make the ZeroActionTrustManager more easily
** accessible via a specific config value.
** 008 CA 20140206 Changed to allow the key and trust stores to be read from the
** bootstrap config (needed by launching the processes via the spawner
** and scheduler infrastructure).
** 009 ECF 20150828 Enable Java 8.
** 010 CA 20180608 Extracted the key store loading into a separate API - loadKeyStore.
** This loads just the certificate, not the actual private keys.
** 011 GBB 20230825 JavaDoc updated.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.security;
import java.io.*;
import java.util.*;
import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.Base64;
/**
* Handles the setup of the JSSE environment to use a custom
* <code>KeyManager</code> and <code>TrustManager</code>.
* <p>
* Instances are obtained using getTransportSecurity method of {@link SecurityManager#certificateSm}.
*/
public class TransportSecurity
{
/** Custom key manager. */
private KeyManager[] km = null;
/** Custom trust manager. */
private TrustManager[] tm = null;
/** In-memory trust store. */
private KeyStore trustStore = null;
/** In-memory key store. */
private KeyStore keyStore = null;
/**
* Uses the truststore and keystore resources defined in a given bootstrap
* configuration to create in-memory <code>KeyStore</code> objects holding
* certificates (trust store) and the private key (key store). Bootstrap
* configuration flag <code>net/connection/secure</code> needs to be
* enabled for this setup to take place.
*
* <p>
* On the client, both the key store and the trust store may be either
* specified or omitted. The interpretation of these combinations is as
* follows:
* <p>
* <ul>
* <li>key store, trust store: fully certified connection where the
* client and server are both certified/verified using the given
* resources
* <li>key store only: client certified, server's certificate is
* verified with the J2SE standard trust store
* <li>trust store only: anonymous connection, server's certificate
* is verified using the specified trust store
* <li>no key store, no trust store: anonymous connection, optionally
* the server's certificate is verified using the J2SE standard
* trust store (if security:certificate:validate=true)
* </ul>
* <p>
* An empty truststore on the server is allowed. This means no client TLS
* authentication will be in effect.
* <p>
* Key stores, if specified, have to contain one or more key entries.
* <p>
* Key entries in use can be specified by processalias or useralias keyword
* on the client, and alias keyword on the server.
* <p>
* Valid combinations on the client are:
* <ul>
* <li>processalias is coded and there is matching alias for a key entry;
* <li>useralias is coded and there is matching alias for a key entry;
* <li>nothing is coded and there is exactly one key entry
* </ul>
* Valid combinations on the server are:
* <ul>
* <li>alias is coded and there is matching alias for a key entry;
* <li>nothing is coded and there is exactly one key entry
* </ul>
*
* @param bc
* Security related configuration information.
* @param srv
* <code>true</code> if this is the server.
* @param trust
* On the server, it is a trust store loaded with certificates,
* built from the directory. <code>null</code> otherwise.
*/
TransportSecurity(BootstrapConfig bc, boolean srv, KeyStore trust)
throws NoSuchAlgorithmException,
KeyStoreException,
UnrecoverableKeyException,
ConfigurationException
{
// bypass all keystore/truststore setup in the insecure case:
boolean secure = bc.getBoolean("net", "connection", "secure", false);
if (!secure)
{
return;
}
// obtain the keystore
keyStore = loadKeyStore(bc, srv);
boolean loaded = (keyStore != null);
if (!loaded)
{
keyStore = KeyStore.getInstance("JKS");
// no keystore file is specified, the keystore will be empty
// but only on the client
if (srv)
{
throw new ConfigurationException("misconfigured keystore: file " +
"must be specified");
}
try
{
keyStore.load(null, null);
}
catch (Exception e)
{
// should not be here
}
}
// create a key manager
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
String apw = bc.getString("access", "password", "keyentry", null);
char[] kspw = (!loaded || apw == null) ? null : apw.toCharArray();
kmf.init(keyStore, kspw);
km = kmf.getKeyManagers();
// create an in-memory truststore
// On the server, truststore is specified as a parameter.
// On the client, there are three choices: default J2SE truststore,
// empty (not specified) trusstore, or with the contens of the specified
// file. A TrustManager is created for the latter two cases to use this
// truststore.
// The default J2SE truststore is used if no truststore was specified
// and the server's certificate validation is requested.
boolean noTrust = false;
if (srv)
{
// server side truststore initialization
trustStore = trust;
}
else
{
// client side truststore initialization (this is a file or URL
// specified in bootstrap config)
InputStream trustIn = bc.getInputStream("security",
"truststore",
"filename",
null);
boolean trustFromFile = true;
if (trustIn == null)
{
String store = bc.getConfigItem("security", "truststore", "bytes");
if (store != null)
{
trustFromFile = false;
byte[] bytes = Base64.base64ToByteArray(store);
trustIn = new ByteArrayInputStream(bytes);
}
}
trustStore = KeyStore.getInstance("JKS");
if (trustIn != null)
{
// a truststore file is specified
try
{
String tpw = bc.getString("access",
"password",
"truststore",
null);
char[] tspw = (tpw == null) ? null : tpw.toCharArray();
trustStore.load(trustIn, tspw);
trustIn.close();
}
catch (Exception e)
{
String txt = bc.getString("security",
"truststore",
(trustFromFile ? "filename" : "bytes"),
null);
throw new ConfigurationException("misconfigured truststore " +
txt,
e);
}
// request server certificate validation
bc.setConfigItem("security", "certificate", "validate", "true");
}
else
{
// no keystore file is specified. the keystore will be empty
try
{
trustStore.load(null, null);
noTrust = true;
}
catch (Exception e)
{
// should not be here
}
}
}
// optionally, create a trust manager for this truststore
// otherwise, the J2SE default will be used
if (!noTrust ||
!bc.getBoolean("security", "certificate", "validate", false))
{
tm = getTrustManagers();
}
// for applet code, to disable server certificate validation it will
// take a special trust manager that disables checking since there is
// no local trust store that can be accessed
String txt = bc.getString("security", "trust_mgr", "disable", "false");
if (txt.equalsIgnoreCase("true"))
{
tm = new TrustManager[] { new ZeroActionTrustManager() };
}
}
/**
* Load the keystore from the specified configuration, and verify if a certificate with the
* configured alias exists.
* <p>
* This loads just the certificate(s), not the private keys.
*
* @param bc
* Security related configuration information.
* @param srv
* <code>true</code> if this is the server.
*
* @return The loaded key store, or null if it couldn't be loaded.
*/
static KeyStore loadKeyStore(BootstrapConfig bc, boolean srv)
throws NoSuchAlgorithmException,
KeyStoreException,
UnrecoverableKeyException,
ConfigurationException
{
String alias = getSubjectAlias(bc, srv);
KeyStore keyStore = null;
// On the client, the keystore can be empty (not specified) or it can
// be initialized with the contents of the specified file. A
// KeyManager is always created to use this keystore.
// On the server, an empty keystore is not allowed.
InputStream keyIn = bc.getInputStream("security",
"keystore",
"filename",
null);
boolean keyFromFile = true;
if (keyIn == null)
{
String store = bc.getConfigItem("security", "keystore", "bytes");
if (store != null)
{
keyFromFile = false;
byte[] bytes = Base64.base64ToByteArray(store);
keyIn = new ByteArrayInputStream(bytes);
}
}
if (keyIn != null)
{
// create an in-memory keystore
keyStore = KeyStore.getInstance("JKS");
// a keystore file is specified
try
{
String kpw = bc.getString("access", "password", "keystore", null);
keyStore.load(keyIn, (kpw == null) ? null : kpw.toCharArray());
keyIn.close();
}
catch (Exception e)
{
String txt = bc.getString("security",
"keystore",
(keyFromFile ? "filename" : "bytes"),
null);
throw new ConfigurationException("misconfigured keystore " + txt,
e);
}
// request server certificate validation
bc.setConfigItem("security", "certificate", "validate", "true");
// verify the keystore: must have an alias that matches alias
// or if not specified, exactly one key entry
alias = verifyKeystore(keyStore, alias);
}
return keyStore;
}
/**
* Initializes the given SSLContext object so that the latter uses the key
* manager and the trust manager embedded into this TransportSecurity
* class.
*
* @param sslc
* a <code>SSLContext</code> that has to be set up to use the
* facilities of this <code>TransportSecurity</code> instance
* @throws KeyManagementException - if this operation fails
*/
public void attach(SSLContext sslc)
throws KeyManagementException
{
sslc.init(km, tm, null);
}
/**
* Returns the trust store.
*
* @return trust store built by this <code>TransportSecurity</code>
*/
KeyStore getTrustStore()
{
return trustStore;
}
/**
* Looks up the proper subject alias in the given configuration.
*
* @param bc
* Security related configuration information.
* @param srv
* <code>true</code> if this is the server.
*
* @return The subject alias or <code>null</code> if no alias is
* specified.
*/
static String getSubjectAlias(BootstrapConfig bc, boolean srv)
throws ConfigurationException
{
String alias;
if (srv)
{
alias = bc.getString("security", "keystore", "alias", null);
}
else
{
alias = bc.getString("security", "keystore", "processalias", null);
String ua = bc.getString("security", "keystore", "useralias", null);
if (alias != null)
{
if (ua != null)
{
throw new ConfigurationException("misconfigured keystore: " +
"processalias and useralias" +
" are mutually exclusive");
}
}
else
{
alias = ua;
}
}
return alias;
}
/**
* Verify and normalize the keystore to ensure that the specified alias
* exists as a key entry OR that there is one and only one key entry.
* <p>
* All unnecessary key entries are removed from the key store.
*
* @param keyStore
* The key store to verify.
* @param alias
* The name used to differentiate between multiple key entries.
*
* @return If the <code>alias</code> was specified as <code>null</code>
* and the key store only has one key entry, then that name is
* returned. Otherwise the alias passed as a parameter is
* returned.
*/
private static String verifyKeystore(KeyStore keyStore, String alias)
throws NoSuchAlgorithmException,
KeyStoreException,
UnrecoverableKeyException,
ConfigurationException
{
if (alias != null)
{
if (!keyStore.isKeyEntry(alias))
{
throw new ConfigurationException("misconfigured keystore: " +
"no matching alias for " +
alias);
}
}
else
{
Enumeration en = keyStore.aliases();
int numkeys = 0;
while (en.hasMoreElements())
{
String next = (String) en.nextElement();
if (keyStore.isKeyEntry(next))
numkeys ++;
// the first entry is used as the alias
if (alias == null)
alias = next;
}
if (numkeys == 0)
{
throw new ConfigurationException("misconfigured keystore: " +
"no private keys");
}
if (numkeys > 1)
{
throw new ConfigurationException("misconfigured keystore: " +
"multiple private keys with " +
"no specified alias to " +
"differentiate");
}
}
Enumeration en = keyStore.aliases();
// normalize the keystore by deleting all unnecessary entries
while (en.hasMoreElements())
{
String next = (String) en.nextElement();
if (!next.equals(alias))
{
keyStore.deleteEntry(next);
}
}
return alias;
}
/**
* Returns the trust managers which have been initialized using the
* instance's defined trust store.
*
* @return The default trust manager.
*/
private TrustManager[] getTrustManagers()
throws NoSuchAlgorithmException,
KeyStoreException
{
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(trustStore);
return tmf.getTrustManagers();
}
/**
* Zero action trust manager class which accepts anything as a valid
* certificate and produces a zero list of accepted issuers.
*/
public class ZeroActionTrustManager
implements X509TrustManager
{
/**
* Accepts anything.
*/
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
// anything is acceptable
}
/**
* Accepts anything.
*/
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
// anything is acceptable
}
/**
* Produces zero size list pf accepter issuers.
*
* @return empty array of accepted issuers
*/
public X509Certificate[] getAcceptedIssuers()
{
return new X509Certificate[0];
}
}
}