Gmssl编程之TLS通信

前言

最近在整理电脑上项目工程时,发现之前用来测试gmtls通信编写的c/s测试代码。因此顺便整理了下,记录下来,方便以后回忆。

概述

进行SSL编程实现之前,我们肯定已经对SSL原理有了一定的了解。这里,小编也不再赘述。(尚不了解的可以参考这篇文章:SSL/TLS原理 详细整理版

实现流程

原理理解的差不多了,就可以开始使用gmssl编程实现基于tls的socket通讯了(PS. gmssl中已经做了大部分事情了,我们只需要调用一些基本的接口就可以完成TLS socket通讯)。具体分为以下几步:

  1. 初始化
    这一步主要是初始化openssl库,创建会话上下文等。调用的主要接口如下:
//SSL 库初始化 
SSL_library_init();
//载入所有 SSL 算法
//OpenSSL_add_all_algorithms();
//加载SSL错误信息 
SSL_load_error_strings();
//指定服务端使用的协议
const SSL_METHOD *GMTLS_server_method(void); /* GMTLSv1.1 */ 
//指定客户端使用的协议
const SSL_METHOD *GMTLS_client_method(void); /* GMTLSv1.1 */
//建立SSL上下文 
SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth);
  1. 加载证书和私钥
    证书和私钥包括CA证书,服务端签名证书和私钥,服务端加密证书和私钥,客户端证书和私钥。调用的接口有:
//指定所支持的密码套件
int SSL_CTX_set_cipher_list(SSL_CTX *, const char *str);
//此函数用来便是加载CA证书文件的
int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath);
//加载自己的证书文件.
int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type);
//加载自己的私钥,以用于签名.
int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
//检查用户私钥是否正确
int SSL_CTX_check_private_key(const SSL_CTX *ctx);
//加载私钥时一般要一个验证密码,这个密码是由生成私钥的一方来设定的,客户端得到这个密码后要通过一个接口传入验证
void SSL_CTX_set_default_passwd_cb_userdata(SSL_CTX *ctx, void *u);
//缺省mode是SSL_VERIFY_NONE,如果想要验证对方的话,便要将此项变成SSL_VERIFY_PEER.SSL/TLS中缺省只验证server,如果没有设置 SSL_VERIFY_PEER的话,客户端连证书都不会发过来.
void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, SSL_verify_cb callback);
  1. 第三步,建立ssl socket连接
    首先要建立普通的socket连接,TCP连接成功后再与ssl进行关联。三个接口就可以完成:
//创建SSL对象
SSL *SSL_new(SSL_CTX *ctx);
//将普通的SOCKET加入到ssl
int SSL_set_fd(SSL *s, int fd);
//服务端接受客户端ssl连接
int SSL_accept(SSL *ssl);
//客户端连接服务端SSL
int SSL_connect(SSL *ssl);
  1. 第四步,发送和接收
//发送数据
int SSL_write(SSL *ssl, const void *buf, int num);
//接收数据
int SSL_read(SSL *ssl, void *buf, int num);
  1. 第五步,释放资源
    最后就是记得把所有占用的资源都释放掉。首先要把普通的socket关闭,然后是和ssl相关的资源释放。调用的主要接口如下:
SSL_CTX_free(ctx);
SSL_shutdown(ssl);
SSL_free(ssl);
ERR_free_strings();

PS. 想了解更多接口信息可以参考这篇文章:openssl编程——SSL实现

注意事项

  • GmSSL实现gmtls协议时,服务端必须设置双证书(签名证书和加密证书)才能正常通信;
  • 在设置双证书时,需要先设置签名证书,然后再设置加密证书;
  • 如果服务端只设置了一种加密套件,那么客户端要么接受要么返回错误。加密套件的选择是由服务端做出的。
  • 这里提供一个很好的国密网站:免费申请国密证书,功能很强大,大家可以在正上面申请证书哦~

示例

服务端

// TestServer.cpp : 定义控制台应用程序的入口点。
//


#include "stdafx.h"

#ifdef WIN32 
#include "Winsock2.h"
#pragma comment(lib, "ws2_32.lib")
#else
#include "sys/socket.h"
#endif

#include <iostream>
#include <conio.h>
using namespace std;

