19 смертных грехов, угрожающих безопасности программ
Шрифт:
Искупление греха
Когда использование SSL или TLS оправдано, проверяйте выполнение следующих условий:
□ используется последняя версия протокола (во время работы над книгой это была версия TLS 1.1);
□ используются стойкие шифры (к нестойким относятся прежде всего RC4 и DES);
□ проверяется, что текущая дата попадает в период действия сертификата;
□ гарантируется, что сертификат прямо или косвенно выпущен доверенным источником (корневым УЦ);
□ проверяется, что хранящееся в сертификате имя хоста соответствует
Кроме того, необходимо реализовать хотя бы один метод работы с отозванными сертификатами: либо сверку с CRL–списком, либо запрос по протоколу OCSP.
Выбор версии протокола
В большинстве языков высокого уровня не существует простого способа указать, каким протоколом вы хотели бы воспользоваться. Вы просто запрашиваете защищенный сокет, а библиотека устанавливает соединение и возвращает результат. Например, в языке Python имеется функция ssl из модуля socket, которая принимает объект сокета и защищает его по протоколу SSL, но не позволяет указать ни версию протокола, ни шифр. Базовым API в таких языках, как Perl или РНР, присуща та же проблема. Для исправления ситуации часто приходится писать код на языке низкого уровня или копаться в скрытом API, обертывающем такой код (написанный, например, с использованием библиотеки OpenSSL).
В языках низкого уровня возможность задать версию протокола встречается чаще. Так, Java хотя и не поддерживает TLS 1.1 (в версии 1.4.2), но, по крайней мере, позволяет сказать, что вас устраивает только протокол TLS версии 1.0:
from javax.net.ssl import SSLSocket;
SSLSocket s = new SSLSocket("www.example.com", 25);
s.setEnabledProtocols("TLSv1");
Каркас .NET Framework 2.0 также поддерживает лишь TLS vl.O. В приведенном ниже фрагменте показано, как можно запросить использование TLS. При этом также затребуется проверка даты, а задание в качестве последнего аргумента метода AuthenticateAsClient равным true говорит, что нужно еще проверять сертификат по CRL–списку:
RemoteCertificateValidationCallback rcvc = new
RemoteCertificateValidationCallback(OnCertificateValidation);
SslStream sslStream = new SslStream(client.GetStream, false, rcvc);
sslStream.AuthenticateAsClient("www.example.com", // Имя сервера
null, // цепочка сертификатов
SslProtocols.Tls, // использовать TLS
true); // проверять по CRL
// Обратный вызов для дополнительных проверок сертификата
private static bool OnCertificateValidation(object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors) {
if (sslPolicyErrors == SslPolicyErrors.None) {
return false;
}
else {
return true;
}
}
В обоих примерах клиент не сможет установить соединение с сервером, поддерживающим только более старые версии протокола.
В написанных на С библиотеках, например OpenSSL или Microsoft Security Support Provider Interface (SSPI –
Выбор семейства шифров
Как и в случае выбора протокола, задать семейство шифров в языке высокого уровня сложно. В низкоуровневых языках такая возможность есть, но умолчания, на наш взгляд, оставляют желать лучшего. Например, в API Java Secure Sockets Extensions QSSE – защищенные сокеты в Java) в качестве симметричных шифров можно выбирать RC4, DES, 3DES и AES. Но первыми двумя лучше не пользоваться.
Вот полный перечень шифров, предлагаемых Sun, в порядке приоритета (в таком порядке Java будет пробовать их, если вы ничего не укажете):
□ SSL_RSA_WITH_RC4_12 8_MD5
□ SSL_RSA_WITH_RC4_12 8_SHA
□ TLS_RSA_WITH_AES_12 8_CBC_SHA
□ TLS_DHE_RSA_WITH_AES_12 8_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_4 0_MD5
□ SSL_RSA_EXPORT_WITH_DES4 0_CBC_SHA
□ SSL_DHE_RSA_EXPORT_WITH_DES4 0_CBC_SHA
□ SSL_DHE_DSS_EXPORT_WITH_DES4 0_CBC_SHA
Первые два семейства шифров нежелательны из соображений долгосрочной безопасности, но именно они, скорее всего, и будут использованы! Мы рекомендуем выбирать любое из последующих трех семейств, поскольку AES считается самым лучшим из современных криптографических алгоритмов. (Вопроса о выборе алгоритма открытого ключа и кода аутентификации сообщений (MAC) мы здесь не касаемся.) Чтобы принять только три указанных алгоритма, надо написать такой код:
private void useSaneCiperSuites(SSLSocket s) {
s.setEnabledCipherSuites({"TLS_RSA_WITH_AES_128_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_DHE_DSS_WITH_AES_128_CBC_SHA"});
}
Проверка сертификата
Разные API в разной степени поддерживают базовую проверку сертификата. Некоторые по умолчанию проверяют дату и цепочку доверия, в других вообще не реализовано ни то, ни другое. Большинство же находятся где–то посередине, например включают средства проверки, но не выполняют ее по умолчанию.
Обычно (хотя и не всегда) для выполнения проверки нужно получить ссылку на сертификат сервера (часто его называют сертификатом «партнера» (peer certificate)). Например, в Java до инициализации SSL–соединения можно зарегистрировать объект–слушатель HandShakeCompletedListener для объекта SSLSocket. Слушатель должен реализовать такой метод:
public void handshakeCompleted(HandShakeCompletedEvent event);
Получив объект, описывающий событие, вы можете далее написать: