Как настроить SSLContext с использованием сертификата от Azure KeyVault в Java

Я работаю над веб-приложением Java, развернутым в экземпляре службы приложений Azure. И мне нужно позвонить в REST API, который защищен с помощью взаимной аутентификации по SSL. Поскольку это сервис приложений, я не могу позволить себе добавить сертификат и открытый ключ в хранилище ключей и хранилище доверенных сертификатов соответственно, и все это должно быть сделано с помощью кода. Хотя с JCE и SSL мне удалось написать следующее консольное приложение, которое успешно обращается к защищенному API (с помощью других вопросов и ответов Stackru):

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());
        }
    }
}

Вместо загрузки файла PFX в хранилище ключей мне нужно получить сертификат от Azure Keyvault, который уже хранит сертификат. KeyVaultClient (клиентская библиотека Java от Azure) предоставляет мне механизм для получения объекта X509Certificate. Можно ли инициировать хранилище ключей с помощью объекта X509Certificate вместо файла PFX?

Моя цель - сделать повторно используемый объект SSLContext для механизма обработки запросов, чтобы я мог использовать его для вызова внешнего безопасного API, когда мое веб-приложение получает запрос. И мне нужно сделать это, не полагаясь ни на какие файлы и внешние ключи / хранилища JVM-ключей в файловой системе.

07/05/2018: следуйте проницательному предложению от GPI, я вручную построил 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);

Но когда я использую полученный SSLSocketFactory в HTTPS-соединении я получаю следующую ошибку:

sun.security.validator.ValidatorException:
  PKIX path building failed: 
    sun.security.provider.certpath.SunCertPathBuilderException:
      unable to find valid certification path to requested target

2 ответа

KeyVaultClient (клиентская библиотека Java от Azure) предоставляет мне механизм для получения объекта X509Certificate. Можно ли инициировать хранилище ключей с помощью объекта X509Certificate вместо файла PFX?

Да, это. Шаги

1) Загрузите сертификат Azure в Cert объект (вероятно, X509Certificate)
2) Создать новый KeyStore экземпляр (независимо от формата, JKS или же PKCS12)
3) Инициируйте это новое KeyStore позвонив load с нулевым входным потоком это создаст новое, пустое хранилище.
4) Вручную добавьте сертификат Azure в качестве доверенной записи в хранилище ключей с вызовом setCertificateEntry
5) Используйте этот склад ключей в качестве основы вашего TrustManagerFactory

Да, вы можете создать KeyStore из сертификата НО НЕТ, вы не можете использовать его для аутентификации клиента или взаимной аутентификации.

Java использует KeyStore Класс и связанные файлы для хранения (в общем) трех разных, хотя и связанных между собой видов вещей, как подробно описано в Javadoc для класса. Для аутентификации у вас должен быть PrivateKeyEntry, который содержит, как можно предположить из его имени, закрытый ключ, ПЛЮС, по крайней мере, один сертификат и обычно цепочку из нескольких сертификатов. TrustedCertEntry используется для аутентификации других сторон, и в частности другой конечной точки (однорангового узла) соединения SSL/TLS; когда вы являетесь клиентом SSL/TLS, как здесь, TrustedCertEntry используется для аутентификации сервера путем проверки его сертификата. (Третья возможность, SecretKeyEntry, не используется для SSL/TLS и даже не поддерживается типом хранилища ключей PKCS12, как это реализовано в Java и обычно используется.)

С X509Certificate объект, вы можете создать TrustedCertEntry, и код, полученный из GPI, делает это. TrustedCertEntry (в KeyStore) используется только для аутентификации другой стороны, в этой ситуации сервера. Чтобы аутентифицировать себя, клиента, на сервере, вам нужен не просто сертификат, а закрытый ключ и цепочка сертификатов, упакованные как PrivateKeyEntry, из которых вы создаете KeyManager положить в SSLContext и используется JSSE в соответствии с вашим первым кодом.

AFAICS Azure хранилище представляет ключи как com.microsoft.azure.keyvault.webkey.JSONWebKey, который, по-видимому, ограничен RSA или AES. Если RSA, есть два toRSA методы (перегрузки), которые из описания должны создать KeyPair, который я предполагаю, означает java.security.KeyPair, содержащий секретный ключ, который вам нужен, если нет ограничений, не указано, где я смотрел. Я не вижу способа получить цепочку сертификатов напрямую, но, похоже, записи сертификатов имеют ссылки на издателей, которых должно быть достаточно для построения цепочки, хотя я не в состоянии сам это проверить / проверить.

Другие вопросы по тегам