使用 OpenSSL API 进行安全编程,第 2 部分: 安全握手

安全套接字层(Secure Sockets Layer,SSL)会话中的安全握手非常重要,这是因为该连接中的所有安全性都是在握手过程中建立的。本文将介绍如何增强 SSL 握手的安全性,从而防止中间人(MITM)攻击 —— 此时入侵的一方会伪装成另外一个可信源。本文还会介绍数字证书的概念,以及 OpenSSL API 如何处理数字证书。

不久之前,安全握手是双方的业务得以实现的一个标记。毕竟,握手是一次面对面的机会,可以对潜在的合作者进行评价。安全且可信的握手意味着事务的双方都相信它们正在做的事情对双方都是有益的。不安全的握手标记着只有一方会对事务有着正确的理解。

握手的工作方式与在线事务相同。

developerWorks 上的前一篇文章“使用 OpenSSL API 进行安全编程,第 1 部分:API 概述” 向您介绍了如何使用 OpenSSL 创建基本、简单的安全连接。然而,这篇文章只是展示了基本的默认设置;它并没有介绍如何对内容进行定制。不过,我仍然建议大家阅读这篇文章,这样可以使您对本文的理解更加完整,因为前一篇文章介绍了数字证书的概念,并且介绍了如何判断一个证书是否成功通过了 OpenSSL 的内部验证。

本文将深入介绍 OpenSSL,向您介绍如何增强握手的安全性,防止所谓的 中间人 (MITM)攻击。

关于数字证书

在本文后面,我们将介绍如何获取数字证书并对数字证书进行验证,因此下面我们将快速讨论一下什么是数字证书,以及它是如何工作的。如果您熟悉数据加密和 SSL 的知识,就可以跳过本节。要了解更多有关加密和 SSL 问题的信息,请参阅在本文后面 参考资料 中所列出的文章和教程。

数字证书的最简单形式就是 不对称加密密钥(asymmetric cryptography key)。目前关于数字证书的标准中都有一些标识信息,在密钥中也都包含了这些信息。一个典型的数字证书包含所有者的名字(如果这个证书是在一个 Web 服务器上使用的,那么名字就是完整的域名)以及联系信息,还有一个有效日期范围,以及一个安全性签名,用来验证这个证书没有被篡改。

数字证书可以使用 OpenSSL 命令行工具或其他用于此目的的工具简单地创建。但是任何人创建的数字证书都有一个信任的问题。数字证书不仅仅是一个加密密钥,它还是一个在线凭证。证书会向那些试图与您进行通信的人证明您的身份。为了显示信任关系,数字证书可以由认证权威(CA)机构进行签名。

认证权威在数字安全性领域充当一个可信的第三方。由于在在线领域中证明某个实体的身份非常困难,认证权威就接管了这个挑战。它们为那些购买证书或对证书进行签名的用户的身份提供证明。因此,要信任一个证书,用户只需要信任证书权威即可。用户通过拥有并使用 CA 的信任证书来表明自己对认证权威的信任。Verisign 和 Thawte 是非常知名的认证权威。

如果一个证书的安全性曾经受到过威胁,那么这个证书就会被丢弃 —— 也就是说,将其声明为无效。当一个证书被声明为无效时,CA 不可能将其通知所有拥有该证书拷贝的人。相反,CA 会发布一个 证书撤销列表(Certificate Revocation List)(CRL)。浏览器和其他使用数字证书的程序都可以验证这个证书已经被其属主或 CA 撤销了。

证书的撤销也可以使用 OCSP 协议进行检查。OCSP 代表 Online Certificate Status Protocol(在线证书状态协议),它是在 RFC 2560 中定义的。OpenSSL 既有 OCSP 的功能,又有 CRL 的功能,但是对这些功能的介绍已经超出了本文的范围。目前数字证书所采用的标准是 X.509,这是在 RFC 3280 中定义的。

 




 

