2

I am working on a Java web application deployed on an Azure App Service instance. And I need to make a call to a REST API that is secured by requiring mutual authentication over SSL. Since this is an app service, I don't have the luxury of adding the certificate and public key to the keystore and truststore respectively, and it has to all be done via code. Although with JCE and SSL, I managed to write the following console application that accesses the secure API successfully (with the help of other StackOverflow Q&A):

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

public class TestPFOM {

    public static void main(String[] args) throws KeyStoreException, NoSuchAlgorithmException, CertificateException,
            IOException, UnrecoverableKeyException, KeyManagementException {

        System.out.println("Start test for mutual authentication");
        KeyStore ks = KeyStore.getInstance("PKCS12");
        File file = new File(System.getProperty("user.dir") + "/client.company.com.pfx");
        System.out.println("Loaded PKCS12 from file");
        try (FileInputStream fis = new FileInputStream(file)) {

            ks.load(fis, "password".toCharArray());
            System.out.println("Loaded keys into keystore");
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(ks, "password".toCharArray());
            System.out.println("Initialized KeyStoreManager");
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(kmf.getKeyManagers(), null, new SecureRandom());
            System.out.println("initialized SSLContext");
            SSLSocketFactory factory = sc.getSocketFactory();
            System.out.println("Obtained SSLSocketFactory");

            URL url = new URL("https://services.company.com/api/company_data");
            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
            System.out.println("Opened secure HTTPS connection");
            connection.setSSLSocketFactory(factory);
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Accept", "application/json");
            StringBuilder stringBuilder = new StringBuilder();
            int responseCode = connection.getResponseCode();
            System.out.println("HTTP response code = " + responseCode);
            try (BufferedReader reader = responseCode == 200
                    ? new BufferedReader(new InputStreamReader(connection.getInputStream()))
                    : new BufferedReader(new InputStreamReader(connection.getErrorStream()))) {
                String line = null;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                    stringBuilder.append(line);
                }
            } catch (Exception e) {
                System.err.println("Error: " + e.getMessage());
            }
            System.out.println(stringBuilder.toString());
        } catch (Exception ex) {
            System.err.println("Error: " + ex.getMessage());
        }
    }
}

Instead of loading the PFX file into the KeyStore, I need to get the certificate from Azure Keyvault which already stores the certificate. The KeyVaultClient (Java client library from Azure) provides me with a mechanism to obtain an X509Certificate object. Is it possible to initiate a KeyStore with a X509Certificate object, instead of from a PFX file?

My goal is to have a reusable SSLContext object available to the request processing mechanism, so I can use it to call the external, secure API when my web application receives a request. And I need to do this without relying on any files and external JVM key/trust stores in the filesystem.

07/05/2018: Follow up to insightful suggestion from GPI I manually built the SSLContext:

KeyStore keyStore = KeyStore.getInstance("PKCS12");
// Initiate and load empty key store
keyStore.load(null, null);
// clientCert is an X509Certificate object
keyStore.setCertificateEntry("clientCert", clientCert);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // PKIX
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

But when I use the resulting SSLSocketFactory in the HTTPS connection, I get the following error:

sun.security.validator.ValidatorException:
  PKIX path building failed: 
    sun.security.provider.certpath.SunCertPathBuilderException:
      unable to find valid certification path to requested target
Web User
  • 7,438
  • 14
  • 64
  • 92

2 Answers2

2

The KeyVaultClient (Java client library from Azure) provides me with a mechanism to obtain an X509Certificate object. Is it possible to initiate a KeyStore with a X509Certificate object, instead of from a PFX file?

Yes it is. The steps are

1) Load the Azure certificate in a Cert object (probably a X509Certificate)
2) Create a new KeyStore instance (whatever the format, JKS or PKCS12)
3) Init this new KeyStore by calling load with a null input stream, this will make a new, empty store.
4) Manually add the Azure certificate as a trusted entry in the KeyStore with a call to setCertificateEntry
5) Use this keystore as the base of your TrustManagerFactory

GPI
  • 9,088
  • 2
  • 31
  • 38
  • I tried the steps you suggested and run into an exception which I have added to the end of my question above. Waiting for your comment. – Web User Jul 05 '18 at 05:10
  • 1
    The error means the certificate you added is not the one the TLS server advertises (nor its "issuer"). Which means either you got the wrong one from the Azure store, or you are mis-using the API somehow. That seems to be Azure an specific topic though. You could try debugging the TLS session by following this : https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/ReadDebug.html – GPI Jul 05 '18 at 07:59
1

YES you can create a KeyStore from a cert BUT NO you cannot use it for client auth aka mutual auth.

