一.从OPENSSL握手过程谈起
当我们尝试着建立一个加密连接的时候,首先需要在客户端和服务器之间进行加密套件的协商以及会话密钥的协商,不幸的是,这一过程是复杂的,并且容易遭到攻击的,因此,单个用户在不使用第三方库的情况下很难建立一个加密连接。还好TLS/SSL协议族为我们提供了一个足够安全的解决方案,而OPENSSL作为最广为人知的也是最常使用的TLS/SSL实现方案也在互联网中得到了广泛应用。
TLS/SSL采用握手(handshake)这个词用来形容客户端与服务器协商会话密钥(session key)的过程,在这一过程中,双方交互信息,并在信息中得到相同的加密套件和会话密钥。这一过程通常而言由客户端发起,客户端会发送client-hello信息给正在监听的服务器,服务器收到该信息之后返回server-hello等信息,并由此开始握手过程。
加密套件(cipher-suite)就是客户端-服务器所用密钥交换算法、认证算法、数据加密算法、摘要算法的组合,加密套件在openssl源码中的./ssl/s3_lib.c中定义,其中第三个加密套件如下所示:
{
1,
SSL3_TXT_RSA_RC4_40_MD5,
SSL3_CK_RSA_RC4_40_MD5,
SSL_kRSA, //密钥协商算法为RSA
SSL_aRSA, //签名认证算法为RSA
SSL_RC4, //加密算法为RC4
SSL_MD5, //摘要算法为MD5
SSL_SSLV3,
SSL_EXPORT|SSL_EXP40,
SSL_HANDSHAKE_MAC_DEFAULT|TLS1_PRF,
40,
128,
}
通过密钥协商算法,我们可以得到用于加密算法的密钥,在SSL/TLS执行过程中,client-hello信息中会包括一个客户端所支持的加密套件的列表,而服务器在收到client-hello信息中会从中选择一个自己也支持的加密套件作为本次会话所用加密套件,并在server-hello信息中返回自己所选择的加密套件。
现在问题来了:服务器究竟是如何选择加密套件的?不同的实现方案中选择的方式不同,在OPENSSL中,加密套件的选择是和服务器所拥有的证书类型、临时公钥设置情况以及client提供的加密套件的顺序有关的。
二.OPENSSL是在哪里选择加密套件的
在server启动SSL_accept函数进行监听之后,整个程序进入一个类似于有限状态机的过程中,每个状态及状态的转移在s3_srvr.c的ssl3_accept()函数中定义,当收到client-hello信息之后,server调用函数ssl3_get_client_hello读取client-hello中的信息,并且选择相应的加密套件。
具体的加密套件选择函数是ssl3_choose_cipher(在s3_lib.c中定义),返回值为选好的加密套件,这个函数在ssl3_get_client_hello中被调用,进入该函数之后,我们可以在循环中看见如下的代码:
ssl_set_cert_masks(cert,c);
mask_k = cert->mask_k;
mask_a = cert->mask_a;
emask_k = cert->export_mask_k;
emask_a = cert->export_mask_a;
这一过程中,首先调用了ssl_set_cert_masks()函数,这个函数的作用是根据证书和临时公钥的设置情况设置掩码,也就是mask,mask里包括了本次连接中支持的密钥交换算法和认证算法的信息,之后的语句将刚才设置的掩码读取出来,前两种是正常的掩码,后两种是和出口限制有关的,我们在这里只关注一般情况下的掩码。
对于服务器端的每一个加密套件(函数参数中的c),openSSL都要执行一遍这样的步骤,这也就是外面所包裹着的循环的意义,接下来程序的执行流程是这样的:
alg_k=c->algorithm_mkey;
alg_a=c->algorithm_auth;
……
if (SSL_C_IS_EXPORT(c))
{
ok = (alg_k & emask_k) && (alg_a & emask_a);
……
}
else
{
ok = (alg_k & mask_k) && (alg_a & mask_a);
…….
}
这段代码中,先获取了处于当前循环的加密套件的密钥交换算法(alg_k)和认证算法(alg_a),在openSSL中,每一个密钥交换算法(认证算法)都表示为一位,具体来说如下所示(源文件ssl/ssl_locl.h中):
define SSL_kRSA 0x00000001L /* RSA key exchange */
define SSL_kDHr 0x00000002L /* DH cert, RSA CA cert */
define SSL_kDHd 0x00000004L /* DH cert, DSA CA cert */
define SSL_kEDH 0x00000008L /* tmp DH key no DH cert */
define SSL_kKRB5 0x00000010L /* Kerberos5 key exchange */
define SSL_kECDHr 0x00000020L /* ECDH cert, RSA CA cert */
define SSL_kECDHe 0x00000040L /* ECDH cert, ECDSA CA cert */
define SSL_kEECDH 0x00000080L /* ephemeral ECDH */
define SSL_kPSK 0x00000100L /* PSK */
define SSL_kGOST 0x00000200L /* GOST key exchange */
define SSL_kSRP 0x00000400L /* SRP */
每一个密钥交换算法都占据着不同的位,而在mask中,如果证书和临时公钥的设置情况允许某种密钥交换算法的使用,那么这一位就会置1,而上面一段代码的ok也会变成1,表示密钥交换算法是可以使用的。
之后,openSSL还会对ok为1的加密套件进行其他检测,但是加密套件(对于密钥交换算法)的选择方式的核心部分就在这里。
三.ssl_set_cert_masks函数
所以,其中起着决定性作用的就是ssl_set_cert_masks这个函数,对这个函数的深入理解有助于我们进一步理解SSL/TLS协议族,并且在实际中将DH\EC-DH一族的密钥交换算法应用到我们自己的协议中(现在网上教人怎么使用openSSL的教程中,证书一般都是以RSA证书为例子的)。
可能细心的同学也已经注意到,在上面openSSL对于密钥交换算法的分类中,对于DH(Diffie-Hellman)协议以及它的椭圆曲线版本EC-DH都有三个变种。这里可以事先说明一下,采用kDHr(以及kECDHr)的情况是:服务器端的采用公钥为DH参数的公钥,也就是 g^x,其中x为指数,也就是所谓的私钥,服务器端的证书是针对这个公钥的,而为这个证书签名的证书(上级CA)是采用RSA私钥对其进行签名的;与此对应的,采用kDHd/kECDHd的情况时,上级CA采用的是DSA/ECDSA私钥对服务器的公钥证书进行的签名,但证书本身所认证的公钥还是DH/EC-DH公钥,对于这两种密钥交换算法来说,服务器端参加密钥交换的参数都由证书本身提供。然而,对于第三类,也就是kEDH/kEECDH来说,服务器端参加密钥交换的数据并非由证书提供,而是由一个事先设定的临时(ephemeral)参数提供。
接下来我们进入ssl_set_cert_masks(源代码在ssl_lib.c中)这个函数来看一看以上机制的实现,首先是这样一段代码:
rsa_tmp = (c->rsa_tmp != NULL || c->rsa_tmp_cb != NULL);
……
dh_tmp = (c->dh_tmp != NULL || c->dh_tmp_cb != NULL);
……
have_ecdh_tmp = (c->ecdh_tmp || c->ecdh_tmp_cb || c->ecdh_tmp_auto);
上面代码的目的是查看在通信之前是否存在了临时公钥,临时公钥的存在途径有两种,要么是设置了实在的某个数作为临时公钥,还有一种就是调用回调函数生成一个临时参数,如果这些途径中有一个成立,则视为本次协议握手过程中存在临时公钥,对应的tmp变量为1。
当查看临时公钥的存在性之后,出现的是下面一段代码:
cpk = &(c->pkeys[SSL_PKEY_RSA_ENC]);//获取证书中的用于加密的RSA公钥
rsa_enc = cpk->valid_flags & CERT_PKEY_VALID;
//如果密钥合法,则RSA_ENC变量为1,表示存在一个合法的用于RSA加密的公钥
rsa_enc_export = (rsa_enc && EVP_PKEY_size(cpk->privatekey) * 8 <= kl);
cpk = &(c->pkeys[SSL_PKEY_RSA_SIGN]);
rsa_sign = cpk->valid_flags & CERT_PKEY_SIGN;//存在用于RSA签名的公钥
cpk = &(c->pkeys[SSL_PKEY_DSA_SIGN]);
dsa_sign = cpk->valid_flags & CERT_PKEY_SIGN;//用于DSA签名的公钥
cpk = &(c->pkeys[SSL_PKEY_DH_RSA]);
dh_rsa = cpk->valid_flags & CERT_PKEY_VALID;
//存在上级CA使用RSA公钥进行签名的DH公钥
dh_rsa_export = (dh_rsa && EVP_PKEY_size(cpk->privatekey) * 8 <= kl);
cpk = &(c->pkeys[SSL_PKEY_DH_DSA]);
dh_dsa = cpk->valid_flags & CERT_PKEY_VALID;
//存在上级CA采用DSA公钥进行签名的DH公钥
dh_dsa_export = (dh_dsa && EVP_PKEY_size(cpk->privatekey) * 8 <= kl);
cpk = &(c->pkeys[SSL_PKEY_ECC]);
ifndef OPENSSL_NO_EC
have_ecc_cert = cpk->valid_flags & CERT_PKEY_VALID;
//存在一个合法的ECC公钥(也就是椭圆曲线上的点g^x)
endif
证书的pkeys是一个数组,而上面的SSL_PKEY_RSA_ENC等数字就是对于这个数组的索引,表示所存的各种类型的公钥,上面的程序目的就是读取证书结构中的公钥,并认证公钥的合法性。
if(rsa_enc || (rsa_tmp && rsa_sign)) //如果证书认证的是RSA加密公钥而且存在RSA临时参数
mask_k |= SSL_kRSA; //就可以使用kRSA作为密钥交换算法
……
if (dh_tmp) //如果存在DH临时参数
mask_k |= SSL_kEDH; //可以使用kEDH密钥交换算法
if (dh_rsa) //如果存在由RSA签名的认证DH公钥的证书
mask_k |= SSL_kDHr; //可以采用kDHr密钥交换算法
……
if (dh_dsa) //如果存在由DSA签名的认证DH公钥的证书
mask_k |= SSL_kDHd; //可以采用kDHd密钥交换算法
接下来对于ECDH密钥交换算法的设置过程也和上面类似,之后掩码就会被设置到证书数据结构(即代码中的变量c)里,ssl3_choose_cipher函数会将这些掩码读出来,看看当前的加密套件所使用的密钥交换算法是否符合证书的要求,以确定究竟使用那个加密套件。
现在,我们就可以根据上面的分析总结出一套在我们自己编写的程序中引入带有DH/EC-DH密钥交换算法的加密套件的条件了:你可以在服务器端的SSL结构中添加一个临时的DH/EC-DH参数或者在服务器端使用一个经过上级CA采用RSA/DSA/EC-DSA算法签名的DH/EC-DH公钥证书,之后就可以使用对应的加密套件了。
文/黄黄的德鲁伊(简书作者)
原文链接:http://www.jianshu.com/p/042dc1ba6815
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。