前言
最近在整理电脑上项目工程时,发现之前用来测试gmtls通信编写的c/s测试代码。因此顺便整理了下,记录下来,方便以后回忆。
概述
进行SSL编程实现之前,我们肯定已经对SSL原理有了一定的了解。这里,小编也不再赘述。(尚不了解的可以参考这篇文章:SSL/TLS原理 详细整理版)
实现流程
原理理解的差不多了,就可以开始使用gmssl编程实现基于tls的socket通讯了(PS. gmssl中已经做了大部分事情了,我们只需要调用一些基本的接口就可以完成TLS socket通讯)。具体分为以下几步:
- 初始化
这一步主要是初始化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);
- 加载证书和私钥
证书和私钥包括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);
- 第三步,建立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);
- 第四步,发送和接收
//发送数据
int SSL_write(SSL *ssl, const void *buf, int num);
//接收数据
int SSL_read(SSL *ssl, void *buf, int num);
- 第五步,释放资源
最后就是记得把所有占用的资源都释放掉。首先要把普通的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打开就能识别了~如下所示: