欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-06 19:06:52
...

使用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的源码,看官方是如何利用库来解决问题,学习他们的算法