source code illustration of TLS support in esp32

嵌入式下实现TLS支持原理

我们知道要实现https,MQTT等协议时,要求通讯安全,客户端就必须实现tls证书的支持。

但是日常我们打来电脑和手机浏览器访问https网站,好像并不需要关注tls的问题?

答案是因为浏览器厂商已经帮助我们兼容好了,但当你要通过嵌入式IOT设备或者单片机实现https访问的时候,你就需要处理TLS证书的问题了。

因为ESP32的开源代码比较清晰简洁,所以我们今天的讲解,以ESP32为硬件平台。其他硬件也是类似的原理,你只要实现自己的过程,替换底层的socket接口就可以实现类似的效果。

此文通过对关键代码的详细讲解,说明在ESP32下支持TLS证书的实现原理和过程。以此原理和过程作为参考,用户也可以实现其他嵌入式硬件和单片机的TLS证书支持,因为ESP的证书的内容和代码都是源码可见的,在弄懂了后就可以很好的移植和仿制。

ESP32 HTTPS demo

官方的例子见 \examples\protocols\https_request,举例了三种支持TLS的方式:

    https_get_request_using_crt_bundle();
    https_get_request_using_cacert_buf();
    https_get_request_using_global_ca_store();

第一种是cert bundle方式,这个是今天我们重点的讲解。后两种就是使用用户指定的证书的方式,代码非常简单,就不展开了,今天主要介绍 cert bundle的方式。把cert bundle方式弄清楚了,后面两种也就清楚了,因为就是等于是cert bundle 方式的简化,简化了证书的创建,匹配和校验过程。

cert bundle的优劣

  • 优点:cert bundle的优势是不需要用户关注证书和手动下载证书,自动实现全球几乎所有TLS根证书的支持。
  • 缺点:因为内置一百多种证书,所以占用空间要大写,经过实际代码测试验证,大概大了60K左右。

ESP32 x509 Certificate Bundle

esp32 针对TLS的场景提供了x509 Certificate Bundle的实现支持,其可以简单理解为 x509证书集合。

关于x509 Certificate Bundle的详细介绍,可以参看:https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_crt_bundle.html

原文已经讲的非常清楚了,我们就不赘述了,今天只重点关注源码细节。

ESP32 TLS实现过程简述

  1. cert bundle 反序列化
  2. 创建空的证书
  3. 在https的访问回调中,根据cert name,查找对应的证书。
  4. 校验证书合法性
  5. 校验通过后,建立链接,进行基于tls的读写过程。
  6. 关闭链接,释放tls对象

源码分析

上面简述了整个过程,接下来我们将针对上面的步骤,对关键源码进行解析说明。

cert bundle 反序列化

外层调用:

if (s_crt_bundle.crts == NULL) {
        ret = esp_crt_bundle_init(x509_crt_imported_bundle_bin_start);
    }

参数 x509_crt_imported_bundle_bin_start 对应的值为 asm("_binary_x509_crt_bundle_start")

这个asm对象表示一段汇编代码,作用是返回x509对象数据地址,这个对象是通过python工具将cacrt_all.pem打包成二进制文件的,具体汇编的内容,可以自己参见 build目录下的 x509_crt_bundle.S汇编文件。

里层函数实现,解析注释:

static esp_err_t esp_crt_bundle_init(const uint8_t* x509_bundle)
{
    // 根据包头的两个字节获取证书个数
    s_crt_bundle.num_certs = (x509_bundle[0] << 8) | x509_bundle[1];
    s_crt_bundle.crts      = calloc(s_crt_bundle.num_certs, sizeof(x509_bundle));

    if (s_crt_bundle.crts == NULL) {
        ESP_LOGE(TAG, "Unable to allocate memory for bundle");
        return ESP_ERR_NO_MEM;
    }

    const uint8_t* cur_crt;
    cur_crt = x509_bundle + BUNDLE_HEADER_OFFSET;

    ESP_LOGW(TAG, "cert num:%d", s_crt_bundle.num_certs);

    // 根据个数一个一个的来偏移获取每个证书的内容
    for (int i = 0; i < s_crt_bundle.num_certs; i++) {
        s_crt_bundle.crts[i] = cur_crt;
        // 每个证书前面4个字节是长度信息
        size_t name_len = cur_crt[0] << 8 | cur_crt[1];
        size_t key_len  = cur_crt[2] << 8 | cur_crt[3];
        ESP_LOGW(TAG, "cert name len:%d,key len:%d", name_len, key_len);
        // 根据长度进行每个证书的偏移
        cur_crt = cur_crt + CRT_HEADER_OFFSET + name_len + key_len;
    }

    return ESP_OK;
}

这段代码的作用就是 将入参的cert bundle 地址内存,根据数据结构定义{BUNDLE_HEADER_OFFSET,{name_len,key_len, name,key}}反序列化成cert结构数组对象 s_crt_bundle。

截止本文撰写日期,esp32 实现的这个x509 cert bundle 支持135个根证书,几乎支持了全球所有的证书。

创建空的证书

mbedtls_x509_crt_init(&s_dummy_crt);
mbedtls_ssl_conf_ca_chain(ssl_conf, &s_dummy_crt, NULL);

