使用C语言开发OpenSSL中TLS证书的校验

使用C语言开发OpenSSL中TLS证书的校验

开发环境

  • ubuntu 16.04 WSL(Windows 10 内建的Linux系统)
  • Libssl-dev (openssl 的库)
  • Clion

开发环境的搭建

  1. WSL环境安装
    参考 WSL(Windows Subsystem for Linux)的安装与使用

  2. 安装必备的开发环境包
    我的Ubuntu 开发环境配置

    sudo apt-get update
    sudo apt-get install build-essential

  3. 安装 Libssl-dev
    sudo apt-get update sudo apt-install openssl Libssl-dev
    使用默认的Ubuntu源中提供的库,我这版本是1.0.2g 如果自己下载源码编译可能会遇到一些问题

  4. 在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 为可执行文件的名字

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 是期待得到的值

总结

  1. OpenSSL是一个非常强大用于密码学的库
  2. TLS/SSL 协议是X509协议,可以通过搜索关键词X509来得到结果
  3. OpenSSL X509 开发文档, 学会利用官方文档来解决问题
  4. 分析OpenSSL的源码,看官方是如何利用库来解决问题,学习他们的算法

发布了36 篇原创文章 · 获赞 23 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/still_night/article/details/80529059