Custom Key Managers in JSSE
JSSE provides the SSL capability to Java. The reference implementation is bootstrapped into JDK versions 1.4 and above. The implementation
provides the crypographic services that covers the requirements in most simple cases. It includes a X509 Key manager and a X509 Trust manager
for key management and trust management of X509 certificates respectively. The key manager is used during the SSL handshake to select a certificate that best identifies
the client to the SSL service. The default Key manager provided in the implementation uses the certificate request attributes - key type (RSA/DSA/etc)
and the preferred issuers - to select a key entry from the keystore. If there are multiple key entries that match the attributes, the first one is presented
to the server. This scheme works fine if the application is interacting with one SSL service, but it fails if the application is interacting with
multiple SSL services each identifying the client differently.
For example, lets say , the application is communicating with a FTPS site and a webservice - each needing
a different client certificate. The application needs to present the service with the required certicate. The default key manager will present the first certificate
that matches the certificate-request attributes. If both services prefer the same key types and issuer DNs - which is very likely within a given
organization, the default key selection will select the first key entry in the key store that matches the creteria and cause one of the hand shakes to fail.
A custom key manager with a more "resource-aware" key selection scheme is required in this case. The JSSE manual does broach the subject of customizing key
managers and trust managers, but the documentation is somewhat arcane or atleast not very demonstrative in its intent (- IMHO, ofcourse!). This
article demonstrates the implementation of a custom key manager using two schemes
Subclasses of java.security.Provider are registered with the JVM to provide security services. Security services include key factories, digital
signature algorithms, key generation algorithms, keystore creation and management , etc. A provider can provide one or more cryptographic services. To
implement a custom key manager, we register a provider that only provides the key management service. The key management interface defined by JSSE is javax.net.ssl.KeyManager .
KeyManager s are created by the factory - javax.net.ssl.KeyManagerFactory based on the default algorithm defined in the system property ssl.KeyManagerFactory.algorithm .
With no customization, the Sun JVM uses the default key manager algorithm - "SunX509" which is provided by the sun provider - com.sun.net.ssl.internal.ssl.Provider .
To use a custom key management scheme for the SSL handshake in an application, the following needs to be done -
- Define a Key selection scheme. Implement
javax.net.ssl.X509KeyManager with the required key selection scheme.
- Define a custom Key Manager SPI (Service provider interface), that creates the custom key manager during the JSSE lifecycle.
- Define the main class of the provider package. Create a subclass of
Provider with the properties describing the service set to the SPI above.
- Deploy the provider package in the classpath of the application.
- Deploy the provider in the application using dynamic registration. (Or deploy it in the JVM by changing security.properties to include the provider)
- Set the system property
ssl.KeyManagerFactory.algorithm to the name of the provider implemented.
The above steps are described in a somewhat more general manner in the
JCA guide. The following sections use the
provider mechanism to create a custom key manager.
Implementing the Key manager
The customization in the key management is in the selection of the certificates only. Hence, we can simply contain the default key manager in a custom
key manager and delegate most of the key management to the default key manager. The scheme of key selection is based on the name of the host which which
the SSL handshake is taking place. The key manager is initialized with a java.util.Map containing the host names and the corresponding
key identifier (alias). Hostnames with no key alias will not be presented with a client certificate. Key selection is not done for hosts whose names are
not found in the map. The key selection is for such hosts are delegated to the default key manager.
The bulk of the implementation will be in the chooseClientAlias method of the X509KeyManager interface. The rest of the methods
have a cursory implementation where the call is simply delegated to the contained key manager.
public class CustomKeyManager implements X509KeyManager
{
private X509KeyManager defaultKeyManager;
private Properties serverMap;
public CustomKeyManager(X509KeyManager defaultKeyManager, Properties serverMap)
{
this.defaultKeyManager = defaultKeyManager;
this.serverMap = serverMap;
}
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket)
{
SocketAddress socketAddress = socket.getRemoteSocketAddress();
String hostName = ((InetSocketAddress)SocketAddress).getHostName().toUpperCase();
String alias = null;
if(serverMap.containsKey(hostName))
{
alias = serverMap.getProperty(hostName.toUpperCase());
if(alias != null &&
alias.length() ==0)
{
alias = null;
}
}
else
{
alias = defaultKeyManager.chooseClientAlias(keyType, issuers, socket);
}
return alias;
}
.
.
.
}
The key manager is initialized with a "real" X509 key manager and a table containing the server-key mapping. The chooseClientAlias
obtains the server name from the socket. The key alias is looked up in the table for the server. If the server name is found in the table,
the corresponding alias is returned. If the alias is empty, a null reference is returned so that no certificate is presented to the server during
the handshake.
Implementing the SPI
JSSE initializes the key manager using the SPI that implements javax.net.ssl.KeyManagerSpi . (The next section will how how it finds the Spi). The SPI that
initializes the key manager is called CustomKeyManagerSpi. The KeyManagerSpi defines a number of "engine" methods. We will only implement two of these methods -
engineInit and engineGetKeyManagers . The former does most of the initialization while the latter returns an instance of the key manager.
The key manager is created with a default X509KeyManager and a map containing the server names and key aliases. The implementation uses two custom
system properties -
- alt.keymanager.alg : The default algorithm for key management provided in the cryptographic provider. For Sun JVMs, this can be set to "SunX509".
- alt.server_key_map.file: The absolute path to the property file containing the server - key mapping.
The server map file contains entries with the server name as the key and the key alias as the value. If a server does not
have a key alias, no client certificate is presented to the server during the hand shake. The excerpt below shows two servers - serverA and serverB. Server B does not
require client certs -
# Key mapping file.
# Entries contain the server names and the corresponding client cert
#
# Server A is requires the key in serverA.keyEntry2
serverA = serverA.keyEntry2
# Server B does not need any key
serverB =
The engineInit reads the two system properties and creates the required Properties and the X509KeyManager . The Properties containing
the key-host mapping and the X509KeyManager provided by the bootstrapped cryptographic provider is used for creating the CustomKeyManager .
..
..
public void engineInit(KeyStore ks, char[] password)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException
{
// VVVVVVV Section I VVVVVVV
String defaultAlg = System.getProperty("alt.keymanager.alg");
if(defaultAlg == null ||
defaultAlg.trim().length() == 0)
{
throw new NoSuchAlgorithmException("The system property alt.keymanager.alg "+
"should be set to the JVM's default keymanagment algorithm");
}
String keyMapFile = System.getProperty("alt.server_key_map.file");
// ^^^^^^^ Section I ^^^^^^^
// VVVVVVV Section II VVVVVVV
Properties serverMap = new Properties();
FileInputStream inputStream = null;
try
{
inputStream = new FileInputStream(new File(keyMapFile));
serverMap.load(inputStream);
}
catch(Exception e)
{
throw new UnrecoverableKeyException("Cannot read key map file "+keyMapFile);
}
finally
{
// close the input stream
.
.
.
}
// ^^^^^^^ Section II ^^^^^^^
// Get the key manager factory for the default algorithm.
KeyManagerFactory factory = KeyManagerFactory.getInstance(defaultAlg);
factory.init(ks, password);
// Get the first X509KeyManager in the list
KeyManager[] keyManagers = factory.getKeyManagers();
if(keyManagers == null ||
keyManagers.length == 0)
{
throw new NoSuchAlgorithmException("The default algorithm :"+
defaultAlg+" produced no key managers");
}
X509KeyManager x509KeyManager = null;
for(int i=0;i<keyManagers.length; i++)
{
if(keyManagers[i] instanceof X509KeyManager)
{
x509KeyManager = (X509KeyManager)keyManagers[i];
break;
}
}
if(x509KeyManager == null)
{
throw new NoSuchAlgorithmException("The default algorithm :"+
defaultAlg+" did not produce a X509 Key manager");
}
this.keyManager = new CustomKeyManager(x509KeyManager, serverMap);
}
public void engineGetKeyManagers()
{
return new KeyManager[] { this.keyManager};
}
The "Section I" of the code excerpt above, retrieves the system properties that refer to the platform provided key manager algorithm and the config file containing
the server - key mapping. The "Section II" of the code creates a Properties instance from the config file. The rest of the code obtains the default
key manager provided by the platform and creates an instance of CustomKeyManager .
The engineGetKeyManagers , returns the only key manager created by the SPI in the engineInit method.
Implementing the Provider
In the previous sections, we actually implemented a "Cryptographic provider package" (in the JSSE parlance).The Provider is the "main" class of the package through
which the JSSE engine deciphers the type of cryptographic service provided and invokes the corresponding SPI. Lets say the provider package is called "custom" , it implements
only the Key management function. The main class is really short and simple -
public final class CustomProvider extends Provider
{
public CustomProvider()
{
super("custom",1.0, "Provider for custom key manager");
put("KeyManagerFactory.custom", "mypackage.CustomKeyManagerSpi");
}
}
The property used to indicate the cryptographic service is "KeyManagerFactory" with the algorithm name as the qualifier. We chose to use "custom" as the provider name and the algorigthm name. Hence, the
property "KeyManagerFactory.custom" is set to the SPI we implemented above.The
Cryptography architecture document provides the naming convention for the cryptographic service.
Deploying the Provider package
Deploying the provider package involves -
- Adding the provider package to the classpath.
- Adding the provider to the provider list - this can be done by editing the security.properties for the JRE and adding the entry for the provider in the or using the
API
Security.addProvider() to register dynamically.
- Set all the required system properties
ssl.KeyManagerFactory.algorithm should be set to the "custom" so that the our custom provider is invoked.
alt.keymanager.alg set to the platform's default key management algorithm ("SunX509" for Sun provided JVMs).
alt.server_key_map.file set to the config file
- Set the keystore and truststore properties as required by the JSSE provider
Resources
- JShell Provider : This is a fully implemented provider based on the schem above. You can download the provider
from here.
Limitations
- This implementation cannot differentiate services on the same host. The creterion for key selection is based on host name only.
Custom providers can be used when the application cannot be changed to use a custom Key manager. But if the application creates its own
SSLContext , then the key manager can be set up at the time the SSL context is being created. See the code excerpt below
public class MyApplication
{
.
.
.
public SSLContext createSSLContext()
{
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(createCustomKeyManangers(),null,null);
return sslContext;
}
SSLContext is a factory used to create SSLSocketFactory s. The sockets use the key manager and trust manager from
the ssl context. In the code excerpt, the createCustomKeyManangers (not shown) returns an array of one element containing an
instance of the custom key manager. The creation of the key manager is shown in the previous section.
Limitations
- This approach works only if the SSL components in the application can be changed. It will not work with out-of-the box applications
that cannot be changed.
|