Как я могу использовать аутентификацию сертификата с HttpsURLConnection? - PullRequest
12 голосов
/ 01 декабря 2011

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

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

Это пример кода C #, который у меня есть

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, потому что не знаю, как найти файл сертификата, и я также не уверен, будет ли он всегда одинаковым в каждой клиентской системе.)

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

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

ЕЩЕ БОЛЬШЕ ОБНОВЛЕНИЙ

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

Ответы [ 3 ]

11 голосов
/ 06 декабря 2011

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

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

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

  1. открыть Internet Explorer ,
  2. open Tools (в Internet Explorer 9 это значок cog ),
  3. нажмите Свойства обозревателя ,
  4. перейдите на вкладку Content ,
  5. нажмите Сертификаты ,
  6. найти сертификат под рукой,
  7. щелкните по нему и нажмите Экспорт и сохраните его в файл ( DER-кодированный двоичный файл X.509 ).

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

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

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

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

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

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

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

    InputStream trustStream = new FileInputStream("path/to/<keystore>");
    char[] trustPassword = "<password>".toCharArray();
    
  2. Инициализация KeyStore .

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

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

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

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

    Authenticator.setDefault(new Authenticator() {
    
      @Override
      protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("<user>", "<password>".toCharArray());
      }
    });
    
  7. Подключитесь к своей конечной точке, передав ранее созданный прокси-сервер соединению. ( Я использовал один из 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, к которому вы подключаетесь) запрашивает учетные данные, верно?Теперь я не смог протестировать только , что само по себе, но, как я вижу, если вы сможете заставить этого ребенка работать с сервером, который не требует аутентификации для доступа к его ресурсам, тогда вы можете пойти, потому что сервер попросит вас аутентифицировать себя только после соединение уже установлено.

Если после этого вы все еще получаете какую-то ошибку, пожалуйста, опубликуйте ее.

0 голосов
/ 08 декабря 2011

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

0 голосов
/ 01 декабря 2011

Насколько я помню из моей последней попытки сделать это, вы не можете использовать 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)
  • Вы можете получить хранилище ключей следующим образом: ks = KeyStore.getInstance("Windows-MY");
  • Вы можете загрузить его следующим образом: ks.load(null, null);. JVM загрузит хранилище ключей Windos и попросит ввести пароль хранилища ключей.
  • Затем вы можете перемещаться по хранилищу ключей, как и по любому другому хранилищу ключей.
...