X509 Certificate Custom validation (for specific paths) - java

Im trying to validate self-signed certificate against self-signed root CA (given by service provider who will make calls to my service). Now, if i enable two way ssl all works fine but SSL requirement is globally enforced. I need to use it just for one request, and for one user. Other services should not be affected and since there is no way (unless im mistaken) of enabling it for specific paths, i deactivated two ssl and instead, in my controller im doing this:
String auth = request.getHeader("Authorization");
X509Certificate[] cert = (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate");
ResourceLoader resourceLoader = new DefaultResourceLoader();
try {
KeyStore trustStore = KeyStore.getInstance("JKS");
InputStream certInStream = resourceLoader.getClassLoader().getResourceAsStream("keystore/CaCertificate.jks");
trustStore.load(certInStream, "changeit".toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(trustStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
for (TrustManager tm: trustManagerFactory.getTrustManagers())
((X509TrustManager)tm).checkClientTrusted(cert, "RSA");
}
Thus my question is, is this method of validating certificate same as default?
Another thing is that unless i configure the following correctly:
server.ssl.trust-store=keystore/truststore.jks
server.ssl.trust-store-password=pass
server.ssl.trust-store-type= JKS
server.ssl.client-auth=want
then getAttribute method returns null. And i very much want to know why it happens. So the certificate still goes validation but request doesnt fail and its attrbute(SSL one) is not set if client certificate is not trusted? So essentially the code i wrote is useless and i can just check if following is null?
X509Certificate[] cert = (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate");
please help im thoroughly lost.
EDIT:
For testing purposes im using .net 4.5 HttpWebRequest. With this:
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) =>
{
return true;
}

Related

SunMSCAPI not recognizing private keys for some keystore entries

I apologize that this is so long. If you are familiar with doing client-auth in Java you can probably skim/skip to the bottom. I took me a long time to find all the relevant bits of information from disparate sources. Maybe this will help someone else.
I'm working on a proof-of-concept to get client-side authentication working from a Java client using the Windows keystore. I have a servlet that I have created that can request or require client certificates. It simply returns an HTML response containing the certificate information including subject DN and the serial number. I've worked through a number of experiments with browsers (mainly Chrome and IE) to verify it is working. I've gotten successful client authentication working with both certs I've generated with SSL and also using certs issued by my companies internal CA.
My next step is to have a Java client that works with the MSCAPI keystore in Java. The reason I went this route is that the certificates we want to use are issued by our internal CA and are automatically added to the Personal keystore in Windows and are marked as non-exportable. I know there are ways to get the private key out of the keystore with some free tools if you are an admin on your workstation. This isn't a viable option in this case for various reasons. Here's the code I ended up with:
private static SSLSocketFactory getFactory() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, KeyManagementException
{
KeyStore roots = KeyStore.getInstance("Windows-ROOT", new SunMSCAPI());
KeyStore personal = KeyStore.getInstance("Windows-MY", new SunMSCAPI());
roots.load(null,null);
personal.load(null,null);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(roots);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(personal, "".toCharArray());
SSLContext context = SSLContext.getInstance("TLS");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return context.getSocketFactory();
}
This works but when I connect, the Java client is authenticating with the client-certs I had previously generated. I don't see an obvious way to force the selection of a specific key so I figured it was just picking the first key it came across that the host trusts. I then removed these certs that I had created from the windows keystore and it would no longer authenticate. I triple-checked that the server-side was still works with the browsers and it does. I then readded these certs to the personal keystore and removed the trust of my pseudo-CA from the server-side. Still works in the browser and not in the client.
I added -Djavax.net.debug=ssl:handshake to my VM parameters and started looking through the output. One thing I see is are lines "found key for : [alias]" for each of the certs that I have added but not for the ones added through the 'self-enrollment' feature of certmgr.msc. Initially I thought it was because they were exportable but I removed them and added one back as non-exportable and java can still use it. I'm at a loss as to why SunMSCAPI won't use these certs. Both the ones created by me and internal CA are sha256RSA. Both are enabled for client authentication. When I add the following code, I see that isKeyEntry() is false for all the key-pairs I want to use:
Enumeration<String> aliases = personal.aliases();
while (aliases.hasMoreElements())
{
String alias = aliases.nextElement();
System.out.println(alias + " " + personal.isKeyEntry(alias));
}
I found someone else with a similar issue here but no definitive answer.

SSL connection using a not-certified certificate - how to import programmatically [duplicate]

A module I'm adding to our large Java application has to converse with another company's SSL-secured website. The problem is that the site uses a self-signed certificate. I have a copy of the certificate to verify that I'm not encountering a man-in-the-middle attack, and I need to incorporate this certificate into our code in such a way that the connection to the server will be successful.
Here's the basic code:
void sendRequest(String dataPacket) {
String urlStr = "https://host.example.com/";
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setMethod("POST");
conn.setRequestProperty("Content-Length", data.length());
conn.setDoOutput(true);
OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
o.write(data);
o.flush();
}
Without any additional handling in place for the self-signed certificate, this dies at conn.getOutputStream() with the following exception:
Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Ideally, my code needs to teach Java to accept this one self-signed certificate, for this one spot in the application, and nowhere else.
I know that I can import the certificate into the JRE's certificate authority store, and that will allow Java to accept it. That's not an approach I want to take if I can help; it seems very invasive to do on all of our customer's machines for one module they may not use; it would affect all other Java applications using the same JRE, and I don't like that even though the odds of any other Java application ever accessing this site are nil. It's also not a trivial operation: on UNIX I have to obtain access rights to modify the JRE in this way.
I've also seen that I can create a TrustManager instance that does some custom checking. It looks like I might even be able to create a TrustManager that delegates to the real TrustManager in all instances except this one certificate. But it looks like that TrustManager gets installed globally, and I presume would affect all other connections from our application, and that doesn't smell quite right to me, either.
What is the preferred, standard, or best way to set up a Java application to accept a self-signed certificate? Can I accomplish all of the goals I have in mind above, or am I going to have to compromise? Is there an option involving files and directories and configuration settings, and little-to-no code?
Create an SSLSocket factory yourself, and set it on the HttpsURLConnection before connecting.
...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...
You'll want to create one SSLSocketFactory and keep it around. Here's a sketch of how to initialize it:
/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ...
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();
If you need help creating the key store, please comment.
Here's an example of loading the key store:
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();
To create the key store with a PEM format certificate, you can write your own code using CertificateFactory, or just import it with keytool from the JDK (keytool won't work for a "key entry", but is just fine for a "trusted entry").
keytool -import -file selfsigned.pem -alias server -keystore server.jks
I read through LOTS of places online to solve this thing.
This is the code I wrote to make it work:
ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream);
String alias = "alias";//cert.getSubjectX500Principal().getName();
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);
trustStore.setCertificateEntry(alias, cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(trustStore, null);
KeyManager[] keyManagers = kmf.getKeyManagers();
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
URL url = new URL(someURL);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
app.certificateString is a String that contains the Certificate, for example:
static public String certificateString=
"-----BEGIN CERTIFICATE-----\n" +
"MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
"BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
... a bunch of characters...
"5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
"-----END CERTIFICATE-----";
I have tested that you can put any characters in the certificate string, if it is self signed, as long as you keep the exact structure above. I obtained the certificate string with my laptop's Terminal command line.
If creating a SSLSocketFactory is not an option, just import the key into the JVM
Retrieve the public key:
$openssl s_client -connect dev-server:443, then create a file dev-server.pem that looks like
-----BEGIN CERTIFICATE-----
lklkkkllklklklklllkllklkl
lklkkkllklklklklllkllklkl
lklkkkllklk....
-----END CERTIFICATE-----
Import the key: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem.
Password: changeit
Restart JVM
Source: How to solve javax.net.ssl.SSLHandshakeException?
We copy the JRE's truststore and add our custom certificates to that truststore, then tell the application to use the custom truststore with a system property. This way we leave the default JRE truststore alone.
The downside is that when you update the JRE you don't get its new truststore automatically merged with your custom one.
You could maybe handle this scenario by having an installer or startup routine that verifies the truststore/jdk and checks for a mismatch or automatically updates the truststore. I don't know what happens if you update the truststore while the application is running.
This solution isn't 100% elegant or foolproof but it's simple, works, and requires no code.
I've had to do something like this when using commons-httpclient to access an internal https server with a self-signed certificate. Yes, our solution was to create a custom TrustManager that simply passed everything (logging a debug message).
This comes down to having our own SSLSocketFactory that creates SSL sockets from our local SSLContext, which is set up to have only our local TrustManager associated with it. You don't need to go near a keystore/certstore at all.
So this is in our LocalSSLSocketFactory:
static {
try {
SSL_CONTEXT = SSLContext.getInstance("SSL");
SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to initialise SSL context", e);
} catch (KeyManagementException e) {
throw new RuntimeException("Unable to initialise SSL context", e);
}
}
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) });
return SSL_CONTEXT.getSocketFactory().createSocket(host, port);
}
Along with other methods implementing SecureProtocolSocketFactory. LocalSSLTrustManager is the aforementioned dummy trust manager implementation.

