From 217447f82bcafa1bec0cad2f0170dc5056ea3c9b Mon Sep 17 00:00:00 2001 From: sanchuanhehe Date: Sat, 24 Oct 2026 12:51:28 +0800 Subject: [PATCH] feat(ws53): validate LoaderBoot and plan preserved application updates --- README.md | 28 ++ .../examples/inspect_ws53_loader.rs | 13 + .../examples/plan_ws53_application.rs | 51 ++++ crates/hisi-fwpkg/src/lib.rs | 1 + crates/hisi-fwpkg/src/ws53.rs | 146 ++++++++++ crates/hisi-fwpkg/src/ws53/application.rs | 257 ++++++++++++++++++ 6 files changed, 496 insertions(+) create mode 100644 crates/hisi-fwpkg/examples/inspect_ws53_loader.rs create mode 100644 crates/hisi-fwpkg/examples/plan_ws53_application.rs create mode 100644 crates/hisi-fwpkg/src/ws53.rs create mode 100644 crates/hisi-fwpkg/src/ws53/application.rs diff --git a/README.md b/README.md index 4268c9e..30814bb 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,34 @@ ELF / .bin ──▶ flatten (objcopy) ──▶ 0x300 image header + body ─ ## CLI +### WS53(实验性 LoaderBoot 检查与应用替换规划) + +新增 `ws53::inspect_loader` 检查 WS53 SDK 的 28 KiB LoaderBoot 下载包: +`0x80` 根头、`0x100` key 头、`0x200` code-info 和正文 SHA-256。 +可运行 `cargo run -p hisi-fwpkg --example inspect_ws53_loader -- 包文件`。 +它只检查格式与完整性,不验证签名、芯片身份,也不访问硬件。 +来源为 `fbb_ws53` 提交 `163d9150`;实际传输由 hisiflash 的 WS53 模块负责。 + +WS53 组合镜像包含现有控制核和应用核;不能使用下方 WS63 的地址和默认参数 +打包烧录。新增 `ws53::plan_unsigned_application` 根据完整4MiB备份规划应用替换: +检查分区、组合正文哈希、明文/非压缩条件;保留控制核、应用占位头、校准/NV, +只输出变化的完整4KiB扇区和对应原始恢复内容,组合头最后写入。 +当前限定已观察布局,入口`0x45d300`,不推断其他WS53板型。 + +```sh +cargo run -p hisi-fwpkg --example plan_ws53_application -- 原始备份.bin 已验证Flash链接应用.bin 新输出目录 +``` + +规划不访问设备、不验证ELF布局、不生成有效签名;调用方必须先只读核实安全启动 +已关闭,不能通过修改eFuse来满足条件。旧签名保留但更换正文/摘要后不再有效, +不能将该计划用于要求签名的板卡。RAM ELF不能直接当作Flash应用。 +2026-10-24 已在已确认安全验证关闭的 A 板上,用本规划器生成的三个扇区, +经 hisiflash 完成 Rust 应用烧录、整片回读及串口/物理复位启动;TIMER0 驱动 +MIO17 的32次10ms翻转经DSLogic和同ELF QEMU对照通过。这个实测结论仅适用于 +已核对的板卡/启动布局,不能外推到其他WS53板型或签名策略。 +断电冷启动及本次应用替换后的原厂恢复仍未验收。普通WS63/BS2X CLI及默认 +地址保持不变。本模块不包含、发布或依赖私有整片备份;单元测试使用合成数据。 + ```bash cargo install --path crates/hisi-fwpkg-cli # installs `hisi-fwpkg` diff --git a/crates/hisi-fwpkg/examples/inspect_ws53_loader.rs b/crates/hisi-fwpkg/examples/inspect_ws53_loader.rs new file mode 100644 index 0000000..114a545 --- /dev/null +++ b/crates/hisi-fwpkg/examples/inspect_ws53_loader.rs @@ -0,0 +1,13 @@ +//! Inspect a local vendor package; no serial/device I/O. +use sha2::{Digest, Sha256}; +fn main() -> Result<(), Box> { + let path = std::env::args() + .nth(1) + .ok_or("usage: inspect_ws53_loader package.bin")?; + let bytes = std::fs::read(path)?; + let loader = hisi_fwpkg::ws53::inspect_loader(&bytes)?; + println!("size={} body={} sha256={:x} placeholder_root={} enc_flag={:#x} signature_verified=false chip_identity_verified=false", + loader.package.len(), loader.body.len(), Sha256::digest(loader.package), + loader.has_placeholder_root, loader.code_encryption_flag); + Ok(()) +} diff --git a/crates/hisi-fwpkg/examples/plan_ws53_application.rs b/crates/hisi-fwpkg/examples/plan_ws53_application.rs new file mode 100644 index 0000000..f8c010a --- /dev/null +++ b/crates/hisi-fwpkg/examples/plan_ws53_application.rs @@ -0,0 +1,51 @@ +//! Development planner only. No device I/O, signing or hardware qualification. +use hisi_fwpkg::ws53::plan_unsigned_application; +use sha2::{Digest, Sha256}; +use std::{io::Write, path::PathBuf}; +fn main() -> Result<(), Box> { + let args: Vec<_> = std::env::args().collect(); + if args.len() != 4 { + return Err( + "usage: plan_ws53_application RAW_FULL_BACKUP FLASH_LINKED_APP_BIN NEW_OUTPUT_DIR" + .into(), + ); + } + let backup = std::fs::read(&args[1])?; + let app = std::fs::read(&args[2])?; + // This raw-bin planner does not infer entry/segments from a RAM ELF. + if app.starts_with(b"\x7fELF") { + return Err("expected validated Flash-linked raw bytes, not ELF".into()); + } + let plan = plan_unsigned_application(&backup, &app)?; + let out = PathBuf::from(&args[3]); + std::fs::create_dir(&out)?; + let mut manifest = String::from("offset\tlength\toriginal_sha256\treplacement_sha256\n"); + for sector in &plan.sectors { + for (suffix, bytes) in [ + ("original", §or.original), + ("replacement", §or.replacement), + ] { + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(out.join(format!("{:06x}-{suffix}.bin", sector.offset)))?; + file.write_all(bytes)?; + file.sync_all()?; + } + manifest += &format!( + "{:#x}\t{}\t{:x}\t{:x}\n", + sector.offset, + sector.original.len(), + Sha256::digest(§or.original), + Sha256::digest(§or.replacement) + ); + } + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(out.join("sectors.tsv"))?; + file.write_all(manifest.as_bytes())?; + file.sync_all()?; + println!("entry={:#x} sectors={} backup_sha256={:x} requires_secure_boot_disabled=true signatures_regenerated=false hardware_verified=false",plan.entry,plan.sectors.len(),Sha256::digest(&backup)); + Ok(()) +} diff --git a/crates/hisi-fwpkg/src/lib.rs b/crates/hisi-fwpkg/src/lib.rs index 7709dfe..af4844a 100644 --- a/crates/hisi-fwpkg/src/lib.rs +++ b/crates/hisi-fwpkg/src/lib.rs @@ -43,6 +43,7 @@ pub mod error; pub mod fwpkg; pub mod image; pub mod plan; +pub mod ws53; #[cfg(feature = "elf")] pub mod elf; diff --git a/crates/hisi-fwpkg/src/ws53.rs b/crates/hisi-fwpkg/src/ws53.rs new file mode 100644 index 0000000..2333c97 --- /dev/null +++ b/crates/hisi-fwpkg/src/ws53.rs @@ -0,0 +1,146 @@ +//! Experimental WS53 image inspection, independent of WS63 address defaults. +//! +//! This validates the SDK LoaderBoot envelope and body hash, not chip identity +//! or a signature. Flash application packaging needs the actual partition +//! table and preservation of the existing control-core image; it is not yet +//! offered through `Chip` or `pack_app_fwpkg`. +use crate::{Error, Result}; +use sha2::{Digest, Sha256}; +mod application; +pub use application::{plan_unsigned_application, ApplicationPlan, ApplicationSector}; + +/// Root public-key header bytes before the LoaderBoot key and code-info areas. +pub const LOADER_ROOT_LEN: usize = 0x80; +/// Body offset in a WS53 SDK LoaderBoot download package. +pub const LOADER_BODY_OFFSET: usize = 0x380; +/// Body size explicitly produced by the SDK WS53 signing script. +pub const SDK_LOADER_BODY_LEN: usize = 28 * 1024; + +/// Parsed SDK LoaderBoot envelope. This is not permission to flash or run it. +#[derive(Debug)] +pub struct LoaderBoot<'a> { + /// Complete package for the ROM YMODEM transfer. + pub package: &'a [u8], + /// Code body, whose SHA-256 was compared with its header. + pub body: &'a [u8], + /// Hash verified against the code-info area. + pub body_sha256: [u8; 32], + /// True for the SDK's missing-root-key fallback of 128 FF bytes. + pub has_placeholder_root: bool, + /// Raw encryption flag; interpretation differs between loader and app. + pub code_encryption_flag: u32, +} + +fn invalid(message: &str) -> Error { + Error::InvalidFwpkg(format!("WS53 LoaderBoot: {message}")) +} + +fn word(bytes: &[u8], offset: usize) -> u32 { + // Only called after the exact envelope length has been validated. + u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) +} + +/// Validate the 28 KiB WS53 SDK LoaderBoot envelope without invoking vendor tools. +/// +/// Sources: `fbb_ws53` 163d9150 `params_and_bin_sign.py`, +/// `loaderboot_ws53_ecc.cfg`, and `secure_verify_boot_v1/secure_verify_boot.h`. +/// Unknown root formats, nonlocal code pointers, compression, wrong structure +/// lengths, truncation, trailing bytes and corrupted code hashes are rejected. +/// Root/key signatures are not verified; callers must separately qualify the +/// package source/hash and select the target. A header is not a chip ID. +pub fn inspect_loader(package: &[u8]) -> Result> { + if package.len() != LOADER_BODY_OFFSET + SDK_LOADER_BODY_LEN { + return Err(invalid("expected 0x380-byte envelope and 28 KiB body")); + } + let placeholder = package[..LOADER_ROOT_LEN].iter().all(|&b| b == 0xff); + if !placeholder && word(package, 0) != 0x4ba5c31e { + return Err(invalid("unknown root header")); + } + for (offset, expected) in [ + (0x80, 0x4bd2f01e), + (0x84, 0x10000), + (0x88, 0x100), + (0xc8, 0), // code-info follows key area, not an external pointer + (0x180, 0x4bd2f02d), + (0x184, 0x10000), + (0x188, 0x200), + (0x1a0, 0), + (0x1a4, SDK_LOADER_BODY_LEN as u32), + (0x1fc, 0), // no compression in this profile + (0x204, 0x10000), // SDK text-segment bound + ] { + if word(package, offset) != expected { + return Err(invalid("unsupported envelope field")); + } + } + let body = &package[LOADER_BODY_OFFSET..]; + let hash: [u8; 32] = Sha256::digest(body).into(); + if package[0x1a8..0x1c8] != hash { + return Err(invalid("body SHA-256 mismatch")); + } + Ok(LoaderBoot { + package, + body, + body_sha256: hash, + has_placeholder_root: placeholder, + code_encryption_flag: word(package, 0x1c8), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + fn fixture() -> Vec { + let mut bytes = vec![0; LOADER_BODY_OFFSET + SDK_LOADER_BODY_LEN]; + bytes[..0x80].fill(0xff); + for (offset, value) in [ + (0x80, 0x4bd2f01eu32), + (0x84, 0x10000), + (0x88, 0x100), + (0x180, 0x4bd2f02d), + (0x184, 0x10000), + (0x188, 0x200), + (0x1a4, 0x7000), + (0x204, 0x10000), + ] { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + bytes[LOADER_BODY_OFFSET..].fill(0x42); + let hash = Sha256::digest(&bytes[LOADER_BODY_OFFSET..]); + bytes[0x1a8..0x1c8].copy_from_slice(&hash); + bytes + } + #[test] + fn loader_views_and_negative_envelopes() { + let bytes = fixture(); + let image = inspect_loader(&bytes).unwrap(); + assert!(image.has_placeholder_root); + assert_eq!(image.body.len(), SDK_LOADER_BODY_LEN); + for offset in [ + 0, + 0x80, + 0x84, + 0x88, + 0xc8, + 0x180, + 0x184, + 0x188, + 0x1a0, + 0x1a4, + 0x1a8, + 0x1fc, + 0x204, + LOADER_BODY_OFFSET, + ] { + let mut bad = bytes.clone(); + bad[offset] ^= 1; + assert!(inspect_loader(&bad).is_err(), "offset {offset:#x}"); + } + for len in [0, 0x80, LOADER_BODY_OFFSET, bytes.len() - 1] { + assert!(inspect_loader(&bytes[..len]).is_err()); + } + let mut extra = bytes; + extra.push(0); + assert!(inspect_loader(&extra).is_err()); + } +} diff --git a/crates/hisi-fwpkg/src/ws53/application.rs b/crates/hisi-fwpkg/src/ws53/application.rs new file mode 100644 index 0000000..42af3ff --- /dev/null +++ b/crates/hisi-fwpkg/src/ws53/application.rs @@ -0,0 +1,257 @@ +//! Combined-control/application image planning from an existing WS53 backup. +use super::word; +use crate::{Error, Result}; +use sha2::{Digest, Sha256}; + +/// One complete sector, with the original bytes needed for explicit recovery. +#[derive(Debug)] +pub struct ApplicationSector { + /// Physical Flash offset, not CPU-mapped address. + pub offset: u32, + /// Original full-sector contents; persist before programming. + pub original: Vec, + /// Replacement full-sector contents. + pub replacement: Vec, +} + +/// Experimental plaintext application replacement. No device access occurs. +#[derive(Debug)] +pub struct ApplicationPlan { + /// CPU entry address expected by the existing vendor flashboot. + pub entry: u32, + /// App sectors first, combined-header sector last. No control payload edit. + pub sectors: Vec, + /// SHA-256 of the complete input backup, including untouched NV. + pub backup_sha256: [u8; 32], + /// SHA-256 of the resulting combined code body. + pub combined_body_sha256: [u8; 32], + /// Always true: this function does not regenerate cryptographic signatures. + pub requires_secure_boot_disabled: bool, +} + +fn invalid(message: &str) -> Error { + Error::InvalidFwpkg(format!("WS53 application: {message}")) +} + +/// Plan a plaintext XIP application at0x45d300, preserving the original control +/// payload, partition table, application placeholder, calibration/NV, key and +/// security policy bytes. Only changed whole sectors are emitted. The original +/// combined body length and unused old application tail remain unchanged. +/// +/// Qualified layout:4MiB, combined0x30000+0x240000, app0x5d000+0x213000. +/// Rejects encrypted/compressed input, malformed/overlapping partitions, bad +/// body hash and applications exceeding the original signed-body envelope. +/// This is NOT a signer: updating the hash invalidates any original code-info +/// signature. Before using this plan, caller must establish that secure boot +/// is disabled without changing eFuse/security settings, validate ELF entry, +/// ABI/segments/startup, and qualify the actual device and raw backup mapping. +/// The bytes must be a Flash-linked app, never a RAM takeover binary. Hardware +/// reset/cold boot and restoration must be tested separately. +pub fn plan_unsigned_application(backup: &[u8], application: &[u8]) -> Result { + if backup.len() != 0x400000 || application.is_empty() { + return Err(invalid("requires full4MiB backup and nonempty application")); + } + let table = 0x380; + if word(backup, table) != 0x4b87a52d || word(backup, table + 4) != 0x10000 { + return Err(invalid("unrecognized partition table")); + } + let count = usize::from(backup[table + 14]); + let items = usize::from(u16::from_le_bytes([backup[table + 12], backup[table + 13]])); + if count == 0 + || count > 32 + || items < 15 + count + || items % 4 != 0 + || table + items + count * 8 > 0x800 + { + return Err(invalid("invalid partition record bounds")); + } + let mut parts = Vec::new(); + for index in 0..count { + let id = backup[table + 15 + index]; + let start = word(backup, table + items + index * 8) as usize; + let len = word(backup, table + items + index * 8 + 4) as usize; + if start > backup.len() + || len > backup.len() - start + || parts.iter().any(|&(i, _, _)| i == id) + { + return Err(invalid("duplicate or out-of-range partition")); + } + parts.push((id, start, len)); + } + if !parts.contains(&(0x20, 0x30000, 0x240000)) || !parts.contains(&(0x21, 0x5d000, 0x213000)) { + return Err(invalid("unsupported combined/application layout")); + } + for (index, &(id, start, len)) in parts.iter().enumerate() { + for &(other, other_start, other_len) in &parts[index + 1..] { + if len != 0 + && other_len != 0 + && start.max(other_start) < (start + len).min(other_start + other_len) + && !((id == 0x20 && other == 0x21) || (id == 0x21 && other == 0x20)) + { + return Err(invalid("unexpected overlapping partitions")); + } + } + } + let base = 0x30000; + for (offset, expected) in [ + (0, 0x4b0f2d1e), + (4, 0x10000), + (8, 0x100), + (0x48, 0), + (0x100, 0x4b0f2d2d), + (0x104, 0x10000), + (0x108, 0x200), + (0x120, 0), + (0x148, 0x3c7896e1), + (0x17c, 0), + ] { + if word(backup, base + offset) != expected { + return Err(invalid("unsupported combined header/policy")); + } + } + let length = word(backup, base + 0x124) as usize; + if !(0x2d000..=0x23fd00).contains(&length) { + return Err(invalid( + "combined body does not include application or exceeds partition", + )); + } + let body_end = base + 0x300 + length; + let body = &backup[base + 0x300..body_end]; + let digest: [u8; 32] = Sha256::digest(body).into(); + if backup[base + 0x128..base + 0x148] != digest { + return Err(invalid("original combined body hash mismatch")); + } + if backup[0x5d000..0x5d300].iter().any(|&b| b != 0) || application.len() > body_end - 0x5d300 { + return Err(invalid( + "unsupported application placeholder or payload size", + )); + } + let mut changed = backup[..body_end].to_vec(); + changed[0x5d300..0x5d300 + application.len()].copy_from_slice(application); + let combined_body_sha256: [u8; 32] = Sha256::digest(&changed[base + 0x300..]).into(); + changed[base + 0x128..base + 0x148].copy_from_slice(&combined_body_sha256); + let mut sectors = Vec::new(); + for offset in (0x5d000..0x5d300 + application.len()) + .step_by(4096) + .chain(std::iter::once(base)) + { + let mut replacement = backup[offset..offset + 4096].to_vec(); + let end = (offset + 4096).min(changed.len()); + replacement[..end - offset].copy_from_slice(&changed[offset..end]); + if replacement != backup[offset..offset + 4096] { + sectors.push(ApplicationSector { + offset: offset as u32, + original: backup[offset..offset + 4096].to_vec(), + replacement, + }); + } + } + Ok(ApplicationPlan { + entry: 0x45d300, + sectors, + backup_sha256: Sha256::digest(backup).into(), + combined_body_sha256, + requires_secure_boot_disabled: true, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + fn put(b: &mut [u8], offset: usize, value: u32) { + b[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + fn fixture() -> Vec { + let mut b = vec![0xff; 0x400000]; + put(&mut b, 0x380, 0x4b87a52d); + put(&mut b, 0x384, 0x10000); + b[0x38c..0x38f].copy_from_slice(&[20, 0, 3]); + b[0x38f..0x392].copy_from_slice(&[0x20, 0x21, 0x10]); + for (i, (start, len)) in [(0x30000, 0x240000), (0x5d000, 0x213000), (0x3fc000, 0x4000)] + .into_iter() + .enumerate() + { + put(&mut b, 0x394 + 8 * i, start); + put(&mut b, 0x398 + 8 * i, len); + } + for (offset, value) in [ + (0, 0x4b0f2d1e), + (4, 0x10000), + (8, 0x100), + (0x48, 0), + (0x100, 0x4b0f2d2d), + (0x104, 0x10000), + (0x108, 0x200), + (0x120, 0), + (0x124, 0x40000), + (0x148, 0x3c7896e1), + (0x17c, 0), + ] { + put(&mut b, 0x30000 + offset, value); + } + b[0x30300..0x5d000].fill(0x42); + b[0x5d000..0x5d300].fill(0); + let hash = Sha256::digest(&b[0x30300..0x70300]); + b[0x30128..0x30148].copy_from_slice(&hash); + b + } + #[test] + fn preserve_control_and_all_nontarget_bytes_with_recovery_sectors() { + let b = fixture(); + let app = vec![0x33; 8192]; + let plan = plan_unsigned_application(&b, &app).unwrap(); + assert_eq!(plan.entry, 0x45d300); + assert!(plan.requires_secure_boot_disabled); + assert_eq!(plan.sectors.last().unwrap().offset, 0x30000); + let mut after = b.clone(); + for sector in &plan.sectors { + let start = sector.offset as usize; + assert_eq!(sector.original, &b[start..start + 4096]); + after[start..start + 4096].copy_from_slice(§or.replacement); + } + assert_eq!(&after[0x5d300..0x5d300 + app.len()], app); + for (index, (&old, &new)) in b.iter().zip(&after).enumerate() { + if old != new { + assert!( + (0x30128..0x30148).contains(&index) + || (0x5d300..0x5d300 + app.len()).contains(&index) + ); + } + } + assert!(plan_unsigned_application(&after, &app) + .unwrap() + .sectors + .is_empty()); + for sector in &plan.sectors { + let start = sector.offset as usize; + after[start..start + 4096].copy_from_slice(§or.original); + } + assert_eq!(after, b); + } + #[test] + fn malformed_tables_hash_and_security_policy_are_rejected() { + let b = fixture(); + for offset in [ + 0x380, 0x38c, 0x390, 0x39c, 0x30000, 0x30120, 0x30124, 0x30148, 0x3017c, 0x5d000, + 0x61000, + ] { + let mut damaged = b.clone(); + damaged[offset] ^= 1; + assert!( + plan_unsigned_application(&damaged, &[1; 4]).is_err(), + "offset {offset:x}" + ); + } + for count in [0, 33, 255] { + let mut damaged = b.clone(); + damaged[0x38e] = count; + assert!(plan_unsigned_application(&damaged, &[1; 4]).is_err()); + } + let mut overlap = b.clone(); + put(&mut overlap, 0x3a4, 0x60000); + assert!(plan_unsigned_application(&overlap, &[1; 4]).is_err()); + assert!(plan_unsigned_application(&b[..1000], &[1; 4]).is_err()); + assert!(plan_unsigned_application(&b, &[]).is_err()); + assert!(plan_unsigned_application(&b, &vec![1; 0x20000]).is_err()); + } +}