Skip to content

本文档剖析 OTX Proxy 中完整的证书生命周期管理机制——从 PKCS#12 文件的底层解析、到环境感知的客户端证书加载、再到服务端安全常量的分级获取与缓存。系统围绕三个核心数据域运转:HTTPS 双向认证所需的客户端 PEM 证书OTA 升级服务的 CA 与客户端证书、以及按 ECU 粒度管理的安全常量(SWDL/Immo/SAL/Common 密钥、AES 通信密钥、EOL 全量密钥)

架构纵览:三层证书处理管线

证书管理遵循一条清晰的解析 → 加载 → 获取三层管线,每层有明确的职责边界:

mermaid
flowchart TD
    subgraph L1["第1层:P12 解析(algorithm.cpp)"]
        A[PKCS#12 文件] --> B["parse_cert_p12()"]
        B --> C["OpenSSL PKCS12_parse()"]
        C --> D["X.509 证书 → PEM"]
        C --> E["私钥 EVP_PKEY → PEM"]
    end

    subgraph L2["第2层:客户端证书加载(JiduClient)"]
        F["loadEnvirment()"] --> G["certDir 定位<br/>(工厂 × 环境)"]
        H["loadParam()"] --> I["pver 文件读取"]
        I --> J["DES-CBC 解密 certPwd"]
        J --> K["gen_pem() → L1"]
        K --> L["certPem / keyPem 就绪"]
    end

    subgraph L3["第3层:安全常量获取(callServer → DataCenter)"]
        M["PKI 服务器<br/>(mTLS 认证)"] --> N["ECU 安全常量<br/>iType=6/59"]
        M --> O["EOL 全量密钥<br/>iType=19/58"]
        M --> P["ECU 证书 + AES<br/>iType=20/56"]
        M --> Q["AES 通信密钥<br/>iType=21/55"]
        M --> R["OTA 证书<br/>iType=27/64"]
        N & O & P & Q & R --> S["DataCenter 全局缓存"]
    end

    L2 --> L3

证书处理流程的第一步是定位正确的 PKI 证书目录。JiduClient::loadEnvirment() 根据两个维度决定 certDir 路径:工厂类型(PMA/DMA/售后工程)和运行环境(Dev/Test/Staging/Prod),生成的路径形如 {appPath}pkiCertificate/PMA/Prod/。对于售后与工程模式(非 PMA 非 DMA),路径简化为 {appPath}pkiCertificate/{Env}/,且仅配置诊断服务主机而不涉及 EOL/PKI 主机。

Sources: jidu_client.cpp

第一层:P12 → PEM 的 OpenSSL 解析

parse_cert_p12() 是整个证书体系的根基函数,位于 algorithm.cpp 底层。它接受 P12 文件路径、密码短语,输出 PEM 格式的 X.509 证书和 RSA/EC 私钥。

核心流程为:通过 BIO_read_filename 将 P12 文件读入 BIO,调用 d2i_PKCS12_bio 将 DER 编码的 PKCS#12 数据反序列化为 PKCS12 结构体;随后 PKCS12_parse(p12, passwd, &pkey, &cert, &ca) 一次性提取出私钥、终端实体证书和 CA 证书链。证书通过 PEM_write_bio_X509 写入内存 BIO 后拷贝到输出缓冲区,私钥通过 PEM_write_bio_PrivateKey 以未加密的 PEM 格式写出。

参数类型说明
p12Pathchar*P12 证书文件的绝对路径
passwdchar*P12 文件的保护密码
pemBufferchar* (输出)接收 PEM 编码的 X.509 证书
keyBufferchar* (输出)接收 PEM 编码的私钥
返回值intEXIT_SUCCESS(0) 成功,EXIT_FAILURE(1) 失败

关键安全细节:解析失败时,函数会依次调用 X509_free(cert)EVP_PKEY_free(pkey)sk_X509_pop_free(ca, X509_free) 释放已分配的资源,防止内存泄漏。成功路径中证书和私钥分别通过独立的 BIO_s_mem() 写入,互不干扰。

Sources: algorithm.cpp

第二层:环境感知的客户端证书加载

证书目录的环境矩阵

loadEnvirment() 构建了一个 工厂 × 环境 的完整矩阵,为 certDir 和四个服务主机(eolHostpkiHostcfgHostdiagHost)赋值:

工厂环境certDir 子路径PKI 主机域名模式
PMADevpkiCertificate/PMA/Dev/pki-thirdparty.jidudev.com
PMATestpkiCertificate/PMA/Test/pma-pki-thirdparty.jidupmatest.com
PMAStagingpkiCertificate/PMA/Staging/pma-pki-thirdparty.jidupmastaging.com
PMAProdpkiCertificate/PMA/Prod/pma-pki-thirdparty.jidupma.com
DMADevpkiCertificate/DMA/Dev/pki-thirdparty.jidudev.com
DMAProdpkiCertificate/DMA/Prod/dma-pki-thirdparty.jidudma.com
售后/工程DevpkiCertificate/Dev/仅 diagHost 有效

Staging 环境下若 Diagnosis.Entry.exe.configeolEnvironment=true,EOL 主机将指向 Prod 环境,但 PKI 主机保持 Staging 不变。此外,loadEnvirment() 还会尝试从 WebApi.json 配置文件覆盖主机地址,实现灵活的部署切换。

Sources: jidu_client.cpp

P12 密码的安全解密与 gen_pem

loadParam() 负责读取 pver 文件(位于应用程序根目录),从中提取设备序列号 deviceID、按环境编码索引的 certPwd 密文和 certName 文件名。证书密码的保护采用 Base64 编码 + DES-CBC 解密 的双层机制:

pver 中的 certPwd 字段 → base64_decode() → des_cbc_pkcs5_decrypt(data, key="siuhuali") → 明文密码

环境编码 environmentCode 在 DMA 工厂下偏移 +6(environmentCode += 6),这是因为 DMA 工厂的密码在 certPwd JSON 对象中使用 7–10 的键名(对应于 DMA Dev/Test/Staging/Prod),而 PMA 使用 1–4。解密后的密码取前 16 个字符传入 gen_pem()

gen_pem() 作为薄封装层,分配两个 1024 字节的栈缓冲区,调用 parse_cert_p12() 完成 P12 到 PEM 的转换,结果存入 JiduClient 的成员变量 certPemkeyPem。加载成功后,deviceSn 会在 JiduClientDataCenter 之间双向同步。

Sources: jidu_client.cpp, jidu_client.cpp

PEM 证书在 HTTPS 请求中的应用

加载完成的 certPemkeyPemcallServer 中被注入到 HTTP 客户端,具体策略取决于目标主机类型:

  • PKI 主机host == "pki"):直接调用 setClientCert(certPem)setClientKey(keyPem) 启用 mTLS 双向认证HttpClient::setClientCert/setClientKey 将 PEM 字符串包装为 curl_blob 结构体(设置 CURL_BLOB_COPY 标志确保 libcurl 内部拷贝),并通过 CURLOPT_SSLCERT_BLOB / CURLOPT_SSLKEY_BLOB 传递给 libcurl。

  • DIAG/CFG 主机host == "diag""cfg"):不使用客户端证书。取而代之的是通过 ecdsa_signature() 对请求内容进行 ECDSA 签名——将 inParams + url + method + serviceName + timestamp 拼接,用 keyPem 中的 ECDSA 私钥签名后放入请求的 signature 字段。keyPem 在此处承担双重角色:同时作为 mTLS 私钥和 API 签名私钥。

  • EOL 主机:不使用客户端证书,直接发送请求。

