14

I'm trying to connect to an HTTPS URL, but I need to use client authentication with a certificate placed on my system by third party software.

I haven't the slightest idea how I'm supposed to either find or use it and all I have to go on is C# sample code, which differs significantly with all the Java answers I've found about this. (for instance, the KeyStore needs some sort of password apparently?)

This is the C# sample code I have

System.Security.Cryptography.X509Certificates.X509CertificateCollection SSC_Certs = 
    new System.Security.Cryptography.X509Certificates.X509CertificateCollection();

Microsoft.Web.Services2.Security.X509.X509CertificateStore WS2_store =
    Microsoft.Web.Services2.Security.X509.X509CertificateStore.CurrentUserStore(
    Microsoft.Web.Services2.Security.X509.X509CertificateStore.MyStore);

WS2_store.OpenRead();

Microsoft.Web.Services2.Security.X509.X509CertificateCollection WS2_store_Certs = WS2_store.Certificates;

And then it just iterates over the WS2_store_Certs CertificateCollection and checks them all that way. A bit further on, it sets the certificates like this:

HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url_string);
httpWebRequest.ClientCertificates = SSC_Certs;

This all looks fairly logical, even if I have no idea how it finds the certificates, but I still haven't been able to find the Java equivalent of this.

UPDATE

The connection I'm making is part of a larger application that depends on JDK 5, but I've managed to just use the sunmscapi jar to find the certificate I'm looking for. It errors when I try to connect using the windows keystore though, so I thought I got around the problem by getting the certificate I need from the windows store and inserting it in the default java one. Now I'm getting an EOFException followed by an SSLHandshakeException saying "Remote host closed connection during handshake". The ssl debug trace doesn't reveal an immediate problem to me, since the certificate I need is displayed in the certificate chain here.

It does the whole ClientKeyExchange thing, says it's finished and then the last messages I get from the debug log right after that are

[write] MD5 and SHA1 hashes:  len = 16
0000: 14 00 00 0C D3 E1 E7 3D   C2 37 2F 41 F9 38 26 CC  .......=.7/A.8&.
Padded plaintext before ENCRYPTION:  len = 32
0000: 14 00 00 0C D3 E1 E7 3D   C2 37 2F 41 F9 38 26 CC  .......=.7/A.8&.
0010: CB 10 05 A1 3D C3 13 1C   EC 39 ED 93 79 9E 4D B0  ....=....9..y.M.
AWT-EventQueue-1, WRITE: TLSv1 Handshake, length = 32
[Raw write]: length = 37
0000: 16 03 01 00 20 06 B1 D8   8F 9B 70 92 F4 AD 0D 91  .... .....p.....
0010: 25 9C 7D 3E 65 C1 8C A7   F7 DA 09 C0 84 FF F4 4A  %..>e..........J
0020: CE FD 4D 65 8D                                     ..Me.
AWT-EventQueue-1, received EOFException: error

and the code I'm using to set up the connection is

KeyStore jks = KeyStore.getInstance(KeyStore.getDefaultType());
jks.load(null, null);

KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
jks.setCertificateEntry("alias", cert1); //X509Certificate obtained from windows keystore
kmf.init(jks, new char[0]);

SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(kmf.getKeyManagers(), new TrustManager[]{tm}, null);

sslsocketfactory = sslContext.getSocketFactory();
System.setProperty("https.proxyHost", proxyurl);
System.setProperty("https.proxyPort", proxyport);

Authenticator.setDefault(new MyAuthenticator("proxyID", "proxyPassword"));
URL url = new URL(null, urlStr, new sun.net.www.protocol.https.Handler());
HttpsURLConnection uc = (HttpsURLConnection) url.openConnection();
uc.setSSLSocketFactory(sslsocketfactory);
uc.setAllowUserInteraction(true);
uc.setRequestMethod("POST");
uc.connect();

(I haven't tried HttpClient yet because I have no idea how to find the certificate file and I'm also not sure if this will always be the same on every client system.)

