Java Rest Client using self-signed Certificate - java

Hello I'm trying to write a little Rest client which accesses our Cloud server (Rest Webservices). The connection is secured with a SSL Client Certificate which if I understand correctly is not signedby any Certification Authority, and am having problems.
I know that the certificate works fine as I can use this in other programming languages (e.g. C#, PHP, etc), and also because I am testing the API using Postman, however I cannot really understand how to do this in Java.
I have tried using the P12 certificate file, and I also have .key and .crt files, but still nothing changed. The .JKS file I have created using keytool.exe, and I presume it is correct (as far as I could understand).
This is the code I am using :
String keyPassphrase = certPwd;
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream("C:\\Test\\Certificate\\idscertificate.jks"), keyPassphrase.toCharArray());
SSLContext sslContext = SSLContexts.custom()
.loadKeyMaterial(keyStore, certPwd.toCharArray())
.build();
HttpClient httpClient = HttpClients.custom().setSslcontext(sslContext).build();
HttpResponse response = httpClient.execute(new HttpGet(
"https://url_that_I_am_using_to_call_my_rest_web_service"));
but every time I launch this I get an error:
"unable to find valid certification path to requested target".
As far as I could see this is because I don't have a Certification Authority to specify, am I correct?
Can anyone help me with this?
Thank you all for your help
Tommaso
/*******************
This is how I imported the P12 into the Keystore. I tried different ways, the last one i tried was:
First created the JKS:
keytool -genkey -alias myName -keystore c:\Test\Certificate\mykeystoreName.jks
then "cleaned up with:
keytool -delete -alias myName -keystore c:\Test\Certificate\myKeystoreName.jks
then imported the P12 file with:
keytool -v -importkeystore -srckeystore c:\Test\Certificate\idscertificate.p12 -srcstoretype PKCS12 -destkeystore c:\Test\Certificate\myKeystoreName.jks -deststoretype JKS
Result obtained:
Entry for alias idsclientcertificate successfully imported.
Import command completed: 1 entries successfully imported, 0 entries failed or cancelled
and if I check the content of the keystore I find my imported certificate.
Nevertheless I still get the same error.
Thank you for your help.
/****************************Update February 8th *******************
Ok I tried everything, but really everything and now slowly giving up... the situation is the following:
using the following code so far:
SSLContextBuilder sslContext = new SSLContextBuilder();
sslContext.loadKeyMaterial(readKeyStore(), userPwd.toCharArray());
//sslContext.loadTrustMaterial(readKeyStore(), new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext.build());
CloseableHttpClient client = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.setSSLHostnameVerifier(new NoopHostnameVerifier())
.build();
HttpGet httpGet = new HttpGet("https://myhost.com/myrest/status");
httpGet.addHeader("Accept", "application/json;charset=UTF8");
httpGet.addHeader("Cookie", "sessionids=INeedThis");
String encoded = Base64.getEncoder().encodeToString((userName+":"+userPwd).getBytes(StandardCharsets.UTF_8));
httpGet.addHeader("Authorization", "Basic "+encoded);
httpGet.addHeader("Cache-Control", "no-cache");
HttpResponse response = client.execute(httpGet);
Unfortunately still not working. I tried the following: - include my certificate in the default java cacerts, - specify the alias as my host name, - create a new jks, - load the p12 file, still nothing, same error.
Error Message I get is:
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
If I don't use a certificate, I get another error indicating that the certificate is missing therefore the certificate is loaded (also I see it in my IDE).
If I use the exact same certificate file from another platform (c# or using a browser) I get the correct response and object (therefore the certificate/password are valid)
Is there any way that I can stop the validation of the certification path?

first of all thank you all for your help. I finally got it to work following these steps:
1 - I determined my root CA Cert using command:
openssl s_client -showcerts -connect my.root.url.com:443
then I imported this certificate using Portecle.exe (https://sourceforge.net/projects/portecle/) but you can also import it using the normal keytool command, into my default Java Keystore (jre/lib/security/cacerts)
--> Make sure you assign the root URL as Alias (e.g. *.google.com if you would connect to a google API). This seems to be very important.
Then I used the following code:
First created the ServerSocketFactory:
private static SSLSocketFactory getSocketFactory()
{
try
{
SSLContext context = SSLContext.getInstance("TLS");
// Create a key manager factory for our personal PKCS12 key file
KeyManagerFactory keyMgrFactory = KeyManagerFactory.getInstance("SunX509");
KeyStore keyStore = KeyStore.getInstance("PKCS12");
char[] keyStorePassword = pk12Password.toCharArray(); // --> This is the Password for my P12 Client Certificate
keyStore.load(new FileInputStream(pk12filePath), keyStorePassword); // --> This is the path to my P12 Client Certificate
keyMgrFactory.init(keyStore, keyStorePassword);
// Create a trust manager factory for the trust store that contains certificate chains we need to trust
// our remote server (I have used the default jre/lib/security/cacerts path and password)
TrustManagerFactory trustStrFactory = TrustManagerFactory.getInstance("SunX509");
KeyStore trustStore = KeyStore.getInstance("JKS");
char[] trustStorePassword = jksTrustStorePassword.toCharArray(); // --> This is the Default password for the Java KEystore ("changeit")
trustStore.load(new FileInputStream(trustStorePath), trustStorePassword);
trustStrFactory.init(trustStore);
// Make our current SSL context use our customized factories
context.init(keyMgrFactory.getKeyManagers(),
trustStrFactory.getTrustManagers(), null);
return context.getSocketFactory();
}
catch (Exception e)
{
System.err.println("Failed to create a server socket factory...");
e.printStackTrace();
return null;
}
}
Then I created the connection using:
public static void launchApi()
{
try
{
// Uncomment this if your server cert is not signed by a trusted CA
HostnameVerifier hv = new HostnameVerifier()
{
public boolean verify(String urlHostname, SSLSession session)
{
return true;
}};
HttpsURLConnection.setDefaultHostnameVerifier(hv);
URL url = new URL("https://myRootUrl.com/to/launch/api");
HttpsURLConnection.setDefaultSSLSocketFactory(getSocketFactory());
HttpsURLConnection urlConn = (HttpsURLConnection)url.openConnection();
String encoded = Base64.getEncoder().encodeToString((userName+":"+userPwd).getBytes(StandardCharsets.UTF_8)); //Acc User Credentials if needed to log in
urlConn.setRequestProperty ("Authorization", "Basic "+encoded);
urlConn.setRequestMethod("GET"); // Specify all needed Request Properties:
urlConn.setRequestProperty("Accept", "application/json;charset=UTF8");
urlConn.setRequestProperty("Cache-Control", "no-cache");
urlConn.connect();
/* Dump what we have found */
BufferedReader in =
new BufferedReader(
new InputStreamReader(urlConn.getInputStream()));
String inputLine = null;
while ((inputLine = in.readLine()) != null)
System.out.println(inputLine);
in.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
This is what worked for me. Thank you all, and also thanks to:
this article that guided me on the right direction
Ciao

Instead of using loadKeyMaterial use loadTrustMaterial, the first one is for creating a SSLContext for a server, and the second one is for a client.
Example:
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(keyStore, new TrustSelfSignedStrategy())
.build();

Related

Secure Sockets in Java without CA [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.

How to add a client side pkcs12 certificate to Postman Chrome, W7 ?

I try to test a 'strange' GET request where I have to provide a BASIC authentication and a client side certificate.
I try to check it with Postman Chrome but I did not understand how to link the certificate from chrome personal certificate to my request.
I saw this discussion : https://github.com/a85/POSTMan-Chrome-Extension/issues/482 but it is about MAC keystore and I can't transpose is to W7/Chrome.
Here is my java code set up that should do the same job as postman to help you understand what I want postman to do. We use that post to write it
InputStream is = context.getResources().getAssets().open("CertificateFile.p12");
KeyStore keyStore = KeyStore.getInstance("PKCS12");
BufferedInputStream bis = new BufferedInputStream(is);
String password ="xxxxx";
keyStore.load(bis, password.toCharArray()); // password is the PKCS#12 password. If there is no password, just pass null
// Init SSL Context
KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
kmf.init(keyStore, password.toCharArray());
KeyManager[] keyManagers = kmf.getKeyManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, null, null);
HttpsURLConnection urlConnection = null;
String strURL = "theUrlITryToHit";
url = new URL(strURL);
urlConnection = (HttpsURLConnection) url.openConnection();
if(urlConnection instanceof HttpsURLConnection) {
((HttpsURLConnection)urlConnection)
.setSSLSocketFactory(sslContext.getSocketFactory());
}
urlConnection.setRequestMethod("GET");
String basicAuth = "Basic " + Base64.encodeToString("pseudo:password".getBytes(), Base64.NO_WRAP);
urlConnection.setRequestProperty ("Authorization", basicAuth);
I'm using a Mac, but its probably similar for you.
If you can use CURL on your PC, see if you can get it to work with CURL first:
curl --insecure --cert-type P12 --cert /path-to/your-file.p12:the-password https://your-host.com/endpoint
Postman Settings:
Postman->preferences->General
SSL certificate verification OFF
Postman Certs:
Postman->preferences->Certificates
Client Certificates:
Host yourhost.com
CRT file
Key file
PFX file /path-to-file/CertificateFile.p12
Passphrase your-file-password
I was having a similar issue and just got it working. My private key and cert were stored in a .pem file, so I first needed to put them in to a format that Windows would use. I did that with the following command:
openssl pkcs12 -inkey mycertandkey.pem -in mycert.crt -export -out mycertandkey.pfx
I did this in linux but it should work in Windows as well, if you have openssl installed.
Run certmgr.msc in Windows. Right-click the 'Personal' folder and select 'All tasks' -> 'Import...' and choose the .pfx file. Enter the passphrase and import it in to the 'Personal' folder.
Once that's done, you'll need to close your running Chrome windows. Then open Postman in a new window. When you attempt to connect to the URL, this time it should ask to confirm the use of the client cert. Once confirmed, you should be able to make calls to the URL from then on.

How to connect to an HTTPS URL from a JavaFX application

I have a Javafx application that sends GET and POST requests to a secure web service via HTTPS. The SSL setup on the server hosting the webservices is one-way ssl, i.e. the Javafx application validates the server's identity but the server does not validate the thick client's identity.
The application server is behind an F5 that has the certificates(signed by an external authority).
For a browser this would not have been a problem as the browser itself handles validating the server's identity and displays the relevant warning to the user. But for a thick client, I am not sure how to validate the server's identity before sending the request. Please let me know how to handle this in a Javafx application.
I did ask a question relating to this earlier here and here, but those did not help. So, please pardon my limited knowledge on this topic.
Any help would be appreciated.
If your certificate don't work in Firefox/java, most likely it issuer is unknown by Firefox/java.
How to make it work:
Get full certificate chain of your server. You can do it with Firefox. View certificate -> details-> export to .pem file. In your case chain will contain at least 2 certificate (cerver cert and CA cert, CA possible self-signed or maybe not) Export CA certificate in .pem file.
Now you can force java to trust that CA, it can be done in various ways, for example, you can add CA certificate in jre cacerts or create custom SSLContext for HttpsURLConnection.
If you do DNS or etc.hosts modification, rollback it. Connection address should match with certificate CN, include wildcards.
Use that code to connect to your server:
public void test() throws Exception {
URL u = new URL(
"https://my-server.com/my-webservices/data");
HttpsURLConnection http = (HttpsURLConnection) u.openConnection();
http.setSSLSocketFactory(createSSLContext().getSocketFactory());
http.setAllowUserInteraction(true);
http.setRequestMethod("GET");
http.connect();
InputStream is = http.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder stringBuilder = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null)
{
stringBuilder.append(line
+ "\n");
}
System.out.println(stringBuilder.toString());
}
private SSLContext createSSLContext() throws Exception {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream in = new FileInputStream("path_to_ca_file.pem");
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(null);
try {
X509Certificate cacert = (X509Certificate) cf.generateCertificate(in);
trustStore.setCertificateEntry("ca", cacert);
} finally {
IOUtils.closeQuietly(in);
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
return sslContext;
}

How to get around web service over https giving back a SSLHandshaking error?

I am attempting to hit a remote server via a get/post web service call over SSL. I am using apaches HttpClient in the following manner:
HttpClient client = new HttpClient();
client.getHostConfiguration().setProxy("my_host", 443);
Credentials defaultcreds = new UsernamePasswordCredentials("dev", "password");
client.getState().setCredentials(new AuthScope("my_host", 443, AuthScope.ANY_REALM), defaultcreds);
// Create a method instance.
GetMethod method = new GetMethod(url);
// Provide custom retry handler is necessary
method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler(3, false));
try {
// Execute the method.
int statusCode = client.executeMethod(method);
if (statusCode != HttpStatus.SC_OK) {
System.err.println("Method failed: " + method.getStatusLine());
}
// Read the response body.
byte[] responseBody = method.getResponseBody();
// Deal with the response.
// Use caution: ensure correct character encoding and is not binary data
System.out.println(new String(responseBody));
} catch (HttpException e) {
System.err.println("Fatal protocol violation: " + e.getMessage());
e.printStackTrace();
} catch (IOException e) {
System.err.println("Fatal transport error: " + e.getMessage());
e.printStackTrace();
} finally {
// Release the connection.
method.releaseConnection();
}
It seems to work in POSTER, but I know that the certificates are located in the browser, and that is handling all authentication and certificate handling. I need to write the code to get this response to be used elsewhere. Any ideas? And is this going to be a problem when the code is pushed onto the server? (Will it need a different certificate).
EDIT: Here is the added error.
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
The proper way to solve this is to add the hosts CA to your JVMs truststore (<jre directory>/lib/security/cacerts). You can get the CA certificate by browsing to the hosts URL in a browser, viewing the certificate for the site (normally done by clicking the lock icon next to the URL in the browser), and exporting the CA to a file. Once you have the .crt file, you can import it to cacerts using the keytool command line tool:
keytool -keystore cacerts -importcert -alias someName -file yourCertFilename
When prompted for a password, the default is changeit
If you'd prefer not to add the CA to the default truststore, you can create your own truststore file use SSLSocketFactory to pass it to the HttpClient when you create it.
keytool -keystore myTrustStore.jks -importcert -alias someName -file yourCertFilename
Enter a password of your choice when prompted.
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream("myTrustStore.jks"), trustStorePassword);
SSLSocketFactory sf = new SSLSocketFactory(trustStore);
Scheme httpsScheme = new Scheme("https", 443, sf);
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(httpsScheme);
ClientConnectionManager cm = new SingleClientConnManager(schemeRegistry);
HttpClient httpClient = new DefaultHttpClient(cm);
(The code above is off the top of my head and hasn't been tested, but it should give you the idea.)
Alternatively, if you want HTTPClient to just bypass certificate validation altogether (I don't recommend it), you can create a TrustManager that will trust any certificate like this.

Java Keystore reload whilst in runtime [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.

Categories

Resources