Sources: jidu_client.cpp, HttpClient.cpp

第三层:安全常量的分类获取与缓存

DataCenter 维护五类安全相关数据,均以静态成员变量的形式全局共享。callServer_preHandler 在每次请求前清空对应的缓存字段callServer_affterHandler 在响应成功后解析并填充

ECU 安全常量(iType=6 PKI / iType=59 DIAG)

JD_ECU_SAFETY_CONSTANT_t 结构体包含四类密钥,按 ECU 名称索引:

字段含义典型用途
keySwdlSoftware Download 密钥刷写文件下载认证
keyImmoImmobilizer 密钥防盗认证
keySalSecurity Access Level 密钥诊断安全访问(SeedToKey)
keyCommon通用密钥通用安全操作

响应中的四个 JSON 数组(swdlimmosalcommon)以 ECU 名为键、密钥值为值,affterHandler 遍历 swdl 数组,以 ECU 名称为索引将四类密钥组装成 JD_ECU_SAFETY_CONSTANT_t 存入 DataCenter::m_EcuSafetyConstant map。

getEcuSafetyConstant(ecuName) 实现了智能 ECU 名称回退:当精确名称未命中时,依次尝试前缀匹配(如 IEM → 匹配任意 IEM*)、特定别名(BECMBECM1WPCWPC 但排除 WPC3)、以及去除电压后缀(BECM1_400VBECM1)。

