Как использовать аутентификацию сертификата с помощью HttpsURLConnection?

Я пытаюсь подключиться к URL-адресу HTTPS, но мне нужно использовать аутентификацию клиента с сертификатом, размещенным в моей системе сторонним программным обеспечением.

У меня нет ни малейшего представления о том, как я должен либо находить, либо использовать его, и все, что мне нужно, это пример кода С#, который значительно отличается от всех ответов Java, которые я нашел об этом. (например, KeyStore, по-видимому, нуждается в каком-то пароле?)

Это код образца С#, который у меня есть

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;

И затем он просто выполняет итерацию над WS2_store_Certs CertificateCollection и проверяет их все так. Еще немного дальше, он устанавливает такие сертификаты следующим образом:

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

Это выглядит довольно логично, даже если я не знаю, как он находит сертификаты, но я до сих пор не смог найти эквивалент Java.

UPDATE

Соединение, которое я создаю, является частью более крупного приложения, которое зависит от JDK 5, но мне удалось просто использовать банку sunmscapi, чтобы найти сертификат, который я ищу. Это ошибки, когда я пытаюсь подключиться, используя хранилище ключей Windows, хотя я решил, что у меня возникла проблема, получив сертификат, который мне нужен, из хранилища Windows и вставив его в java-версию по умолчанию. Теперь я получаю исключение EOFException, за которым следует SSLHandshakeException, в котором говорится: "Удаленное соединение с узлом удаленного доступа во время рукопожатия". Трассировка отладки ssl не показывает мне непосредственной проблемы, так как необходимый мне сертификат отображается в цепочке сертификатов.

Он делает всю вещь ClientKeyExchange, говорит, что он закончен, а затем последние сообщения, которые я получаю из журнала отладки сразу после этого,

