Повторное согласование SSL с сертификатом клиента вызывает переполнение буфера сервера

Я написал клиентское приложение Java, которое подключается к веб-серверу Apache через HTTPS с использованием клиентского сертификата и выполняет HTTP-загрузку файла на сервер. С маленькими файлами работает нормально, а с большими вылетает.

Журнал сервера Apache показывает следующее:

...
OpenSSL: Handshake: done
...
Changed client verification type will force renegotiation
...
filling buffer, max size 131072 bytes
...
request body exceeds maximum size (131072) for SSL buffer
could not buffer message body to allow SSL renegotiation to proceed
...    
OpenSSL: I/O error, 5 bytes expected to read on BIO
(104)Connection reset by peer: SSL input filter read failed.
(32)Broken pipe: core_output_filter: writing data to the network
Connection closed to child 20 with standard shutdown

Ответ на клиенте:

java.io.IOException: Server returned HTTP response code: 401 for URL

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

        URL url = new URL("my url goes here");
        con = (HttpsURLConnection) url.openConnection();
        con.setSSLSocketFactory(getMyCustomClientCertSocketFactory());
        con.setRequestMethod("PUT");
        con.setDoOutput(true);
        con.connect();
        writer = new OutputStreamWriter(con.getOutputStream());
        writer.write(xml);
        writer.close();

        parseServerResponse(con.getInputStream());

Я думаю, может быть, мне нужно использовать API более низкого уровня, например SSLSocket, и использовать HandshakeCompletedListener?

Мне также интересно, имеет ли директива Apache SSLVerifyDepth какое-либо отношение к тому, почему происходит повторное согласование. У меня есть директива в контексте для каждого каталога (только один каталог загрузки) со значением 2, и в руководстве Apache говорится об этом:

В контексте для каждого каталога он вызывает повторное согласование SSL с перенастроенной глубиной проверки клиента после того, как HTTP-запрос был прочитан, но до отправки HTTP-ответа.

По запросу вот вывод отладки Java:

keyStore is : 
keyStore type is : jks
keyStore provider is : 
init keystore
init keymanager of type SunX509
trustStore is: C:\Program Files\Java\jdk1.6.0_35\jre\lib\security\cacerts
trustStore type is : jks
trustStore provider is : 
init truststore
adding as trusted cert:
 ...
trigger seeding of SecureRandom
done seeding SecureRandom
***
found key for : key-alias
chain [0] = [
[
...
]
***
trigger seeding of SecureRandom
done seeding SecureRandom
Allow unsafe renegotiation: false
Allow legacy hello messages: true
Is initial handshake: true
Is secure renegotiation: false
%% No cached client session
*** ClientHello, TLSv1
RandomCookie:  ...
Session ID:  {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods:  { 0 }
***
main, WRITE: TLSv1 Handshake, length = 75
main, WRITE: SSLv2 client hello message, length = 101
main, READ: TLSv1 Handshake, length = 81
*** ServerHello, TLSv1
RandomCookie:  ...
Session ID:  ...
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA
Compression Method: 0
Extension renegotiation_info, renegotiated_connection: <empty>
***
%% Created:  [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
** TLS_RSA_WITH_AES_128_CBC_SHA
main, READ: TLSv1 Handshake, length = 4392
*** Certificate chain
chain [0] = [
[
...
Certificate Extensions: 8
[1]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
  [
   accessMethod: ...
   accessLocation: URIName: ...
   accessMethod: ...
   accessLocation: URIName: ...
]

[2]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
...
]
]
[3]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
  CA:false
  PathLen: undefined
]
[4]: ObjectId: 2.5.29.31 Criticality=false
CRLDistributionPoints [
  [DistributionPoint:
     [URIName: ...
]]
[5]: ObjectId: 2.5.29.32 Criticality=false
CertificatePolicies [
  [CertificatePolicyId: ...
[PolicyQualifierInfo: [
  qualifierID: ...
  qualifier: ...
]]  ]
]
[6]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
  serverAuth
  clientAuth
]
[7]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
  DigitalSignature
  Key_Encipherment
]
[8]: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
  DNSName: ...
]
]
  Algorithm: [SHA1withRSA]
  Signature:
...
]
...
***
main, READ: TLSv1 Handshake, length = 4
*** ServerHelloDone
*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
main, WRITE: TLSv1 Handshake, length = 518
SESSION KEYGEN:
PreMaster Secret:
...
CONNECTION KEYGEN:
Client Nonce:
...
Server Nonce:
...
Master Secret:
...
Client MAC write Secret:
...
Server MAC write Secret:
...
Client write key:
...
Server write key:
...
Client write IV:
...
Server write IV:
...
main, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished
verify_data:  { 18, 162, 18, 251, 82, 111, 87, 133, 53, 240, 114, 155 }
***
main, WRITE: TLSv1 Handshake, length = 48
main, READ: TLSv1 Change Cipher Spec, length = 1
main, READ: TLSv1 Handshake, length = 48
*** Finished
verify_data:  { 46, 206, 8, 40, 63, 252, 99, 190, 251, 183, 110, 201 }
***
%% Cached client session: [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
main, WRITE: TLSv1 Application Data, length = 256
main, WRITE: TLSv1 Application Data, length = 32
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
...
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 512
main, READ: TLSv1 Application Data, length = 304 

В соответствии с запросом здесь приведен источник getMyCustomClientCertSocketFactory (получает сертификат и ключ из файла PEM):

public static SSLSocketFactory getMyCustomClientCertSocketFactory(String pemPath,
        boolean verifyPeer)
        throws NoSuchAlgorithmException, FileNotFoundException, IOException,
        KeyStoreException, CertificateException, UnrecoverableKeyException,
        KeyManagementException, InvalidKeySpecException {
    SSLContext context = SSLContext.getInstance("TLS");

    byte[] certAndKey = IOUtil.fileToBytes(new File(pemPath));
    byte[] certBytes = parseDERFromPEM(certAndKey,
            "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
    byte[] keyBytes = parseDERFromPEM(certAndKey,
            "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");

    X509Certificate cert = generateX509CertificateFromDER(certBytes);
    RSAPrivateKey key = generateRSAPrivateKeyFromDER(keyBytes);

    KeyStore keystore = KeyStore.getInstance("JKS");
    keystore.load(null);
    keystore.setCertificateEntry("cert-alias", cert);
    keystore.setKeyEntry("key-alias", key, "changeit".toCharArray(),
            new Certificate[]{cert});

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(keystore, "changeit".toCharArray());

    KeyManager[] km = kmf.getKeyManagers();

    TrustManager[] tm = null;

    if (!verifyPeer) {
        tm = new TrustManager[]{new TrustyTrustManager()};
    }

    context.init(km, tm, null);

    return context.getSocketFactory();
}

person Ryan    schedule 11.01.2013    source источник
comment
Если я использую утилиту UNIX 'curl', я могу передавать большие файлы без происшествий, поэтому мне интересно, что она делает по-другому...   -  person Ryan    schedule 11.01.2013
comment
Вот отчет об ошибке, в котором обсуждается проблема: bugzilla.redhat.com/show_bug.cgi? идентификатор=491763. Выдача HTTP GET (или HEAD или OPTIONS) в каталоге загрузки до PUT, похоже, не работает при использовании HttpsUrlConnections - я полагаю, что поддержка активности не соблюдается?   -  person Ryan    schedule 12.01.2013
comment
Запустите свой клиент с -Djavax.net.debug=ssl,handshake и опубликуйте результат в своем ответе.   -  person user207421    schedule 12.01.2013
comment
Вывод отладки слишком подробен, чтобы его можно было вставить в комментарий, но в целом похоже, что данные приложения отправляются еще до того, как сервер запрашивает повторное согласование, и к тому времени уже слишком поздно, потому что сокет закрывается. Есть ли способ надежно предвидеть повторные переговоры? Что делает керлинг?   -  person Ryan    schedule 15.01.2013
comment
Отредактируйте его в своем вопросе. Пока вы этого не сделаете, никто не сможет помочь.   -  person user207421    schedule 16.01.2013
comment
Эта проблема, как вы правильно определили, находится на стороне Apache. Изменение вещей в конце Java не будет иметь никакого эффекта.   -  person user207421    schedule 19.01.2013
comment
Имейте в виду, что когда я загружаю огромный файл в каталог, защищенный сертификатом клиента, с помощью утилиты curl UNIX, он отлично работает. Следовательно, есть что-то, что клиент может сделать, чтобы предотвратить переполнение сервера, а мой Java-клиент этого не делает.   -  person Ryan    schedule 22.01.2013


Ответы (3)


Может показаться, что средство HttpsUrlConnection, встроенное в Sun Java, не может обработать большой HTTP PUT с клиентским сертификатом в удобном для сервера виде (т. е. без переполнения серверного буфера повторного согласования SSL).

Я проверил, что делает curl, чтобы понять, что означает «дружественный к серверу», и оказалось, что есть заголовок HTTP 1.1 с именем «Expect», который curl отправляет со значением «100-continue» (см. спецификацию http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20). Этот заголовок, по сути, говорит: «У меня огромная полезная нагрузка, но прежде чем я отправлю ее, дайте мне знать, сможете ли вы с ней справиться». Это дает конечным точкам время для повторного согласования сертификата клиента перед отправкой полезной нагрузки.

В реализации Sun HttpUrlConnection кажется, что этот заголовок не разрешен и фактически находится в списке запрещенных заголовков; это означает, что даже если вы установите его с помощью метода HttpUrlConnection.setRequestProperty, заголовок фактически не отправляется на сервер. Вы можете переопределить заголовки с ограниченным доступом с помощью системного свойства sun.net.http.allowRestrictedHeaders, но тогда клиент просто вылетит из-за исключения сокета, поскольку реализация Sun не знает, как обрабатывать эту часть протокола.

Интересно, что реализация Java в OpenJDK поддерживает этот заголовок. Кроме того, этот заголовок поддерживается библиотекой HTTP-клиента Apache (http://hc.apache.org/); Я реализовал тестовую программу с клиентской библиотекой HTTP Apache, и она может успешно выполнять HTTP-запрос PUT для большого файла с использованием сертификата клиента и заголовка Expect.

Напомним, решения:

  1. Задайте для директивы Apache SSLRenegBufferSize большое число (например, 64 МБ). По умолчанию 128K. Это решение может создать риск отказа в обслуживании
  2. Настройте хост, который всегда требует клиентских сертификатов, в отличие от хоста, на котором они требуются только для нескольких каталогов. Это позволит избежать повторных переговоров. Это не лучший вариант в моем сценарии, потому что большинство пользователей анонимны или аутентифицированы по имени пользователя и паролю. Существует только один каталог загрузки для программной загрузки файлов. Нам пришлось бы создать новый виртуальный хост с собственным SSL-сертификатом только для этого каталога.
  3. Используйте клиент, который поддерживает заголовок HTTP 1.1 Expect. К сожалению, Sun Java не поддерживает это из коробки. Необходимо использовать третью сторону, такую ​​как клиентская библиотека компонентов HTTP Apache, или свернуть собственное решение с использованием API сокетов Java.
  4. Используйте постоянные соединения HTTP 1.1 (конвейерная обработка с поддержкой активности), сначала отправляя HTTP-запрос, который не имеет большой полезной нагрузки, но вызывает повторное согласование, а затем повторно использует соединение для HTTP PUT. Теоретически клиент должен иметь возможность выдавать HTTP HEAD или OPTIONS в каталоге загрузки, а затем повторно использовать то же соединение для выполнения PUT. Чтобы это работало, пул постоянных соединений, вероятно, должен содержать только одно соединение, чтобы избежать «заполнения» одного соединения, а затем выдачи другого для PUT. Однако не похоже, что класс HttpUrlConnection будет сохранять/повторно использовать постоянные соединения с использованием клиентских сертификатов или SSL, потому что мне не удалось заставить это решение работать. См. (HttpsUrlConnection и keep-alive).
person Ryan    schedule 13.03.2013
comment
Похоже, Java 7 действительно поддерживает заголовок Expect. В моем тестировании он работал в сочетании с ChunkedStreamingMode. Итак, появились две новые строки кода: con.setChunkedStreamingMode(0); и con.setRequestProperty(Ожидать, 100-Продолжить); - person Ryan; 20.03.2014

java.io.IOException: сервер вернул код ответа HTTP: 401 для URL

Это ошибка приложения. Это не вызвано уровнем SSL. Я не уверен, почему вы получаете 401 Unauthorized для больших файлов, но вы также опускаете то, что getMyCustomClientCertSocketFactory()
Также вы пробовали другой метод, например. POST? У вас была такая же проблема?

person Cratylus    schedule 16.01.2013
comment
Да, Apache отправляет 401, когда его буфер переполняется во время повторного согласования. Использование GET, POST, PUT и т. д. не имеет значения, если запрос большой — проблема в том, что буфер сервера переполняется, если данные приложения больше, чем может вместить буфер. И да, я могу поднять буфер и проблема исчезнет, ​​но это уязвимость для отказа в обслуживании, а не идеал. Я не предоставил источник для getMyCustomClientCertSocketFactory, потому что он связан только косвенно — чтобы использовать клиентский сертификат с HttpsUrlConnection, вы должны установить свой собственный SocketFactory. - person Ryan; 16.01.2013
comment
I can bump the buffer up and the problem goes away, but this is a vulnerability for a denial of service and not ideal То есть вы говорите, что проблема в размере буфера Apache? Тогда вам следует увеличить его, иначе он будет уязвим для DoS. DoS - ваша забота - person Cratylus; 17.01.2013
comment
Мне нужно поддерживать загрузку файлов размером не менее 64 МБ. Буфер в Apache по умолчанию довольно устойчив к атакам DoS и составляет 128 КБ. Если я увеличу его до 64 МБ, то отключить мой сервер будет очень легко (и это может быть сделано случайно обычными пользователями). Опять же, НАСТОЯЩАЯ проблема заключается в том, что клиент Java недостаточно умен, чтобы предотвратить переполнение сервера, в то время как некоторые клиенты (такие как curl) умнее и не переполняют сервер во время повторного согласования. - person Ryan; 17.01.2013
comment
Нет, это не ошибка приложения, это ошибка аутентификации, отправленная Apache. - person user207421; 19.01.2013
comment
@EJP: Но HTTP 401 является ошибкой HTTP, а HTTP выполняется поверх SSL. Как тогда возможно, что ответ HTTP отправляется в результате проблемы с рукопожатием? Если бы вы могли написать ответ, объясняющий это, я бы проголосовал за него, и я уверен, что другие найдут это полезным. - person Cratylus; 20.01.2013
comment
Я переверну это. Как приложение могло отправить ошибку 401 после неудачного рукопожатия? Ответ: не может. Никто не может. Ergo нет сбоя рукопожатия. Что есть, так это сбой до повторного согласования в OpenSSL, поэтому нет рукопожатия, нет сертификата клиента, нет аутентификации, поэтому Apache отправляет 401. - person user207421; 21.01.2013
comment
@EJP: Вы имеете в виду, что OpenSSL выдает какое-то исключение, из-за которого Apache отправляет 401? - person Cratylus; 21.01.2013
comment
@Cratylus Не лезь мне в рот. Я имел в виду то, что сказал: ни больше, ни меньше. Я не знаю, что делает OpenSSL, но я знаю, что если Apache хочет, чтобы клиентский сертификат аутентифицировал доступ к ресурсу, и он не получит его, он вернет 401, и я также знаю, что никто не может доставить узнаваемый 401 после неудачного рукопожатия. - person user207421; 22.01.2013
comment
@EJP: если Apache хочет сертификат клиента для аутентификации, он запросит его как часть рукопожатия SSL. Если он не получит его, а аутентификация клиента является обязательной, соединение будет отклонено сервером. Единственное объяснение, которое я могу думаю, что в соответствии с вашими комментариями, рукопожатие прошло успешно, и Apache не отклоняет соединение из-за отсутствия сертификата во время рукопожатия, но позже с поведением 401. Странное поведение. Tomcat не работает так - person Cratylus; 22.01.2013
comment
@Cratylus Ничего странного в этом нет. Сертификат клиента не требуется отправлять в ответ на CertificateRequest. Рукопожатие по-прежнему удается. Это определяется не Apache, OpenSSL, Tomcat, JSSE и т. д., а RFC 2246. OpenSSL, Apache, JSSE и, следовательно, Tomcat действительно работают так. В терминах JSSE (повторное) рукопожатие завершается успешно, но последующий вызов getPeerCertificate() сервером вызывает исключение PeerUnverifiedException. Подобные вещи происходят в OpenSSL и, следовательно, в Apache. На этом этапе Tomcat, Apache и т. д. могут отправить 401 через все еще существующее соединение HTTPS. - person user207421; 27.01.2013
comment
@EJP: Понятно. Спасибо за объяснение! - person Cratylus; 27.01.2013

Основываясь на всей предоставленной дополнительной информации, я думаю, что вам следует писать XML в несколько фрагментов, а не в один. В настоящее время вы пишете один кусок, который будет разделен SSL на куски по 16 КБ, что по какой-то причине душит Apache (так не должно быть). Я бы попробовал размеры блоков не больше 4k. Настройте размер фрагмента, пока он не заработает.

Вы вполне можете столкнуться с проблемами сертификата клиента, как только преодолеете эту проблему. Не расстраивайтесь, это доказательство того, что вы, по крайней мере, решили эту проблему.

person user207421    schedule 03.02.2013
comment
Я не понимаю этот ответ. Как вы пришли к выводу, что Apache задыхается от 16-тысячных чанков? Если проблема в этом, то как изменить размер чанка? - person Ryan; 07.03.2013