Is it possible to add an additional subdomain to a existing SSL certificate for HttpClient 4.4?

We are using HttpClient 4.4 to communicate with some external servers (server1.company.com, server2.company.com, server3.company.com). They recently added an additional environment (server4.company.com) which uses the same certificate as the other 3. The certificate in question has the first 3 servers listed as "Certificate Subject Alternative Names", but server4 is not mentioned.
Is it possible for me to tell keytool that the certificate is valid for additional SANs? Or is there any other way to tell HttpClient to 'trust' this one certificate for some extra domains? Are there any other options or must I go back to company.com and ask them to get a new certificate?
There are two aspects of certificate verifications (in general):
Verifying that the certificate is genuine and issued by someone you trust (that's the PKI aspect).
Verifying that it belongs to the host name you want to connect to (that's the host name verification).
(Perhaps this question, about libcurl might be of interest if you need analogies.)
According to what you are saying, that particular certificate is trusted and valid for other host names. Hence, it will pass the PKI verification (what the TrustStrategy implements).
What you need is to make build an exceptional case, only for that particular certificate, for the host name verification aspect.
I can't remember off the top of my head how it is used with Apache HTTP Client 4.4, but you should use your own verifier instead of the DefaultHostnameVerifier.
The methods to implement are verify(String hostYouAreAfter, SSLSession sessionYouActuallyGet) and verify(String hostYouAreAfter, X509Certificate certYouActuallyGet).
You can provide your own implementation along these lines:
public verify(String hostYouAreAfter, X509Certificate certYouActuallyGet) {
if (certYouActuallyGet.equals(referenceCertificate)) {
if ("server4.company.com".equalsIgnoreCase(hostYouAreAfter)) {
// All good, don't fail and throw an exception.
} else {
super.verify(hostYouAreAfter, certYouActuallyGet);
}
} else {
super.verify(hostYouAreAfter, certYouActuallyGet);
}
}
You can do the same with verify(String,SSLSession) and get the X509Certificate from the SSLSession's peer chain (position 0). The logic is the same, but you need to return true/false instead of throwing exceptions.
Here, I'm assuming that you've loaded referenceCertificate from a place of reference where have that particular certificate. You could for example load it from a known keystore, or load it with a CertificateFactory from a reference PEM file configured in your application.
There are two key differences with a TrustStrategy where you'd implement isTrusted(final X509Certificate[] chain, final String authType) as return "nice guy".equalsIgnoreCase(issuerDN.getName());:
You're actually making this exceptional case only for that very certificate, not for any other certificate that would also happen to be issue with the name you're after.
It only affects the connections where you expect to connect to that particular host (not other hosts). You indeed have access to the first String parameter of HostnameVerifier.verify(...), which is the host name you intend to connect to. At least you have it to use for comparison with the certificate you get, which is something you don't get with a TrustStrategy.
One can trust certain select certificates by using a custom TrustStrategy
SSLContext sslcontext = SSLContexts.custom()
.loadTrustMaterial(new TrustStrategy() {
#Override
public boolean isTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {
X509Certificate x509Certificate = chain[0];
Principal issuerDN = x509Certificate.getIssuerDN();
return "nice guy".equalsIgnoreCase(issuerDN.getName());
}
}).build();
CloseableHttpClient client = HttpClients.custom()
.setSslcontext(sslcontext)
.build();

how to use a ssl certificate [duplicate]

A module I'm adding to our large Java application has to converse with another company's SSL-secured website. The problem is that the site uses a self-signed certificate. I have a copy of the certificate to verify that I'm not encountering a man-in-the-middle attack, and I need to incorporate this certificate into our code in such a way that the connection to the server will be successful.
Here's the basic code:
void sendRequest(String dataPacket) {
String urlStr = "https://host.example.com/";
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setMethod("POST");
conn.setRequestProperty("Content-Length", data.length());
conn.setDoOutput(true);
OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
o.write(data);
o.flush();
}
Without any additional handling in place for the self-signed certificate, this dies at conn.getOutputStream() with the following exception:
Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Ideally, my code needs to teach Java to accept this one self-signed certificate, for this one spot in the application, and nowhere else.
I know that I can import the certificate into the JRE's certificate authority store, and that will allow Java to accept it. That's not an approach I want to take if I can help; it seems very invasive to do on all of our customer's machines for one module they may not use; it would affect all other Java applications using the same JRE, and I don't like that even though the odds of any other Java application ever accessing this site are nil. It's also not a trivial operation: on UNIX I have to obtain access rights to modify the JRE in this way.
I've also seen that I can create a TrustManager instance that does some custom checking. It looks like I might even be able to create a TrustManager that delegates to the real TrustManager in all instances except this one certificate. But it looks like that TrustManager gets installed globally, and I presume would affect all other connections from our application, and that doesn't smell quite right to me, either.
What is the preferred, standard, or best way to set up a Java application to accept a self-signed certificate? Can I accomplish all of the goals I have in mind above, or am I going to have to compromise? Is there an option involving files and directories and configuration settings, and little-to-no code?
Create an SSLSocket factory yourself, and set it on the HttpsURLConnection before connecting.
...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...
You'll want to create one SSLSocketFactory and keep it around. Here's a sketch of how to initialize it:
/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ...
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();
If you need help creating the key store, please comment.
Here's an example of loading the key store:
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();
To create the key store with a PEM format certificate, you can write your own code using CertificateFactory, or just import it with keytool from the JDK (keytool won't work for a "key entry", but is just fine for a "trusted entry").
keytool -import -file selfsigned.pem -alias server -keystore server.jks
I read through LOTS of places online to solve this thing.
This is the code I wrote to make it work:
ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream);
String alias = "alias";//cert.getSubjectX500Principal().getName();
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);
trustStore.setCertificateEntry(alias, cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(trustStore, null);
KeyManager[] keyManagers = kmf.getKeyManagers();
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
URL url = new URL(someURL);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
app.certificateString is a String that contains the Certificate, for example:
static public String certificateString=
"-----BEGIN CERTIFICATE-----\n" +
"MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
"BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
... a bunch of characters...
"5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
"-----END CERTIFICATE-----";
I have tested that you can put any characters in the certificate string, if it is self signed, as long as you keep the exact structure above. I obtained the certificate string with my laptop's Terminal command line.
If creating a SSLSocketFactory is not an option, just import the key into the JVM
Retrieve the public key:
$openssl s_client -connect dev-server:443, then create a file dev-server.pem that looks like
-----BEGIN CERTIFICATE-----
lklkkkllklklklklllkllklkl
lklkkkllklklklklllkllklkl
lklkkkllklk....
-----END CERTIFICATE-----
Import the key: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem.
Password: changeit
Restart JVM
Source: How to solve javax.net.ssl.SSLHandshakeException?
We copy the JRE's truststore and add our custom certificates to that truststore, then tell the application to use the custom truststore with a system property. This way we leave the default JRE truststore alone.
The downside is that when you update the JRE you don't get its new truststore automatically merged with your custom one.
You could maybe handle this scenario by having an installer or startup routine that verifies the truststore/jdk and checks for a mismatch or automatically updates the truststore. I don't know what happens if you update the truststore while the application is running.
This solution isn't 100% elegant or foolproof but it's simple, works, and requires no code.
I've had to do something like this when using commons-httpclient to access an internal https server with a self-signed certificate. Yes, our solution was to create a custom TrustManager that simply passed everything (logging a debug message).
This comes down to having our own SSLSocketFactory that creates SSL sockets from our local SSLContext, which is set up to have only our local TrustManager associated with it. You don't need to go near a keystore/certstore at all.
So this is in our LocalSSLSocketFactory:
static {
try {
SSL_CONTEXT = SSLContext.getInstance("SSL");
SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to initialise SSL context", e);
} catch (KeyManagementException e) {
throw new RuntimeException("Unable to initialise SSL context", e);
}
}
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) });
return SSL_CONTEXT.getSocketFactory().createSocket(host, port);
}
Along with other methods implementing SecureProtocolSocketFactory. LocalSSLTrustManager is the aforementioned dummy trust manager implementation.

