ESP32 证书存储
ESP32 证书存储
本节介绍 TLS 证书在 ESP32 中的存储方案。不同的存储方式影响安全性、固件大小和升级灵活性。学习完成后,您将能够:
- 比较不同的证书存储方案
- 选择最适合项目需求的存储方式
- 实现证书的固件内和文件系统存储
- 理解证书更新的策略
在开始本节之前,请确保:
- 已了解证书集成方法
- 了解 ESP32 文件系统(SPIFFS / LittleFS)
- 了解 PROGMEM 概念
Storage Options Overview
Section titled “Storage Options Overview”证书存储方案对比
Section titled “证书存储方案对比”| 存储方式 | 安全性 | 灵活性 | 固件大小影响 | 更新方式 |
|---|---|---|---|---|
| 代码内嵌 | ✅ 高 | ❌ 低 | 增加 ~2KB | 需 OTA 更新固件 |
| 文件系统 | ⚠️ 中 | ✅ 高 | 无影响 | 可单独上传/OTA |
| NVS 分区 | ✅ 高(加密) | ✅ 高 | 无影响 | 可远程配置 |
| eFuse | ✅ ✅ 最高 | ❌ 最低 | 无影响 | 一次性写入 |
Method 1: Code Embedding
Section titled “Method 1: Code Embedding”PROGMEM 存储(Flash)
Section titled “PROGMEM 存储(Flash)”#ifndef CERT_STORAGE_H#define CERT_STORAGE_H
#include <pgmspace.h>
// 使用 PROGMEM 将证书存储在 Flash 而非 RAM// 节省约 2KB 的宝贵 RAM 空间
// Let's Encrypt ISRG Root X1static const char ROOT_CA_PEM[] PROGMEM = R"EOF(-----BEGIN CERTIFICATE-----MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4...-----END CERTIFICATE-----)EOF";
// 服务器证书(可选)static const char SERVER_CERT_PEM[] PROGMEM = R"EOF(-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----)EOF";
// 客户端证书和私钥(双向验证用)static const char CLIENT_CERT_PEM[] PROGMEM = R"EOF(-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----)EOF";
static const char PRIVATE_KEY_PEM[] PROGMEM = R"EOF(-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----)EOF";
#endif优点:
- 证书固件中固化,不可篡改
- 占用 Flash 而非 RAM(PROGMEM)
- 不需要文件系统初始化
缺点:
- 证书更新需重新编译和 OTA
- 增加固件大小 ~2KB
Method 2: File System Storage
Section titled “Method 2: File System Storage”LittleFS 存储证书
Section titled “LittleFS 存储证书”#include <LittleFS.h>
bool initFileSystem() { if (!LittleFS.begin(true)) { Serial.println("LittleFS 挂载失败"); return false; } Serial.println("LittleFS 已挂载"); return true;}
// 从文件系统加载证书String loadCertFromFile(const char* path) { File file = LittleFS.open(path, "r"); if (!file) { Serial.printf("无法打开证书文件: %s\n", path); return ""; }
String cert = file.readString(); file.close(); return cert;}
// 保存证书到文件系统bool saveCertToFile(const char* path, const char* cert) { File file = LittleFS.open(path, "w"); if (!file) { Serial.printf("无法写入证书文件: %s\n", path); return false; }
file.print(cert); file.close(); return true;}
void setupCertFromFS() { initFileSystem();
// 从 LittleFS 加载 CA 证书 String caCert = loadCertFromFile("/certs/ca.pem");
if (caCert.length() > 0) { espClient.setCACert(caCert.c_str()); Serial.println("从文件系统加载 CA 证书成功"); } else { // 回退到内置证书 Serial.println("文件系统证书不存在,使用内置证书"); espClient.setCACert(rootCACertificate); }}文件系统目录结构
Section titled “文件系统目录结构”LittleFS 文件系统:/├── certs/│ ├── ca.pem # CA 根证书│ ├── server.pem # 服务器证书(可选)│ ├── client.pem # 客户端证书(双向验证)│ └── private.pem # 私钥(双向验证)├── config.json # 设备配置└── ... # 其他文件Method 3: NVS Storage
Section titled “Method 3: NVS Storage”非易失性存储
Section titled “非易失性存储”#include <Preferences.h>#include <nvs_flash.h>
Preferences preferences;
// 将证书保存到 NVSbool saveCertToNVS(const char* key, const char* cert) { preferences.begin("tls-certs", false); bool success = preferences.putString(key, cert); preferences.end(); return success;}
// 从 NVS 加载证书String loadCertFromNVS(const char* key) { preferences.begin("tls-certs", true); String cert = preferences.getString(key, ""); preferences.end(); return cert;}
// NVS 方式加载证书void setupCertFromNVS() { // 检查 NVS 中是否有自定义证书 String caCert = loadCertFromNVS("ca_cert");
if (caCert.length() > 0) { espClient.setCACert(caCert.c_str()); Serial.println("从 NVS 加载 CA 证书成功"); } else { // 首次运行,将默认证书保存到 NVS saveCertToNVS("ca_cert", ROOT_CA_PEM); espClient.setCACert(ROOT_CA_PEM); Serial.println("已将默认证书保存到 NVS"); }}Certificate Update Strategy
Section titled “Certificate Update Strategy”OTA 更新证书
Section titled “OTA 更新证书”// MQTT 回调:处理证书更新命令void mqttCallback(char* topic, byte* payload, unsigned int length) { String message; for (int i = 0; i < length; i++) { message += (char)payload[i]; }
if (String(topic) == "esp32/cert/update") { // 接收到新证书 DynamicJsonDocument doc(2048); deserializeJson(doc, message);
const char* newCert = doc["cert"]; const char* type = doc["type"]; // "ca", "client", "private"
// 保存到文件系统 String path = "/certs/" + String(type) + ".pem"; saveCertToFile(path.c_str(), newCert);
// 重新加载证书 setupCertFromFS();
// 断开 MQTT 重连(使用新证书) client.disconnect(); Serial.println("证书已更新,重新连接..."); }}Hybrid Approach
Section titled “Hybrid Approach”推荐:分级存储策略
Section titled “推荐:分级存储策略”// 分级证书加载策略:// 1. 尝试从 LittleFS 加载(灵活,可 OTA 更新)// 2. 失败则从 NVS 加载(可运行时更新)// 3. 失败则使用内置证书(固件固化,兜底)
void loadCertificate() { // 级别 1: LittleFS(最高优先级) if (LittleFS.begin(false)) { String cert = loadCertFromFile("/certs/ca.pem"); if (cert.length() > 0) { espClient.setCACert(cert.c_str()); Serial.println("使用 LittleFS 证书 ✅"); return; } }
// 级别 2: NVS preferences.begin("tls-certs", true); String nvsCert = preferences.getString("ca_cert", ""); preferences.end();
if (nvsCert.length() > 0) { espClient.setCACert(nvsCert.c_str()); Serial.println("使用 NVS 证书 ✅"); return; }
// 级别 3: 内置证书(兜底) espClient.setCACert(ROOT_CA_PEM); Serial.println("使用内置证书 ✅");}Storage Recommendation
Section titled “Storage Recommendation”| 项目场景 | 推荐存储方案 | 理由 |
|---|---|---|
| 产品原型 | 代码内嵌 | 简单直接 |
| 小批量产品 | 代码内嵌 + OTA 更新固件 | 安全可靠 |
| 大批量产品 | LittleFS | 证书可独立更新 |
| 高安全产品 | 代码内嵌 + Flash 加密 | 防止证书被读取 |
| 远程管理 | 分级策略(FS→NVS→内嵌) | 灵活性和安全性平衡 |
Pre-sales Key Points
Section titled “Pre-sales Key Points”证书存储选型建议
Section titled “证书存储选型建议”| 买家需求 | 推荐方案 | 沟通要点 |
|---|---|---|
| ”证书能远程更新吗” | LittleFS 或 NVS | ”证书可通过 MQTT 远程更新" |
| "安全性要求高” | 代码内嵌 + Flash 加密 | ”证书固化在固件中,不可篡改" |
| "需要灵活管理” | 分级存储策略 | ”支持多种存储方式,自动选择” |
Summary
Section titled “Summary”本节介绍了 ESP32 的证书存储方案:
- 代码内嵌:PROGMEM 存储,固化在固件,安全但更新需 OTA
- 文件系统:LittleFS 存储,灵活可单独更新
- NVS 存储:非易失性存储,可运行时更新
- 分级策略:LittleFS → NVS → 内嵌,兼具灵活性和可靠性
- 证书更新:通过 MQTT 接收新证书并保存