ANOTHER UPDATE

I've found the CAs for the certificate I need in the WINDOWS-ROOT keystore (and checked with .verify() to see that they all check out), I've added them to the java keystore as well but still nothing changes. I guess they're supposed to go in the TrustStore, but I have yet to find a way to do that programatically. (Would prefer not to rely on end users to do this kind of thing, as all I can guarantee from them is that the certificate and CAs will be present due to the third party software mentioned at the start of this ridiculously long question.)

YET MORE UPDATES

Adding on the previous update, I've come to the conclusion that my problem must lie in the fact that my CAs are not in Java's cacerts file, so it gets the list of trusted CAs from the server, but doesn't recognise them and subsequently doesn't send a single certificate back causing the connection failure. So the problem remains, how do I get Java to either use it's keystore as truststore or add certificates to cacerts programmatically (without the need for file paths)? Because if those aren't possible that just leaves me with secret option C, voodoo. I'll start stabbing a Duke doll with needles, just in case.

Valyrion
  • 2,342
  • 9
  • 29
  • 60
  • Is your goal here to use the certificates in the user's Windows keystore? I don't think there's a way of doing this in Java, but you can create a separate keystore using keytool. – Tom Elliott Dec 01 '11 at 09:54

3 Answers3

14

Okay, so your question's title is How can I use certificate authentication with HttpsURLConnection? I have a working example for that. For it to work it has one prerequisite:

  1. you have to have a key store which has your certificate file in it. (I know that your scenario not exactly permits this, but please just follow me here a bit so that we can narrow your problem down a little bit, because it's a bit too complicated to answer right off the bat.)

So, first get your hand on the actual certificate file. If you're on Windows 7 this can be done by the following steps:

  1. open Internet Explorer,
  2. open Tools (in Internet Explorer 9 it is the cog icon),
  3. click Internet Options,
  4. go to the Content tab,
  5. click Certificates,
  6. find the certificate at hand,
  7. click on it and click Export and save it to a file (DER encoded binary X.509).

(After exporting it delete it from among the other certificates, making sure Java won't use it one way or another. I don't know if it can use it, but it couldn't hurt.)

Now, you have to create a key store file and import the exported certificate into it, which can be done by the following.

> keytool -importcert -file <certificate> -keystore <keystore> -alias <alias>

(Obviously, keytool has to be on your path to work. It's part of the JDK.)

It'll prompt you for a password (the key store's password; it doesn't have to do anything with the certificate), which I don't know how to set to "" right now, so set let it be password or whatever.

After this, with the following steps you can establish a secure connection to your endpoint via a proxy.

  1. First, load the key store file.

    InputStream trustStream = new FileInputStream("path/to/<keystore>");
    char[] trustPassword = "<password>".toCharArray();
    
  2. Initialize a KeyStore.

    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustStore.load(trustStream, trustPassword);
    
  3. Initialize TrustManager objects. (I think these handle certificate resolution or something like that, however as far as I'm concerned this is magic.)

    TrustManagerFactory trustFactory =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustFactory.init(trustStore);
    TrustManager[] trustManagers = trustFactory.getTrustManagers();
    
  4. Create a new SSLContext, load the TrustManager objects into it and set it as default. Take care, because SSLContext.getDefault() returns a non-modifiable instance of the class (or more like the default one can't be re-initialized, but whatever), that's why we have to use SSLContext.getInstance("SSL"). Also, don't forget to set this new SSLContext as the default, because without that the code goes poof.

    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    
  5. Create your proxy and setup authentication for it. (Instead of using System.setProperty(...) use the Proxy class. Oh and don't be mislead by Type.HTTP.)

    Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("<host>", <port>));
    
  6. Setup authentication for your proxy. (I've used a free proxy which didn't require authentication so I couldn't test that part of the problem right now.)

    Authenticator.setDefault(new Authenticator() {
    
      @Override
      protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("<user>", "<password>".toCharArray());
      }
    });
    
  7. Connect to your end point by passing the previously created proxy to the connection. (I've used one of my company's service's URL, which asks for a certificate—which certificate I imported in my own key store of course.)

    URL url = new URL("<endpoint>");
    URLConnection connection = url.openConnection(proxy);
    connection.connect();
    

If it doesn't work like this (you get errors) then try it with HttpsURLConnection.

   HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
   httpsConnection.setAllowUserInteraction(true);
   httpsConnection.setRequestMethod("POST");

Basically, the setAllowUserInteraction thingy kicks in if the server (where the resource pointed by the URL you connect to is located) asks for credentials, right? Now, I couldn't test just that per se, but as I see if you can get this baby working with a server that doesn't require authentication to access its resources, then you're good to go, because the server will ask you to authenticate yourself only after the connection is already established.

If after these you still receive some error, then please post it.

Kohányi Róbert
  • 9,791
  • 4
  • 52
  • 81
  • Thank you for your detailed reply. I've done what you said, but when I just export and register the requested certificate, it says `PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target`. If I also add the CA certificates (either by exporting the ones I find through IE or programmatically through the method used here [link](http://nodsw.com/blog/leeland/2006/12/06-no-more-unable-find-valid-certification-path-requested-target)) I get the generic `Remote host closed connection during handshake` again. – Valyrion Dec 07 '11 at 10:33
  • 1
    OK. The first exception is probably thrown, because by itself the certificate can't be validate (it doesn't *leads up* to valid CA or something like that). The *Remote host closed ...* thingy could be related to the fact that you are using a proxy which doesn't allow you to establish a secure connection through it. I've written that *Type.HTTP* is misleading ... maybe it isn't at all. [I've found the following related question/answer](http://stackoverflow.com/a/1512268/433835), take a look at that, I think it'll help. Basically it says, that you can't reach an HTTPS resource via an HTTP proxy. – Kohányi Róbert Dec 07 '11 at 10:52
  • 1
    @Slyder I've ran out of characters, but I've wanted to ask you this: can you test the connection without a proxy? – Kohányi Róbert Dec 07 '11 at 11:19
  • I came across that but didn't really follow up on it because I do get a response if I leave everything as it is, but just connect to a different https url that _doesn't_ require a certificate. So I don't think my problem is the proxy (unless it somehow interferes with certificates, which would be strange since I can connect to the URL I need through a browser or through the C# program I mentioned in my original question) Edit - I get a Hostname not found exception if I don't use the proxy – Valyrion Dec 07 '11 at 11:30
  • 1
    *I don't think my problem is the proxy (unless it somehow interferes with certificates* If you connect to your proxy's HTTP port, I think it can cause a problem. You establish a single *line* to a resource. The line consists of two *parts*: `you -> proxy` and `proxy -> resource`. Both parts have to be secured. *I can connect to the URL I need through a browser or through the C# program I mentioned* Do you use proxy settings in your browser and/or in the C# program? – Kohányi Róbert Dec 07 '11 at 11:52
  • The browser gets its proxy settings set through a script, as far as I can see it doesn't distinguish between secure or normal connections and I don't see any proxy settings being explicitly defined in the C# code, so I would imagine it just gets the proxy info from windows. You've been a big help already and I'm just going to give you the bounty anyway, while I try to figure out if this proxy has a working https port or something like that. – Valyrion Dec 07 '11 at 12:10
  • @Slyder C# probably uses whatever proxy settings are set in Internet Explorer, so *these* settings are good. Now, I've found something interesting. Check out _[this](http://www.java.com/en/download/help/proxy_setup.xml)_ link. It says Java uses your browser's default settings for networking. This probably applies to the system's default JRE, so if you run your code against a JDK then that can stir up things. Try to tune these settings (specify your proxy explicitly) and make sure you run your code on the default JRE used by your system. I hope it'll help! – Kohányi Róbert Dec 07 '11 at 12:52
0

From what I remember from my last attempt of doing that, you can't use HttpsURLConnection. You can have a look to the Apache HttpClient library that has support for this.

Here is a code sample giving an idea of the process:

String server = "example.com";
int port = 443;
EasySSLProtocolSocketFactory psf = new EasySSLProtocolSocketFactory();
InputStream is = readFile("/path/to/certificate");
KeyMaterial km = new KeyMaterial(is, "certpasswd".toCharArray());
easy.setKeyMaterial(km);

Protocol proto = new Protocol("https", (ProtocolSocketFactory) psf, port);
HttpClient httpclient = new HttpClient();
httpclient.getHostConfiguration().setHost(server, port, proto);

Edit (regarding Tom comment):

Here are some thoughts on how you can obtain the certificates stored in the Windows key store:

  • You need to use the Sun Cryptography suite (ie, a Sun Java 6 JDK)
  • You can obtain the KeyStore like this: ks = KeyStore.getInstance("Windows-MY");
  • You can load it this way: ks.load(null, null);. The JVM will load the Windos keystore and take care of asking for the keystore password.
  • You can then navigate the keystore like any other keystore.
Vivien Barousse
  • 20,555
  • 2
  • 63
  • 64
  • Wrong answer.The `ks.load(null,null)` does not load the Windows keystore.You can not load the windows keystore from java – Cratylus Dec 01 '11 at 10:44
  • @user384706: Yes, you can. I invite you to check this project I worked on, that load the Windows keystore and lets you access all the certificates in it: https://adullact.net/projects/libersign/ – Vivien Barousse Dec 01 '11 at 10:55
  • 1
    @user384706: Oh, and here is the Sun (Oracle) documentation about it. Just in case you want to check it out: http://java.sun.com/developer/technicalArticles/J2SE/security/#1 – Vivien Barousse Dec 01 '11 at 10:57
  • `ks.load(null,null)` only initializes and empty in memory Keystore object.It does not load the Windows keystore! – Cratylus Dec 01 '11 at 11:03
  • Where do you say in your answer that the OP has to use a specific provider? You don't mention this. – Cratylus Dec 01 '11 at 11:05
  • I don't know france do I can't check out your project.If you have a specific link I can read please send it to me – Cratylus Dec 01 '11 at 11:06
  • 1
    `KeyStore.getInstance("Windows-MY")` -> This line uses a specific procider. The `Windows-MY` providerm in taht case. And I sent you a link earlier, in a second comment. – Vivien Barousse Dec 01 '11 at 11:08
0

Turned out to be a Private Key issue since it was set as not exportable. As this meant I could only get the private key from the windows store, I caved and "Fixed" the issue with a lot of messing around to get the necessary jdk6 classes working without influencing the rest of the application too much.

Valyrion
  • 2,342
  • 9
  • 29
  • 60
  • What kind of _private key issue_? You've talked about a _certificate_ (presumably the server's where you've tried to connect). A certificate _doesn't_ contain private key. Using SSL your peer sends you its *public key* which by you can authenticate it using your trusted copy of a root CAs public key. If your peer's public key isn't signed by a root CA or you don't have that root CA's public key who signed it, then you'll need to import its certificate into your trusted key store to _trust_ it. SSL uses secret keys to encrypt bulk data transfer, but you shouldn't need to bother with _that_. – Kohányi Róbert Dec 22 '11 at 11:24
  • When accessing the certificate when it's in the Windows keystore I get prompted for a password, this didn't happen when I exported it to the Java keystore and accessed it there. The only thing I noticed that looked like it could make a difference was that when exporting the certificate through IE it said "private key not exportable" – Valyrion Dec 22 '11 at 15:14
  • It looks like your certificate contains the public/private key pair. I assume some CAs hand out certificates like this to people not already owning a key pair ... interesting, however also absurd as a _certificate_ should never contain a private key. It should be extracted. I guess, on one hand there is the concept of certificates and on the other there are a bunch of half-assed certificates file format which could contain almost anything. Its sad. Thanks for the follow-up though. – Kohányi Róbert Dec 22 '11 at 15:58