My network code is written in NDK (cURL + OpenSSL) and I'd like to use a certificate from Android's credential storage as a client certificate for a SSL connection. Moreover, I'd like to offer a list of available certificates to the user, so he can choose the certificate for the connection. Unfortunately, I cannot obtain a certificate from the key storage.
I installed a client certificate to "Credential storage" (Settings -> Secutrity -> ...) on my Android device (5.0.2), but I'm not able to access it from Java. I tried to call following code, but the key storage is empy, athough the certificate is installed in the Credential storage:
//KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
Enumeration<String> aliases = ks.aliases();
while(aliases.hasMoreElements()) {
String alias = (String)aliases.nextElement();
Log.i("app", "alias name: " + alias);
Certificate certificate = ks.getCertificate(alias);
Log.i("app", certificate.toString());
}
What am I doing wrong?
User credentials installed on the device are available through Android KeyChain, not Android KeyStore
The KeyChain class provides access to private keys and their corresponding certificate chains in credential storage.
Use choosePrivateKeyAlias to prompt the user for selecting the certificate. The system launches an Activity for the user to select the alias and returns it via a callback. Then use getPrivateKey and getCertificate to recover the key and the corresponding certificate chain
KeyChain.choosePrivateKeyAlias(activity, new KeyChainAliasCallback() {
public void alias(String alias) {
//do something with the selected alias
}
},
new String[] { KeyProperties.KEY_ALGORITHM_RSA, "DSA"}, // List of acceptable key types. null for any
null, // issuer, null for any
null, // host name of server requesting the cert, null if unavailable
-1, // port of server requesting the cert, -1 if unavailable
""); // alias to preselect, null if unavailable
PrivateKey privateKey = KeyChain.getPrivateKey(activity, alias);
X509Certificate chain[] = KeyChain.getCertificateChain(activity, alias);
Try something like this:
X509TrustManager manager = null;
FileInputStream fs = null;
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try
{
fs = new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
keyStore.load(fs, null);
}
finally
{
if (fs != null) { fs.close(); }
}
trustManagerFactory.init(keyStore);
TrustManager[] managers = trustManagerFactory.getTrustManagers();
for (TrustManager tm : managers)
{
if (tm instanceof X509TrustManager)
{
manager = (X509TrustManager) tm;
break;
}
}
I have this simple JMX client
public void testTomcatBasicAuthentication() throws Exception
{
System.out.println("Test Server Basic Authentication");
try
{
String truststore = "C:\\client.jks";
String trustStorePassword = "password";
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://xxx.xxx.xxx.xxx:9999/jmxrmi");
HashMap environment = new HashMap();
String[] credentials = new String[]
{
"user", "passwd"
};
environment.put(JMXConnector.CREDENTIALS, credentials);
// environment.put("javax.net.ssl.trustStore", truststore);
// environment.put("javax.net.ssl.trustStorePassword", trustStorePassword);
// environment.put("javax.net.ssl.keyStore", truststore);
// environment.put("javax.net.ssl.keyStorePassword", trustStorePassword);
KeyManager[] kms = getKeyManagers(truststore, trustStorePassword);
TrustManager[] tms = getTrustManagers(truststore, trustStorePassword);
System.setProperty("javax.net.ssl.trustStore", truststore);
System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword);
System.setProperty("javax.net.ssl.keyStore", truststore);
System.setProperty("javax.net.ssl.keyStorePassword", trustStorePassword);
JMXConnector jmxc = JMXConnectorFactory.connect(url, environment);
MBeanServerConnection server = jmxc.getMBeanServerConnection();
Set<ObjectName> s2 = server.queryNames(new ObjectName("Catalina:type=Server,*"), null);
for (ObjectName obj : s2)
{
ObjectName objname = new ObjectName(obj.getCanonicalName());
System.out.println("serverInfo " + server.getAttribute(objname, "serverInfo"));
System.out.println("address " + server.getAttribute(objname, "address"));
System.out.println("stateName " + server.getAttribute(objname, "stateName"));
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
How I can replace System.setProperty(....) with Java code? I don't want to use System.setProperty.
Edit. I found this example
Can we use this code?
KeyManager[] kms = getKeyManagers(truststore, trustStorePassword);
TrustManager[] tms = getTrustManagers(truststore, trustStorePassword);
SslContext.setCurrentSslContext(new SslContext(kms, tms, null));
private static TrustManager[] getTrustManagers(String location, String password)
throws IOException, GeneralSecurityException
{
// First, get the default TrustManagerFactory.
String alg = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmFact = TrustManagerFactory.getInstance(alg);
FileInputStream fis = new FileInputStream(location);
KeyStore ks = KeyStore.getInstance("jks");
ks.load(fis, password.toCharArray());
fis.close();
tmFact.init(ks);
// And now get the TrustManagers
TrustManager[] tms = tmFact.getTrustManagers();
return tms;
}
private static KeyManager[] getKeyManagers(String location, String password)
throws IOException, GeneralSecurityException
{
// First, get the default KeyManagerFactory.
String alg = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory kmFact = KeyManagerFactory.getInstance(alg);
FileInputStream fis = new FileInputStream(location);
KeyStore ks = KeyStore.getInstance("jks");
ks.load(fis, password.toCharArray());
fis.close();
// Now we initialise the KeyManagerFactory with this KeyStore
kmFact.init(ks, password.toCharArray());
// And now get the KeyManagers
KeyManager[] kms = kmFact.getKeyManagers();
return kms;
}
private static KeyStore keyStoreFromCertificateString(String alias, String certificateString)
throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException
{
KeyStore ks = KeyStore.getInstance("jks");
ks.load(null); // Create empty key store
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate cert = cf.generateCertificate(new ByteArrayInputStream(certificateString.getBytes()));
ks.setEntry(alias, new KeyStore.TrustedCertificateEntry(cert), null);
return ks;
}
Can you give some idea how we can integrate this code or there should be some other solution?
It seems like it should be relatively easy, but it's not.
You need to pass actual socket factory classes in the environment, see this example. However, the implementations used in that example use the jvm default socket factories. Instead, you need to setup your own SSL*SocketFactory instances with the appropriate key store and trust store. Then you need to implement your own RMI*SocketFactory instances using your configured socket factory(s). You can use the jdk impls as guides, SslRMIClientSocketFactory and SslRMIServerSocketFactory.
I fear your question is not very well formulated. I write you want to replace System.setProperty but for me it looks like, actually you want to use custom trust/key stores.
This has been answered already: Using a custom truststore in java as well as the default one
The example that you have found is only half of the solution. You have to use the respective managers when creating the connections. Something like this:
sslContext.init(null, trustManagers, null);
connection.setSSLSocketFactory(sslContext.getSocketFactory());
Source: https://planet.jboss.org/post/creating_https_connection_without_javax_net_ssl_truststore_property
But if you don't control the actual connection creation you probably have to use the global properties. (Or whatever config mechanism your application server has)
A simple and easy workaround to make this work is to use a separate copy of system properties for each thread as explained very well in here (interestingly, the main question self concerns the same problem as yours). After that, setting keyStore and trustStore on system properties will be thread-local.
Make sure you use different threads for your two different ssl connections.
I am testing SSL communication between client and server locally.
So I generated certificate using OpenSSL commands. Added this certificate in cacert file. Also generated .p12 file.
I am using the same .p12 file in server and client.
This is the server code
Server server = component.getServers().add(Protocol.HTTPS, port);
Series<Parameter> params = server.getContext().getParameters();
params.add("keystorePath", ".p12 file path");
params.add("keystoreType", "PKCS12");
params.add("needClientAuthentication","true");
component.getDefaultHost().attach("", "/AA"), new AAClass());
component.start();
And this is client code:
Client client = trustAllCerts();
clientResource = new ClientResource(url);
clientResource.setNext(client);
try{
clientText = clientResource.post"");
}
catch(ResourceException e){
e.printStackTrace();
}
public Client trustAllCerts() {
Client client = null;
try {
client = new Client(new Context(), Protocol.HTTPS);
Context context = client.getContext();
final SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
context.getAttributes().put("sslContextFactory", new SslContextFactory() {
public void init(Series<Parameter> parameters) {
}
public SSLContext createSslContext() {
return sslContext;
}
});
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
context.getAttributes().put("hostnameVerifier", new HostnameVerifier() {
#Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
});
sslContext.init(null, new TrustManager[] { tm }, null);
} catch (KeyManagementException e) {
LOGGER.error("Exception in Key Management" + e);
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Exception in Algorithm Used" + e);
}
return client;
}
I am getting following exception:
Restlet-1299242, fatal error: 42: null cert chain
javax.net.ssl.SSLHandshakeException: null cert chain
%% Invalidated: [Session-25, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256]
Restlet-1299242, SEND TLSv1.2 ALERT: fatal, description = bad_certificate
Restlet-1299242, WRITE: TLSv1.2 Alert, length = 2
Restlet-1299242, fatal: engine already closed. Rethrowing javax.net.ssl.SSLHandshakeException: null cert chain
Restlet-1299242, called closeInbound()
Restlet-1299242, fatal: engine already closed. Rethrowing javax.net.ssl.SSLException: Inbound closed before receiving peer's close_notify: possible truncation attack?
Restlet-1299242, called closeOutbound()
Restlet-1299242, closeOutboundInternal()
I tried to add keystore and truststore using System.setProperty() but it didn't work.
Please help. Thanks in advance.
First, lets create a JKS formatted keystore. PKCS12 is usually used in browser and on default java applications uses JKS (as far as I know). Java also supports PKCS12 but I do not know exact parameters for it.
Preparing JKS File
Lets look in our PKCS12 file and get the certificate aliases that we want to extract our JKS file.
keytool -list \
-keystore [*.p12 file] \
-storepass [password] \
-storetype PKCS12 \
-v
Note the aliases you want to export. And now lets create a JKS file.
keytool -keystore [*.jks file path] -genkey -alias client
This will ask bunch of questions. You can fill them as you like. Now, you can export your aliases from *.p12 file to *.jks file.
keytool -importkeystore \
-srckeystore [*.p12 file path] \
-srcstoretype pkcs12 \
-srcalias [alias from first command] \
-destkeystore [*.jks file path] \
-deststoretype jks \
-deststorepass [*.jks file password] \
-destalias [new alias]
If you do not have any PKCS12 file, or your certificates are in CER, DER or PEM format you can add your certificates to your keystore using the command below.
keytool -import \
-alias [new alias] \
-keystore [*.jks file path] \
-file [*.DER file path]
And please be sure that you imported, your certificate, your certificate provider's certificate (intermediate certificate) and root certificate.
Now you can check that your JKS file contains all the certificates you are needed.
keytool -list \
-keystore [*.jks file path] \
-storepass [password] \
-storetype jks \
-v
Setting up Server
You can use your JKS file both on client and server side. According to Restlet documentation you can use JKS file like this to provide HTTPS connection.
Server server = component.getServers().add(Protocol.HTTPS, port);
Series<Parameter> parameters = server.getContext().getParameters();
parameters.add("sslContextFactory","org.restlet.engine.ssl.DefaultSslContextFactory");
parameters.add("keyStorePath", "*.jks file");
parameters.add("keyStorePassword", "password");
parameters.add("keyPassword", "password");
parameters.add("keyStoreType", "JKS");
After that if you check your port from browser you must see a secure sign. Or you can use some online tool(like this one) to check your certificate.
Setting up Client
Now lets look at client side. Since you are developing both side of the application you can use already created JKS file.
Context con = new Context();
Series<Parameter> clParameters = con.getParameters();
clParameters.add("truststorePath", "*.jks file");
clParameters.add("truststorePassword", "password");
clParameters.add("truststoreType", "JKS");
Client restletClient = new Client(con, Protocol.HTTPS);
While testing or in other circumstances, your certificate hostname and your actual hostname may not match. In order to disable hostname checks you can add this block to your application.
static{
javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(
new javax.net.ssl.HostnameVerifier(){
public boolean verify(String hostname,
javax.net.ssl.SSLSession sslSession ) {
return true ;
}
});
}
Some Thoughts
Since I cannot test it on my locale, I am not exactly sure that your client and server JKS file must be the same. You may only need to add your own certificate to your server.jks. SSL and certificates are always tricky for me. I usually get it work after some trial and error. I hope this will help you.
Also, You may also want to consider, using a reverse proxy kind of web server like Apache2 or Nginx. If you want to use them, you must merge your certificates to a single file. If you look at your certificate file you see that each file (your own certificate, intermediate certificate and root certificate) is like this
-----BEGIN CERTIFICATE-----
MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUA...
....
-----END CERTIFICATE-----
You need to simply add one to other to create a merged certificate. And than use that certificate to end SSL on Apache2 or Nginx. This is what I usually do. But on client side you still need to create JKS files.
I am using the same .p12 file in server and client
This is already a mistake. The client and the server are different identities and should not have the same private key, public key, or certificate.
I suggest you ditch all the OpenSSL stuff and start again with the keytool as follows:
At the server, generate a keypair, and a certificate request; get it signed; import the signer's certificate chain with the -trustcacerts option; and import the signed certificate using the same alias you used when creating the keypair and CSR.
At the client, ditto, but using (of course) a different keystore file.
You're done. Forget about
OpenSSL
PKCS#12
self-signed certificates
all forms of trustAllCerts, custom TrustManagers, and custom code of any kind whatsoever
using the same keypair/certificate for the server and client
importing the server certificate to the client, and vice versa
any system properties other than those that identify the javax.net.ssl.keyStore and javax.net.ssl.keyStorePassword
setting a password on the keypair or the imported signed certificate.
Steps (1) and (2) are how it is intended to be done. Depart from those and you are in for trouble and strife.
One option is to read the p12/pfx-file, get the certificates and use them to programmatically construct KeyStores and TrustStores.
If the input is one pfx-file containing a CA root certificate and a related client certificate,
the methods shown in the class SslUtils below will let you do that.
There is one caveat though: the default Restlet server (version 2.3.4) will not pickup the certificates send by the client.
I did manage to work-around this issue (it is not pretty though), see my answer on this question.
I will focus on configuring the secure connections here, but all source code and a working example is available in the
restlet-clientcert Github project.
The Github project is a result of me thinking I know what I'm doing, having no luck and no experience with Restlet,
but biting the bullet anyway so I can feel a little bit better knowing that I could get this basic stuff to work.
On the server side, use a custom ServerSslContextFactory that programmatically configures the used SSLContext.
Register the custom factory with:
ServerSslContextFactory sslCtx = new ServerSslContextFactory();
sslCtx.init(certFileName, certFilePwd);
ConcurrentMap<String, Object> attribs = server.getContext().getAttributes();
attribs.put("sslContextFactory", sslCtx);
and attach a "guard" to extract the client certificate info:
CertificateAuthenticator guard = new CertificateAuthenticator(server.getContext());
guard.setNext(MyRestlet.class);
component.getDefaultHost().attachDefault(guard);
The ServerSslContextFactory:
public class ServerSslContextFactory extends DefaultSslContextFactory {
private static final Logger log = LoggerFactory.getLogger(ServerSslContextFactory.class);
protected DefaultSslContext wrappedCtx;
public void init(String certFileName, char[] certFilePwd) throws Exception {
if (log.isDebugEnabled()) {
log.debug("Loading certificates from [" + certFileName + "] and using "
+ (certFilePwd != null && certFilePwd.length > 0 ? "a" : "no") + " password.");
}
Path certFilePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource(certFileName).toURI());
KeyManagerFactory kmf = SslUtils.loadKeyStore(certFilePath, certFilePwd);
KeyManager[] kms = kmf.getKeyManagers();
List<X509Certificate> certs = SslUtils.getClientCaCerts(kms);
TrustManagerFactory tmf = SslUtils.createTrustStore(Constants.CERT_CA_ALIAS, certs.get(0));
TrustManager[] tms = tmf.getTrustManagers();
super.setNeedClientAuthentication(true);
SSLContext ctx = SSLContext.getInstance(SslUtils.DEFAULT_SSL_PROTOCOL);
ctx.init(kms, tms, null);
wrappedCtx = (DefaultSslContext) createWrapper(ctx);
}
#Override
public void init(Series<Parameter> parameters) {
log.debug("Not using parameters to initialize server SSL Context factory.");
}
#Override
public SSLContext createSslContext() throws Exception {
return wrappedCtx;
}
#Override
public boolean isNeedClientAuthentication() {
if (log.isDebugEnabled()) {
//log.debug("Needing client auth: " + super.isNeedClientAuthentication(), new RuntimeException("trace"));
log.debug("Needing client auth: " + super.isNeedClientAuthentication());
}
return super.isNeedClientAuthentication();
}
}
On the client side, a similar thing:
ClientSslContextFactory sslCtx = new ClientSslContextFactory();
sslCtx.init(certFileName, certFilePwd);
attribs.put("sslContextFactory", sslCtx);
Also set a hostnameVerifier (as shown in your question) to not verify hostnames.
The ClientSslContextFactory:
public class ClientSslContextFactory extends SslContextFactory {
private static final Logger log = LoggerFactory.getLogger(ClientSslContextFactory.class);
protected KeyManager[] kms;
protected TrustManager[] tms;
public void init(String certFileName, char[] certFilePwd) throws Exception {
log.debug("Loading certificates from [" + certFileName + "] and using "
+ (certFilePwd != null && certFilePwd.length > 0 ? "a" : "no") + " password.");
Path certFilePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource(certFileName).toURI());
KeyManagerFactory kmf = SslUtils.loadKeyStore(certFilePath, certFilePwd);
kms = kmf.getKeyManagers();
/*
List<X509Certificate> certs = SslUtils.getClientCaCerts(kms);
TrustManagerFactory tmf = SslUtils.createTrustStore(Constants.CERT_CA_ALIAS, certs.get(0));
tms = tmf.getTrustManagers();
*/
tms = new TrustManager[1];
tms[0] = new TrustServerCertAlways();
}
#Override
public void init(Series<Parameter> parameters) {
log.debug("Not using parameters to initialize client SSL Context factory.");
}
#Override
public SSLContext createSslContext() throws Exception {
SSLContext ctx = SSLContext.getInstance(SslUtils.DEFAULT_SSL_PROTOCOL);
ctx.init(kms, tms, null);
return ctx;
}
static class TrustServerCertAlways implements X509TrustManager {
#Override public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
log.debug("Trusting all client certificates.");
}
#Override public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
log.debug("Trusting all server certificates.");
}
#Override public X509Certificate[] getAcceptedIssuers() {
log.debug("No accepted issuers.");
return null;
}
}
}
And finally the SslUtils class containing the "read and reconstruct" methods
(full version including "get email-address from certificate" methods is available in the previously mentioned Github project):
import java.io.InputStream;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStore.LoadStoreParameter;
import java.security.cert.X509Certificate;
import java.util.*;
import javax.net.ssl.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SslUtils {
private static final Logger log = LoggerFactory.getLogger(SslUtils.class);
/**
* List of SSL protocols (SSLv3, TLSv1.2, etc.). See also {#link SslUtils#DEFAULT_SSL_PROTOCOL}.
* <br>Documented at http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext
*/
public static final String[] SSL_PROTOCOLS = new String[] { "SSL", "SSLv2", "SSLv3", "TLS", "TLSv1", "TLSv1.1", "TLSv1.2" };
/**
* Default SSL protocol to use ("TLSv1.2").
*/
public static final String DEFAULT_SSL_PROTOCOL = "TLSv1.2";
/**
* Creates a default SSL context with an empty key-store and the default JRE trust-store.
*/
public static SSLContext createDefaultSslContext() throws Exception {
return createSslContext(null, null, null, null);
}
/**
* Creates a default SSL socket factory.
* <br>All system properties related to trust/key-stores are ignored, eveything is done programmatically.
* This is because the Sun implementation reads the system-properties once and then caches the values.
* Among other things, this fails the unit tests.
* <br>For reference, the system properties (again, NOT USED):
* <br> - javax.net.ssl.trustStore (default cacerts.jks)
* <br> - javax.net.ssl.trustStorePassword
* <br>and for client certificate:
* <br> - javax.net.ssl.keyStore (set to "agent-cert.p12")
* <br> - javax.net.ssl.keyStoreType (set to "pkcs12")
* <br> - javax.net.ssl.keyStorePassword
* <br>See for a discussion:
* https://stackoverflow.com/questions/6340918/trust-store-vs-key-store-creating-with-keytool
* <br>See for client certificates in Java:
* https://stackoverflow.com/questions/1666052/java-https-client-certificate-authentication
* #param keyStoreFileName The name (ending with pfx) of the file with client certificates.
* #param trustStoreFileName The name (ending with jks) of the Java KeyStore with trusted (root) certificates.
* #return null or the SSLContext.
*/
public static SSLContext createSslContext(Path keyStoreFile, String keyStorePwd,
Path trustStoreFile, String trustStorePwd) throws Exception {
return createSslContext(keyStoreFile, keyStorePwd, trustStoreFile, trustStorePwd, DEFAULT_SSL_PROTOCOL);
}
/**
* See {#link #createSslContext(Path, String, Path, String)}.
* #param sslProtocol a value from {#link #SSL_PROTOCOLS}.
*/
public static SSLContext createSslContext(Path keyStoreFile, String keyStorePwd,
Path trustStoreFile, String trustStorePwd, String sslProtocol) throws Exception {
KeyManagerFactory kmf = loadKeyStore(keyStoreFile, keyStorePwd == null ? null : keyStorePwd.toCharArray());
TrustManagerFactory tmf = loadTrustStore(trustStoreFile, trustStorePwd == null ? null : trustStorePwd.toCharArray());
//set an Authenticator to generate username and password
SSLContext ctx = SSLContext.getInstance(sslProtocol);
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return ctx;
}
/**
* Calls {#link #createSslContextFromClientKeyStore(Path, String, Path, String)} with the {#link #DEFAULT_SSL_PROTOCOL}.
*/
public static SSLContext createSslContextFromClientKeyStore(Path keyStoreFile, String keyStorePwd,
String caAlias) throws Exception {
return createSslContextFromClientKeyStore(keyStoreFile, keyStorePwd, caAlias, DEFAULT_SSL_PROTOCOL);
}
/**
* Creates a SSL context from the given key-store containing a client certificate and a (CA) root certificate.
* The root certificate is set in the trust-store of the SSL context.
* #param keyStoreFileName key-store file name (ending with .pfx).
* #param keyStorePwd key-store password
* #param caAlias the alias to use for the CA (root) certificate (e.g. "mycaroot").
* #param sslProtocol the ssl-protocol (e.g. {#link #DEFAULT_SSL_PROTOCOL}).
*/
public static SSLContext createSslContextFromClientKeyStore(Path keyStoreFile, String keyStorePwd,
String caAlias, String sslProtocol) throws Exception {
KeyManagerFactory kmf = loadKeyStore(keyStoreFile, keyStorePwd == null ? null : keyStorePwd.toCharArray());
List<X509Certificate> certs = getClientCaCerts(kmf.getKeyManagers());
if (certs.size() < 1) {
throw new Exception("Cannot find CA (root) certificate in key-managers from key store " + keyStoreFile.getFileName());
}
TrustManagerFactory tmf = createTrustStore(caAlias, certs.get(0));
SSLContext ctx = SSLContext.getInstance(sslProtocol);
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return ctx;
}
public static KeyManagerFactory loadKeyStore(Path storeFile) throws Exception {
return loadKeyStore(storeFile, null);
}
public static KeyManagerFactory loadKeyStore(Path storeFile, char[] storePwd) throws Exception {
return loadKeyStore(storeFile, storePwd, null, null);
}
public static KeyManagerFactory loadKeyStore(Path storeFile, char[] storePwd,
String storeType, String algorithm) throws Exception {
KeyManagerFactory kmf = null;
if (storeFile == null) {
kmf = loadKeyStore((InputStream)null, storePwd, storeType, algorithm);
} else {
try (InputStream storeIn = Files.newInputStream(storeFile)) {
kmf = loadKeyStore(storeIn, storePwd, storeType, algorithm);
log.info("Initialized certificate key-store from [" + storeFile.getFileName() + "]");
}
}
return kmf;
}
public static KeyManagerFactory loadKeyStore(InputStream storeIn, char[] storePwd,
String storeType, String algorithm) throws Exception {
if (storePwd == null && storeIn != null) {
storePwd = "changeit".toCharArray();
log.debug("Using default key store password.");
}
if (storeType == null) {
storeType = "pkcs12";
log.debug("Using default key store type " + storeType);
}
if (algorithm == null) {
algorithm = KeyManagerFactory.getDefaultAlgorithm(); // "SunX509"
log.debug("Using default key store algorithm " + algorithm);
}
KeyManagerFactory kmf = null;
KeyStore keyStore = loadStore(storeIn, storePwd, storeType);
kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(keyStore, storePwd);
if (storeIn == null) {
log.info("Initialized a default certificate key-store");
}
return kmf;
}
/**
* Creates a trust-store with the given CA (root) certificate.
* #param certAlias the alias for the certificate (e.g. "mycaroot")
* #param caCert the CA (root) certificate
* #return an initialized trust manager factory.
*/
public static TrustManagerFactory createTrustStore(String certAlias, X509Certificate caCert) throws Exception {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load((LoadStoreParameter)null); // must initialize the key-store
ks.setCertificateEntry(certAlias, caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
return tmf;
}
public static TrustManagerFactory loadTrustStore(Path storeFile) throws Exception {
return loadTrustStore(storeFile, null);
}
public static TrustManagerFactory loadTrustStore(Path storeFile, char[] storePwd) throws Exception {
return loadTrustStore(storeFile, storePwd, null, null);
}
public static TrustManagerFactory loadTrustStore(Path storeFile, char[] storePwd,
String storeType, String algorithm) throws Exception {
TrustManagerFactory tmf = null;
if (storeFile == null) {
tmf = loadTrustStore((InputStream)null, storePwd, storeType, algorithm);
} else {
try (InputStream storeIn = Files.newInputStream(storeFile)) {
tmf = loadTrustStore(storeIn, storePwd, storeType, algorithm);
}
log.info("Initialized certificate trust-store from [" + storeFile.getFileName() + "]");
}
return tmf;
}
public static TrustManagerFactory loadTrustStore(InputStream storeIn, char[] storePwd,
String storeType, String algorithm) throws Exception {
if (storePwd == null && storeIn != null) {
storePwd = "changeit".toCharArray();
log.debug("Using default trust store password.");
}
if (storeType == null) {
storeType = KeyStore.getDefaultType();
log.debug("Using default trust store type " + storeType);
}
if (algorithm == null) {
algorithm = TrustManagerFactory.getDefaultAlgorithm();
log.debug("Using default trust store algorithm " + algorithm);
}
TrustManagerFactory tmf = null;
KeyStore trustStore = loadStore(storeIn, storePwd, storeType);
tmf = TrustManagerFactory.getInstance(algorithm);
tmf.init(trustStore);
if (storeIn == null) {
log.info("Initialized a default certificate trust-store");
}
return tmf;
}
/**
* Creates a default trust store containing the JRE certificates in {#code JAVA_HOME\lib\security\cacerts.jks}
* <br>To view loaded certificates call
* <br>{#code System.setProperty("javax.net.debug", "ssl,trustmanager");}
* <br>before calling this method.
*/
public static TrustManagerFactory createDefaultTrustStore() throws Exception {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore)null);
return tmf;
}
/**
* #param in if null, null is returned.
*/
public static KeyStore loadStore(InputStream in, char[] pwd, String type) throws Exception {
if (in == null) {
return null;
}
KeyStore ks = KeyStore.getInstance(type);
ks.load(in, pwd);
return ks;
}
/**
* Finds any CA (root) certificates present in client certificate chains.
* <br>Uses {#link #getClientAliases(KeyManager)}
* #param kms key-managers (from a key-store).
* #return an empty list or a list containing CA (root) certificates.
*/
public static List<X509Certificate> getClientCaCerts(KeyManager[] kms) {
List<X509Certificate> caCerts = new LinkedList<X509Certificate>();
for (int i = 0; i < kms.length; i++) {
if (!(kms[i] instanceof X509KeyManager)) {
continue;
}
X509KeyManager km = (X509KeyManager) kms[i];
List<String> aliases = getClientAliases(km);
for (String alias: aliases) {
X509Certificate[] cchain = km.getCertificateChain(alias);
if (cchain == null || cchain.length < 2) {
continue;
}
// first certificate in chain is the user certificate
// last certificate is the CA (root certificate).
caCerts.add(cchain[cchain.length-1]);
if (log.isDebugEnabled()) {
log.debug("Found 1 root certificate from client certificate alias " + alias);
}
}
}
return caCerts;
}
/**
* List of key types for client certificate aliases, used in {#link #getAliases(KeyManager)}
* <br>List is documented at
* http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#jssenames
*/
public static final String[] KEY_TYPES = new String[] {"RSA", "DSA", "DH_RSA", "DH_DSA", "EC", "EC_EC", "EC_RSA" };
/**
* Searches for client aliases in the given key-manager.
* Does nothing when the given key-manager is not an instance of {#link X509KeyManager}.
* #return an empty list or a list containing client aliases found in the key-manager.
*/
public static List<String> getClientAliases(KeyManager keyManager) {
List<String> aliases = new LinkedList<String>();
if (keyManager instanceof X509KeyManager) {
X509KeyManager km = (X509KeyManager) keyManager;
for (String keyType: KEY_TYPES) {
String[] kmAliases = km.getClientAliases(keyType, null);
if (kmAliases != null) {
for (String alias: kmAliases) {
if (!isEmpty(alias)) {
aliases.add(alias);
}
}
}
} // for keytypes
}
return aliases;
}
/**
* Sets the default authenticator which can be used for example with http-request that require basic authoriation.
* <br>See also {#link Authenticator#setDefault(Authenticator)}.
*/
public static void setDefaultAuthenticator(final String userName, final char[] pwd) throws Exception {
Authenticator auth = new Authenticator() {
#Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(userName, pwd);
}
};
Authenticator.setDefault(auth);
}
/**
* #return true if s is not null and not empty after trimming, false otherwise.
*/
public static boolean isEmpty(String s) { return (s == null || s.trim().isEmpty()); }
}
On a side-node: Java is transitioning the default keystore type from JKS to PKCS12 (see JEP 229).
You probably didn't add the full certificate chain in your keystore, and just included the keypair itself. In that case, the client just receives the public key, but it cannot validate if that key can be trusted. The certificate chain is there to be able to check if the signatures on the public key match, and lead up to a trusted certificate authority.
See e.g: Adding certificate chain to p12(pfx) certificate
openssl pkcs12 -in certificate.p12 -out clientcert.pem -nodes -clcerts
openssl x509 -in trusted_ca.cer -inform DER -out trusted_ca.pem
openssl x509 -in root_ca.cer -inform DER -out root_ca.pem
cat clientcert.pem trusted_ca.pem root_ca.pem >> clientcertchain.pem
openssl pkcs12 -export -in clientcertchain.pem -out clientcertchain.pfx
You can do it the java way, too, using e.g. portecle: http://portecle.sourceforge.net/import-ca-reply.html, but you also need to combine the certificate chain in one file to import. Just copy-paste all certificates after one another, starting from your own, and ending with the root CA.
This way, the resulting pfx file can be used on the server to return the certificate chain to the client.