#include <openssl/err.h>
#include <openssl/ssl.h>

#define IPADDRESS   "127.0.0.1"
#define PORT        443
#define LISTENQ     1
#define MAXBUF      1024 

#define CA_CERT_FILE 		"GMCert_GMCA01.cert.pem"
#define SIGN_CERT_FILE 		"server.cert.pem"
#define SIGN_KEY_FILE 		"server.key.pem"
#define ENC_CERT_FILE 	    "server_enc.cert.pem"
#define ENC_KEY_FILE 	    "server_enc.key.pem"
#define CIPHER_


int config_ssl_ctx(SSL_CTX *ctx)
{
	//SSL_CTX_set_cipher_list(ctx, "SM2-WITH-SMS4-SM3");

#if 1
	// 是否要求校验对方证书 若不验证客户端身份则设置为: SSL_VERIFY_NONE
	SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
#else
	//验证对方
	SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
	//若验证,则放置CA证书
	int ret = SSL_CTX_load_verify_locations(ctx, CA_CERT_FILE, NULL);
	if (ret < 0)
	{
		printf("SSL_CTX_load_verify_locations failed.");
	}
	//设置pass phrase
	SSL_CTX_set_default_passwd_cb_userdata(ctx, "12345678");
#endif

	//双证书模式,需要先设置签名证书,然后再设置加密证书
	//载入服务端数字签名证书
	if (SSL_CTX_use_certificate_file(ctx, SIGN_CERT_FILE, SSL_FILETYPE_PEM) <= 0)
	{
		ERR_print_errors_fp(stderr);
		return(1);
	}
	//载入服务端签名私钥
	if (SSL_CTX_use_PrivateKey_file(ctx, SIGN_KEY_FILE, SSL_FILETYPE_PEM) <= 0)
	{
		ERR_print_errors_fp(stdout);
		return(1);
	}
	//检查用户私钥是否正确
	if (!SSL_CTX_check_private_key(ctx))
	{
		ERR_print_errors_fp(stdout);
		return(1);
	}

	//载入服务端加密证书
	if (SSL_CTX_use_certificate_file(ctx, ENC_CERT_FILE, SSL_FILETYPE_PEM) <= 0)
	{
		ERR_print_errors_fp(stderr);
		return(1);
	}
	//载入加密私钥
	if (SSL_CTX_use_PrivateKey_file(ctx, ENC_KEY_FILE, SSL_FILETYPE_PEM) <= 0)
	{
		ERR_print_errors_fp(stdout);
		return(1);
	}
	//检查用户私钥是否正确
	if (!SSL_CTX_check_private_key(ctx))
	{
		ERR_print_errors_fp(stdout);
		return(1);
	}

	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	SOCKET sockfd;
	struct sockaddr_in my_addr;   //一般是储存地址和端口的。用于信息的显示及存储使用
	SOCKET new_fd;
	struct sockaddr_in their_addr;  //用于存储 当连接成功时所返回的客户的TCP连接信息。
	char buf[MAXBUF + 1];

	struct fd_set fds;  //定义一个读(接受消息)的集合
	struct timeval timeout = { 2, 0 }; // select 等待 3 秒,3 秒轮询, 要非阻塞就置 0
	int maxfdp;  //集合中所有文件描述符的范围,即所有文件描述符的最大值加1
	int len;
	
	SSL_CTX *ctx = NULL;
	SSL_METHOD *meth;

	//SSL 库初始化 
	SSL_library_init();
	//载入所有 SSL 算法
	//OpenSSL_add_all_algorithms();
	//加载SSL错误信息 
	SSL_load_error_strings();

	//meth = (SSL_METHOD *)SSLv23_server_method();	
	//gmssl双证书通信
	meth = (SSL_METHOD *)GMTLS_server_method();  

	//建立新的SSL上下文 
	ctx = SSL_CTX_new(meth);
	if (ctx == NULL)
	{
		ERR_print_errors_fp(stdout);
		cout << "SSL_CTX_new error!" << endl;
		exit(1);
	}
	if (0 != config_ssl_ctx(ctx))
	{
		ERR_print_errors_fp(stdout);
		cout << "config_ssl_ctx error!" << endl;
		SSL_CTX_free(ctx);
		getchar();
		exit(1);
	}
	
#ifdef WIN32 
	WSADATA wsaData;
	//初始化windows socket资源   绑定socket库 版本为2.2
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		cout << "WSAStartup error" << endl;
		return 1;
	}
#endif
	//创建socket
	sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sockfd == INVALID_SOCKET)
	{
		cout << "socket error" << endl;
		goto END2;
	}

	//设置 sockaddr_in 结构体中相关参数
	memset(&my_addr, 0, sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(PORT);
	my_addr.sin_addr.s_addr = inet_addr(IPADDRESS);  //
	//将一本地地址与一套接口捆绑
	if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1)
	{
		cout << "bind error" << endl;
		goto END1;
	}
	cout << "binded" << IPADDRESS << PORT << endl;

	//监听这个socket
	if (listen(sockfd, LISTENQ) == -1)
	{
		cout << "listen error" << endl;
		goto END1;
	}
	cout << "begin listen..." << endl;


	//不停的select才可以读取套接字的状态改变
	while (true)
	{
		//控制退出循环
		if (_kbhit())
			break;

		FD_ZERO(&fds);        // 每次循环都要清空,否则不能检测描述符变化
		FD_SET(sockfd, &fds); // 添加套接字描述符
		maxfdp = sockfd + 1;  //描述符最大值加1

		//利用select选择出集合中可以读写的多个套接字,有点像筛选
		int ret = select(maxfdp, &fds, &fds, NULL, &timeout);
		if (ret == SOCKET_ERROR)
		{
			//select 错误,退出程序
			break;
		}
		else if (ret == 0)
		{
			//等待超时,没有可读写或错误的文件, 则再次轮询
			continue;
		}
		else
		{
			if (FD_ISSET(sockfd, &fds)) // 测试sockfd是否可读,即是否网络上有数据
			{
				len = sizeof(struct sockaddr);
				//取得一个套接口接受的一个连接
				new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &len);
				if (new_fd == INVALID_SOCKET)
				{
					perror("accept error");
					//exit(errno);
					break;
				}
				printf("server: got connection from %s, port %d, socket %d \n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);

				//TCP连接已经建立,执行Server SSL
				SSL *ssl = SSL_new(ctx);
				//SOCKET加入到ssl
				SSL_set_fd(ssl, new_fd);
				//建立ssll连接
				if (SSL_accept(ssl) == -1)
				{
					perror("SSL_accept failed.");
#ifdef WIN32 
					closesocket(new_fd);
#else
					close(new_fd);
#endif
					break;
				}
				/*打印所有加密算法的信息(可选)*/
				printf("SSL connection using %s\n", SSL_get_cipher(ssl));

				//发消息给客户端		
				memset(buf, 0, MAXBUF + 1);
				strcpy(buf, "let's do someting...");
				//len = send(new_fd, buf, strlen(buf), 0);
				len = SSL_write(ssl, buf, strlen(buf));
				if (len < 0)
				{
					printf("message send failed! [%s] errcode=%d,error message'%s' \r\n", buf, errno, strerror(errno));
					goto finish;
				}
				printf("message send succes, [%s] total send %d bytle!  \r\n", buf, len);


				//接收客户端消息	
				memset(buf, 0, MAXBUF + 1);
				//len = recv(new_fd, buf, sizeof(buf), 0);
				len = SSL_read(ssl, buf, MAXBUF);
				if (len > 0)
					printf("recive message succes :'%s',total %d bytle data  \r\n", buf, len);
				else
					printf("recive message error! errcode id=%d,error message is '%s'  \r\n", errno, strerror(errno));


			finish:
				SSL_shutdown(ssl);
				SSL_free(ssl);
#ifdef WIN32 
				closesocket(new_fd);
#else
				close(new_fd);
#endif
			}
		}
	}


END1:
	SSL_CTX_free(ctx);
	ERR_free_strings();
	//关闭套接字
#ifdef WIN32 
	closesocket(sockfd);
#else
	close(sockfd);
#endif
END2:
#ifdef WIN32 
	WSACleanup(); //解除绑定,释放资源
#endif


	return 0;
}



客户端