Java SSL connect, add server cert to keystore programmatically

I am connecting an SSL client to my SSL server.
When the client fails to verify a certificate due to the root not existing in the client's key store, I need the option to add that certificate to the local key store in code and continue.
There are examples for always accepting all certificates, but I want the user to verify the cert and add it to local key store without leaving the application.
SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", 23467);
try{
sslsocket.startHandshake();
} catch (IOException e) {
//here I want to get the peer's certificate, conditionally add to local key store, then reauthenticate successfully
}
There is a whole lot of stuff about custom SocketFactory, TrustManager, SSLContext, etc and I don't really understand how they all fit together or which would be the shortest path to my goal.
You could implement this using a X509TrustManager.
Obtain an SSLContext with
SSLContext ctx = SSLContext.getInstance("TLS");
Then initialize it with your custom X509TrustManager by using SSLContext#init. The SecureRandom and the KeyManager[] may be null. The latter is only useful if you perform client authentication, if in your scenario only the server needs to authenticate you don't need to set it.
From this SSLContext, get your SSLSocketFactory using SSLContext#getSocketFactory and proceed as planned.
As concerns your X509TrustManager implementation, it could look like this:
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
//do nothing, you're the client
}
public X509Certificate[] getAcceptedIssuers() {
//also only relevant for servers
}
public void checkServerTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
/* chain[chain.length -1] is the candidate for the
* root certificate.
* Look it up to see whether it's in your list.
* If not, ask the user for permission to add it.
* If not granted, reject.
* Validate the chain using CertPathValidator and
* your list of trusted roots.
*/
}
};
Edit:
Ryan was right, I forgot to explain how to add the new root to the existing ones. Let's assume your current KeyStore of trusted roots was derived from cacerts (the 'Java default trust store' that comes with your JDK, located under jre/lib/security). I assume you loaded that key store (it's in JKS format) with KeyStore#load(InputStream, char[]).
KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream in = new FileInputStream("<path to cacerts"");
ks.load(in, "changeit".toCharArray);
The default password to cacerts is "changeit" if you haven't, well, changed it.
Then you may add addtional trusted roots using KeyStore#setEntry. You can omit the ProtectionParameter (i.e. null), the KeyStore.Entry would be a TrustedCertificateEntry that takes the new root as parameter to its constructor.
KeyStore.Entry newEntry = new KeyStore.TrustedCertificateEntry(newRoot);
ks.setEntry("someAlias", newEntry, null);
If you'd like to persist the altered trust store at some point, you may achieve this with KeyStore#store(OutputStream, char[].
In the JSSE API (the part that takes care of SSL/TLS), checking whether a certificate is trusted doesn't necessarily involve a KeyStore. This will be the case in the vast majority of cases, but assuming there will always be one is incorrect. This is done via a TrustManager.
This TrustManager is likely to use the default trust store KeyStore or the one specified via the javax.net.ssl.trustStore system property, but that's not necessarily the case, and this file isn't necessarily $JAVA_HOME/jre/lib/security/cacerts (all this depends on the JRE security settings).
In the general case, you can't get hold of the KeyStore that's used by the trust manager you're using from the application, more so if the default trust store is used without any system property settings.
Even if you were able to find out what that KeyStore is, you would still be facing two possible problems (at least):
You might not be able to write to that file (which is not necessarily a problem if you're not saving the changes permanently).
This KeyStore might not even be file-based (e.g. it could be the Keychain on OSX) and you might not have write access to it either.
What I would suggest is to write a wrapper around the default TrustManager (more specifically, X509TrustManager) which performs the checks against the default trust manager and, if this initial check fails, performs a callback to the user-interface to check whether to add it to a "local" trust store.
This can be done as shown in this example (with a brief unit test), if you want to use something like jSSLutils.
Preparing your SSLContext would be done like this:
KeyStore keyStore = // ... Create and/or load a keystore from a file
// if you want it to persist, null otherwise.
// In ServerCallbackWrappingTrustManager.CheckServerTrustedCallback
CheckServerTrustedCallback callback = new CheckServerTrustedCallback {
public boolean checkServerTrusted(X509Certificate[] chain,
String authType) {
return true; // only if the user wants to accept it.
}
}
// Without arguments, uses the default key managers and trust managers.
PKIXSSLContextFactory sslContextFactory = new PKIXSSLContextFactory();
sslContextFactory
.setTrustManagerWrapper(new ServerCallbackWrappingTrustManager.Wrapper(
callback, keyStore));
SSLContext sslContext = sslContextFactory.buildSSLContext();
SSLSocketFactory sslSocketFactory = sslContext.getSslSocketFactory();
// ...
// Use keyStore.store(...) if you want to save the resulting keystore for later use.
(Of course, you don't need to use this library and its SSLContextFactory, but implement your own X509TrustManager, "wrapping" or not the default one, as you prefer.)
Another factor you have to take into account is the user interaction with this callback. By the time the user has made the decision to click on accept or reject (for example), the handshake may have timed out, so you may have to make a second attempt to connect when the user has accepted the certificate.
Another point to take into account in the design of the callback is that the trust manager doesn't know which socket is being used (unlike its X509KeyManager counterpart), so there should be as little ambiguity as to what user action caused that pop-up (or however you want to implement the callback). If multiple connections are made, you wouldn't want to validate the wrong one.
It seems possible to solve this by using a distinct callback, SSLContext and SSLSocketFactory per SSLSocket that's supposed to make a new connection, some way of tying up the SSLSocket and the callback to the action taken by the user to trigger that connection attempt in the first place.

Categories