1. 问题现象与初步分析
最近在调试SF32开发板上的小智语音助手时,遇到了一个典型问题:设备连接时提示"OTA获取失败,请检查网络连接后重试"。这个错误看似简单,但背后涉及证书验证、网络通信等多个技术环节。作为一名嵌入式开发者,我决定深入剖析这个问题,并分享三种不同的解决方案。
首先需要明确的是,这个错误提示出现在设备尝试获取OTA(Over-The-Air)更新时。从日志中可以清晰看到mbedtls报错:"verify peer certificate fail.... The certificate is not correctly signed by the trusted CA"。这表明问题核心在于SSL/TLS证书验证失败,而非简单的网络连接问题。
提示:在嵌入式开发中,证书验证失败是HTTPS通信的常见问题之一,通常由证书过期、根证书缺失或系统时间不正确导致。
2. 解决方案一:修改代码绕过证书验证
2.1 定位关键代码
通过逆向工程和日志分析,我们可以快速定位到问题出现的具体位置。在mbedtls的证书验证回调函数中,系统会对服务器证书进行严格校验。以下是典型的验证流程:
static int verify_cert(void *data, mbedtls_x509_crt *crt, int depth, uint32_t *flags) { if (*flags != 0) { rt_kprintf("verify peer certificate fail....\n"); rt_kprintf("verification info: ! The certificate is not correctly signed by the trusted CA\n"); return -1; // 验证失败 } return 0; // 验证成功 }2.2 修改方案实施
最简单的解决方案是直接修改这段验证逻辑。有两种修改方式:
- 完全跳过验证:将整个函数体替换为
return 0; - 忽略验证结果:保留日志输出但强制返回成功
static int verify_cert(void *data, mbedtls_x509_crt *crt, int depth, uint32_t *flags) { if (*flags != 0) { rt_kprintf("[Warning] Certificate verification failed, but we choose to ignore it\n"); } return 0; // 总是返回成功 }注意:这种方法会降低系统安全性,仅建议在开发测试阶段使用。生产环境必须使用正确的证书方案。
3. 解决方案二:更新根证书文件
3.1 获取正确的根证书
更规范的解决方案是更新设备的根证书存储。DigiCert Global CA G2是当前广泛使用的根证书之一,可以从DigiCert官网下载:
- 访问DigiCert根证书下载页面
- 搜索"DigiCert Global CA G2"
- 下载PEM格式的证书文件
3.2 替换设备证书
将下载的证书文件替换设备中的旧证书。典型路径为:
sdk/external/mbedtls_228/certs/DigiCert Global Root CA2.crt证书文件内容示例:
-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh ... -----END CERTIFICATE-----3.3 验证证书有效性
更新后,建议通过以下命令验证证书链:
openssl verify -CAfile DigiCertGlobalRootCA.crt server.crt4. 解决方案三:自定义证书验证策略
4.1 实现灵活的验证逻辑
对于需要平衡安全性和灵活性的场景,可以实现自定义验证策略。例如,仅验证证书指纹而非完整链:
static int verify_cert(void *data, mbedtls_x509_crt *crt, int depth, uint32_t *flags) { const uint8_t expected_sha256[] = {0x12,0x34,...}; // 预期的证书指纹 uint8_t actual_sha256[32]; mbedtls_sha256(crt->raw.p, crt->raw.len, actual_sha256, 0); if(memcmp(expected_sha256, actual_sha256, 32) != 0) { *flags |= MBEDTLS_X509_BADCERT_NOT_TRUSTED; return -1; } return 0; }4.2 动态加载证书
更高级的方案是实现证书的动态加载,允许通过OTA更新证书:
int load_certificate(const char *cert_pem) { mbedtls_x509_crt_free(&trusted_cert); return mbedtls_x509_crt_parse(&trusted_cert, (const unsigned char *)cert_pem, strlen(cert_pem) + 1); }5. 问题排查方法论
5.1 系统化的调试流程
遇到类似问题时,建议按照以下步骤排查:
- 收集日志:启用详细日志,特别是SSL/TLS相关日志
- 网络抓包:使用Wireshark或tcpdump分析HTTPS握手过程
- 证书检查:
- 验证服务器证书有效性
- 检查设备时间是否正确
- 确认根证书是否匹配
- 代码追踪:
- 定位网络请求发起点
- 跟踪证书验证回调
- 分析错误处理逻辑
5.2 典型错误模式速查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 证书签名无效 | 根证书缺失/不匹配 | 更新根证书 |
| 证书已过期 | 系统时间不正确 | 校正RTC时钟 |
| 主机名不匹配 | 服务器配置错误 | 检查SNI配置 |
| 连接超时 | 网络防火墙拦截 | 检查网络策略 |
6. 安全考量与最佳实践
6.1 各方案的安全评估
| 方案 | 安全性 | 适用场景 | 维护成本 |
|---|---|---|---|
| 跳过验证 | 低 | 开发测试 | 低 |
| 更新证书 | 高 | 生产环境 | 中 |
| 自定义策略 | 中高 | 特殊需求 | 高 |
6.2 生产环境建议
对于量产设备,推荐采用以下安全措施:
- 证书固定(Pinning):在代码中内置证书指纹
- 双重验证:同时验证证书链和有效期
- 安全更新:实现安全的证书更新机制
- 防御性编程:处理各种异常情况
int secure_connect() { // 1. 初始化TLS上下文 mbedtls_ssl_config conf; mbedtls_ssl_config_init(&conf); // 2. 设置证书验证回调 mbedtls_ssl_conf_verify(&conf, verify_cert, NULL); // 3. 启用严格模式 mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_REQUIRED); // 4. 设置超时 mbedtls_ssl_conf_read_timeout(&conf, 5000); // ... 其余连接逻辑 }7. 扩展知识与进阶技巧
7.1 mbedTLS深度配置
对于性能敏感的应用,可以优化mbedTLS配置:
// 仅启用必要的加密套件 static const int ciphersuites[] = { MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 0 // 结束标记 }; mbedtls_ssl_conf_ciphersuites(&conf, ciphersuites);7.2 内存优化技巧
嵌入式设备通常内存有限,可以通过以下方式优化:
- 禁用不用的加密算法
- 减小SSL缓冲区大小
- 使用静态内存分配
// 在mbedtls_config.h中定义 #define MBEDTLS_SSL_MAX_CONTENT_LEN 2048 // 默认是16K #define MBEDTLS_MPI_MAX_SIZE 256 // 减小大数运算缓冲区7.3 调试工具推荐
- OpenSSL命令行工具:验证证书链
openssl s_client -connect example.com:443 -showcerts - mbedtls_test:mbedTLS自带的测试工具
- GDB调试:单步跟踪SSL握手过程
在实际项目中,我通常会结合多种调试手段。比如先用Wireshark确认网络连通性,再用OpenSSL检查证书有效性,最后通过代码调试定位具体问题点。这种分层排查的方法能显著提高效率。
对于时间敏感型设备,特别要注意RTC时钟的准确性。我曾遇到一个案例:设备因电池耗尽导致时钟重置,使得所有证书验证都因"证书未生效"而失败。这个bug花了很长时间才定位到,教训深刻。