// TestClient.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#ifdef WIN32 
#include "Winsock2.h"
#pragma comment(lib, "ws2_32.lib")
#else
#include "sys/socket.h"
#endif

#include <iostream>
using namespace std;

#include <openssl/err.h>
#include <openssl/ssl.h>

#define MAXBUF 1024
#define SERVER_IP      "127.0.0.1"  //"172.16.0.175"
#define SERVER_PORT    443

#define CA_CERT_FILE 		"GMCert_GMCA01.cert.pem"
#define USR_CERT_FILE 	    "client.cert.pem"
#define USR_KEY_FILE 	    "client.key.pem"

void ShowCerts(SSL *ssl)
{
	X509 *cert;
	char *subj = NULL, *issuer = NULL;
	EVP_PKEY *pstPubKey;
	int iPubKeyLen;

	printf("-------show cert information---------\n");
	//从SSL连接中获取证书信息
	cert = SSL_get_peer_certificate(ssl);
	if (cert != NULL)
	{
		// 获取真实证书的公钥
		pstPubKey = X509_get_pubkey(cert);
		// 获取真实证书中公钥的密钥长度
		iPubKeyLen = EVP_PKEY_bits(pstPubKey);
		if (iPubKeyLen == 0)
		{
			cout << "Get num bits from real cert failed!" << endl;
		}
		else
		{
			printf("Bytes size: %d, Bits length: %d. \n", EVP_PKEY_size(pstPubKey), iPubKeyLen);
		}

		// 获取真实证书的持有者信息X509_get_subject_name (同上,假设已经提前获取了指向X509结构真实证书的指针cert)  
		// 将结构体形式的持有者信息输出为一行的形式(X509_NAME_oneline):/type0=value0/type1=value1/type2=...
		subj = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
		printf("cert: %s\n", subj);
		OPENSSL_free(subj);
		issuer = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
		printf("Issuer: %s\n", issuer);
		OPENSSL_free(issuer);
		X509_free(cert);
	}
	else
	{
		printf("no cert message \n");
	}
	printf("-------show cert info end---------\n");
}

int config_ssl_ctx(SSL_CTX *ctx)
{
	//SSL_CTX_set_cipher_list(ctx, "SM2-WITH-SMS4-SM3");

#if 0
	//要求校验对方证书,表示需要验证服务器端,若不需要验证则使用  SSL_VERIFY_NONE
	SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
#else
	/* Set flag in context to require peer (server) certificate verification */
	SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
	/* Load the  CA certificate into the SSL_CTX structure */
	/* This will allow this client to verify the server's certificate. */
	int ret = SSL_CTX_load_verify_locations(ctx, CA_CERT_FILE, NULL);
	if (ret < 0)
	{
		printf("SSL_CTX_load_verify_locations failed.");
	}	
#endif

#if 0
	//载入客户端数字证书
	if (SSL_CTX_use_certificate_file(ctx, USR_CERT_FILE, SSL_FILETYPE_PEM) <= 0)
	{
		ERR_print_errors_fp(stderr);
		exit(1);
	}

	//载入客户端私钥
	if (SSL_CTX_use_PrivateKey_file(ctx, USR_KEY_FILE, SSL_FILETYPE_PEM) <= 0)
	{
		ERR_print_errors_fp(stdout);
		exit(1);
	}
	//检查私钥是否正确
	if (!SSL_CTX_check_private_key(ctx))
	{
		ERR_print_errors_fp(stdout);
		exit(1);
	}
#endif

	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	SOCKET sockfd;
	struct sockaddr_in dest;
	char buffer[MAXBUF + 1] = { 0 };
	int len = 0;

	SSL_CTX *ctx;
	SSL *ssl;
	SSL_METHOD *meth;

	//SSL 库初始化 
	SSL_library_init();
	//载入所有 SSL 算法
	//OpenSSL_add_all_algorithms();
	//加载SSL错误信息 
	SSL_load_error_strings();

	//meth = (SSL_METHOD *)SSLv23_client_method();
	//gmssl双证书通信
	meth = (SSL_METHOD *)GMTLS_client_method();

	//建立新的SSL上下文 
	ctx = SSL_CTX_new(meth);
	if (ctx == NULL)
	{
		ERR_print_errors_fp(stdout);
		exit(1);
	}
	if (0 != config_ssl_ctx(ctx))
	{
		ERR_print_errors_fp(stdout);
		cout << "config_ssl_ctx error!" << endl;
		SSL_CTX_free(ctx);
		getchar();
		exit(1);
	}

#ifdef WIN32 
	WSADATA wsaData;
	//初始化socket资源   绑定socket库 版本为2.2
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		cout << "WSAStartup error" << endl;
		return 1;
	}