Java uses the KeyStore class and related files to store (in general) three different though related kinds of things as detailed in Javadoc for the class. To authenticate yourself, you must have a PrivateKeyEntry which contains, as its name might suggest, a private key, PLUS at least one certificate and usually a chain of multiple certificates. A TrustedCertEntry is used to authenticate other parties, and in particular the other endpoint (peer) of an SSL/TLS connection; when you are the SSL/TLS client, as here, a TrustedCertEntry is used to authenticate the server by validating the server's cert. (The third possibility, SecretKeyEntry, is not used for SSL/TLS, and not even supported by PKCS12 keystore type as implemented by Java and commonly used.)

With an X509Certificate object, you can create a TrustedCertEntry, and the code you got from GPI does so. A TrustedCertEntry (in a KeyStore) is only usable to authenticate the other party, in this situation the server. To authenticate yourself, the client, to the server, you need not simply a certificate but a private key and certificate chain, packaged as a PrivateKeyEntry, from which you create a KeyManager put in the SSLContext and used by JSSE, as per your first code.

AFAICS Azure vault represents keys as com.microsoft.azure.keyvault.webkey.JSONWebKey, which appears to be limited to RSA or AES. If RSA, there are two toRSA methods (overloads) that from the description should produce a KeyPair, which I presume means java.security.KeyPair, containing the private key you need, unless there are limitations not stated where I looked. I don't see any way to get a certificate chain directly, but it appears certificate entries have issuer links, which should be sufficient for you to build the chain, although I'm not in a position to test/verify this myself.

dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70
  • You pointed out what I am missing, although I have not yet figured out how to built a `PrivateKeyEntry` using the private key and certificate chain. I have a fundamental question. For mutual authentication to work, the client presents its certificate that is in the server's trust store. If I was provided with a PFX file with Usage tagged as `Encrypt,Verify,Wrap,Derive`, I think the cert is used not just to verify the identity but also encrypt whatever it sends. The server cert is also has the same purpose. Does the PFX contain the server's public key, or is it the client public key? – Web User Jul 06 '18 at 00:34
  • 1
    (1) `KeyStore.setKeyEntry` takes a PrivateKey (as Key) and password and cert chain (array) (2) actually SSL/TLS client presents a cert that _validates against_ the server's truststore; this can be because the (specific) cert is trusted but usually and better the cert is issued (usually indirectly) by a _CA_ that is trusted (3) client cert in SSL/TLS is never used to encrypt, only to sign; see the epic https://security.stackexchange.com/a/20847/39571 under 'Full Handshake'. ... – dave_thompson_085 Jul 06 '18 at 02:26
  • 1
    ... (4) Java ignores any 'usage' specified in PFX/PKCS12 format, plus it may not survive going through Azure vault. KeyUsage and if present ExtendedKeyUsage extensions in the _cert_ do matter, and those must allow digSign and (ssl)clientAuth respectively; server is different and too complicated for this comment (5) PFX used by client contains the client's _private_ key and client's cert chain which in turn contains the client's public key; if server uses a PFX (it need not) it similarly contains server's key and server's cert chain. – dave_thompson_085 Jul 06 '18 at 02:31
  • I need just the `KeyManager` in the SSLContext init method right? My first code loads the `KeyStore` from the PFX file, essentially creating a `TrustedCertEntry` as well as a `PrivateKeyEntry` if I am understanding this correctly. I am trying to build the `KeyStore` by hand, starting with an empty instance. I retrieve the certificate from Azure KeyVault and add it as a `TrustedCertEntry` in the `KeyStore`. Then I get the `KeyBundle` from KeyVault. The object has a `key()` method yielding the `JsonWebKey`. But `key.hasPrivateKey()` returns false. How do I create the `PrivateKeyEntry` then? – Web User Jul 07 '18 at 07:52
  • 1
    SSLContext.init with only keymanagers (null for trustmanagers) is fine _if_ the server cert chain validates against your default truststore, which depends on both of those. If you are limited to a key from the vault and it doesn't have privatekey, you're out of luck. Since every keypair starts out with a privatekey, getting it may depend on how you got the key into the vault and/or how you got it out, and I'm not familiar with details of either. Creating a TrustedCertEntry for your _own_ cert is useless, and as I said you usually need the chain (not just one), in addition to the privatekey. – dave_thompson_085 Jul 08 '18 at 03:20
  • When I import a PFX in Azure KeyVault, as part of storing a certificate, KeyVault creates a corresponding Key (`JsonWebKey`) and Secret. I tried extracting the `KeyPair` from the Key, and that's where I don't see a private key. Azure documentation is sparse in this specific topic, with examples limited to .NET. For now, I will create the `SSLSocketFactory` from the PFX file; the poor compromise is that the PFX is part of the application code. If I figure out a solution, I will certainly update this SO thread. Thanks for staying in the loop and providing insightful inputs! – Web User Jul 08 '18 at 04:53