feat(cp02): add firmware processing and signing utilities

- Add get_last_bytes.sh to extract final 2 bytes from IUM files
  - Add read_bytes.sh for reading specific byte offsets from binaries
  - Add pack_resources.sh to package and upload CP02S resources to S3
  - Add signer_new.py for firmware and bootloader signing with custom SHA1
  - Update extract_firmware.sh to use fixed output filenames
  - Update README.md with comprehensive documentation for new tools
This commit is contained in:
Ching L 2026-01-04 15:08:17 +08:00
parent c86d02552f
commit 8f928a93e8
6 changed files with 458 additions and 3 deletions

View File

@ -28,6 +28,49 @@
- 解析并生成 metadata.json (blocks 和 last_block_size) - 解析并生成 metadata.json (blocks 和 last_block_size)
- 如未指定前缀,自动使用原文件名 - 如未指定前缀,自动使用原文件名
### get_last_bytes.sh
- **功能**: 获取 IUM 文件的最后两个字节并以十六进制格式返回
- **用法**: `./get_last_bytes.sh [ium_file_path]`
- **描述**:
- 读取 IUM 文件的最后 2 个字节
- 反转字节顺序(交换第一和第二个字节)
- 以十六进制格式输出结果
- 如未指定文件路径,默认使用 ium.bin
### read_bytes.sh
- **功能**: 从二进制文件的特定偏移量读取 2 个字节
- **用法**: `./read_bytes.sh <bin_file>`
- **描述**:
- 从偏移量 0x1dcfe 读取 2 个字节
- 反转字节顺序(小端序转换)
- 返回十六进制格式的结果
- 包含文件大小和偏移量验证
### pack_resources.sh
- **功能**: 打包 CP02S 资源目录中的 .bin 文件
- **用法**: `./pack_resources.sh`(无参数)
- **描述**:
- 从指定目录收集所有 .bin 文件
- 创建包含 resources/ 作为顶级目录的 tar.gz 压缩包
- 自动上传到 S3 存储(需要配置 AWS CLI
- 源目录:`/Users/ching/develop/IonBridge/files/CP02S/littlefs/resources`
### signer_new.py
- **功能**: 用于签名 CP02 固件和引导加载程序的 Python 工具
- **用法**: `python3 signer_new.py --output_dir <output_directory> [options]`
- **选项**:
- `--firmware <path>` - 未签名的用户应用程序固件路径
- `--bootloader <path>` - 未签名的引导加载程序路径
- `--output_dir <path>` - 保存签名文件的目录(必需)
- `--enable_swd` - 启用 SWD 功能
- `--boot_directly` - 启用直接引导到用户应用程序
- **描述**:
- 实现自定义 SHA1 签名算法(基于逆向工程)
- 为固件和引导加载程序生成签名的二进制文件
- 创建包含版本号、校验和和版权信息的元数据
- 支持多种芯片 ID 的引导加载程序变体
- 输出签名的二进制文件和相关 JSON 元数据
## 使用示例 ## 使用示例
```bash ```bash
@ -40,4 +83,19 @@
# 提取固件组件 # 提取固件组件
./extract_firmware.sh 40_ufcs2.bin firmware # 指定输出前缀 ./extract_firmware.sh 40_ufcs2.bin firmware # 指定输出前缀
./extract_firmware.sh merged.bin # 使用原文件名作为前缀 ./extract_firmware.sh merged.bin # 使用原文件名作为前缀
# 获取 IUM 文件的最后两个字节
./get_last_bytes.sh ium.bin # 使用默认文件
./get_last_bytes.sh custom_ium.bin # 指定文件路径
# 从二进制文件读取特定偏移量的字节
./read_bytes.sh 40_ufcs2.bin
# 打包资源文件
./pack_resources.sh # 打包并上传到 S3
# 签名固件
python3 signer_new.py --firmware firmware.bin --output_dir ./output
python3 signer_new.py --bootloader bootloader.bin --output_dir ./output --enable_swd
python3 signer_new.py --firmware firmware.bin --bootloader bootloader.bin --output_dir ./output --boot_directly
``` ```

View File

@ -79,7 +79,7 @@ echo " 计算出 firmware 大小: $FIRMWARE_SIZE bytes"
echo "" echo ""
# 提取 head file (firmware: 从 0x4000 开始) # 提取 head file (firmware: 从 0x4000 开始)
HEAD_FILE="${PREFIX}.firmware" HEAD_FILE="firmware.bin"
echo "提取 head file (firmware)..." echo "提取 head file (firmware)..."
dd if="$INPUT" of="$HEAD_FILE" bs=1 skip=$FIRMWARE_OFFSET count=$FIRMWARE_SIZE status=none dd if="$INPUT" of="$HEAD_FILE" bs=1 skip=$FIRMWARE_OFFSET count=$FIRMWARE_SIZE status=none
@ -92,7 +92,7 @@ else
fi fi
# 提取 tail file (IUM: 从 0x1dc00 开始, 256 bytes) # 提取 tail file (IUM: 从 0x1dc00 开始, 256 bytes)
TAIL_FILE="${PREFIX}.ium" TAIL_FILE="ium.bin"
echo "提取 tail file (IUM)..." echo "提取 tail file (IUM)..."
dd if="$INPUT" of="$TAIL_FILE" bs=1 skip=$IUM_OFFSET count=$IUM_SIZE status=none dd if="$INPUT" of="$TAIL_FILE" bs=1 skip=$IUM_OFFSET count=$IUM_SIZE status=none
@ -105,7 +105,7 @@ else
fi fi
# 生成 metadata.json # 生成 metadata.json
METADATA_FILE="${PREFIX}_metadata.json" METADATA_FILE="metadata.json"
echo "生成 metadata.json..." echo "生成 metadata.json..."
cat > "$METADATA_FILE" << EOF cat > "$METADATA_FILE" << EOF

28
CP02/get_last_bytes.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
# Script to get the last two bytes of ium file
# Returns the bytes in 0x format
# Usage: ./get_last_bytes.sh [ium_file_path]
# Use provided path or default to ium.bin
ium_file=${1:-ium.bin}
if [ ! -f "$ium_file" ]; then
echo "Error: $ium_file file not found"
exit 1
fi
# Get file size
file_size=$(stat -f%z "$ium_file")
if [ "$file_size" -lt 2 ]; then
echo "Error: File is too small (less than 2 bytes)"
exit 1
fi
# Extract last 2 bytes and reverse byte order
last_bytes=$(xxd -s -2 "$ium_file" | cut -d' ' -f2 | tr -d '\n')
# Reverse the byte order (swap first and second byte)
byte1=${last_bytes:0:2}
byte2=${last_bytes:2:2}
echo "${byte2}${byte1}"

71
CP02/pack_resources.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/bash
# Script to pack CP02S resources directory containing only .bin files
# Creates tar.gz with resources as top-level directory
SOURCE_DIR="/Users/ching/develop/IonBridge/files/CP02S/littlefs/resources"
OUTPUT_FILE="cp02s_resources.tar.gz"
# Check if source directory exists
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Source directory does not exist: $SOURCE_DIR"
exit 1
fi
# Create temporary directory structure
TEMP_DIR=$(mktemp -d)
TEMP_RESOURCES="$TEMP_DIR/resources"
# Create resources directory in temp
mkdir -p "$TEMP_RESOURCES"
# Copy only .bin files preserving directory structure
bin_count=0
find "$SOURCE_DIR" -name "*.bin" -type f | while read -r file; do
# Get relative path from source directory
relative_path="${file#$SOURCE_DIR/}"
target_path="$TEMP_RESOURCES/$relative_path"
# Create target directory if needed
mkdir -p "$(dirname "$target_path")"
# Copy the file
cp "$file" "$target_path"
((bin_count++))
done
# Check if any .bin files were found
bin_files=$(find "$TEMP_RESOURCES" -name "*.bin" 2>/dev/null | wc -l)
if [ "$bin_files" -eq 0 ]; then
echo "Warning: No .bin files found in $SOURCE_DIR"
rm -rf "$TEMP_DIR"
exit 1
fi
# Save current directory before changing
ORIGINAL_DIR="$PWD"
# Create tar.gz from temp directory
cd "$TEMP_DIR"
tar -czf "$OUTPUT_FILE" resources/
# Move the archive back to original directory
mv "$OUTPUT_FILE" "$ORIGINAL_DIR/"
# Return to original directory
cd "$ORIGINAL_DIR"
# Cleanup temp directory
rm -rf "$TEMP_DIR"
echo "Created $OUTPUT_FILE with resources/ containing $bin_files .bin files"
# Upload to S3
echo "Uploading $OUTPUT_FILE to S3..."
if aws --endpoint=https://s3.cn-southeast-1.ifanrprod.com --profile=iot s3 cp "$OUTPUT_FILE" s3://iot-private/batch_input/; then
echo "Successfully uploaded $OUTPUT_FILE to S3"
else
echo "Error: Failed to upload to S3"
exit 1
fi

32
CP02/read_bytes.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 <bin_file>"
exit 1
fi
BIN_FILE="$1"
OFFSET=0x1dcfe
if [ ! -f "$BIN_FILE" ]; then
echo "Error: File '$BIN_FILE' not found"
exit 1
fi
FILE_SIZE=$(stat -f%z "$BIN_FILE" 2>/dev/null || stat -c%s "$BIN_FILE" 2>/dev/null)
DECIMAL_OFFSET=$((OFFSET))
if [ $DECIMAL_OFFSET -ge $FILE_SIZE ]; then
echo "Error: Offset $OFFSET ($DECIMAL_OFFSET) exceeds file size ($FILE_SIZE bytes)"
exit 1
fi
if [ $((DECIMAL_OFFSET + 2)) -gt $FILE_SIZE ]; then
echo "Error: Cannot read 2 bytes starting from offset $OFFSET (file too small)"
exit 1
fi
BYTES=$(xxd -s $DECIMAL_OFFSET -l 2 -p "$BIN_FILE")
BYTE1=$(echo "$BYTES" | cut -c1-2)
BYTE2=$(echo "$BYTES" | cut -c3-4)
echo "$BYTE2$BYTE1"

266
CP02/signer_new.py Normal file
View File

@ -0,0 +1,266 @@
#!/usr/bin/env python3
from absl import app
from absl import flags
from absl import logging
import json
import shutil
import struct
import os
import time
import datetime
FLAGS = flags.FLAGS
# Define command-line flags
flags.DEFINE_string('signer', None, 'Path to the unsigned user application firmware.')
flags.DEFINE_string('firmware', None, 'Path to the unsigned user application firmware.')
flags.DEFINE_string('bootloader', None, 'Path to the unsigned bootloader.')
flags.DEFINE_string('output_dir', None, 'Directory to save the signed files.')
flags.DEFINE_boolean('enable_swd', False, 'Enable SWD feature')
flags.DEFINE_boolean('boot_directly', False, 'Enable boot directly to user application')
# Ensure the flags are required
flags.mark_flag_as_required('output_dir')
def leftrotate(x, n):
"""Left rotate a 32-bit integer x by n bits."""
return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF
def sha1_signer_python(data):
"""Python implementation matching sha1_signer binary exactly (based on reverse engineering)"""
BLOCK_LEN = 64
FIXED_SIZE = 0x1DF00 # sha1_signer processes exactly 0x1DF00 bytes
# Create fixed-size buffer initialized with zeros (malloc + memset in C)
buffer = bytearray(FIXED_SIZE)
# Copy file data to buffer (fread in C)
data_to_copy = min(len(data), FIXED_SIZE)
buffer[:data_to_copy] = data[:data_to_copy]
# Remaining bytes stay as 0
# Custom initial hash values (confirmed from reverse engineering)
hash_state = [0xef343ca7, 0x5c41a2de, 0x7ce9704f, 0x62312814, 0x4de11904]
def sha1_compress(block, state):
"""Compress one 64-byte block (16 rounds only, confirmed from disassembly)"""
a, b, c, d, e = state[0], state[1], state[2], state[3], state[4]
# Process only 16 rounds (cmpl $0xf in disassembly)
for i in range(16):
# Load schedule using big-endian (confirmed from disassembly)
schedule = (block[i * 4] << 24) | (block[i * 4 + 1] << 16) | \
(block[i * 4 + 2] << 8) | block[i * 4 + 3]
# Round function with constant 0x7ebce2a7 (confirmed from disassembly)
f = ((b & c) | (~b & d)) & 0xFFFFFFFF
temp = (leftrotate(a, 5) + f + e + schedule + 0x7ebce2a7) & 0xFFFFFFFF
e = d
d = c
c = leftrotate(b, 30)
b = a
a = temp
# Update state
state[0] = (state[0] + a) & 0xFFFFFFFF
state[1] = (state[1] + b) & 0xFFFFFFFF
state[2] = (state[2] + c) & 0xFFFFFFFF
state[3] = (state[3] + d) & 0xFFFFFFFF
state[4] = (state[4] + e) & 0xFFFFFFFF
# Process complete 64-byte blocks from fixed-size buffer
data_len = FIXED_SIZE
offset = 0
while data_len - offset >= BLOCK_LEN:
sha1_compress(buffer[offset:offset + BLOCK_LEN], hash_state)
offset += BLOCK_LEN
# Handle remaining bytes and padding (standard SHA1 padding)
block = bytearray(BLOCK_LEN)
rem = data_len - offset
if rem > 0:
block[0:rem] = buffer[offset:offset + rem]
# Add padding byte
block[rem] = 0x80
rem += 1
# If not enough room for length, process this block and start a new one
LENGTH_SIZE = 8
if BLOCK_LEN - rem < LENGTH_SIZE:
sha1_compress(bytes(block), hash_state)
block = bytearray(BLOCK_LEN)
# Add message length in bits (big-endian, standard SHA1 encoding)
length_bits = data_len * 8
for i in range(LENGTH_SIZE):
block[BLOCK_LEN - 1 - i] = (length_bits >> (i * 8)) & 0xFF
# Process final block
sha1_compress(bytes(block), hash_state)
return hash_state
def generate_metadata_python(file_path):
"""Generate metadata using Python implementation of sha1_signer."""
with open(file_path, 'rb') as f:
data = f.read()
# Use exact clone of sha1_signer binary (based on reverse engineering)
hash_values = sha1_signer_python(data)
# Convert hash values to hex strings (same format as original)
checksum = [f"{val:08x}" for val in hash_values]
# Generate version number using UTC date format (YYYYMMDDHH)
utc_now = datetime.datetime.utcnow()
version_number = int(f"{utc_now.year:04d}{utc_now.month:02d}{utc_now.day:02d}{utc_now.hour:02d}")
# Set copyright
copyright_text = "Copyright (c) ifanr Inc, All Rights Reserved."
return {
"version_number": version_number,
"checksum": checksum,
"copyright": copyright_text
}
def extract_release_id(file_path):
"""Extract the release ID from the binary."""
with open(file_path, 'rb') as f:
f.seek(0xA8)
release_id = struct.unpack('<I', f.read(4))[0]
return f"{release_id:x}"
def modify_firmware(firmware_bin, metadata, enable_swd = False, boot_directly = False):
"""Modifies the firmware binary based on the metadata."""
if not enable_swd:
# Insert 0xb10cdeb9 at 0x1DF10 to disable SWD
firmware_bin = (
firmware_bin[:0x1DF10] # Everything before 0x1DF10
+ struct.pack("<I", 0xb10cdeb9) # Insert 0xb10cdeb9 as 4-byte little-endian
+ firmware_bin[0x1DF14:] # Everything after 0x1DF10
)
if boot_directly:
# Insert 0xb007b007 at 0x1DF14 to boot directly to user application
firmware_bin = (
firmware_bin[:0x1DF14] # Everything before 0x1DF14
+ struct.pack("<I", 0xb007b007) # Insert 0xb007b007 as 4-byte little-endian
+ firmware_bin[0x1DF18:] # Everything after 0x1DF14
)
version_number = metadata.get("version_number", 0)
firmware_bin = (
firmware_bin[:0x1DFF0]
+ struct.pack("<I", version_number)
+ firmware_bin[0x1DFF4:]
)
checksum = metadata.get("checksum", [0] * 5)
for i, hexstr in enumerate(checksum):
firmware_bin = (
firmware_bin[: 0x1DFA0 + i * 4]
+ struct.pack("<I", int(hexstr, 16))
+ firmware_bin[0x1DFA4 + i * 4 :]
)
copyright = metadata.get("copyright", "").encode()
copyright = copyright[:0x30]
if len(copyright) < 0x30:
copyright += b"\x00" * (0x30 - len(copyright))
firmware_bin = firmware_bin[:0x1DFC0] + copyright + firmware_bin[0x1DFEF + 1 :]
return firmware_bin
def modify_bootloader(firmware_bin, metadata):
"""Modifies the bootloader binary based on the metadata."""
version_number = metadata.get('version_number', 0)
firmware_bin = firmware_bin[:0x1ff4] + struct.pack('<I', version_number) + firmware_bin[0x1ff8:]
flash_timestamp = metadata.get('flash_timestamp', 0)
firmware_bin = firmware_bin[:0x1ff8] + struct.pack('<I', flash_timestamp) + firmware_bin[0x1ffc:]
chip_id = metadata.get('chip_id', 0)
firmware_bin = firmware_bin[:0x1ffe] + struct.pack('<B', chip_id) + firmware_bin[0x1fff:]
copyright = metadata.get('copyright', '').encode()
copyright = copyright[:0x30]
if len(copyright) < 0x30:
copyright += b'\x00' * (0x30 - len(copyright))
firmware_bin = firmware_bin[:0x1fc0] + copyright + firmware_bin[0x1fef + 1:]
return firmware_bin
def copy_associated_files(file_path, output_dir):
"""Copy the input file and associated map, txt, and elf files to the output directory."""
base_name, _ = os.path.splitext(os.path.basename(file_path))
for ext in ['.map', '.txt', '.elf']:
src_file = file_path.replace('.bin', ext)
if os.path.exists(src_file):
shutil.copy(src_file, os.path.join(output_dir, os.path.basename(src_file)))
shutil.copy(file_path, os.path.join(output_dir, os.path.basename(file_path)))
def main(argv):
del argv # Unused.
if FLAGS.firmware:
with open(FLAGS.firmware, 'rb') as f:
firmware_bin = f.read()
if len(firmware_bin) >= 0x1DF00:
raise ValueError("Firmware is too big")
firmware_bin += b"\xFF" * (0x1DF00 - len(firmware_bin))
release_id = extract_release_id(FLAGS.firmware)
unsigned_firmware_path = os.path.join(FLAGS.output_dir, f"firmware-unsigned-{release_id}.bin")
with open(unsigned_firmware_path, 'wb') as f:
f.write(firmware_bin)
# Sign the firmware using Python implementation
metadata = generate_metadata_python(unsigned_firmware_path)
firmware_bin += b"\xFF" * 0x100
signed_firmware_path = os.path.join(FLAGS.output_dir, f"firmware-{release_id}.bin")
with open(signed_firmware_path, 'wb') as f:
modified_firmware_bin = modify_firmware(firmware_bin, metadata, FLAGS.enable_swd, FLAGS.boot_directly)
f.write(modified_firmware_bin)
logging.info('Firmware %s written.', signed_firmware_path)
# Write the metadata file
metadata_path = os.path.join(FLAGS.output_dir, f"firmware-{release_id}.json")
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=4)
logging.info('Metadata %s written.', metadata_path)
copy_associated_files(FLAGS.firmware, FLAGS.output_dir)
if FLAGS.bootloader:
# Sign the bootloader using Python implementation
metadata = generate_metadata_python(FLAGS.bootloader)
release_id = extract_release_id(FLAGS.bootloader)
with open(FLAGS.bootloader, 'rb') as f:
bootloader_bin = f.read()
if len(bootloader_bin) > 0x1f00:
raise ValueError('Bootloader is too big')
bootloader_bin += b'\x00' * (0x1f00- len(bootloader_bin))
bootloader_bin += b'\xff' * 0x10
bootloader_bin += b'\x00' * 0xf0
for chip_id in range(5):
metadata['chip_id'] = chip_id
signed_bootloader_path = os.path.join(FLAGS.output_dir, f"bootloader-chip{chip_id}-{release_id}.bin")
with open(signed_bootloader_path, 'wb') as f:
modified_bootloader_bin = modify_bootloader(bootloader_bin, metadata)
f.write(modified_bootloader_bin)
logging.info('Bootloader %s written.', signed_bootloader_path)
# Write the metadata file
metadata_path = os.path.join(FLAGS.output_dir, f"bootloader-{release_id}.json")
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=4)
logging.info('Metadata %s written.', metadata_path)
copy_associated_files(FLAGS.bootloader, FLAGS.output_dir)
if __name__ == '__main__':
app.run(main)