Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
13 changes: 13 additions & 0 deletions crates/hisi-fwpkg/examples/inspect_ws53_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Inspect a local vendor package; no serial/device I/O.
use sha2::{Digest, Sha256};
fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}
51 changes: 51 additions & 0 deletions crates/hisi-fwpkg/examples/plan_ws53_application.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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", &sector.original),
("replacement", &sector.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(&sector.original),
Sha256::digest(&sector.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(())
}
1 change: 1 addition & 0 deletions crates/hisi-fwpkg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
146 changes: 146 additions & 0 deletions crates/hisi-fwpkg/src/ws53.rs
Original file line number Diff line number Diff line change
@@ -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<LoaderBoot<'_>> {
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<u8> {
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());
}
}
Loading
Loading