#endif

	//创建客户端套节字
	sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sockfd == INVALID_SOCKET)
	{
		cout << "socket error" << endl;
#ifdef WIN32 
		WSACleanup();
#endif
		exit(errno);
	}
	cout << "socket created." << endl;

	//设置远程服务器的地址信息(端口号、IP地址等)
	memset(&dest, 0, sizeof(dest));
	dest.sin_family = AF_INET;
	dest.sin_port = htons(SERVER_PORT);
	dest.sin_addr.s_addr = inet_addr(SERVER_IP);
	//连接服务器   连接后可以用sockfd来使用这个连接   dest保存了远程服务器的地址信息 
	if (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0)
	{
		perror("Connect error!");
		exit(errno);
	}
	printf("server connected\n");

	//TCP连接已经建立,将连接付给SSL
	ssl = SSL_new(ctx);
	//socket加入到ssl
	SSL_set_fd(ssl, sockfd);
	//建立ssl连接
	if (SSL_connect(ssl) == -1)
	{
		printf("SSL_connect failed\n");
		ERR_print_errors_fp(stderr);
	}
	else
	{
		/*打印所有加密算法的信息(可选)*/
		printf("-------show used cipher---------\n");
		printf("%s\n", SSL_get_cipher(ssl));
		printf("-------show cipher end---------\n");

		ShowCerts(ssl);
	}

	//接收服务器来的消息
	memset(buffer, 0, MAXBUF + 1);
	//len = recv(sockfd, buffer, sizeof(buffer), 0);
	len = SSL_read(ssl, buffer, MAXBUF);
	if (len <= 0)
	{
		printf("recive message failed %d,error message is '%s'  \r\n", errno, strerror(errno));
		goto END;
	}
	printf("recive message succes:'%s',total %d bytle data \r\n", buffer, len);

	//发消息给服务器
	len = 0;
	memset(buffer, 0, MAXBUF + 1);
	strcpy(buffer, "Hi server, i am client.");
	//len = send(sockfd, buffer, strlen(buffer), 0);
	len = SSL_write(ssl, buffer, strlen(buffer));
	if (len < 0)
		printf("message'%s'send failed!error code id %d,error message is '%s'  \r\n", buffer, errno, strerror(errno));
	else
		printf("message'%s'send success,total send %d bytle data!  \r\n", buffer, len);

	getchar();


END:
	SSL_shutdown(ssl);
	SSL_free(ssl);
#ifdef WIN32 
	closesocket(sockfd);
#else
	close(sockfd);
#endif
	SSL_CTX_free(ctx);
	ERR_free_strings();
#ifdef WIN32 
	WSACleanup(); //解除绑定 释放资源
#endif
	system("pause");
	return 0;
}


运行结果

在这里插入图片描述
在这里插入图片描述

抓包分析

通过wireshark抓包如下。
在这里插入图片描述
从上面的抓包结果我们看到SSL通信过程中的每一包数据,但用过抓包工具的童鞋都知道这个结果似乎不是很直观(并没有将其client hello,server hello, Key Exchange…等这些过程标准出来)。需要我们对于每一包数据进行解析,与SSL通信协议进行比以确定去确实是遵循的ssl协议。

其实还有一个取巧的办法:wireshark之所以无法解析通信的每一个步骤,是因为其无法识别gmtls的协议版本(国际规范中TLS协议号为0x0301、0x0302、0x0303,分别表示TLS1.0 、TLS1.1 、TLS1.2;而国密SSL版本号为0x0101,其参考了TLS1.1)。
因此,我们可以将数据包中的版本号(16 01 01)手动修改为wireshark可识别的TLS协议号(如:16 03 01),然后重新用wireshark打开就能识别了~如下所示:
在这里插入图片描述

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值