这个空证书的作用主要是承载访问服务器时获取的服务器证书特征,这个特征会通过回调的方式传给 mbedtls_ssl_conf_verify 函数参数。

查找和校验证书

代码如下,解析见注释:

int esp_crt_verify_callback(void* buf, mbedtls_x509_crt* crt, int depth, uint32_t* flags)
{
    mbedtls_x509_crt* child = crt;

    /* It's OK for a trusted cert to have a weak signature hash alg.
       as we already trust this certificate */
    uint32_t flags_filtered = *flags & ~(MBEDTLS_X509_BADCERT_BAD_MD);

    if (flags_filtered != MBEDTLS_X509_BADCERT_NOT_TRUSTED) {
        return 0;
    }

    if (s_crt_bundle.crts == NULL) {
        ESP_LOGE(TAG, "No certificates in bundle");
        return MBEDTLS_ERR_X509_FATAL_ERROR;
    }

    ESP_LOGD(TAG, "%d certificates in bundle", s_crt_bundle.num_certs);

    size_t name_len = 0;
    const uint8_t* crt_name;

    // start 和 end 是证书数组的index。这里实现的是二分查找算法,说明cert bundle     
    // 数组的名字是增加了排序特征的,具体细节,需要查看python的打包工具的实现代码。
    bool crt_found = false;
    int start      = 0;
    int end        = s_crt_bundle.num_certs - 1;
    int middle     = (end - start) / 2;

    /* Look for the certificate using binary search on subject name */
    while (start <= end) {
        name_len = s_crt_bundle.crts[middle][0] << 8 | s_crt_bundle.crts[middle][1];
        crt_name = s_crt_bundle.crts[middle] + CRT_HEADER_OFFSET;

        int cmp_res = memcmp(child->issuer_raw.p, crt_name, name_len);
        if (cmp_res == 0) {
            crt_found = true;
            break;
        } else if (cmp_res < 0) {
            end = middle - 1;
        } else {
            start = middle + 1;
        }
        middle = (start + end) / 2;
    }
    // 二分查找结束

    // 校验证书合法性
    int ret = MBEDTLS_ERR_X509_FATAL_ERROR;
    if (crt_found) {
        size_t key_len = s_crt_bundle.crts[middle][2] << 8 | s_crt_bundle.crts[middle][3];
        ret = esp_crt_check_signature(child, s_crt_bundle.crts[middle] + CRT_HEADER_OFFSET + name_len, key_len);
    }

    if (ret == 0) {
        ESP_LOGI(TAG, "Certificate validated");
        *flags = 0;
        return 0;
    }

    ESP_LOGE(TAG, "Failed to verify certificate");
    return MBEDTLS_ERR_X509_FATAL_ERROR;
}

代码很简单,注释中已经说了,就是一个二分查找过程。

校验证书合法性

相关代码在上面的部分已经注释过了,就是esp_crt_check_signature函数。校验的细节,感兴趣的朋友可以自己查看此函数源码。

TLS读写过程

写请求过程:

do {
        ret = esp_tls_conn_write(tls, REQUEST + written_bytes, sizeof(REQUEST) - written_bytes);
        if (ret >= 0) {
            ESP_LOGI(TAG, "%d bytes written", ret);
            written_bytes += ret;
        } else if (ret != ESP_TLS_ERR_SSL_WANT_READ && ret != ESP_TLS_ERR_SSL_WANT_WRITE) {
            ESP_LOGE(TAG, "esp_tls_conn_write  returned: [0x%02X](%s)", ret, esp_err_to_name(ret));
            goto exit;
        }
    } while (written_bytes < sizeof(REQUEST));

esp_tls_conn_write根据长度来循环写。

读响应过程:

    do {
        len = sizeof(buf) - 1;
        bzero(buf, sizeof(buf));
        ret = esp_tls_conn_read(tls, (char*) buf, len);

        if (ret == ESP_TLS_ERR_SSL_WANT_WRITE || ret == ESP_TLS_ERR_SSL_WANT_READ) {
            continue;
        }

        if (ret < 0) {
            ESP_LOGE(TAG, "esp_tls_conn_read  returned [-0x%02X](%s)", -ret, esp_err_to_name(ret));
            break;
        }

        if (ret == 0) {
            ESP_LOGI(TAG, "connection closed");
            break;
        }

        len = ret;
        ESP_LOGD(TAG, "%d bytes read", len);
        /* Print response directly to stdout as it is read */
        for (int i = 0; i < len; i++) {
            putchar(buf[i]);
        }
        putchar('\n'); // JSON output doesn't have a newline at end
    } while (1);

esp_tls_conn_read 循环读,直到链接关闭为止,跳出循环

释放 tls对象:

esp_tls_conn_delete(tls);

结束

好了,整个代码过程还是封装的非常干净和清晰的,如果你需要在自己的嵌入式系统中实现TLS证书过程,那么也可以仿照以上的过程,实现自己的bundle和匹配校验过程,并且可以根据实际需要定制和优化。

希望以上的分析,能帮你快速实现自己的TLS访问功能。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注