How do I set up a SSLContext using certificate from Azure KeyVault in Java
Asked Answered
T

2

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
Thao answered 3/7, 2018 at 14:16 Comment(1)
just came across this thread. I am trying to do something similar so wanted to know were you able to resolve this?Centillion
P
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

Porbeagle answered 4/7, 2018 at 9:39 Comment(2)
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.Thao
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 : docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/…Porbeagle
A
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.

Abubekr answered 5/7, 2018 at 11:39 Comment(6)
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?Thao
(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 security.stackexchange.com/a/20847/39571 under 'Full Handshake'. ...Abubekr
... (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.Abubekr
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?Thao
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.Abubekr
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!Thao

© 2022 - 2024 — McMap. All rights reserved.