[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

и код, который я использую для настройки соединения,

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

(Я еще не пробовал HttpClient, потому что я понятия не имею, как найти файл сертификата, и я также не уверен, что это будет всегда одинаково для каждой клиентской системы.)

ДРУГОЕ ОБНОВЛЕНИЕ

Я нашел центры сертификации для необходимого мне сертификата в хранилище ключей WINDOWS-ROOT (и проверял с помощью .verify(), чтобы убедиться, что все они проверяют), я добавил их в хранилище ключей java, но все же ничего не меняется. Я предполагаю, что они должны войти в TrustStore, но мне еще предстоит найти способ сделать это программно. (Предпочитаете не полагаться на конечных пользователей, чтобы делать такие вещи, поскольку все, что я могу гарантировать от них, это то, что сертификат и ЦС будут присутствовать из-за программного обеспечения третьей стороны, упомянутого в начале этого смехотворно длинного вопроса.)

ДАЛЕЕ ОБНОВЛЕНИЯ

Добавляя предыдущее обновление, я пришел к выводу, что моя проблема заключается в том, что мои ЦС не находятся в файле Java cacerts, поэтому он получает список доверенных ЦС с сервера, но не делает распознать их и впоследствии не отправлять один сертификат обратно, что приведет к сбою соединения. Таким образом, проблема остается, как я могу заставить Java использовать ее в качестве хранилища доверия или добавлять сертификаты в cacerts программно (без необходимости пути к файлам)? Потому что, если это невозможно, это просто оставляет меня с секретным вариантом C, voodoo. Я начну колоть куклу Duke с помощью игл на всякий случай.

Ответ 1

Оказалось, что это вопрос с закрытым ключом, поскольку он был установлен как не экспортируемый. Поскольку это означало, что я мог получить только закрытый ключ из магазина Windows, я сбежал и "исправил" проблему с большим количеством беспорядков, чтобы получить необходимые классы jdk6, не слишком сильно влияя на остальную часть приложения.

Ответ 2

Хорошо, так что заголовок вопроса: как я могу использовать аутентификацию сертификата с помощью HttpsURLConnection? У меня есть рабочий пример для этого. Для этого он имеет одно предварительное условие:

  • у вас должен быть хранилище ключей, в котором есть файл сертификата. (Я знаю, что ваш сценарий не совсем разрешает это, но, пожалуйста, просто следуйте за мной здесь немного, чтобы немного сократить вашу проблему, потому что это слишком сложно ответить сразу же с места.)

Итак, сначала возьмитесь за файл фактического сертификата. Если вы работаете в Windows 7, это можно сделать с помощью следующих шагов:

  • откройте Internet Explorer,
  • открыть Инструменты (в Internet Explorer 9 это значок cog),
  • нажмите "Свойства обозревателя",
  • перейдите на вкладку "Содержание",
  • нажмите Сертификаты,
  • найдите сертификат под рукой,
  • щелкните по нему и нажмите "Экспорт" и сохраните его в файле (DER закодированный двоичный файл X.509).

(После экспорта удалите его из других сертификатов, убедившись, что Java не будет использовать его так или иначе. Я не знаю, может ли он его использовать, но это не помешает.)суб >

Теперь вам нужно создать файл хранилища ключей и импортировать в него экспортированный сертификат, который может быть выполнен следующим образом.

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

(Очевидно, keytool должен быть на вашем пути к работе. Это часть JDK.)

Он подскажет вам пароль (пароль хранилища ключей, он не должен ничего делать с сертификатом), который я не знаю, как установить в "" прямо сейчас, поэтому установите let it be password или что-то еще.

После этого со следующими шагами вы можете установить безопасное соединение с конечной точкой через прокси.

  • Сначала загрузите файл хранилища ключей.

    InputStream trustStream = new FileInputStream("path/to/<keystore>");
    char[] trustPassword = "<password>".toCharArray();
    
  • Инициализируйте KeyStore.

    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustStore.load(trustStream, trustPassword);
    
  • Инициализировать объекты TrustManager. (Я думаю, что они обрабатывают разрешение сертификата или что-то в этом роде, однако, насколько мне известно, это волшебство.)

    TrustManagerFactory trustFactory =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustFactory.init(trustStore);
    TrustManager[] trustManagers = trustFactory.getTrustManagers();
    
  • Создайте новый SSLContext, загрузите в него объекты TrustManager и установите его как значение по умолчанию. Позаботьтесь, потому что SSLContext.getDefault() возвращает немодифицируемый экземпляр класса (или более похожий по умолчанию, который нельзя повторно инициализировать, но что угодно), поэтому нам нужно использовать SSLContext.getInstance( "SSL" ). Кроме того, не забудьте установить этот новый SSLContext как значение по умолчанию, потому что без этого код переходит в poof.

    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    
  • Создайте свой прокси-сервер и установите для него аутентификацию. (Вместо использования System.setProperty(...) используйте класс Proxy. Ох и не вводите в заблуждение Type.HTTP.)

    Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("<host>", <port>));
    
  • Настройка аутентификации для вашего прокси. (Я использовал бесплатный прокси, который не требовал аутентификации, поэтому я не мог проверить эту часть проблемы прямо сейчас.)

    Authenticator.setDefault(new Authenticator() {
    
      @Override
      protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("<user>", "<password>".toCharArray());
      }
    });
    
  • Подключитесь к своей конечной точке, передав ранее созданный прокси для подключения. (Я использовал один из URL-адресов моей компании, который запрашивает сертификат, сертификат которого я импортировал в своем собственном хранилище ключей, конечно.)

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

Если это не работает (вы получаете ошибки), попробуйте его с помощью HttpsURLConnection.

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

В принципе, функция setAllowUserInteraction срабатывает, если сервер (где находится ресурс, указанный указанным вами URL-адресом) запрашивает учетные данные, не так ли? Теперь я не смог протестировать это сам по себе, но, как я вижу, если вы можете заставить этого ребенка работать с сервером, который не требует аутентификации для доступа к своим ресурсам, тогда вам хорошо идти, потому что сервер будет спрашивать вы должны аутентифицироваться только после того, как соединение уже установлено.

Если после этого вы все равно получите некоторую ошибку, отправьте его.

Ответ 3

Из того, что я помню из моей последней попытки сделать это, вы не можете использовать HttpsURLConnection. Вы можете посмотреть библиотеку Apache HttpClient, которая поддерживает это.

Вот пример кода, дающий представление о процессе:

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

Изменить (относительно комментария Тома):

Вот несколько соображений о том, как вы можете получить сертификаты, хранящиеся в хранилище ключей Windows:

  • Вам нужно использовать набор Sun Cryptography (например, Sun Java 6 JDK).
  • Вы можете получить KeyStore следующим образом: ks = KeyStore.getInstance("Windows-MY");
  • Вы можете загрузить его следующим образом: ks.load(null, null);. JVM загрузит хранилище ключей Windos и позаботится о том, чтобы запросить пароль хранилища ключей.
  • Затем вы можете перемещаться по хранилищу ключей, как и в любом другом хранилище ключей.