开展业务之前的握手

由于本文重点要介绍在握手过程中服务器数字证书的处理,因此让我们来深入介绍一下握手是如何工作的。如果您熟悉 SSL 的过程,可以跳过本节。

在一个连接上开始握手通常是从客户机向服务器说“Hello”开始的。helllo 消息(在规范中就是这么说的)包含了客户机的安全性参数:

  • SSL 版本号
  • 随机生成数据
  • 密码设置
  • 通信所需要的其他内容

服务器会使用自己的 hello 消息进行响应,其中包含了服务器的安全性参数,这些信息与客户机所提供的信息的类型相同。此时服务器还会发送自己的数字证书。如果客户机还要为这个连接进行认证,那么服务器还会发送一个请求,索取客户机的证书。

当客户机接收到服务器端的 hello 消息之后,数字证书就要进行验证了。这包括检查证书的各种参数,从而确保该证书从未受到过侵害,同时是在它的有效期内使用的。

此处还要执行的另外一个步骤是根据连接所使用的主机名对证书的名字进行检查。虽然这不是 SSL 标准的一部分,但是这个步骤却是高度建议的,它可以防止中间人攻击。这个步骤会验证证书就是来自您认为它所来自的那个实体。如果这两个实体在此处不能匹配,那么就只能怀疑这个证书是无效的。

在客户机和服务器之间共享的随机数据用来创建 premaster secret,这是一个只有服务器和客户机才会知道的共享秘密值,并且只用于这个会话。这个秘密值会对服务器的数字证书进行加密,并发送给服务器用于验证客户机的身份。

如果服务器请求客户机认证,那么客户机就会对这个在握手过程中随机生成的数据(只有服务器和客户机知道它)创建一个单向的 hash 值。客户机使用自己的私钥对这个 hash 值进行签名,并将签名后的数据和数字证书发送给服务器。服务器使用这些信息对客户机进行认证。

如果认证成功,那么服务器和客户机就会通过一个算法使用这个共享的随机数据来创建 master secret。从 master secret 中,客户机和服务器可以创建 session keys(会话密钥),这是选择用来对会话数据进行加密所使用的对称密码中的对称密钥。

客户机通过向服务器发送一个表明自己已经完成握手的消息,以及一组加密的单向 hash 值让服务器进行验证,从而结束握手的过程。服务器也会向客户机发送一个类似的消息。客户机和服务器在结束握手并开始通信之前,都要对这些数据进行验证。

 




 

中间人

虽然这是孩子们玩的一个游戏,但却也是在公钥基础设施(PKI)上可能发生的一种很严重的攻击。当我们在讨论有关数字证书的问题时,就必须考虑中间人攻击的问题,因为不管 SSL 连接之后的安全参数如何,中间人攻击都可以让这些防范措施形同虚设。

假设 Casey 和 Samantha 希望使用 SSL 进行通信。Isabel 是一个第三方,她截获了这个连接请求,并在他们两个之前充当代理。当她注意到正在建立一个 SSL 连接时,就向 Samantha 伪装成 Casey,向 Casey 伪装成 Samantha。由于她位于中间,可以截获会话双方的内容。如果这个会话中包含帐号和个人信息,那么她就可以使用这些信息窃取他人的身份了。

在这种情况中,信任链和证书中的通用名可以防止中间人攻击的发生。在握手过程中,会对证书进行交换。在分析证书的有效性时,同时还会检查可信签名。如果服务器证书中的通用名是与证书的其余部分一起验证的,那么这种攻击就不攻自破了,对么?其实不完全对。

让我们假设 Isabel 有一个证书,其中有 Samantha 的名字;并且这个证书由 Casey 信任模型中的一个 CA 进行了签名。此时只通过检查通用名并不能避免 MITM 攻击的发生。证书及其信任关系都是有效的,名字也检查出来了。我们现在有了一个大麻烦。

