戰(zhàn):從原理到量產(chǎn))
1. 項(xiàng)目背景與核心需求解析1.1 這個(gè)項(xiàng)目解決什么問題OEMiROT全稱是 OEM Immutable Root of Trust屬于 STM32Trust 安全框架里的一環(huán)。提它之前先問一個(gè)問題你的固件燒進(jìn)芯片之后怎么保證它是可信的怎么防止別人把非法的固件刷進(jìn)設(shè)備里冒充你的產(chǎn)品怎么保證運(yùn)行時(shí)的代碼沒有被人篡改過傳統(tǒng)做法是給產(chǎn)品加一個(gè)外部安全芯片或者依賴 MCU 內(nèi)部唯一的 ID 做簡單的校驗(yàn)。但這些方案要么增加 BOM 成本要么強(qiáng)度不夠。STM32 家族里帶 TrustZone 的芯片比如 STM32C5 這顆基于 Cortex-M33 內(nèi)核的 MCU內(nèi)置了專門的安全啟動引擎OEMiROT 就是在這個(gè)引擎上實(shí)現(xiàn)的一套完整方案。OEMiROT 做的事情概括起來有三件一是上電后先驗(yàn)證固件簽名確保固件確實(shí)是廠商簽發(fā)的二是通過 TrustZone 把安全資源密鑰、安全存儲區(qū)與普通應(yīng)用隔離三是支持固件升級時(shí)的驗(yàn)簽流程防止升級過程被中間人劫持。這套方案最大的價(jià)值在于安全啟動的根密鑰和驗(yàn)證邏輯被放在芯片內(nèi)置的不可修改區(qū)域即使應(yīng)用代碼被完全逆向也無法偽造出合法的固件。1.2 為什么用 STM32C5 做 OEMiROTSTM32C5 是 ST 主推的新一代主流型 MCUCortex-M33 內(nèi)核帶 TrustZone主頻能跑到 250MHz。選擇它做 OEMiROT 項(xiàng)目有幾個(gè)現(xiàn)實(shí)理由Cortex-M33 原生支持 TrustZone 隔離這是硬件層面的安全基礎(chǔ)OEMiROT 需要這種隔離能力來保護(hù)密鑰和驗(yàn)證邏輯。STM32C5 的 Flash 和 SRAM 容量足夠承載雙鏡像固件應(yīng)用 A/B 分區(qū) bootloader產(chǎn)品方案容易落地。這顆芯片定位是“安全性能成本”的平衡點(diǎn)比 STM32H5 便宜比 STM32L5 性能強(qiáng)正好適合物聯(lián)網(wǎng)網(wǎng)關(guān)、邊緣節(jié)點(diǎn)、工業(yè)控制器這類對安全有要求但又不希望成本失控的產(chǎn)品。另外ST 的官方固件包STM32CubeC5里已經(jīng)集成了 OEMiROT 的完整參考例程不需要從零寫安全啟動邏輯難度大幅降低。你要做的只是學(xué)會配置、編譯、燒錄和驗(yàn)證流程理解它背后的機(jī)制然后移植到自己的板子上。1.3 適合誰看這篇如果你是在 STM32 上做產(chǎn)品開發(fā)的工程師想把安全啟動集成進(jìn)自己的方案正在用 STM32CubeIDE那么這篇內(nèi)容就是給你的。閱讀之前建議先了解一點(diǎn) TrustZone 的基礎(chǔ)概念知道 secure/non-secure 世界是怎么回事。如果你完全沒碰過安全啟動也不用慌我會把鏈路理清楚包括一些容易踩坑的細(xì)節(jié)。2. OEMiROT 工作原理與整體方案拆解2.1 OEMiROT 的信任根模型要理解 OEMiROT先得理解 ROT 這個(gè)概念。ROTRoot of Trust信任根是安全世界里所有信任的起點(diǎn)。它必須滿足一個(gè)條件自身是不可偽造、不可篡改的。OEMiROT 之所以叫“OEM Immutable”因?yàn)樗囊龑?dǎo)代碼BootROM 或不可寫的 Flash 區(qū)域在芯片出廠時(shí)就固定下來或者由廠商在首次燒錄后通過 Option Byte 鎖定之后任何人都改不了這段代碼。OEMiROT 的完整信任鏈?zhǔn)沁@樣的芯片上電 | v BootROM不可修改加載 OEMiROT 引導(dǎo)代碼 | v OEMiROT 校驗(yàn)鏡像簽名RSA/ECDSA | v 通過 - 加載應(yīng)用固件啟動 TrustZone 隔離 | v 失敗 - 進(jìn)入恢復(fù)模式/等待升級OEMiROT 本身是“Immutable Root of Trust”里的一環(huán)它負(fù)責(zé)校驗(yàn)可變區(qū)域應(yīng)用固件的真實(shí)性。應(yīng)用固件內(nèi)部還可以繼續(xù)建立自己的信任鏈比如校驗(yàn)外置 Flash 里的資源文件簽名這就形成了完整的鏈?zhǔn)叫湃巍?.2 鏡像簽名與驗(yàn)證機(jī)制OEMiROT 的驗(yàn)簽依賴非對稱加密。芯片內(nèi)置或存儲在受保護(hù)區(qū)域一把公鑰廠商持有對應(yīng)的私鑰。固件編譯完成后用私鑰對固件鏡像做簽名簽名值連同鏡像一起燒入芯片。上電后 OEMiROT 從受保護(hù)區(qū)域讀出公鑰對鏡像做驗(yàn)簽。這里的幾個(gè)關(guān)鍵點(diǎn)公鑰存儲在不可篡改的區(qū)域在 STM32C5 上公鑰通常存放在 UFBUser Flash Bank的保留區(qū)域或者由 TrustZone 保護(hù)的安全存儲區(qū)。簽名算法常見為 RSA-2048 或 ECDSA-P256。ECDSA 簽名短、驗(yàn)證快適合嵌入式RSA 兼容性更好很多老的升級鏈路還在用。反回滾保護(hù)固件鏡像里帶一個(gè)版本號OEMiROT 會把它和當(dāng)前記錄在案的最低允許版本比較如果版本比現(xiàn)存的還低就拒絕加載。這是防降級攻擊的關(guān)鍵。2.3 安全狀態(tài)機(jī)與生命周期管理OEMiROT 還有一個(gè)容易被忽略的部分芯片安全生命周期。STM32 的 TrustZone 芯片內(nèi)部維護(hù)一個(gè)三態(tài)安全狀態(tài)Open開放、Closed封閉、Locked鎖定。Open 狀態(tài)下可以自由調(diào)試、讀寫 Flash適合開發(fā)階段。Closed 狀態(tài)下 TrustZone 的隔離生效非安全世界無法訪問安全資源調(diào)試接口受限。Locked 狀態(tài)下安全啟動徹底封閉任何外部手段都無法改寫安全區(qū)內(nèi)容適合量產(chǎn)。在開發(fā)階段你基本保持在 Open 或 Closed燒錄時(shí)要注意一旦切到 Closed再想用 ST-Link 調(diào)試器全功能訪問內(nèi)核就難了需要先做一次 Full Reset 或者通過專用流程才能回到 Open。這個(gè)特性你后面部署 CI 自動燒錄時(shí)肯定會遇到提前記住。3. 開發(fā)環(huán)境搭建與 CubeMX 工程配置3.1 工具鏈版本選擇做 OEMiROT 這種安全開發(fā)工具鏈版本非常重要。ST 對安全功能的支持是跟隨固件包和 IDE 版本迭代的老版本可能缺功能新版本可能引入行為變更。我的建議是直接裝當(dāng)前主流的穩(wěn)定版本STM32CubeIDE1.16 或更高。我用的 1.16.1實(shí)測穩(wěn)定。STM32CubeMX6.12 或更高。從 CubeIDE 1.16 開始已經(jīng)集成了 CubeMX不需要單獨(dú)裝但如果你習(xí)慣單獨(dú)用 CubeMX 生成工程再導(dǎo)入 IDE注意版本別差太多。STM32CubeC5 Firmware PackageV1.0.0 或更新版本。這個(gè)包在 CubeMX 里選芯片時(shí)會自動下載也可以去 ST 官網(wǎng)手動下載然后導(dǎo)入。注意一點(diǎn)STM32CubeC5 的固件包比較大包含所有外設(shè)驅(qū)動、中間件和例程。首次下載如果網(wǎng)絡(luò)不穩(wěn)定容易失敗。解決辦法是手動下載 ZIP 包然后在 CubeMX 的 Firmware Package Manager 里從本地導(dǎo)入。3.2 CubeMX 工程初始化要點(diǎn)用 CubeMX 創(chuàng)建 OEMiROT 工程最關(guān)鍵的一步是選對例程目錄。不要從空工程開始配置因?yàn)?OEMiROT 涉及 TrustZone 工程分區(qū)、鏈接腳本、啟動文件等一系列復(fù)雜設(shè)置手搓容易出問題。正確姿勢是直接從固件包里拷貝 OEMiROT 例程然后在 CubeMX 里調(diào)整配置。具體的步驟如下打開 STM32CubeMX選擇 New Project在 MCU Selector 里搜 STM32C5選具體型號比如 STM32C531R6Tx。如果項(xiàng)目不想從零配可以直接 File - New Project 后選 Example Selector在列表里搜 OEMiROT。ST 在 CubeC5 里提供了OEMiROT_Boot引導(dǎo)工程和OEMiROT_App應(yīng)用示例兩個(gè)工程。如果沒有看到 Example說明固件包沒裝全?;氐?Help - Manage Embedded Software Packages勾選 STM32CubeC5 并完成安裝。選定芯片和例程后CubeMX 會自動生成 TrustZone 相關(guān)的工程結(jié)構(gòu)包括 secure 和 non-secure 兩個(gè)子工程。3.3 TrustZone 內(nèi)存布局配置OEMiROT 成功運(yùn)行的一個(gè)核心條件內(nèi)存布局必須正確。CubeMX 會生成默認(rèn)的 TrustZone 分區(qū)但不同板子的 Flash/SRAM 大小不同需要手動核對。在 Project Manager - Linker Settings 里重點(diǎn)檢查這幾項(xiàng)Secure Flash 起始地址OEMiROT 的 boot 代碼通常放在 Flash 起始處的 Secure 區(qū)比如 0x0C0000 起始的 64KB 區(qū)域。Non-Secure Flash 起始地址應(yīng)用固件放在 Non-Secure 區(qū)比如 0x0D0000 起始。Secure SRAM 與 Non-Secure SRAM 的劃分注意 Cortex-M33 的 TrustZone 通過 SAUSecurity Attribution Unit控制地址空間的屬性CubeMX 生成的配置腳本會處理好但你要確認(rèn) SRAM 分界的起始地址沒有重疊。提示Debug 配置下 ST-Link 燒錄時(shí)如果地址配置和鏈接腳本不一致最常見的表現(xiàn)是燒錄成功但一運(yùn)行就進(jìn) HardFault。排查第一步永遠(yuǎn)是核對內(nèi)存地址。3.4 生成工程并導(dǎo)入 CubeIDECubeMX 配置完成后點(diǎn)擊右上角的 GENERATE CODE選擇 Toolchain 為 STM32CubeIDE指定工程名和路徑。生成完成后先用 CubeIDE 打開工程。這時(shí)你會看到工程里有兩個(gè)子項(xiàng)目OEMiROT_Boot安全引導(dǎo)工程編譯生成 bootloader。OEMiROT_App應(yīng)用工程編譯生成用戶應(yīng)用固件。注意這兩個(gè)工程是關(guān)聯(lián)的編譯順序有講究。先編譯 boot再編譯 app最后燒錄時(shí)也是先燒 boot再燒 app。CubeIDE 會把兩個(gè)工程都放進(jìn) workspace但你需要手動切換 active project。4. 核心實(shí)操編譯、燒錄與驗(yàn)證全流程4.1 配置密鑰與簽名工具OEMiROT 的根密鑰是整個(gè)安全方案的心臟。ST 的例程里已經(jīng)生成了測試密鑰對位于固件包的Middlewares/ST/STM32_TRUSTZONE/OEMiROT/Keys目錄下OEMiROT_Boot_private.pem私鑰用于簽名固件。OEMiROT_Boot_public.pem公鑰會被編譯進(jìn) bootloader用于驗(yàn)證固件簽名。OEMiROT_Boot_public.pem對應(yīng)的.der或.c文件轉(zhuǎn)換格式后嵌入工程。開發(fā)階段直接用 ST 提供的測試密鑰沒問題但量產(chǎn)前一定要替換成自己生成的密鑰對。用 OpenSSL 生成 ECDSA 密鑰的命令openssl ecparam -name prime256v1 -genkey -noout -out OEMiROT_Boot_private.pem openssl ec -in OEMiROT_Boot_private.pem -pubout -out OEMiROT_Boot_public.pem生成新密鑰后需要把公鑰轉(zhuǎn)換成 C 數(shù)組并替換工程中的key.c文件。具體格式可以參考 ST 例程自帶的轉(zhuǎn)換腳本執(zhí)行一次替換后要重新編譯 boot。4.2 編譯步驟與參數(shù)設(shè)置編譯前把 CubeIDE 的 active project 切到OEMiROT_Boot然后 Project - Build。編譯過程中如果報(bào)錯(cuò)先看是不是密鑰文件路徑問題再排查是不是鏈接腳本和內(nèi)存配置不一致。Boot 編譯成功之后切到OEMiROT_App編譯應(yīng)用工程。這里有個(gè)關(guān)鍵點(diǎn)應(yīng)用工程的鏡像需要先生成帶簽名的燒錄文件。CubeIDE 的 build 步驟會自動調(diào)用 STM32CubeProgrammer 的簽名工具完成簽名前提是你在工程屬性里配置了 Post-build 命令。在OEMiROT_App的 Project Properties - C/C Build - Settings - Build Steps - Post-build steps 里確認(rèn)里面的簽名命令正確。典型命令長這樣STM32_Signing_Tool_CLI.exe -s OEMiROT_Boot_private.pem -a 0x0D0000 -d app.bin -o app_signed.bin參數(shù)含義-s指定私鑰-a指定應(yīng)用固件的加載地址必須和鏈接腳本里的 Non-Secure Flash 起始地址一致-d輸入 bin 文件-o輸出簽名后的 bin 文件4.3 燒錄順序與操作細(xì)節(jié)燒錄工具推薦用 STM32CubeProgrammer圖形界面和命令行都可以。命令行方式適合腳本化我實(shí)際量產(chǎn)用的就是命令行方式。第一塊板子建議按以下順序操作先燒錄 bootloader。把 CubeIDE 的 debug configuration 指到 boot 工程的 elf 文件用 ST-Link 燒錄。用 STM32CubeProgrammer 燒錄簽名后的應(yīng)用固件STM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -w app_signed.bin 0x0D0000驗(yàn)證燒錄是否正確STM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -r8 0x0D0000 16如果一切正常復(fù)位后 LED 按應(yīng)用代碼邏輯閃爍說明安全啟動鏈路已經(jīng)跑通。4.4 驗(yàn)證 OEMiROT 是否真的在起作用這里有個(gè)很容易犯的錯(cuò)誤燒錄成功、代碼能跑就以為 OEMiROT 生效了。錯(cuò)誤的認(rèn)知。你需要主動做一次負(fù)向測試。操作方法用隨便一個(gè)文本編輯器打開 app_signed.bin修改其中一個(gè)字節(jié)比如把 0x00 改成 0xFF保存后重新燒錄。然后復(fù)位看現(xiàn)象如果 OLEDiROT 拒絕啟動LED 不閃說明驗(yàn)簽真的在工作。如果代碼照樣跑說明你燒的固件沒走 OEMiROT 路徑問題出在啟動配置上。這個(gè)測試我每次切板子型號后都會做一遍花兩分鐘能省掉后面一堆排查時(shí)間。5. 常見問題與排查技巧實(shí)錄5.1 啟動失敗LED 不亮代碼沒跑排查這類問題不要急著看應(yīng)用代碼先從鏈路源頭查。我自己的排查順序是用 STM32CubeProgrammer 讀取芯片的狀態(tài)和當(dāng)前啟動地址確認(rèn) boot 有沒有執(zhí)行。確認(rèn) Option Bytes 配置OEMiROT 要求TZEN1、DBANK1、SECBOOT1任何一個(gè)不對都會卡在啟動早期。確認(rèn) boot 工程里鏈接的公鑰和簽名用的私鑰是對應(yīng)的。測試密鑰替換場景最容易犯這個(gè)錯(cuò)。用調(diào)試器在 boot 的驗(yàn)簽函數(shù)入口打斷點(diǎn)逐步看是走到驗(yàn)簽失敗分支還是根本沒進(jìn)到驗(yàn)簽邏輯。注意如果芯片已經(jīng)切到 Closed 狀態(tài)調(diào)試器訪問受限需要先做一次全擦除回到 Open 狀態(tài)才能繼續(xù)調(diào)試。5.2 簽名校驗(yàn)失敗的典型原因驗(yàn)簽失敗是安全啟動項(xiàng)目里最常見的錯(cuò)誤。我整理了一份速查表現(xiàn)象可能原因檢查方法一直卡在 boot 不進(jìn) app公鑰和私鑰不匹配重新生成密鑰對確認(rèn) boot 和簽名用的是同一對啟動偶爾成功偶爾失敗燒錄地址不對核對 app 的鏈接地址與燒錄命令地址升級后無法啟動反回滾版本號比當(dāng)前低更新固件版本號確認(rèn)版本號單調(diào)遞增燒錄后校驗(yàn)失敗bin 文件沒有簽名確認(rèn) Post-build 命令正確執(zhí)行燒錄的是簽名后的文件5.3 調(diào)試接口被鎖定怎么辦安全項(xiàng)目的必然結(jié)果隨著安全狀態(tài)推進(jìn)調(diào)試口會越來越難訪問。我在用 STM32C5 的早期階段就把兩塊板子的調(diào)試口鎖死過當(dāng)時(shí)以為芯片廢了其實(shí)有解?;謴?fù)方法使用 STM32CubeProgrammer 的 HOTPLUG 模式連接在復(fù)位向量處做 mass erase。前提是芯片還沒到 Locked 狀態(tài)。如果已經(jīng) Locked只能通過 boot 引腳進(jìn)入系統(tǒng) bootloader 模式再用 UART 或 USB DFU 做全擦除。這個(gè)流程建議你在開發(fā)板上一開始就測通否則量產(chǎn)階段遇到問題會非常被動。5.4 CubeIDE 編譯報(bào)依賴錯(cuò)誤的處理OEMiROT 例程涉及兩個(gè)子工程互相引用頭文件和庫CubeIDE 的 build 順序配置一旦丟了就會報(bào)各種找不到符號的錯(cuò)誤。處理方法右鍵 boot 工程 - Properties - C/C Build - Project References勾選依賴的 app 工程反過來 app 工程也要勾選依賴的 boot 工程。兩邊都勾好后Clean 一次重新 Build。另外建議把 cubeide 的 build 并發(fā)線程數(shù)調(diào)低Window - Preferences - C/C - Build - Jobs避免多線程編譯時(shí)亂序?qū)е侣窂浇馕霎惓!?. 量產(chǎn)準(zhǔn)備與腳本化發(fā)布6.1 替換量產(chǎn)密鑰并重新編譯全鏈路工程項(xiàng)目驗(yàn)證通過后第一件事就是替換測試密鑰。這個(gè)操作不復(fù)雜但涉及面廣容易遺漏生成新密鑰對ECDSA P-256 或 RSA-2048。把新公鑰轉(zhuǎn)換成 C 數(shù)組文件替換 boot 工程里舊的key.c。重新編譯 boot 和 app。把新私鑰放到簽名工具調(diào)用的固定路徑更新 Post-build 命令里的私鑰路徑。全鏈路的固件重新簽名重新燒錄驗(yàn)證。替換密鑰后舊簽名固件在新 boot 下會驗(yàn)簽失敗這是預(yù)期行為。6.2 用腳本固化燒錄流程到了要燒幾十塊板子的時(shí)候手工操作 STM32CubeProgrammer 太慢了。我把整個(gè)燒錄流程寫成批處理核心邏輯分三步燒錄 bootSTM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -w OEMiROT_Boot.elf燒錄簽名 appSTM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -w app_signed.bin 0x0D0000配置 Option Byte 切到 Closed 狀態(tài)STM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -ob TZEN1 DBANK1 SECBOOT1注意Option Byte 的寫入要放在所有燒錄動作之后一旦寫入 Closed后續(xù)調(diào)試接口受限所以這個(gè)動作要設(shè)計(jì)成單獨(dú)的腳本步驟來執(zhí)行。6.3 生產(chǎn)環(huán)境下的反回滾策略反回滾做得好不好直接決定產(chǎn)品的可維護(hù)性。OEMiROT 的版本號機(jī)制我建議這樣設(shè)計(jì)開發(fā)階段版本號從 0 開始每發(fā)一版 1。量產(chǎn)前的 final 版本版本號抬高到 100 以上給后面留足空間。生產(chǎn)線的固件版本號固定不跟隨日常開發(fā)版本遞增。原因是一旦某個(gè)版本在產(chǎn)線上被批量燒錄反回滾機(jī)制會拒絕低于當(dāng)前版本號的固件回刷。如果版本號規(guī)劃不好后期想修復(fù) bug 卻發(fā)不了新版就只能全部返廠成本極高。7. 一點(diǎn)個(gè)人經(jīng)驗(yàn)總結(jié)做了幾年的 STM32 安全啟動方案我的感受是OEMiROT 的難點(diǎn)不在“跑通”而在“想清楚”。跑通只需要跟著例程走一遍想清楚卻需要理解安全模型、密鑰管理、生命周期管理這些底層邏輯。STM32C5 上跑 OEMiROT 整體體驗(yàn)比老平臺順暢很多CubeIDE 的集成度高CubeMX 生成的工程結(jié)構(gòu)干凈出錯(cuò)率低。最后再分享一個(gè)實(shí)際操作中的小技巧OEMiROT 工程第一次跑通后馬上把整個(gè) workspace 打一個(gè) zip 備份注釋里寫上日期和芯片型號。后面每次調(diào)整配置或換板子都從這個(gè)基線開始而不是從最新的工程狀態(tài)開始。這能幫你避免很多“改了一堆東西最后不知道哪一步把鏈路搞壞了”的窘境。安全啟動這種東西一旦遇到問題排查鏈路非常長有一個(gè)干凈基線在手很多問題都能快速對照出差異來。