跳转到内容

ESP32 证书存储

ESP32 证书存储

本节介绍 TLS 证书在 ESP32 中的存储方案。不同的存储方式影响安全性、固件大小和升级灵活性。学习完成后,您将能够:

  • 比较不同的证书存储方案
  • 选择最适合项目需求的存储方式
  • 实现证书的固件内和文件系统存储
  • 理解证书更新的策略

在开始本节之前,请确保:

  • 已了解证书集成方法
  • 了解 ESP32 文件系统(SPIFFS / LittleFS)
  • 了解 PROGMEM 概念
存储方式安全性灵活性固件大小影响更新方式
代码内嵌✅ 高❌ 低增加 ~2KB需 OTA 更新固件
文件系统⚠️ 中✅ 高无影响可单独上传/OTA
NVS 分区✅ 高(加密)✅ 高无影响可远程配置
eFuse✅ ✅ 最高❌ 最低无影响一次性写入
cert_storage.h
#ifndef CERT_STORAGE_H
#define CERT_STORAGE_H
#include <pgmspace.h>
// 使用 PROGMEM 将证书存储在 Flash 而非 RAM
// 节省约 2KB 的宝贵 RAM 空间
// Let's Encrypt ISRG Root X1
static const char ROOT_CA_PEM[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
...
-----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
#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);
}
}
LittleFS 文件系统:
/
├── certs/
│ ├── ca.pem # CA 根证书
│ ├── server.pem # 服务器证书(可选)
│ ├── client.pem # 客户端证书(双向验证)
│ └── private.pem # 私钥(双向验证)
├── config.json # 设备配置
└── ... # 其他文件
#include <Preferences.h>
#include <nvs_flash.h>
Preferences preferences;
// 将证书保存到 NVS
bool 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");
}
}
// 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("证书已更新,重新连接...");
}
}
// 分级证书加载策略:
// 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("使用内置证书 ✅");
}
项目场景推荐存储方案理由
产品原型代码内嵌简单直接
小批量产品代码内嵌 + OTA 更新固件安全可靠
大批量产品LittleFS证书可独立更新
高安全产品代码内嵌 + Flash 加密防止证书被读取
远程管理分级策略(FS→NVS→内嵌)灵活性和安全性平衡
买家需求推荐方案沟通要点
”证书能远程更新吗”LittleFS 或 NVS”证书可通过 MQTT 远程更新"
"安全性要求高”代码内嵌 + Flash 加密”证书固化在固件中,不可篡改"
"需要灵活管理”分级存储策略”支持多种存储方式,自动选择”

本节介绍了 ESP32 的证书存储方案:

  1. 代码内嵌:PROGMEM 存储,固化在固件,安全但更新需 OTA
  2. 文件系统:LittleFS 存储,灵活可单独更新
  3. NVS 存储:非易失性存储,可运行时更新
  4. 分级策略:LittleFS → NVS → 内嵌,兼具灵活性和可靠性
  5. 证书更新:通过 MQTT 接收新证书并保存