然而,如果考虑一下认证权威,这个问题就并不重要了。大部分认证权威都会在发行带有个人名的数字证书之前就试图验证他的身份就是本人。由于这个原因,Isabel 就很难从一个知名的认证权威那里获得一个带有 Samantha 名字的数字证书。

如果在 CA 中工作,则可以消弱这种安全设施的作用。例如,如果 CA 和 Isabel 都在一个公司工作(换而言之,是一个“内部工作”)。那么用来签名的密钥就有可能会被 CA 内部工作的人员窃取,他们随后就可以使用任何人的名字来创建任意的证书。尽管在创建签名时要使用证书的私有部分,但是采用一些工程的方法或类似的技术也可以窃取密码。

MITM 攻击在使用 代理服务器 的情况中尤其重要。因为安全连接必须由代理服务器提供一个隧道才能到达目的地,因此恶意的代理服务器就可以很容易地窃取任何会话。恶意的代理可以在并不存在隧道时却伪装成仿佛隧道真正存在一样。在使用 Internet 上的“匿名代理”时,记得这一点尤其重要:您正在通过它们的系统来发送用户名和密码,怎样才能相信它们呢?

然而我们要认识到,这种攻击并不仅仅存在于计算机和数字安全领域。有一个女人曾经使用类似于 MITM 攻击的技术抢劫了很多家的很多钱(请参阅 参考资料)。

 




 

OpenSSL 和数字证书

OpenSSL 有一个专门用于数字证书的库。假设您现在已经有了 OpenSSL 的源代码,那么这个库的源代码就位于 crypto/x509 和 crypto/x509v3 目录中。源代码为数字证书的处理定义了几个结构。表 1 中列出了这些结构。

表 1. 与 X.509 证书有关的 OpenSSL 结构

结构 功能
X509 包含所有有关数字证书的数据
X509_ALGOR 提供该证书设计所针对的算法
X509_VAL 该证书有效的时间跨度
X509_PUBKEY 证书的公钥算法,通常是 RSA 或 DSA
X509_SIG 证书的 hash 签名
X509_NAME_ENTRY 证书所包含的数据的各个项
X509_NAME 包含名字项的堆栈

 

这些只是其中涉及的几个结构。在 OpenSSL 中使用的大部分 X.509 结构您自己在应用程序中几乎都不会用到。在上面列出的这些结构中,本文中只使用了两个:X509 和 X509_NAME。

在这些结构之上,有一些用来处理数字证书的函数。这些函数得名于它们所适用的结构。例如,一个名字以 X509_NAME 开始的函数,通常会应用于一个 X509_NAME 结构。后面我们会根据需要介绍一些函数。

 




 

提供自己的信任证书

在数字证书进行信任验证之前,必须为在为安全连接设置时创建的 OpenSSL SSL_CTX 对象提供一个默认的信任证书,这可以使用几种方法来提供,但是最简单的方法是将这个证书保存为一个 PEM 文件,并使用 SSL_CTX_load_verify_locations(ctx, file, path); 将其加载到 OpenSSL 中。file 是包含一个或多个 PEM 格式的证书的文件的路径。path 是到一个或多个 PEM 格式文件的路径,不过文件名必须使用特定的格式。将信任证书保存在一个 PEM 文件中比较简单,这样可以使 path 参数为空,如下所示: SSL_CTX_load_verify_locations(ctx, "/path/to/trusted.pem", NULL);

尽管当信任证书在一个目录中有多个单独的文件时更容易添加或更新,但是您不太可能会如此频繁地更新信任证书,因此不必担心这个问题。

 




 

验证证书

在通信继续连接或接收证书之前,请使用 SSL_get_verify_result() 来确定 OpenSSL 内部对证书的验证结果。如果 SSL_get_verify_result() 返回的代码不是 X509_V_OK,那么这就意味着这个证书无效吗?这要取决于返回代码。

通常,如果返回代码不是 X509_V_OK,那么这个证书就有问题,或者这个证书存在安全性隐患。时刻要记住一件事情:OpenSSL 在对证书进行验证时,有一些安全性检查并没有执行,包括证书的失效检查和对证书中通用名的有效性验证。

SSL_get_verify_result() 所返回的代码在 OpenSSL 文档的 verify 部分中都进行了介绍,这是在 apps 之下列出的。有些代码的说明是尚未使用,意味着它们永远不会返回。有些代码非常重要,而有些则不太重要。例如,如果由于没有加载所保存的信任证书,而不能对信任证书进行验证,那么是否继续进行通信,就完全取决于开发者了。

不管验证结果如何,是否继续使用一些可能不安全的参数也完全取决于开发者。由于证书可能是不安全的,因此会返回错误代码。

 




 

检索证书

如果您希望向用户显示证书的内容,或者要根据主机名或证书权威对证书进行验证,那么就需要检索证书的内容。要在验证测试结果之后再检索证书,请调用 SSL_get_peer_certificate()。它返回一个指向该证书的 X509 指针,如果证书不存在,就返回 NULL(参见清单 1)。


清单 1. 检索证书

				
X509 * peerCertificate;
if(SSL_get_verify_result(ssl) == X509_V_OK)
    peerCertificate = SSL_get_peer_certificate(ssl);
else
{
    /* Handle verification error here */
}

 




 

验证证书

在握手时所提供的服务器的证书应该有一个名字与该服务器的主机名匹配。如果没有,那么这个证书就应该标记为值得怀疑的。内部验证过程已经对证书进行信任和有效期的验证;如果这个证书已经超期,或者包含一个不可信的签名,那么这个证书就会被标记为无效的。由于这不是 SSL 标准的一部分,因此 OpenSSL 并不需要根据主机名对该证书的名字进行检查。

证书的“名字”实际上是证书中的 Common Name 字段。这个字段应该从证书中进行检索,并根据主机名进行验证。如果二者不能匹配,就只有怀疑这个证书无效了。有些公司(例如 Yahoo)就在不同的主机上使用相同的证书,即使证书中的 Common Name 只是用于一个主机的。为了确保这个证书是来自于相同的公司,可以进行更深入的检查,但是这完全取决于项目的安全性需要。

从证书中检索通用名需要两个步骤:

  • 从证书结构检索 X509_NAME 对象。
  • 然后从 X509_NAME 对象检索名字。

使用 X509_get_subject_name() 从证书中检索 X509_NAME 结构。这会返回一个指向 X509_NAME 的对象。从现在开始,请使用 X509_NAME_get_text_by_NID() 来检索通用名,并保存到一个字符串中(如清单 2 所示)。


清单 2. 检索并验证 Common Name

				
char commonName [512];
X509_NAME * name = X509_get_subject_name(peerCertificate);
X509_NAME_get_text_by_NID(name, NID_commonName, commonName, 512);
/* More in-depth checks of the common name can be used if necessary */
if(stricmp(commonName, hostname) != 0)
{
    /* Handle a suspect certificate here */
}

 

使用标准的 C 字符串函数或您习惯使用的字符串库对通用名和主机名进行比较。对不匹配的处理,完全取决于项目的需要或用户的决策。如果要更深入地进行检查,我建议使用一个单独的字符串库来降低复杂性。

 




 

获得信任

在本文中,我们已经介绍了如何增强握手的安全性,从而防止中间人攻击(攻击一方伪装成另外一个可信源),我们还介绍了数字证书的概念,以及 OpenSSL API 如何处理数字证书。

记住,在 SSL 会话过程中增强会话的安全性非常重要,这是因为该连接中的所有安全性都是在握手过程中建立的。遵循本文中概要介绍的每个步骤到底有多重要取决于项目的要求以及开发者的决定。

你可能感兴趣的:(使用 OpenSSL API 进行安全编程,第 2 部分: 安全握手)