Sources: jidu_dataCenter.cpp, jidu_client.cpp, jidu_macro.h

EOL 全量密钥(iType=19/58 PKI, iType=11/57 DIAG)

JD_EOL_KEY_t 聚集了产线终端(End-of-Line)所需的全部 immobilizer 和域控密钥:

字段说明
immoKeyEcm发动机控制模块 Immo 密钥
immoKeyIem逆变器控制模块 Immo 密钥
immoKeyMgm电机控制模块 Immo 密钥
immoKeyRemote遥控钥匙 Immo 密钥
dkBncmBgmBNCM/BGM 域控密钥
powerRootKey高压根密钥
powerRootKeyId高压根密钥 ID

这些密钥在 PKI 服务器端创建后(createEolAllKey),通过 getEolAllKey 获取并缓存在 DataCenter::m_EolKey 中。

Sources: jidu_macro.h, jidu_client.cpp

ECU 证书信息(iType=20/56 PKI, iType=23/65 DIAG)

JD_EcuCertInfo_t 是最高粒度的 ECU 证书容器,包含三个层次:

mermaid
flowchart LR
    subgraph JD_EcuCertInfo_t
        A["cls_strVid<br/>车辆 VID"]
        B["cls_strEcuCa<br/>ECU CA 证书"]
        C["ecuCertInfo<br/>map&lt;ecuType, JD_EcuCertItem_t&gt;"]
    end

    subgraph JD_EcuCertItem_t
        D["cls_strDvcKey<br/>域控设备密钥"]
        E["cls_strP12<br/>ECU P12 证书内容"]
        F["cls_strP12Key<br/>P12 密码"]
        G["cls_strAESKey<br/>AES 密钥"]
        H["cls_strSOAKey<br/>SOA 密钥"]
    end

    C --> D & E & F & G & H

affterHandler 解析逻辑首先构建 dvcMapdvcKey 数组以 ECU 类型为键),然后遍历 certList 数组,为每个 ECU 类型组装 JD_EcuCertItem_t——其中 cls_strDvcKey 来自 dvcMap 的查找结果,cls_strP12cls_strAESKeycls_strP12Keycls_strSOAKey 直接从服务器响应对应字段提取。

Sources: jidu_macro.h, jidu_client.cpp

AES 通信密钥(iType=21 PKI / iType=55 DIAG)

DataCenter::m_aesKey 是一个 map<string, string>,以 ECU 名称为键存储 Base64 编码的 AES 密钥。服务器响应为一个 JSON 对象数组,每个元素的键为 ECU 名称、值为密钥字符串。

特殊处理:当 iType=21iType=55 的响应返回错误码 9002/9004/9005(表示 PKI 服务异常)时,系统会静默降级——使用硬编码的固定密钥 \x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF 经 Base64 编码后为 ACU、BGM、CDC、TCAM 四个关键 ECU 赋值,确保刷写流程不因 PKI 不可用而完全中断。

Sources: jidu_client.cpp, jidu_client.cpp

OTA 证书(iType=27 PKI / iType=64 DIAG)

JD_OTA_CERT_t 结构最为精简,仅含两个字段:

字段含义
caCertOTA 服务的 CA 证书(服务端验证)
clientCertOTA 服务的客户端证书(mTLS 认证)

响应中 serviceCa 映射到 caCertotaCert 映射到 clientCert。OTA 证书独立于 PKI 通信证书——前者用于 OTA 升级包下载时的安全验证,后者用于与 PKI 服务器本身的通信。

Sources: jidu_macro.h, jidu_client.cpp

安全常量获取的完整请求路由

以下表格汇总了所有证书与安全常量相关的 iType 及其路由目标:

