I'm trying to create a Java software that connects to a Netgear WAG102 (Access Point) which has to get the connection logs for my wifi network.
my software already works with other Netgear Access Points (WG302v2, for istance), but I can find no way to get it to work with the WAG102.
I keep on receiving a
javax.net.ssl.SSLProtocolException: Extensions not allowed in v2 certificate
while trying to open a secure SSL connection to the AP.
additional info: the certificate sent from the AP expired 1 year ago, so I implemented the infamous "TrustAllCerts" trick, but that alone didn't seem to help.
Google chrome says that the certificate is version v4, but my java software keeps on saying it's version v2, giving then that exception when it checks for the certificate extensions (version v2 doesn't support extensions, as far as I know).
My question is: is there any way to make it work despite of this issue?
here is my code:
private HttpsURLConnection createConnection(URL url) throws IOException{
HttpsURLConnection con=(HttpsURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setDoOutput(true);
con.setRequestProperty("Authorization", "Basic " + "**********");
con.setHostnameVerifier(new HostnameVerifier(){public boolean verify(String hostname, SSLSession session){return true;}});
TrustManager[] trustAllCerts=null;
SSLContext sslContext=null;
SSLSocketFactory sslSocketFactory;
try{
trustAllCerts = new TrustManager[]{ new X509TrustManager(){
public X509Certificate[] getAcceptedIssuers(){return null;}
public void checkClientTrusted(X509Certificate[] chain, String authType){}
public void checkServerTrusted(X509Certificate[] chain, String authType){}
}};
sslContext = SSLContext.getInstance( "SSL" );
sslContext.init( null, trustAllCerts, new java.security.SecureRandom() );
sslSocketFactory = sslContext.getSocketFactory();
con.setSSLSocketFactory( sslSocketFactory );
System.out.println("Response Code : " + con.getResponseCode());
System.out.println("Cipher Suite : " + con.getCipherSuite());
Certificate[] certs = con.getServerCertificates();
for(Certificate cert : certs){
System.out.println("Cert ext : "+cert);
System.out.println("Cert Type : " + cert.getType());
System.out.println("Cert Hash Code : " + cert.hashCode());
System.out.println("Cert Public Key Algorithm : " + cert.getPublicKey().getAlgorithm());
System.out.println("Cert Public Key Format : " + cert.getPublicKey().getFormat());
System.out.println("\n");
}
} catch (Exception e){e.printStackTrace();}
//printHTTPSCert(con);
return con;
}
I'm getting the exception when calling con.getResponseCode(), basically because it's when the connection is getting opened I think.
This program works correctly with www.google.com and all the other sites with a good certificate.
There seem to be two issues here:
Firstly, there it shouldn't be a V4 certificate. It's possible to put this number in the version field, with custom made tools, but there's no specification that matches it. The latest X.509 specification only goes up to V3:
Version ::= INTEGER { v1(0), v2(1), v3(2) }
Secondly, the Java code that reads the certificate uses a != condition:
(version.compare(CertificateVersion.V3) != 0)
If it's not V3, it assumes that it's V2 (which makes sense if you consider there shouldn't be a V4). I presume other implementations might let this incorrect certificate through using a >= condition instead.
The easiest would be to install a new, correct certificate on the router if possible. (According to your screenshot, it's also using an MD5 signature, which isn't recommended nowadays.)
EDIT: It also appears that this certificate was issued with a www.netgear.com Subject Alternative Name (i.e. it's valid for that host name), which is non-sense on a router. You should really install your own certificate if you're deploying this, even if it's self-signed.
Anyway, there might be a workaround. It seems that the BouncyCastle implementation CertificateFactory (used by the JSSE) is more flexible in terms of versions. If you use the BC provider in a position before the Sun providers, this should work. You can get the BC provider jar and use this (before making connections), for example:
Security.insertProviderAt(new BouncyCastleProvider(), 1);
Related
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.
Premise: I have a certificate and I want to verify that the system 'trusts' this certificate (signed by a trusted root CA by Java / Operating System)
I have found some varying solutions on how to accomplish this.
Option 1:
Use SSL classes to derive trust.
TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init((KeyStore) null);
for (TrustManager trustManager : tmfactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
try {
((X509TrustManager) trustManager).checkClientTrusted(new X509Certificate[] {new JcaX509CertificateConverter().getCertificate(holder)}, "RSA");
System.out.println("This certificate is trusted by a Root CA");
} catch (CertificateException e) {
e.printStackTrace();
}
}
}
Since this approach relies heavily on SSL classes (which are not needed by the current project) we are looking for alternatives.
Option 2:
Load Java's cacertsfile into a keystore and check each 'most-trusted' certificate against my certificate for equality.
String filename = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar);
FileInputStream is = new FileInputStream(filename);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
String password = "changeit";
keystore.load(is, password.toCharArray());
// This class retrieves the most-trusted CAs from the keystore
PKIXParameters params = new PKIXParameters(keystore);
// Get the set of trust anchors, which contain the most-trusted CA certificates
Set<X509Certificate> rootCertificates = params.getTrustAnchors().parallelStream().map(TrustAnchor::getTrustedCert).collect(Collectors.toSet());
return rootCertificates.contains(holderX509);
The problem with this approach is that it requires a password to verify integrity of the JKS encoded file. While the SSL one seemingly does not (or rather uses System.getProperty("javax.net.ssl.trustStorePassword") which again is heavily tied to SSL.
Question: Does there exist a solution that is in between manually loading certificates from a file and pure SSL? I feel as if there should be some class that I can call to simply verify the system trust of a certificate without having to jump through a couple hoops.
After reading Beginning Cryptography With Java by David Hook I have produced the following example to verify a certificate chain (which accomplishes the original goal of using the system truststore to verify Root CA's)
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", new BouncyCastleProvider());
InputStream is = new ByteArrayInputStream(some bytes in an array);
CertPath certPath = certificateFactory.generateCertPath(is, "PKCS7"); // Throws Certificate Exception when a cert path cannot be generated
CertPathValidator certPathValidator = CertPathValidator.getInstance("PKIX", new BouncyCastleProvider());
PKIXParameters parameters = new PKIXParameters(KeyTool.getCacertsKeyStore());
PKIXCertPathValidatorResult validatorResult = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, parameters); // This will throw a CertPathValidatorException if validation fails
This also accomplishes the goal of not having to use SSL classes - instead Java security classes / algorithms are used.
Short of downloading a third-party library, there probably isn't another alternative.
Why are you trying to avoid the "SSL" library? It's part of the standard library and so puts no burden on your program.
In any case, certificate verification is a big part of SSL. I doubt anyone's gone to the trouble of creating a library that does so without also implementing some substantial subset of the SSL protocol. There's just no real reason to do so.
I got host name wrong exception.I have used this code(got it from some link) in my program.My program is working fine.My question is it secure enough?? (as it is not validating certificate chains)
public class Host {
public String subscribe() throws Exception {
String resp = "";
String urlString="https://xxx.xxx.xx.xx:8443/WebApplication3/NewServlet";
URL url;
URLConnection urlConn;
DataOutputStream printout;
DataInputStream input;
String str = "";
int flag=1;
try {
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String urlHostName, SSLSession session) {
System.out.println("Warning: URL Host: " + urlHostName + " vs. "
+ session.getPeerHost());
return true;
}
};
trustAllHttpsCertificates();
HttpsURLConnection.setDefaultHostnameVerifier(hv);
url = new URL(urlString);
urlConn = url.openConnection();
urlConn.setDoInput(true);
Object object;
urlConn.setUseCaches(false);
urlConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
input = new DataInputStream(urlConn.getInputStream());
while (null != ((str = input.readLine()))) {
if (str.length() >0) {
str = str.trim();
if(!str.equals("")) {
//System.out.println(str);
resp += str;
}
}
}
input.close();
} catch ( MalformedURLException mue) {
mue.printStackTrace();
} catch(IOException ioe) {
ioe.printStackTrace();
}
return resp;
}
public static class miTM implements javax.net.ssl.TrustManager,
javax.net.ssl.X509TrustManager {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public boolean isServerTrusted(java.security.cert.X509Certificate[] certs) {
return true;
}
public boolean isClientTrusted(java.security.cert.X509Certificate[] certs) {
return true;
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
return;
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
return;
}
}
private static void trustAllHttpsCertificates() throws Exception {
// Create a trust manager that does not validate certificate chains:
javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1];
javax.net.ssl.TrustManager tm = new miTM();
trustAllCerts[0] = tm;
javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
}
The code in miTM actually disables any SSL security checks, so the security level is pretty low (you will only get errors if the SSL certificate is broken but you don't get errors when the certificate doesn't match the domain).
Basically, you try to make a connection without any security at all. If that's what you want, the solution might be "secure enough" but most likely, the answer is "no."
The correct solution for this kind of problem is to create a matching certificate for this domain.
Unfortunately, this isn't possible when your HTTP server is using "virtual hosting" (= many domain names map to the same IP address). The correct solution for this problem is to get your own IP address.
If you still want to try a Java-only solution, have a look at this answer: https://stackoverflow.com/a/3293720/34088
Here is a way to clean up your code and to remain secure. I suppose the code connects to a known service (trusted). To make Java SSL stack accept connection even with hostname mismatch, the best way is to add the server certificate to the JVM trust store.
First you can export the server certificate from your browser and save it on disk. From Linux, you can use openssl s_client -connect xxx.xxx.xx.xx:8443 and copy/paster the server certificate in ascii-armored format to a text file.
Then import the server certificate into jre/lib/security/cacerts JKS file with keytool
keytool -import -alias myservice -file servercertificate.cer
Another option I prefer, to avoid regression when Java is updated, is to copy cacerts in your own place and declares it thanks to the javax.net.ssl.trustStore system property.
As the server certificate is in the trust store... it is trusted, until it expires. This is often used for self-signed server certificates.
many times in java used to get such kind of exceptions
problem could be ipconflict/ip-domain mismatch/invalid certificate
i have solved it by using its appropriate ip address and installing certificate.
To make the connection secure you MUST (at least):
verify that you trust the certificate,
verify the host name (unless you know for sure that this is the one and only certificate that you trust perhaps).
Your code fails on those two points:
The TrustManager you're using doesn't check the certificate at all (it never throws an exception, whereas the API expects it to throw a form of CertificateException when the certificate is not trusted).
Your hostname verifier always returns true.
To fix your code:
Keep the default trust managers, or initialise them with your own trust store and the default TrustManagerFactory.
Keep the default host name verifier.
The title of your question ("host name wrong exception") and your example URL https://xxx.xxx.xx.xx:8443 seems to suggest you're connecting to an IP address.
Unlike some browsers, Java follows the specification (RFC 2818) quite strictly on this:
If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common
Name field in the Subject field of the certificate MUST be used.
Although the use of the Common Name is existing practice, it is
deprecated and Certification Authorities are encouraged to use the
dNSName instead.
[...]
In some cases, the URI is specified as an IP address rather than a hostname. In this case, the iPAddress subjectAltName must be present
in the certificate and must exactly match the IP in the URI.
This means that you can't just get away with putting the IP address in the Common Name (CN) of your Subject DN in your server certificate. If you're using an IP address, it MUST be in a Subject Alternative Name entry. (Starting with Java 7, keytool has options to generate such certificates.)
You will find more details about which commands to use in this answer.
This being said, using IP addresses can only really work at most in a test environment. I don't think any commercial CA will give you an IP-address based certificate. I'd suggest setting up DNS entries (even if it's just in the hosts files in a test environment).
Even if you're not using IP address, you must make sure that that certificate is valid for the host name with which you're trying to contact the server: if you have Subject Alternative Name entries, one of them must match the host name; otherwise, the host name must be in the CN RDN of the Subject DN of this certificate.
Oh wise and noble Oracle,
I'm adding SSL to a TCP client I've written on my Android phone. I can
successfully connect to servers with properly signed certificates, and I can
connect to self-certifying hosts by cooking up a TrustManager implementation
that always thinks everything is fine.
I now have a decorator TrustManager capturing the certificates (before
delegating to its decoratee) for self-certifying hosts and presenting them for
my breathless perusal, but what I can't work out is how to implement ssh's
behaviour of warning that a host is unknown and offering to remember it for
next time - and doing so.
I presumed all I needed was to store the public key - as ssh does with
known_hosts - and re-represent it, but with this code and 'sslTrust' holding
the public key:
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null); // initialise!
ks.setKeyEntry("dbentry", Base64.decode(sslTrust, Base64.NO_WRAP), null);
tmf.init(ks);
tms = tmf.getTrustManagers();
ss.stm = new SnoopyTrustManager((X509TrustManager) tms[0]);
// ...
SLContext context = SSLContext.getInstance("SSL");
context.init(null, new TrustManager[] { ss.stm } , null);
ss.factory = context.getSocketFactory();
// ...
SocketFactory factory = ss.getFactory();
mSocket = factory.createSocket(host, port);
attempting to establish a connection results in
SSLHandshakeException: InvalidAlgorithmParameterException: trustAnchors.isEmpty()
which is fair enough: I don't know how to cook things up from the certificate
offered by the remote server. I'm also fairly sure this isn't how I tell a
TrustManager about a remote server's public key anyway.
Since the site is self-certifying, I imagine could probably just verify that
the public keys match in a trivial TrustManager, but I'd like to understand
how this 'should' be done - adding a CA on a per-connection basis, since
I won't trust that CA for anything else.
You need to use your own trust store on pre-ICS version, and add the serer's certificates to it on first error. Subsequent connections will load it from the trust store and thus trust the remote certificate. This is not a complete solution, but here's one way to do it (code on Github), along with some discussion:
http://nelenkov.blogspot.jp/2011/12/using-custom-certificate-trust-store-on.html
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.