相信各位对OpenSSL库已经不陌生了,目前笔者使用这个库实现了RSA、AES加解密和tcp的双向认证功能,下面来看tcp的双向认证。
1、什么是双向认证
简单说双向认证就是:客户端认证服务端是否合法,服务端认证客户端是否合法。 可以借助于HTTPS来说明,http网络传输协议是超文本的明文协议,也就是说经过网卡传输的字节序列都是明文,那么HTTPS上的s就是双向认证的操作(ssl),实际上就是在http的逻辑上再套一层ssl握手,进程想要发送的字节序列数据经过http传输时再加上一层ssl来让c和s两端先相互认证是绝对正确的对端,然后ssl使用AES产生一个加密密钥,进行数据加密,传输给对端。而tcp的双向认证也是一样,只是tcp协议网络接口是socket套接字,因此需借助其配合开发,
在代码实现时有俩种:ssl、tsl,我们可以简单的将其看做tsl是ssl的版本升级,下面列举历史版本: 1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。 1995年,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。 1996年,SSL 3.0版问世,得到大规模应用。 1999年,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。 2006年和2008年,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的修订版。 目前,应用最广泛的是TLS 1.0,接下来是SSL 3.0。但是,主流浏览器都已经实现了TLS 1.2的支持。TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。因此如果当项目计划使用ssl或者tsl做双向认证,无需疑惑,两者根本就是一回事,只不过是版本的差异而已,而且这个版本的选择,OpenSSL会自动在cs两端协商,开发者无需写死,且不能写死,防止cs两端因版本号无法进行ssl握手(ssl握手就是指双向认证的过程,参照tcp的三次握手四次挥手)
不论是Java、qt、Android移动端、c语言,本质都是一样的,也不论是https协议、tcp协议,ssl双向认证的逻辑都一样:都是在正常的明文传输基础上套一层ssl,用来实现安全传输,这个安全传输我理解有俩层含义: 1、数据的安全,在网络节点路由器上传输加密后数据序列; 2、访问的对端合法,站在自己的角度上确保对端是期望访问的合法进程节点,例如银行APP,在访问服务器时,必须确保访问的服务器是真的银行,不然我们把账号密码短信验证码都发给黑客的某个进程服务器,它收到后以我们的身份去访问银行,把我们的钱全卷走 注:因此,在安全要求高的产品上双向认证和数据加密传输很有必要的
2、双向认证流程
2.1、大体上了解了双向认证的概念,开始介绍基本的认证逻辑
ssl的基本逻辑是使用公钥加密、私钥解密(也就是加密是用RSA非对称算法)。也就是说,客户端先发起对服务器的访问,索要服务端的公钥证书,然后使用公钥证书加密一个随机数,发给服务端,如果服务端能够解密出来,那么说明对端服务器没有被黑客劫持,因为私钥是不会在网络中公开传播的,安全的前提就是私钥是绝对安全的,不会被其他人获取到
2.2、插入证书相关知识点
这里出现了公钥证书(数字证书)、私钥等等,插入一些知识点:在非对称的RSA加密算法中,公钥加密,只能由私钥解密,反之也成立,怎么保证公钥不被篡改,OpenSSL库的做法就是把公钥放到证书中,因此出现了公钥证书(因私钥不传播,因此无须制作成证书)。
我们还应该思考一个问题,对于某个client端来说,如果是黑客劫持的中间服务器给c端发了它自己的公钥证书,c端用一个随机数根据劫持服务器的公钥证书去加密传输给它,黑客的劫持服务器能不能解密,必然是可以的,公钥证书和私钥是配套的,这样就会导致client端认为这个服务器是合法的,开始给它传输大量数据,这是致命的bug,于是就出现了根证书
上述的问题实际上根本无法解决,因此就出现一个权威的认证机构,由它来生成颁发证书,只有记录在根证书里的公钥证书才被看做是合法的,国际权威就是CA机构,需要花钱,国内的华为、阿里都有这种业务,便宜也更方便一些。 综上所述,ssl协议的双向认证就出现了根证书(chain.crt)、公钥证书(数字证书certificate.crt)、私钥(privateKey.key)这三种,
这三种证书是有俩种后缀名称,也就是文件后缀类型: 1、pem 2、crt 也有两种编码格式: 1、ascll码,也就是字符串 2、asn1、也就是二进制字节
这两种后缀和两种编码格式没有固定的对应关系,需要生成证书的时候才能看出来(谁给证书,谁一定知道证书的编码格式,找他问),OpenSSL库可以生成自己测试的证书,搜一些博客有操作指南,是什么编码格式是需要确定的,因为代码里读取证书的时候需要把编码格式传入进去。一般来说Unix OS多数用字符串编码格式,window OS好像是der二进制编码用的多。
2.3、继续ssl认证逻辑
上面说到公钥加密、私钥解密,根据三个证书相关,现在可以保证两端的正确性,但是RSA非对称加密算法非常耗时,出现第二个问题:公钥加密计算量太大,如何减少耗用的时间
解决方法:每一次对话(session),客户端和服务器端都生成一个"对话密钥"(session key),用它来加密信息。由于"对话密钥"是对称加密,所以运算速度非常快,而服务器公钥只用于加密"对话密钥"本身,这样就减少了加密运算的消耗时间。
由此可知,SSL的主体流程是这样的:
(1) 客户端向服务器端索要并验证公钥。 (2) 双方协商生成"对话密钥"。 (3) 双方采用"对话密钥"进行加密通信。
其中1、2叫ssl握手,3是建立安全通道后开始加密的数据传输。用一个手绘图来说明1、2 上图中就是1、2的步骤也被称为ssl握手,由OpenSSL库实现,开发者无需关注代码实现,懂得原理就行。
2.4、握手阶段的详细过程
握手涉及四次通信,且握手阶段都是明文,详细来看
2.4.1、c端发出请求(clientHello)
首先,客户端先向服务器发出加密通信的请求,这被叫做ClientHello请求。在这一步,客户端主要向服务器提供以下信息。
(1) 支持的协议版本,比如TLS 1.0版。 (2) 一个客户端生成的随机数,稍后用于生成"对话密钥"。 (3) 支持的加密方法,比如RSA公钥加密。 (4) 支持的压缩方法。
2.4.2、服务器回应(SeverHello)
服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。
(1) 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。 (2) 一个服务器生成的随机数,稍后用于生成"对话密钥"。 (3) 确认使用的加密方法,比如RSA公钥加密。 (4) 服务器证书。 除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供"客户端证书"。这是可以代码配置实现的
2.4.3、客户端回应
客户端收到服务器的回应后,首先验证服务器证书,如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。 如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。
(1) 一个随机数。该随机数用服务器公钥加密,防止被窃听。 (2) 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。 (3) 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。
上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把"会话密钥"。至于为什么一定要用三个随机数,来生成"会话密钥",dog250解释得很好:
1、“不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。 2、对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。 3、pre master的存在在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。”
此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。
2.4.4、服务器的最后回应
服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息
(1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送 (2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
这样,ssl握手就完成,但是需要注意,上述的流程是HTTPS,OpenSSL的tcp socket流程和它稍有不同,就是tcp的证书验证是在建立ssl_connect后开始验证对端发过来的数字证书,而不是握手过程中验证,理解本质的话都是一样的,ssl_connect完成后,假如证书不符合规定,会返回0终止此次ssl握手连接。
3、c++的代码实现
Java中可以使用SSLSockets来实现,qt中也有Qsslsocket,他们都是语言集成了OpenSSL库的双向认证与加密api工具,而纯c++没有,则开发者需要在程序中集成OpenSSL库,下面来看实现:
3.1、Android.mk中链接OpenSSL库
LOCAL_SHARED_LIBRARIES := \
libcrypto\
libssl
$(shell mkdir -p $(TARGET_OUT)/etc/crt)
$(shell cp $(LOCAL_PATH)/crt/chain.crt $(TARGET_OUT)/etc/crt)
3.2、实现双向认证
#include <openssl/ssl.h>
#include <openssl/err.h>
static int internalCertificateVerificationCallback(int preverify_ok, X509_STORE_CTX* x509_ctx)
{
ALOGE("111wwwww:start vefity");
return 1;
}
bool ShowCerts(SSL * ssl)
{
X509 *cert;
char *line;
cert = SSL_get_peer_certificate(ssl);
if(SSL_get_verify_result(ssl) == X509_V_OK){
ALOGI("证书验证通过\n");
}
if (cert != NULL) {
ALOGI("数字证书信息:\n");
line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
ALOGI("证书: %s\n", line);
free(line);
line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
ALOGI("颁发者: %s\n", line);
free(line);
X509_free(cert);
return TRUE;
} else{
ALOGI("无证书信息!\n");
return FALSE;
}
}
void createSocket(){
SSL_CTX *ctx;
SSL *ssl;
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
ctx = SSL_CTX_new(TSL_client_method());
if (ctx == NULL) {
ERR_print_errors_fp(stdout);
ALOGE("SSL_CTX_new kill myself");
kill(getpid(), SIGKILL);
}
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
if (SSL_CTX_load_verify_locations(ctx, "chain.crt",NULL)<=0){
ERR_print_errors_fp(stdout);
ALOGE("SSL_CTX_load_verify_locations kill myself,%s",ERR_error_string( ERR_get_error(), NULL ));
kill(getpid(), SIGKILL);
}
if (SSL_CTX_use_certificate_file(ctx, "certificate.crt", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
ALOGE("SSL_CTX_crt load error kill myself,%s,code = %d",ERR_error_string( ERR_get_error(), NULL ),SSL_CTX_use_certificate_file(ctx, "/data/cipherdata/crt.crt", SSL_FILETYPE_PEM));
kill(getpid(), SIGKILL);
}
if (SSL_CTX_use_PrivateKey_file(ctx, "key.key", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stdout);
ALOGE("SSL_CTX_key load error kill myself,error = %s",ERR_error_string( ERR_get_error(), NULL ));
kill(getpid(), SIGKILL);
}
if (!SSL_CTX_check_private_key(ctx)) {
ERR_print_errors_fp(stdout);
ALOGE("SSL_CTX_key verity error kill myself,error = %s",ERR_error_string( ERR_get_error(), NULL ));
kill(getpid(), SIGKILL);
}
mSocketFd= (int)socket(AF_INET, SOCK_STREAM, 0);
connect(mSocketFd,address,sizeof(address))
if (SSL_connect(ssl) == -1){
ERR_print_errors_fp(stderr);
ALOGE("SSL_connect error :%s",ERR_error_string( ERR_get_error(), NULL ));
closeSocket();
return false;
}else {
ShowCerts(ssl);
}
}
4、重点和难点和遇到的问题
4.1、因ssl协议版本不匹配导致的connect失败
上述代码中,ctx = SSL_CTX_new(TSL_client_method());这个形参的类型是有挺多协议类型的:
1、SSLv2_client_method() 2、SSLv3_client_method() 3、SSLv23_client_method()//包含v2和v3两种 4、TSL_client_method() 5、TSLv1_client_method() …
从笔者开发测试来看,OpenSSL库是可以向下兼容的,也就是说在ssl握手过程中,会优先选择高版本的协议,这里是使用TSL_client_method(),尽量不要写死某个协议(例如TSLv1_client_method),否则一旦对端的协议版本不匹配,ssl_connect就会失败,而这种问题极难看出来原因,解决虽然很容易但是一时半刻想不到这里,会浪费很多时间。笔者在开发测试的过程中就遇到服务器写死了TSLv1_client_method这个类型,导致一直无法连接。
4.2、因socket是非阻塞模式导致的ssl_connect失败
在使用OpenSSL库实现双向认证的前提下,TCP socket套接字如果是非阻塞模式下进行ssl握手,就会一直连接失败,目前笔者这里是用阻塞式,暂时未找到原因!
5、总结
上述两个问题,解决起来其实很容易,问题是定位非常麻烦,c++的程序不像java抛异常给你提示的那么详细,而且实现代码又都在三方库里,看源码很不方便,那么上述俩问题是怎么定位出来的呢?网络相关的程序出问题,使用万能手段网络抓包工具wireshark,先把这个工具安装到电脑上,它可以自己抓包保存成文件,然后在工具中打开文档查看,当然我们要知道,任何抓包工具只能抓当前所在的网卡网络,你不能让电脑上的wireshark去抓手机网卡上的网络数据包,勿慌,adb shell命令可以抓网络包:
adb shell tcpdump i any-w /data/log/xxx.cap
执行此命令即开始抓包,ctrl+c停止命令执行,数据包文件就保存到相应目录,导出来用wireshark查看,同一时刻肯定有很多网络请求经过网卡,根据自己进程访问的是哪个服务器IP来过滤数据,定位到具体的网络请求以后,查看你这一次的ssl握手执行到哪一步,对应去解决问题,wireshark网络数据抓包不只是用在查看ssl握手这里,只要有网络请求,必然能抓包,以后调试服务器接口等等都可以使用它,甚至是追踪网络请求的具体数据字节流,因此称之为万能手段
|