iType方法主机URL 路径获取内容DataCenter 存储
5POSTpki/api/dataMappingApi/queryConstantsByEcuName按 ECU 名查询常量
6POSTpki/api/dataMappingApi/queryConstantsByKeyType按密钥类型查询全部 ECU 安全常量m_EcuSafetyConstant
11POSTpki/api/dataMappingApi/createEolAllKey创建 EOL 全量密钥m_EolKey
19GETpki/api/dataMappingApi/getEolAllKey查询 EOL 全量密钥m_EolKey
20POSTpki/api/ca/vehicle/suitECU 证书套件m_EcuCertInfo
21POSTpki/api/ca/vehicle/getAesKeyAES 通信密钥m_aesKey
23POSTpki/api/ca/vehicle/factoryAndReturnDummy工厂 Dummy 证书m_EcuCertInfo
27GETpki/api/ca/service/otaCertOTA 服务证书m_OtaCert
55POSTdiag/vehicle/cert/seedAES 通信密钥(售后)m_aesKey
56POSTdiag/vehicle/cert/suitECU 证书套件(售后)m_EcuCertInfo
57POSTdiag/vehicle/cert/createEolAllKey创建 EOL 密钥(售后)m_EolKey
58GETdiag/vehicle/cert/getEolAllKey查询 EOL 密钥(售后)m_EolKey
59POSTdiag/vehicle/cert/queryConstantsByKeyTypeECU 安全常量(售后)m_EcuSafetyConstant
64GETdiag/vehicle/cert/otaCertOTA 证书(售后)m_OtaCert
65POSTdiag/vehicle/cert/factoryAndReturnDummyDummy 证书(售后)m_EcuCertInfo

对称性设计:PKI 主机(iType 5–27)服务于工厂产线场景,DIAG 主机(iType 55–65)服务于售后诊断场景。两者在接口语义上完全对称——例如 iType=6(PKI)与 iType=59(DIAG)获取相同结构的 ECU 安全常量,但经由不同的网关和服务实例。

Sources: jidu_uri.h

HttpClient 中的证书应用机制

HttpClient 类作为 HTTP 通信基类,通过 curl_blob 结构体管理 PEM 证书数据:

struct curl_blob blob_clientCert;  // 文件作用域全局变量
struct curl_blob blob_clientKey;

setClientCert(string& cert)setClientKey(string& key) 将 string 内容的指针和长度赋值给 curl_blob,并设置 flags = CURL_BLOB_COPY 确保 libcurl 内部复制数据。在 post()get() 方法中,仅当 clientCert/clientKey 非空时才设置对应的 CURL 选项:

cpp
curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "PEM");
curl_easy_setopt(curl, CURLOPT_SSLCERT_BLOB, &blob_clientCert);
curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM");
curl_easy_setopt(curl, CURLOPT_SSLKEY_BLOB, &blob_clientKey);

值得注意的是,每次请求完成后curl_easy_perform 返回后),clientCertclientKey 会被 clear() 清空,确保证书数据不会在内存中残留超过单次请求的生命周期。此外,SSL 验证选项被显式关闭(CURLOPT_SSL_VERIFYPEER = falseCURLOPT_SSL_VERIFYHOST = false),因为 PKI 通信完全依赖客户端证书的 mTLS 双向认证,不再需要 CA 证书链验证。

Sources: HttpClient.cpp, HttpClient.cpp, HttpClient.cpp

数据流全景:从 P12 到安全刷写

mermaid
sequenceDiagram
    participant App as OTX 运行时
    participant JC as JiduClient
    participant DC as DataCenter
    participant PKI as PKI 服务器

    App->>JC: init()
    JC->>JC: loadEnvirment() → certDir, hosts
    JC->>JC: loadParam()
    Note over JC: 读取 pver → DES-CBC 解密密码
    JC->>JC: gen_pem(certPath, certPwd)
    Note over JC: parse_cert_p12() → certPem, keyPem

    App->>JC: callServer(iType=6)
    JC->>JC: callServer_preHandler → 清空 m_EcuSafetyConstant
    JC->>PKI: POST + mTLS(certPem, keyPem)
    PKI-->>JC: swdl/immo/sal/common JSON
    JC->>JC: callServer_affterHandler
    JC->>DC: setEcuSafetyConstant(ecuName, constants)
    Note over DC: m_EcuSafetyConstant 缓存就绪

    App->>DC: getEcuSafetyConstant("IEM_SIC")
    DC-->>App: keySwdl, keyImmo, keySal, keyCommon

后续阅读建议

本文档聚焦于证书与安全常量的获取与缓存。以下页面提供了上下游的衔接: