c++使用OpenSSL基于socket实现tcp双向认证ssl(使用TSL协议)代码实现

相信各位对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
//项目中可以将某个文件拷贝到image镜像中,TARGET_OUT指的是根目录下的system/目录
$(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>
//验证证书的函数回调,返回1的时候,无论对端证书是否正确都会放行
static int internalCertificateVerificationCallback(int preverify_ok, X509_STORE_CTX* x509_ctx)
{
    //preverify_ok contains 1 if the pre-verification succeeded, 0 otherwise.
	ALOGE("111wwwww:start vefity");

    return 1; // This accepts every certificate
}

bool ShowCerts(SSL * ssl)
{
    X509 *cert;
    char *line;

    cert = SSL_get_peer_certificate(ssl);
    // SSL_get_verify_result()是重点,SSL_CTX_set_verify()只是配置启不启用并没有执行认证,调用该函数才会真证进行证书认证
    // 如果验证不通过,那么程序抛出异常中止连接
    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 库初始化 */
    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);
    }
    /* 加载三个证书 */
    // 双向验证, internalCertificateVerificationCallback
	// SSL_VERIFY_PEER---要求对证书进行认证,没有证书也会放行
	// SSL_VERIFY_FAIL_IF_NO_PEER_CERT---要求客户端需要提供证书,但验证发现单独使用没有证书也会放行
	//SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT
	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);
	}
    //**这里创建一个socket,从这里开始是伪代码!!!就是创建一个正常连接成功的socket
    mSocketFd= (int)socket(AF_INET, SOCK_STREAM, 0);
    //connect,假设这里成功
    connect(mSocketFd,address,sizeof(address))
     /* 建立 SSL 连接 */
     ssl = SSL_new(ctx);
     if(ssl){
         SSL_set_fd(ssl, m_socket_id);
     }
     //SSL_shutdown(ssl);
     //SSL_free(ssl);
     //SSL_CTX_free(ctx);
      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);//返回true就是成功
             }
}
//当连接成功以后就可以使用SSL_write、SSL_read来收发数据,这里有个技巧,可能读者觉得这俩函数不太熟悉,不知道怎么使用,但是socket的send、recv函数应该是一直在使用的,直接把socket的send、recv替换成这俩ssl的读写函数就行,总归一句话,tcp的ssl双向认证,是基于socket开发的,先实现正常明文socket开发,然后把上述代码套进去,OpenSSL的双向认证就完成了

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握手这里,只要有网络请求,必然能抓包,以后调试服务器接口等等都可以使用它,甚至是追踪网络请求的具体数据字节流,因此称之为万能手段

  • 11
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 OpenSSL 库可以更方便地实现 RSA 加密算法。以下是一个简单的示例程序,展示了如何使用 OpenSSL 库进行 RSA 1024 加密。 首先,需要安装 OpenSSL 库,并在程序中包含相应的头文件: ```c++ #include <openssl/rsa.h> #include <openssl/pem.h> ``` 接着,可以使用 OpenSSL 库中的函数生成 RSA 密钥对: ```c++ RSA *keypair = RSA_new(); BIGNUM *e = BN_new(); int bits = 1024; unsigned long e_value = RSA_F4; BN_set_word(e, e_value); RSA_generate_key_ex(keypair, bits, e, NULL); ``` 这里使用了 `RSA_new()` 函数创建了一个 RSA 结构体,使用 `BN_new()` 函数创建了一个大数结构体 `e`,表示公钥指数 `e` 的值,使用 `RSA_generate_key_ex()` 函数生成了 RSA 密钥对,其中 `bits` 表示密钥长度,`e` 表示公钥指数,`NULL` 表示使用默认的随机数生成器。 生成密钥对后,就可以使用公钥加密数据了。以下是一个示例函数,用于 RSA 加密: ```c++ // 使用公钥加密数据 // 返回加密后的数据长度,加密失败返回0 int rsa_encrypt(const unsigned char *src, int src_len, unsigned char *dst, RSA *key) { int flen = RSA_size(key); int ret = RSA_public_encrypt(src_len, src, dst, key, RSA_PKCS1_PADDING); if (ret != flen) { return 0; } return ret; } ``` 其中,`src` 表示要加密的数据,`src_len` 表示数据长度,`dst` 表示加密后的数据,`key` 表示公钥。 接着,可以使用私钥解密数据。以下是一个示例函数,用于 RSA 解密: ```c++ // 使用私钥解密数据 // 返回解密后的数据长度,解密失败返回0 int rsa_decrypt(const unsigned char *src, int src_len, unsigned char *dst, RSA *key) { int flen = RSA_size(key); int ret = RSA_private_decrypt(src_len, src, dst, key, RSA_PKCS1_PADDING); if (ret <= 0 || ret >= flen) { return 0; } return ret; } ``` 其中,`src` 表示要解密的数据,`src_len` 表示数据长度,`dst` 表示解密后的数据,`key` 表示私钥。 完整的示例程序如下: ```c++ #include <iostream> #include <cstring> #include <openssl/rsa.h> #include <openssl/pem.h> using namespace std; // 使用公钥加密数据 // 返回加密后的数据长度,加密失败返回0 int rsa_encrypt(const unsigned char *src, int src_len, unsigned char *dst, RSA *key) { int flen = RSA_size(key); int ret = RSA_public_encrypt(src_len, src, dst, key, RSA_PKCS1_PADDING); if (ret != flen) { return 0; } return ret; } // 使用私钥解密数据 // 返回解密后的数据长度,解密失败返回0 int rsa_decrypt(const unsigned char *src, int src_len, unsigned char *dst, RSA *key) { int flen = RSA_size(key); int ret = RSA_private_decrypt(src_len, src, dst, key, RSA_PKCS1_PADDING); if (ret <= 0 || ret >= flen) { return 0; } return ret; } int main() { RSA *keypair = RSA_new(); BIGNUM *e = BN_new(); int bits = 1024; unsigned long e_value = RSA_F4; BN_set_word(e, e_value); RSA_generate_key_ex(keypair, bits, e, NULL); // 输出公钥和私钥 BIO *bp_private = BIO_new(BIO_s_mem()); BIO *bp_public = BIO_new(BIO_s_mem()); PEM_write_bio_RSAPrivateKey(bp_private, keypair, NULL, NULL, 0, NULL, NULL); PEM_write_bio_RSAPublicKey(bp_public, keypair); char *private_key; char *public_key; long private_key_len = BIO_get_mem_data(bp_private, &private_key); long public_key_len = BIO_get_mem_data(bp_public, &public_key); cout << "Private Key: " << endl << string(private_key, private_key_len) << endl; cout << "Public Key: " << endl << string(public_key, public_key_len) << endl; // 加密 const unsigned char *msg = (const unsigned char *)"Hello, world!"; int msg_len = strlen((const char *)msg); unsigned char ciphertext[4096]; int ciphertext_len = rsa_encrypt(msg, msg_len, ciphertext, keypair); cout << "Encrypted Message: " << endl; for (int i = 0; i < ciphertext_len; i++) { printf("%02x", ciphertext[i]); } cout << endl; // 解密 unsigned char plaintext[4096]; int plaintext_len = rsa_decrypt(ciphertext, ciphertext_len, plaintext, keypair); cout << "Decrypted Message: " << endl; cout << string((const char *)plaintext, plaintext_len) << endl; RSA_free(keypair); BN_free(e); BIO_free_all(bp_private); BIO_free_all(bp_public); return 0; } ``` 该程序使用 OpenSSL 库生成 RSA 密钥对,并使用公钥加密和私钥解密数据。注意,为了方便输出公钥和私钥,该程序使用了 `PEM_write_bio_RSAPrivateKey()` 和 `PEM_write_bio_RSAPublicKey()` 函数将密钥转换成 PEM 格式输出。在实际应用中,可以直接使用 RSA 结构体表示密钥。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值