C语言使用openssl库解析TLS报文(SNI和证书)
前言
项目中需要对TLS报文中SNI和证书部分进行过滤,并且之前并没有接触过这方面的内容,为了解决这一需求,期间学习了不少知识,也走了不少弯路。就写下这篇博客记录分享一下。
项目背景:项目是一个类似防火墙的软件,可以过滤特定的TLS流量。实现原理就是通过对TLS数据包流量进行解析,得到想要过滤的字段数据,根据过滤规则决定是否放行该TLS流量。
一、需求概述
本次需要过滤的TLS数据包主要针对SNI和证书,TLS报文的版本为1.2。使用版本为1.0.2k的openssl库进行C编程。
二、总体设计
1.SNI字段的过滤
sni字段存在于Client hello 报文中,在https中表示客户端浏览器对自己要访问域名的说明。openssl中获取sni字段的api也有:
const char *SSL_get_servername(const SSL *s, const int type);
但这个函数依赖于SSL连接,在项目开始探索的初期,我尝试借鉴博客旁路解密https模拟构建ssl连接的方法,发现这种方案需要修改openssl源码,而我本身对openssl库都不了解,修改源码更是难上加难,当时并没有放弃,我想着先了解openssl再去看源码,当时还发现了一本好书:《深入浅出 https从原理到实战》,如果你和我一样并不了解openssl库以及tls,可以先看这本书搭建起来知识框架,实现对openssl以及tls的一些基本了解。
看完这本书后,我尝试阅读openssl源码,发现对openssl的了解依旧不够深入,现在我已经对openssl的大致框架有了了解,想着先用好openssl再去修改源码。这一阶段我阅读了openssl库的中文api手册,对openssl有了更近一步的了解,也尝试参考openssl提供的examples写出了一对简易的server和client程序。
在阅读api和写server/client程序中,我发现一些库并不依赖于ssl连接,于是我去搜索有没有不依赖于ssl连接提取sni的api,在stack overflow上有大牛自己动手实现了一个(链接我找不到了)。代码我贴在下面:
static char* get_server_extension_name(const char* data, uint32_t datalen) {
/* Skip past fixed length records:
1 Handshake Type
3 Length
2 Version (again)
32 Random
next Session ID Length
*/
int pos = 38;
/* session id */
if (datalen < pos + 1)
return NULL;
uint16_t len = data[pos];
pos += len + 1;
/* Cipher Suites */
if (datalen < pos + 2)
return NULL;
memcpy(&len, &data[pos], 2);
len = ntohs(len);
pos += len + 2;
/* Compression Methods */ if (datalen < pos + 1)
return NULL;
len = data[pos];
pos += len + 1;
/* Extensions */
if (datalen < pos + 2)
return NULL;
memcpy(&len, &data[pos], 2);
len = ntohs(len); // 此时len为Extensions的长度
pos += 2;
// parse extensions to get sni
uint16_t extension_item_len;
/* Parse each 4 bytes for the extension header */
while (pos + 4 <= len) {
memcpy(&extension_item_len, &data[pos + 2], 2);
extension_item_len = ntohs(extension_item_len);
if (data[pos] == 0x00 && data[pos + 1] == 0x00) { // sni 字段
if (pos + 4 + extension_item_len > len)
return NULL;
// get sni string
pos += 6;
uint16_t server_name_len;
uint16_t extension_end = pos + extension_item_len - 2;
while (pos + 3 < extension_end) {
memcpy(&server_name_len, &data[pos + 1], 2);
server_name_len = ntohs(server_name_len);
if (pos + 3 + server_name_len > extension_end)
return NULL;
char* hostname = (char*)malloc(server_name_len + 1);
switch (data[pos]) {
case 0x00: /*host name*/
if (hostname == NULL) {
fprintf(stderr, "malloc hostname failed!\n");
return NULL;
}
strncpy(hostname, (char*)(data + pos + 3),
server_name_len);
hostname[server_name_len] = '\0';
return hostname;
break;
default:
puts("encouter error! debug me....");
}
pos += 3 + len;
}
}
pos += 4 + extension_item_len;
}
return NULL;
}
虽然不是我自己动手写出来的sni字段提取的代码,但在此期间学习一些openssl和tls知识也算是有点收获吧。
2.证书的解析
TLS中的证书在openssl库中用X509表示,X509证书库封装了一些对证书的操作,这部分调库即可。对于证书,需要验证证书链、服务器实体证书的校验:与域名匹配、有效期等。
参考博客中内容,实现了以下函数
// 读取内存中(数组)的证书,将其解析为X509结构体
const unsigned char *data = ... ;
size_t len = ... ;
X509 *cert = d2i_X509(NULL, &data, len);
if (!cert) {
fprintf(stderr, "unable to parse certificate in memory\n");
return EXIT_FAILURE;
}
// any additional processing would go here..
X509_free(cert);
/**
* @brief 创建一个证书链,方便后续新增需求,方便后续验证证书链
*
* @param data
* @param datalen
* @return int
*/
int create_stack(const unsigned char* data,
uint32_t datalen,
STACK_OF(X509) * sk) {
uint32_t len;
uint32_t pos = 0;
while (pos + 3 < datalen) {
ntoh(data + pos, &len);
if (len == 0)
return 1;
const unsigned char* tmp = data + pos + 3;
X509* cert = d2i_X509(NULL, &tmp, len);
if (cert == NULL) {
fprintf(stderr, "parse certificates failed!\n");
return -1;
}
// verify signature,只能实现对self signature的验证
EVP_PKEY* pkey = X509_get_pubkey(cert);
if (pkey == NULL) {
fprintf(stderr, "%s\n", ERR_error_string(ERR_get_error(), NULL));
return 0;
}
int r = X509_verify(cert, pkey);
if (r <= 0) {
fprintf(stderr, "certificate signature error!\n");
return 0;
}
EVP_PKEY_free(pkey);
sk_X509_push(sk, cert);
pos += 3 + len;
}
return 1;
}
/**
* @brief 检查证书的有效期
*
* @param cert
* @return true
* @return false
*/
bool check_certificate_validity(X509* cert) {
if (cert == NULL)
return false;
ASN1_TIME* not_before = X509_get_notBefore(cert);
ASN1_TIME* not_after = X509_get_notAfter(cert);
int day, sec;
if (!ASN1_TIME_diff(&day, &sec, NULL, not_before)) {
fprintf(stderr, "asn1 time format error!\n");
return false;
}
if (day >= 0 || sec >= 0) {
return false;
}
if (!ASN1_TIME_diff(&day, &sec, NULL, not_after)) {
fprintf(stderr, "asn1 time format error!\n");
return false;
}
if (day <= 0 || sec <= 0) {
return false;
}
return true;
}
/**
* @brief 从证书中加载位置信息到location中
*
* @param location
* @return int
*/
int get_subject_location_string(STACK_OF(X509) * sk,
char* location[ENTRY_DEPTH]) {
unsigned len = sk_X509_num(sk);
if (len == 0) { //空的证书链
return -1;
}
// 提取subject的信息,进行过滤
X509* cert = sk_X509_value(sk, 0);
X509_NAME* subj = X509_get_subject_name(cert);
for (int i = 0; i < X509_NAME_entry_count(subj); i++) {
X509_NAME_ENTRY* e = X509_NAME_get_entry(subj, i);
ASN1_STRING* d = X509_NAME_ENTRY_get_data(e);
int nid = OBJ_obj2nid(X509_NAME_ENTRY_get_object(e));
char* value = (char*)ASN1_STRING_data(d);
location[nid - NID_commonName] = new char[strlen(value) + 1];
// (char*)malloc(sizeof(char) * (strlen(value) + 1));
strncpy(location[nid - NID_commonName], value, strlen(value));
location[nid - NID_commonName][strlen(value)] = '\0';
// puts(value);
}
return 0;
}
总结
作为一个编程菜鸟,对于开源库的使用并没有什么经验,通过此次openssl的学习使用,我能得到如下经验教训:
对于openssl的学习过程,应该要先看书/资料对openssl本身要有一个大致的了解,然后再去学习怎么使用,最后才应该是挖掘它的实现原理以及阅读/修改源码。这样循序渐进的学习方式看似耗时实则效率最高。
最后,附上此项目的源代码:实现了从捕获流量-->tcp报文的乱序重组-->tls的解析过滤