174

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?

erickson
  • 265,237
  • 58
  • 395
  • 493
skiphoppy
  • 97,646
  • 72
  • 174
  • 218
  • small, working fix: http://www.rgagnon.com/javadetails/java-fix-certificate-problem-in-HTTPS.html –  Aug 16 '11 at 14:06
  • 23
    @Hasenpriester: please don't suggest this page. It disables all trust verification. You're not only going to accept the self-signed certificate you want, you're also to accept any certificate that a MITM attacker will present you with. – Bruno Dec 23 '11 at 13:23

5 Answers5

173

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
erickson
  • 265,237
  • 58
  • 395
  • 493
  • 4
    Thank you very much! The other guys helped point me in the right direction, but in the end yours is the approach I took. I had a gruelling task of converting the PEM certificate file to a JKS Java keystore file, and I found assistance for that here: http://stackoverflow.com/questions/722931/ssl-socket-php-code-needs-to-be-converted-to-java – skiphoppy May 13 '09 at 21:37
  • 2
    I'm glad it worked out. I'm sorry you struggled with the key store; I should have just included it in my answer. It's not too difficult with CertificateFactory. In fact, I think I'll do an update for anyone coming later. – erickson May 13 '09 at 21:40
  • Wow. That means of converting PEM to JKS is substantially easier than what I did! Thanks! :) – skiphoppy May 14 '09 at 19:19
  • 1
    All this code does is replicate what you can accomplish by setting three system properties described in the JSSE Refernence Guide. – user207421 May 31 '12 at 19:36
  • 5
    @EJP: This was a while ago, so I don't remember for sure, but I'm guessing the rationale was that in a "large Java application", there are likely to be other HTTP connections established. Setting global properties could interfere with working connections, or allow this one party to spoof servers. This is an example of using the built-in trust manager on a case-by-case basis. – erickson May 31 '12 at 22:26
  • 5
    As the OP said, "my code needs to teach Java to accept this one self-signed certificate, for this one spot in the application, and nowhere else." – erickson May 31 '12 at 22:29
  • I've tried to implement your solution to no avail please help me http://stackoverflow.com/questions/20190364/how-to-bypass-certificateexception-by-java –  Nov 25 '13 at 10:59
  • If you wanted it to be accepted "globally" use keytool to install the certificate as a trusted cert in the cacerts file. Then it will be trusted for any VM started that uses that JRE. – KyleM Dec 06 '13 at 19:59
  • That's why I put "global" in quotes and explained how it actually works. Nobody else mentioned doing it that way although it's a very common and accepted method. – KyleM Dec 06 '13 at 20:07
  • Many large organizations do not accept that method. It would be a security violation with serious consequences. – erickson Dec 06 '13 at 21:09
  • @erickson Hi - it's four years later and the approach you taught us has been humming along nicely and has been in use for several different servers we communicate with. Now I've got a new trick I need to figure out (may make a new StackOverflow question for it): I'd like to trust both the default keystore and the custom keystore with the server's self-signed certificate. We had a case where the server upgraded to a real certificate and the connection broke, but it would have been fine if we were trusting the default keystore. – skiphoppy Feb 13 '14 at 19:25
  • @erickson The API is a bit misleading: it implies you can pass an array of TrustManagers to SSLContext.init(), but the javadoc on that method indicates that only the first TrustManager in the array is used. I'd love to hear any insights you can share. – skiphoppy Feb 13 '14 at 19:26
  • @skiphoppy I'll have to experiment a bit with that and get back to you. I haven't done it before, but I have an idea that might work. – erickson Feb 13 '14 at 20:03
  • 1
    @skiphoppy Hmm, the only way I have found is to create a temporary `KeyStore` (in memory) and add root certs from all of your "real" key stores to that, or (almost equivalently) create a `Set` from all of the entries in your trusted key stores and use it to initialize `PKIXParameters`, and with that, in turn, the `TrustManagerFactory`. – erickson Feb 13 '14 at 21:48
  • Thanks @erickson I am still playing with it and am wandering through a long maze of documentation. I will give this a shot. – skiphoppy Feb 13 '14 at 22:03
  • Setting a leystore has nothing to do with what certificates you will *accept*. It has to do with what you will *send.* Not what was asked for. – user207421 May 19 '21 at 10:35
19

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.

Kalle Richter
  • 8,008
  • 26
  • 77
  • 177
Josh
  • 6,251
  • 2
  • 46
  • 73
  • 2
    Thanks for sharing @Josh. I created a small Github project that demonstrates your code in use: https://github.com/aasaru/ConnectToTrustedServerExample – Master Drools Nov 05 '19 at 11:23
14

If creating a SSLSocketFactory is not an option, just import the key into the JVM

  1. 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-----
    
  2. Import the key: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Password: changeit

  3. Restart JVM

Source: How to solve javax.net.ssl.SSLHandshakeException?

user454322
  • 7,300
  • 5
  • 41
  • 52
  • 1
    I don't think this solves the original question, but it solved *my* problem, so thanks! – Ed Norris May 15 '13 at 15:10
  • 1
    It's also not a great idea to do this: now you have this random extra system wide self signed certificate which *all* Java processes will trust by default. – user268396 Oct 05 '18 at 15:56
12

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.

Mr. Shiny and New 安宇
  • 13,822
  • 6
  • 44
  • 64
12

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.

araqnid
  • 127,052
  • 24
  • 157
  • 134
  • 9
    If you disable all trust verification, there's little point using SSL/TLS in the first place. It's OK for testing locally, but not if you want to connect outside. – Bruno Dec 23 '11 at 13:26
  • I get this exception when running it on Java 7. javax.net.ssl.SSLHandshakeException: no cipher suites in common Can you assist? – Uri Lukach Dec 19 '13 at 12:55