使用C语言开发OpenSSL中TLS证书的校验
使用C语言开发OpenSSL中TLS证书的校验
开发环境
- ubuntu 16.04 WSL(Windows 10 内建的Linux系统)
- Libssl-dev (openssl 的库)
-
Clion
开发环境的搭建
- WSL环境安装
参考 WSL(Windows Subsystem for Linux)的安装与使用 -
安装必备的开发环境包
我的Ubuntu 开发环境配置sudo apt-get update
sudo apt-get install build-essential
安装 Libssl-dev
sudo apt-get update
sudo apt-install openssl Libssl-dev
使用默认的Ubuntu源中提供的库,我这版本是1.0.2g 如果自己下载源码编译可能会遇到一些问题在windows上安装CLion IDE
CLion 2018.1.2 支持WSL 环境的开发
部署方式参见官方文档 How to Use WSL Development Environment in CLion
新建项目,在CMakeLists.txt
中添加
target_link_libraries(certcheck crypto)
target_link_libraries(certcheck ssl)
其中certcheck
为可执行文件的名字
- WSL环境安装
OpenSSL 的 API
X509是TLS/SSL证书的标准,所以在证书的处理中,很多OpenSSL 证书处理的函数的都是以它开头
* 关于TLS/SSL 证书的协议 X.509
1. 引入头文件
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/bio.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/rsa.h>
2. 读取证书文件
BIO *certificate_bio = NULL;
X509 *cert = NULL;
//initialise openSSL
OpenSSL_add_all_algorithms();
ERR_load_BIO_strings();
ERR_load_crypto_strings();
//create BIO object to read certificate
certificate_bio = BIO_new(BIO_s_file());
//Read certificate into BIO
if (!(BIO_read_filename(certificate_bio, cert_path))) {
fprintf(stderr, "Error in reading cert BIO filename");
exit(EXIT_FAILURE);
}
if (!(cert = PEM_read_bio_X509(certificate_bio, NULL, 0, NULL))) {
fprintf(stderr, "Error in loading certificate");
exit(EXIT_FAILURE);
}
其中 cert
就是我们在后面处理中证书变量的指针
3. 证书有效日期的校验
证书的时间可以读取到一个ASN1_TIME
的结构类型中读取证书时间
ASN1_TIME *notBefore = X509_get_notBefore(cert); //读取开始时间
ASN1_TIME *notAfter = X509_get_notAfter(cert); //读取结束时间
与当前时间比较
int day = 0;
int sec = 0;
ASN1_TIME_diff(&day, &sec, notBefore, NULL);
NULL表示拿当前时间减notBefore的时间,然后把结果存在day和sec中,如果day和sec有一个为负数,则表示当前时间小于notBefore
ASN1_TIME_diff(&day, &sec, NULL, notAfter);//那证书的结束时间与当前时间比较
4. 域名校验
这是一个用于域名匹配的函数,TLS证书的域名是支持通配符*的,但是通配符只能用于最末一级域名
颁发给 *.example.com, 的证书可以用于下列域名
payment.example.com
contact.example.com
login-secure.example.com
www.example.com
由于通配符证书只能覆盖一级子域(*不匹配所有子域),该证书无法有效服务于下面的域名:
test.login.example.com
当裸域名被列入可选DNS名称,该证书也可被用于裸域名(又称根域)
example.com
某些数字证书认证机构存在例外情况, 例如DigiCert的wildcard Plus证书自动包括了裸域。
引用: Wikipedia通配符证书
Bool check_string(ASN1_STRING *as, const char *s) {
//这是一个用于比较域名字符串的函数
// compare domain string
if (!as->length || !as->data)
return False;
if (*(as->data) != '*') { //如果不包含通配符
if (!memcmp((char *) as->data, s, (size_t) as->length)) {
return True;
} else {
return False;
}
} else { //如果包含通配符
int i;
for (i=0; s[i] != '\0'; i++) {
if (s[i] == '.') {
break;
}
}
if (!memcmp((char *) as->data + 1, &s[i], (size_t) as->length - 1)) {
return True;
} else {
return False;
}
}
}
下面是对主题备用名称进行校验
gens = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
if (gens) { // 这是对于DNS域中的域名的匹配
for (i = 0; i < sk_GENERAL_NAME_num(gens); i++) {
GENERAL_NAME *gen;
ASN1_STRING *cstr;
gen = sk_GENERAL_NAME_value(gens, i);
if (gen->type != GEN_DNS)
continue;
if ((rv = check_string(gen->d.dNSName, domain_name)) != False)
break;
}
GENERAL_NAMES_free(gens);
}
下面是对Subject Name 字段的进行校验
i = -1;
name = X509_get_subject_name(cert);
while ((i = X509_NAME_get_index_by_NID(name, NID_commonName, i)) >= 0) {
X509_NAME_ENTRY *ne;
ASN1_STRING *str;
ne = X509_NAME_get_entry(name, i);
str = X509_NAME_ENTRY_get_data(ne);
/* Positive on success, negative on error! */
if ((rv = check_string(str, domain_name)) != False)
break;
}
校验RSA Key的长度
Bool valid_rsa_size(X509 *cert) {
// key length valid
EVP_PKEY *pkey;
RSA *rsa;
int rsa_size;
pkey = X509_get_pubkey(cert);
rsa = EVP_PKEY_get1_RSA(pkey);
if (!rsa) {
return False;
}
rsa_size = RSA_size(rsa);
if (rsa_size * 8 < RSAKEY_MIN_LEN) {
return False;
}
return True;
}
对扩展域进行校验
Bool valid_extension(X509 *cert, int ext, const char *expect) {
// ∗ Enhanced Key Usage includes "TLS Web Server Authentication"
X509_EXTENSION *ex = X509_get_ext(cert, X509_get_ext_by_NID(cert, ext, -1));
ASN1_OBJECT *obj = X509_EXTENSION_get_object(ex);
char buff[1024];
OBJ_obj2txt(buff, 1024, obj, 0);
BUF_MEM *bptr = NULL;
char *buf = NULL;
BIO *bio = BIO_new(BIO_s_mem());
if (!X509V3_EXT_print(bio, ex, 0, 0)) {
fprintf(stderr, "Error in reading extensions");
}
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bptr);
//bptr->data is not NULL terminated - add null character
buf = (char *) malloc((bptr->length + 1) * sizeof(char));
memcpy(buf, bptr->data, bptr->length);
buf[bptr->length] = '\0';
//Can print or parse value
Bool rv;
if (!strstr(buf, expect)) {
rv= False;
} else {
rv= True;
}
BIO_free_all(bio);
free(buf);
return rv;
}
函数的输入ext是一个int常量,参见OpenSSl的文档中 SUPPORTED EXTENSIONS
expect 是期待得到的值
总结
- OpenSSL是一个非常强大用于密码学的库
- TLS/SSL 协议是X509协议,可以通过搜索关键词X509来得到结果
- OpenSSL X509 开发文档, 学会利用官方文档来解决问题
- 分析OpenSSL的源码,看官方是如何利用库来解决问题,学习他们的算法