Безопасность с HTTPS и SSL

Протокол защищенных сокетов (SSL), теперь технически известный как Безопасность транспортного уровня (TLS) — является общим строительным блоком для защищенной передачи данных между клиентами и серверами. Вполне возможно, что приложение может использовать SSL неправильно и вредоносные объекты смогут перехватить данные приложение по сети. Чтобы помочь вам убедиться, что этого не произойдет в вашем приложении, эта статья подчеркивает распространенные ошибки при использовании защищенных протоколов сети и адресов, а так же некоторые крупные проблемы и вопросы по поводу использования Инфраструктуры открытых ключей (PKI).

Основные понятия

В типичном сценарии использования SSL, сервер настроен с сертификатом, содержащим открытый ключ, а также с соответствующим закрытым ключом. В рамках рукопожатия между клиентом SSL и сервером, сервер доказывает, что имеет закрытый ключ, подписав свой сертификат используя шифрование с открытым ключом.

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

Есть несколько недостатков в этот простом подходе. Сервера должны быть в состоянии перейти на более сильные ключи с течением времени (выполнить "ротацию ключей"), заменив открытый ключ в сертификате на новый. К сожалению, сейчас клиентское приложение должно быть обновлено из-за того, что по существу является изменением конфигурации сервера. Это особенно проблематично, если сервер не находится под контролем приложения разработчика, например, если это веб-сервис третьей стороны. Этот подход также имеет проблемы для приложений, таких как веб-браузер или приложение электронной почты, когда приложение должно поговорить с произвольным сервером.

В целях решения этих недостатков, серверы, как правило, настроены с сертификатами от известных издателей называемых Центры сертификации (CA). Платформа обычно содержит список известных центров сертификации(CA), которым она доверяет. По состоянию на Android 4.2 (Jelly Bean), Android в настоящее время содержит более 100 центров сертификации, которые обновляются в каждом выпуске. Подобно серверам, CA имеет сертификат и закрытый ключ. При выдаче сертификата для сервера, CA подписывает сертификат сервера, используя свой закрытый ключ. Затем клиент может убедиться, что сервер имеет сертификат, выданный центром сертификации известным платформе.

Тем не менее, решая некоторые проблемы, использование CA вводит другие. Поскольку CA выдает сертификаты для многих серверов, вам все еще нужен способ убедиться, что вы разговариваете с нужным вам сервером. Для решения этой проблемы, сертификат, выданный центром сертификации идентифицирует сервер либо с определенным именем, таким как gmail.com или с набором хостов, таким как *.google.com.

Следующий пример сделает эти понятия немного более конкретными. В приведенном ниже фрагменте из командной строки, openssl инструмента s_client команда смотрит на информацию сертификата сервера Википедии. Он определяет порт 443, потому что это порт по умолчанию для HTTPS. Команда посылает выходные данные openssl s_client в openssl x509, который форматирует информацию о сертификате в соответствии со стандартом X.509. В частности, команда просит для субъекта, который содержит информацию об имени сервера, и издателя, который идентифицирует CA.

$ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer
subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org
issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA

Вы можете видеть, что сертификат был выдан для серверов, соответствующих *.wikipedia.org центром сертификации RapidSSL.

HTTPS пример

Предположим у вас есть веб-сервер с сертификатом, выданным известным центром сертификации, можно сделать безопасный запрос простым кодом как этот:

URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

Да, это действительно может быть так просто. Если вы хотите адаптировать запрос для HTTP, можно привести к HttpURLConnection. Документация Android для HttpURLConnection имеет дополнительные примеры о том, как работать с заголовками запросов и ответов, посылкой контента, управлять cookies, использовать прокси, кэшировать ответы, и так далее. Но с точки зрения деталей для проверки сертификатов и имён хостов, Android заботится об этом для вас через эти API. Это то, где вы хотите быть, если это вообще возможно. Тем не менее, ниже приведены некоторые другие соображения.

Общие проблемы проверки сертификатов сервера

Предположим, вместо того чтобы получать содержимое из getInputStream(), он вызывает исключение:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
        at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
        at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
        at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
        at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
        at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
        at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

Это может произойти по нескольким причинам, в том числе:

  1. Центр сертификации, выдавший сертификат сервера, неизвестен
  2. Сертификат сервера не был подписан центром сертификации, но был самоподписанным
  3. В конфигурации сервера отсутствует промежуточный центр сертификации

В следующих разделах обсуждается, как решать эти проблемы, при этом поддерживая ваше соединение с сервером в безопасности.

Неизвестный центр сертификации

В этом случае, SSLHandshakeException происходит потому, что у вас есть центр сертификации, который не является доверенным для системы. Это может быть потому, что у вас есть сертификат от нового центра сертификации, которому пока не доверяют Android или ваше приложение, работающее на старой версии без CA. Чаще всего CA неизвестно, потому что это не является публичным центом сертификации, а внутренний CA организации, такой как правительство, корпорации, или учебного заведения для своих собственных нужд.

К счастью, вы можете научить HttpsURLConnection доверять определенному набору CA. Процедура может быть немного запутанным, так внизу приведен пример, который принимает определенный CA от InputStream, использует его для создания KeyStore, который затем используется для создания и инициализации TrustManager TrustManager это то, что система использует для проверки сертификатов сервера и — создав один из KeyStore с одним или более CA — это будут единственные CA, которым будет доверять TrustManager.

Данный новый TrustManager, пример инициализирует новый SSLContext который предоставляет SSLSocketFactory вы можете использовать, чтобы переопределить значение по умолчанию SSLSocketFactory из HttpsURLConnection. Таким образом, соединение будет использовать свои центры сертификации для подтверждения сертификатов.

Вот пример в полном объеме с использованием CA организации Университета Вашингтона:

// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));
Certificate ca;
try {
    ca = cf.generateCertificate(caInput);
    System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} finally {
    caInput.close();
}

// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);

// Tell the URLConnection to use a SocketFactory from our SSLContext
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =
    (HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

Со специальным TrustManager , который знает о ваших CA, система способна проверить, что ваш сертификат сервера приходит от надежного поставщика.

Внимание: Многие веб-сайты описывают плохое альтернативное решение, где нужно установить TrustManager который ничего не делает. Если вы сделаете это, вы можете также не шифровать ваше общение, потому что любой сможет атаковать пользователей на публичном Wi-Fi с помощью DNS уловки, чтобы передавать трафик ваших пользователей через свои собственные прокси, которые притворяются вашим сервером. Злоумышленник может записать пароли и другие личные данные. Это работает, потому злоумышленник может генерировать сертификат и — без TrustManager который на самом деле проверяет, что сертификат из надежного источника — ваше приложение будет говорить с кем угодно. Так что не делайте этого, даже временно. Вы всегда можете сделать, чтобы ваше приложение доверяло издателю сертификата сервера, поэтому просто сделайте это.

Самоподписанный сертификат сервера

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

Вы можете создать свой собственный TrustManager, на этот раз доверяя сертификату сервера напрямую. Это имеет все минусы рассмотренные ранее при связывании своего приложения непосредственно с сертификатом, но это может быть сделано надежно. Тем не менее, вы должны быть осторожны и убедиться, что ваш сертификат, подписанный самостоятельно, имеет достаточно сильный ключ. По состоянию на 2012 год, 2048-битная RSA подпись с показателем 65537 истекающая через год является приемлемой. При ротации ключей, вы должны проверить рекомендации от органа (такого, как NIST) о том, что является приемлемым.

Отсутствует промежуточный источник сертификации

Третий случай SSLHandshakeException происходит из-за отсутствующего промежуточного CA. Большинство публичный CA не подписывают сертификаты сервера напрямую. Вместо этого они используют свой главный сертификат CA, ссылающийся на корневой CA, чтобы подписать промежуточный CA. Они делают так, чтобы корневой CA мог просто где-то храниться, тем самым снижая риск компрометации. Тем не менее, операционные системы, как Android, как правило, доверяют только корневым центрам сертификации непосредственно, оставляя короткий промежуток доверия между сертификатом сервера — подписанным промежуточным CA — и сертификатом верификатора, который знает корневой CA. Чтобы решить эту проблему, сервер отправляет клиенту не только свой сертификат во время SSL рукопожатия, но но цепочку сертификатов от CA сервера, но и любые промежуточные, необходимые для достижения доверия корневому центру сертификации.

Чтобы увидеть, как это выглядит на практике, вот mail.google.com цепочка сертификатов, если смотреть с помощью openssl s_client команды:

$ openssl s_client -connect mail.google.com:443
---
Certificate chain
 0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mail.google.com
   i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
 1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
   i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
---

Это показывает, что сервер отправляет сертификат для mail.google.com выданный Thawte SGC CA, который является промежуточным CA, и второй сертификат для Thawte SGC CA, выданный Verisign CA, которая является основным CA, которому доверяет Android.

Тем не менее, это не редкость для настройки сервера не включить необходимые промежуточные CA. Например, вот это сервер, который может привести к ошибке в Android браузеров и исключений в приложениях Android:

$ openssl s_client -connect egov.uscis.gov:443
---
Certificate chain
 0 s:/C=US/ST=District Of Columbia/L=Washington/O=U.S. Department of Homeland Security/OU=United States Citizenship and Immigration Services/OU=Terms of use at www.verisign.com/rpa (c)05/CN=egov.uscis.gov
   i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)10/CN=VeriSign Class 3 International Server CA - G3
---

Из интересного можно отметить, что посещение этого сервера в большинстве браузеров настольных систем не вызывает ошибки, как при совершенно неизвестном СА или самоподписанном сертификате сервера. Это потому, что большинство браузеров настольных систем кэшируют доверенные промежуточные центры сертификации в течение долгого времени. После того, как при посещенные браузером одного сайта и получения промежуточного CA, не нужно будет получать промежуточный CA в цепочке сертификатов в следующий раз.

Некоторые сайты намеренно это делать для средних веб-серверов, используемых для обслуживания ресурсов. Например, они могли бы иметь их главную HTML страницу, обслуживаемую сервером с полной цепочкой сертификатов, но серверы для ресурсов, таких как изображения, CSS или JavaScript не включают CA, предположительно с целью экономии трафика. К сожалению, иногда эти серверы могут предоставлять веб-сервис, который вы пытаетесь использовать с вашего Android приложения, которое этого не прощает.

Есть два подхода для решения этой проблемы:

  • Настройка сервера и включение промежуточных CA в цепочке серверов. Большинство центров сертификации предоставляют документацию о том, как сделать это для всех распространенных веб-серверов. Это единственный подход, если вам нужно работать с браузером Android по умолчанию по крайней мере до Android 4.2.
  • Или, рассматривать промежуточных CA как и любой другой неизвестной CA, и также создавать TrustManager доверяющий ему непосредственно, как это сделано в двух предыдущих разделах.

Общие проблемы с проверкой имени хоста

Как уже упоминалось в начале этой статьи, есть две ключевые части проверки подключения по SSL. Первый заключался в проверке сертификата из доверенного источника, который был в центре внимания в предыдущих разделах. В центре внимания этого раздела является вторая часть: убедиться, что сервер с которым вы разговариваете представляет правильный сертификат. Когда этого не происходит, вы обычно видите ошибку вроде этой:

java.io.IOException: Hostname 'example.com' was not verified
        at libcore.net.http.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446)
        at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
        at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
        at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
        at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
        at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

Одной из причин этого может являться ошибка конфигурации сервера. Сервер настроен с сертификатом, который не имеет subject или subject alternative полей, которые соответствуют серверу, который вы пытаетесь достичь. Можно иметь один сертификат для использования со многими различными серверами. Например, глядя на google.com сертификат с openssl s_client -connect google.com:443 | openssl x509 -text вы можете видеть, что subject, который поддерживает *.google.com имеет также и альтернативные имена для *.youtube.com, *.android.com, и других. Ошибка возникает только тогда, когда имя сервера, к которому вы подключаетесь нет в списке сертификата.

К сожалению, это может случиться и по другой причине: виртуальный хостинг. Когда общий сервер для более одного хоста с HTTP, веб-сервер может сказать на основе запроса HTTP/1.1, какое имя хоста ищет клиент. К сожалению, это сложно с HTTPS, поскольку сервер должен знать, какой сертификат возвращать, прежде чем он увидит запрос HTTP. Для решения этой проблемы, новые версии SSL, в частности TLSv.1.0 и более поздние, поддерживают Указание имени сервера (SNI), который позволяет клиенту SSL указать предполагаемое имя хоста на сервере, чтобы был возвращен надлежащий сертификат.

К счастью, HttpsURLConnection поддерживает SNI начиная с Android 2.3. К сожалению, Apache HTTP-клиент не поддерживает, что является одной из многих причин, по которой мы не рекомендуем его использование. Одним из способов, если вам необходимо поддерживать Android 2.2 (и старше) или Apache HTTP-клиент, является создание альтернативного виртуального хоста с уникальным портом, так что можно будет однозначно определить, какой сертификат сервера нужно вернуть.

Более радикальная альтернатива это заменить HostnameVerifier , который использует не имя вашего виртуального хоста, а возвращенное сервером по умолчанию.

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

Если вы все еще уверены, что хотите переопределит проверку имени хоста, вот пример, который заменяет верификатор для одного URLConnection таким, который все еще проверяет, что имя хоста по крайней мере ожидаемое:

// Create an HostnameVerifier that hardwires the expected hostname.
// Note that is different than the URL's hostname:
// example.com versus example.org
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        HostnameVerifier hv =
            HttpsURLConnection.getDefaultHostnameVerifier();
        return hv.verify("example.com", session);
    }
};

// Tell the URLConnection to use our HostnameVerifier
URL url = new URL("https://example.org/");
HttpsURLConnection urlConnection =
    (HttpsURLConnection)url.openConnection();
urlConnection.setHostnameVerifier(hostnameVerifier);
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

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

Предостережения при непосредственном использовании SSLSocket

До сих пор, примеры были сосредоточены на HTTPS с использованием HttpsURLConnection. Иногда приложениям нужно использовать SSL отдельно от HTTP. Например, приложение электронной почты может использовать варианты SSL и SMTP, POP3, или IMAP. В этих случаях, приложения хотели бы использовать SSLSocket непосредственно, почти таким же способом, как HttpsURLConnection работает внутри.

Методики, описанные до сих пор для решения вопросов проверки сертификатов применяются также к SSLSocket. На самом деле, при использовании пользовательских TrustManager, который передается в HttpsURLConnection это SSLSocketFactory. Так что, если вам нужно использовать пользовательский TrustManager с SSLSocket, выполните тем же шагам и используйте этот SSLSocketFactory для создания своего SSLSocket.

Внимание: SSLSocket не выполняет проверки хоста. Вашего приложения должно сделать собственную проверку имени хоста, предпочтительно вызвав getDefaultHostnameVerifier() с ожидаемым именем хоста. Кроме того нужно учитывать, что HostnameVerifier.verify() не выбрасывает исключение в случае ошибки, а вместо этого возвращает логический результат, который вы должны явно проверять.

Вот пример, показывающий, как вы можете это сделать. Он показывает, что при подключении к gmail.com порт 443 без поддержки SNI, вы будете получать сертификат для mail.google.com. Это ожидается в данном случае, так что нужно проверить и убедиться, что сертификат на самом деле для mail.google.com:

// Open SSLSocket directly to gmail.com
SocketFactory sf = SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) sf.createSocket("gmail.com", 443);
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
SSLSession s = socket.getSession();

// Verify that the certicate hostname is for mail.google.com
// This is due to lack of SNI support in the current SSLSocket.
if (!hv.verify("mail.google.com", s)) {
    throw new SSLHandshakeException("Expected mail.google.com, "
                                    "found " + s.getPeerPrincipal());
}

// At this point SSLSocket performed certificate verificaiton and
// we have performed hostname verification, so it is safe to proceed.

// ... use socket ...
socket.close();

Черные списки

SSL в значительной мере опирается на CA при выдаче сертификатов только правильным проверенным владельцам серверов и доменов. В редких случаях CA либо из-за обмана или, как в случае с Comodo или DigiNotar, из-за дыры в безопастности, могут выдать сертификаты для хостов для кого-то другого, а не для владельца сервера или домена.

Для того, чтобы уменьшить этот риск, Android имеет возможность поместить в черный список определенные сертификаты или даже весь CA. В то время как этот список был исторически встроенный в операционную систему, начиная с Android 4.2 этот список может быть удаленно обновлен в связи с будущей возможной компрометацией.

Закрепление

Приложение может дополнительно защитить себя от обманных сертификатов по технологии, известной как закрепление. Она, в основном, используя пример, для приведенного выше случая с неизвестным CA, для ограничения до небольшого набора доверенных центров сертификации приложения, которые используются серверами приложения. Это предотвращает компрометирование одной из других 100+ CA в системе из-за нарушения защищенного канала приложения.

Клиентские сертификаты

Эта статья была сосредоточена на пользователе SSL для защиты коммуникаций с серверами. SSL также поддерживает понятие клиентских сертификатов, которые позволяют серверу проверять идентификацию клиента. В то время как это выходит за рамки данной статьи, методики похожи на указание использования пользовательского TrustManager. Смотрите обсуждение о создании пользовательских KeyManager в документации HttpsURLConnection.