Протокол защищенных сокетов (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)
Это может произойти по нескольким причинам, в том числе:
- Центр сертификации, выдавший сертификат сервера, неизвестен
- Сертификат сервера не был подписан центром сертификации, но был самоподписанным
- В конфигурации сервера отсутствует промежуточный центр сертификации
В следующих разделах обсуждается, как решать эти проблемы, при этом поддерживая ваше соединение с сервером в безопасности.
Неизвестный центр сертификации
В этом случае, 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
.