--- feature: ecat-dcu-flash-crypto-audit status: delivered updated: 2026-09-24 branch: ecat-dcu-flash-crypto-audit commits: 28d8426..28d8426 --- # EtherCAT 总线刷写 DCU:加密与握手完整性审计 ## Report **What was built** — 对「EtherCAT 总线刷写 DCU」路径完成加密与握手完整性审计(docs-only,无代码改动)。结论:本仓库可见的 APP 交接与布局契约上 **无载荷加密**(仅镜像尾 CRC32);所谓握手是 **0x2502 交接 + 配置页 arm 门**,不是密码学握手,且 arm/令牌语义在 BOOT(本仓库不可验证)、APP 擦页会抹掉 word8 令牌字。与电机 OTA(AES-128 + nonce 会话)严格区分。 **Verification** — 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` + `motor_ota_relay.cpp`(PASS);`config.h:333` word 计数仍为 8(PASS,缺陷);`ota_boot_request.cpp:198` CAN 路径仍同步 `requestEnterBoot`(PASS,缺陷);`ota_boot_request.cpp` 无 `gracefulTeardown`/`fwdgt_*`(PASS,缺陷);`0x2500` 仅注释(PASS,BOOT 盲区);spec self-check PASS。独立审查 FAIL(1 critical 事实错误 + 1 major 范围过宽)→ 修正后 re-review PASS。 **Journey log** - `clearCrashCount()` 写 `kOtaAval` 走 `writeWord`,**不会**栈越界;OOB 仅擦除路径且 word 下标 ≥8。活缺陷是擦页抹掉 word8/9,不是该调用点。 - BOOT `ecatArmed()` / `0x2500` 会话只在 `dc_app.c` 注释里;凡 BOOT 侧行为必须标「不可验证」,不可用 APP 证据替它下无加密定论。 - 本环境再次拦截 `git worktree add`,落在分支 `ecat-dcu-flash-crypto-audit`(非隔离 worktree)。 - 工作区另有未跟踪/未提交改动(`esc_lan9252.cpp`、`config.h`、`ecat-wkc-periodic-dip.md`),与本单无关,未纳入提交。 ## [S1] Problem 需要确认当前 **EtherCAT 总线刷写 DCU 固件** 路径是否具备加密,以及握手/鉴权链路是否完整。该路径与「电机 OTA 中继」(AES-128 + nonce 会话)不同,不能混谈。审计对象为本仓库(`GD32E508_DCU_APP`)可见的 APP 交接入口、共享布局、以及文档/注释中对 BOOT 侧 `0x2500/0x2501` 直灌会话的契约描述。 ## [S2] Design ### S2.1 路径范围(唯一真源) ```text 主站 EtherCAT │ │ SDO 0x2502 = 1 ← APP 字典「GD32 boot request」 ▼ EcatEnterBootRequestAsync() ← 只置 pending(异步) │ ▼ OtaBootRequest::process() ← 主循环写配置页 + 软复位 │ CHECK=AVAL, AVAL=AVAL, EN=NOT_AVAL ▼ BOOT 起机(10s OTA 窗口) │ ecatArmed() = !(CHECK==AVAL && EN==AVAL) │ 交接后 EN=NOT_AVAL ⇒ 直灌解锁(可不依赖 word8 令牌) ▼ BOOT 对象 0x2500 / 0x2501 ← 固件灌写会话(源码不在本仓库) │ ▼ APP 区镜像 + 末尾 CRC32 ``` 对照旁路(**不在本次范围**,仅防混淆): | 路径 | 加密 | 握手 | |------|------|------| | 电机 OTA 中继(CAN 0x100/0x101) | AES-128 ECB + keyed-CRC32 | nonce → K_session → confirm 三步 | | EtherCAT ESI/EEPROM 刷写 | 无 | 无(仅 SII CRC8) | | **EtherCAT 总线刷写 DCU(本次)** | **见 S2.2** | **见 S2.3** | ### S2.2 加密结论:**本仓库可见路径没有载荷加密** | 检查项 | 结论 | 证据 | |--------|------|------| | 固件载荷 AES/对称加密(APP 可见代码) | **无** | 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` 与 `sys/motor_ota_relay.cpp`;`comm/ssc`/`ota_boot_request`/`dc_app` 零加密调用 | | 传输层机密性(APP 交接 + 布局契约) | **未见** | `dc_app.c:305-306` 注明直灌会话在 BOOT 的 `0x2500/0x2501`;本仓库可见路径无载荷加密 | | BOOT `0x2500/0x2501` 是否解密/验签 | **本仓库不可验证** | 源码不在本 repo;不能据 APP 侧断言 BOOT 一定无加密 | | 完整性校验(布局契约) | **仅有镜像尾 CRC32** | `shared/ota_layout.h:19` `OTA_LAYOUT_APP_MAX_SIZE`(216KB-4B,末尾 CRC32) | | 源码/固件签名 | **未见** | 无 ECDSA/HMAC/secure boot 引用 | **一句话(限定范围)**:在 **本仓库可见的 APP 交接与布局契约** 上,EtherCAT 刷写 DCU 是 **明文 SDO 灌写意图 + 镜像尾 CRC32**,**没有载荷加密**;BOOT 侧是否另有加密 **无法在本仓库证实或证伪**。 ### S2.3 握手结论:**不是密码学握手,只是交接 + arm 门;链路不完整** 实际存在的「握手」是 **交接(handoff)+ 配置页 arm 门**,不是 challenge-response: | 步骤 | 完整性 | 证据 | |------|--------|------| | 1. APP 交接入口 `0x2502` | **完整(异步)** | `dc_app.c:329-350` `ObjBootReqWrite` → `EcatEnterBootRequestAsync`;`ota_boot_request.cpp:257-260` 只置 pending;Flash/复位在 `process()` (`:209-248`) | | 2. 配置页 arm 语义 | **注释契约(BOOT 行为,本仓库不可验证)** | `dc_app.c:316-318` 描述 `ecatArmed() = !(CHECK==AVAL && EN==AVAL)`;实现不在 APP | | 3. word8 令牌 `ECAT_ARM_KEY=0xACA70001` | **按注释可被交接旁路;APP 擦页会毁掉令牌字** | `dc_app.c:316-318` 称交接后不必 word8;`config.h:333` word 计数=8;`ota_boot_request.cpp:54-78` 擦页只回写 8 字 → 页上 word8/9 被抹 | | 4. nonce / 会话密钥 / 双向认证 | **无** | 对比 `motor_ota_relay.cpp:896-955` 的 Req→Nonce→Confirm | | 5. BOOT 侧会话对象协议 | **本仓库不可验证** | `dc_app.c:305-306`:`0x2500/0x2501` 在 BOOT 字典;APP 无实现 | | 6. CAN 旁路 `0x320+"OTA!"` | **不完整** | `ota_boot_request.cpp:187-200` `handleCanFrame` 仍 **同步** `requestEnterBoot()`(VULN-08 未落地) | **一句话**:所谓握手 = **「让我进 BOOT」+「配置页门禁」**,**无加密握手、无会话密钥、无双向认证**;门禁语义本身在 BOOT(本仓库不可验证),且 APP 擦页会把 word8 令牌字抹掉。 ### S2.4 阻断「完整」结论的现存缺陷(APP 侧可证实) 1. **VULN-11 仍在源码(擦页抹字已证实;栈越界仅限擦除路径)**:`sys/config.h:333` `CONFIG_OTA_CONFIG_WORD_COUNT = 8U`,应为 `OTA_LAYOUT_WORD_COUNT`(`shared/ota_layout.h:48` = 10)。`requestEnterBoot()` 对 word2 写 `NOT_AVAL` 会走 `otaConfigSet` 擦除路径(`ota_boot_request.cpp:50-80`):只备份/回写 8 字 → 页上 word8 `ECAT_ARM` / word9 `CRASH_COUNT` 被抹成 `0xFFFFFFFF`。栈 `words[]` 越界写仅发生在 **目标 word 下标 ≥8 且 value==NOT_AVAL** 时(`:58`);`clearCrashCount()`(`:129-131`)写的是 `kOtaAval`,走 `writeWord`(`:50-51`),**不会**触发该越界。 2. **VULN-08 仍在源码**:`ota_boot_request.cpp:187-200` CAN `0x320` 路径在 `taskCanRxParse` 上下文同步 `requestEnterBoot()`(内部 `fmc_page_erase` `:62`,20–30ms),未走 `requestEnterBootAsync`(`:175-181`)。 3. **交接后 Flash 擦写防护未按审计加固落地**(对照 `docs/audit_ecat_spi_wifi_ota_compatibility.md` §3): - 仍调用 Flash 库 `fmc_word_program`/`fmc_page_erase`(`ota_boot_request.cpp:30,62,73`),非 SRAM 寄存器直写; - `process()` 在擦写前只关 SPI0/EXTI10_15(`:216-217`);`__disable_irq()` 仅在复位前(`:244`),**不覆盖** `fmc_page_erase` 窗口; - 未见 FWDGT 复位前扩窗(无 `fwdgt_*`); - 未见 LAN9252 PREOP / CAN STB / EXMC 优雅关停(无 `gracefulTeardown`)。 4. **BOOT 侧盲区**:`0x2500/0x2501` 是否校验 CRC32、是否接受任意明文、是否有会话状态机,**本仓库无法证明完整**(对象仅出现在 `dc_app.c:305-306` 注释)。 ### S2.5 与电机 OTA 的对照(防误读) | 维度 | 电机 OTA 中继 | EtherCAT 刷写 DCU | |------|---------------|-------------------| | 载荷加密 | AES-128 ECB 48B/包 | **APP 可见路径无**(BOOT 不可验证) | | 密钥协商 | `K_master` + nonce → `K_session` | **无** | | 确认帧 | confirm = AES(nonce⊕0x55) | **无** | | 完整性 | keyed-CRC32(session‖plain) | 镜像尾 CRC32 | | 会话绑定 | session_num + pkt_num | **未见** | | 传输保护 | ARD 单发 + BRS 压制 + 通道锁 | SDO 邮箱;通道锁与本路径无关 | ## [S3] Out of Scope - 电机 OTA 中继的 AES/握手实现质量(另有完整状态机,不在本单)。 - EtherCAT ESI/EEPROM(`gen_eeprom.py` / `ecat_flash_eeprom.py`)刷写。 - BOOT 工程 `GD32E50X_BOOT` 内 `0x2500/0x2501` 具体实现(源码不在本仓库)。 - 对上述缺陷的代码修复(本单为「检查」;修复需另开 implement 任务并获确认)。 - 电机/DCU 联合端到端刷写台架实测。 ## Tasks - [x] T1: 梳理 EtherCAT 刷写 DCU 全路径与对象边界 — acceptance: S2.1 流程与对象归属可对照源码/注释逐点定位 (covers: S2.1) - [x] T2: 判定载荷加密有无 — acceptance: S2.2 表中每行有仓库内证据或明确「未见」 (covers: S2.2) - [x] T3: 判定握手/arm 链完整性 — acceptance: S2.3 表逐步给出完整/不完整/不可验证,并指出门禁旁路与令牌破坏路径 (covers: S2.3) - [x] T4: 列出阻断「完整」结论的 APP 侧缺陷 — acceptance: S2.4 各条可指到 `file:line` (covers: S2.4) - [x] T5: 输出与电机 OTA 的对照 — acceptance: S2.5 无混淆两路径的加密/握手结论 (covers: S2.5)