169 lines
6.6 KiB
Markdown
169 lines
6.6 KiB
Markdown
# CrealityOS OTA Packaging & Update Subsystem Specification
|
||
|
||
This document provides a technical specification of Creality's Over-The-Air (OTA) firmware update container format, encryption model, payload chunking, and the on-device update execution engine used across the **Ender-3 V3 KE** (board target `F005`) and the **Nebula Smart Kit / Nebula Pad** (board target `NEBULA`).
|
||
|
||
---
|
||
|
||
## 1. Overview & Security Model
|
||
|
||
Creality's local (USB) and network OTA update mechanism on Ingenic X2000 devices is **integrity-checked but unauthenticated**:
|
||
|
||
* **Archive Envelope**: A password-protected 7z archive with encrypted headers (`-mhe=on`).
|
||
* **Encryption Key**: Derived deterministically from the hardware board name using standard Unix MD5 crypt.
|
||
* **Payload Verification**: Each sub-payload (`xImage`, `rootfs.squashfs`, `zero.bin`) is checked using MD5 checksums.
|
||
* **No Cryptographic Signatures**: The device does **not** enforce RSA digital signatures or public key verification during OTA updates. Unsigned custom kernels and root filesystems flash and boot cleanly through stock firmware.
|
||
|
||
---
|
||
|
||
## 2. Key Derivation Formula
|
||
|
||
The 7z archive password is generated per board type using standard Unix MD5 password encryption (`$1$` format) with a hardcoded salt:
|
||
|
||
$$\text{Password} = \text{MD5-Crypt}(\text{Key} = \text{BOARD\_NAME} + \text{"C3\_7e\_bz"}, \text{Salt} = \text{"cxswfile"})$$
|
||
|
||
In shell (using `openssl`):
|
||
```bash
|
||
openssl passwd -1 -salt cxswfile "${BOARD_NAME}C3_7e_bz"
|
||
```
|
||
|
||
### Known Board Encryption Keys
|
||
|
||
| Target Device | Board Identifier | Input String | Resulting 7z Password |
|
||
| :--- | :--- | :--- | :--- |
|
||
| **Ender-3 V3 KE** | `F005` | `F005C3_7e_bz` | `$1$cxswfile$ZFd0RWFYkJQugbtKVGL9y0` |
|
||
| **Nebula Pad** | `NEBULA` | `NEBULAC3_7e_bz` | `$1$cxswfile$XG7ANbYX3bq2H2xmmwrka.` |
|
||
| **Creality K1** | `CR4CU220812S12` | `CR4CU220812S12C3_7e_bz` | Board-derived key |
|
||
|
||
> [!TIP]
|
||
> `tools/pack_openke.py` includes a pure-Python implementation of Unix MD5-crypt, making password derivation completely cross-platform without external tools.
|
||
|
||
---
|
||
|
||
## 3. Archive Structure & Layout
|
||
|
||
Creality's update extractor (`/etc/ota_bin/local_ota_update.sh`) has strict structural expectations.
|
||
|
||
### Directory Hierarchy
|
||
|
||
For an archive file named `<ARCHIVE_STEM>.img` with package version `<VERSION>` (e.g. `NEBULA_ota_img_V1.1.0.34.img` with version `1.1.0.34`):
|
||
|
||
```
|
||
<ARCHIVE_STEM>/ # Top-level directory inside 7z
|
||
├── ota_config.in # Root version pointer
|
||
└── ota_v<VERSION>/ # Versioned payload folder
|
||
├── ota_v<VERSION>.ok # Readiness marker (touch file)
|
||
├── ota_update.in # Primary payload manifest
|
||
├── ota_md5_xImage.<FULL_MD5> # Chunk MD5 sidecar for kernel
|
||
├── ota_md5_rootfs.squashfs.<FULL_MD5> # Chunk MD5 sidecar for rootfs
|
||
├── ota_md5_zero.bin.<FULL_MD5> # Chunk MD5 sidecar for RTOS
|
||
├── xImage.0000.<FULL_MD5> # 1 MiB chunk 0
|
||
├── xImage.0001.<CHUNK0_MD5> # 1 MiB chunk 1
|
||
├── ...
|
||
├── rootfs.squashfs.0000.<FULL_MD5>
|
||
├── ...
|
||
└── zero.bin.0000.<FULL_MD5>
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Manifest Formats
|
||
|
||
### 1. `ota_config.in`
|
||
Located at the root of the extracted folder:
|
||
```ini
|
||
current_version=1.1.0.34
|
||
```
|
||
|
||
### 2. `ota_v<VERSION>.ok`
|
||
An empty 0-byte or 1-byte newline file placed in `ota_v<VERSION>/`. If missing, the updater aborts.
|
||
|
||
### 3. `ota_update.in`
|
||
Lists each payload with its partition type, internal filename, byte size, and full-file MD5:
|
||
```ini
|
||
ota_version=1.1.0.34
|
||
|
||
img_type=kernel
|
||
img_name=xImage
|
||
img_size=5541952
|
||
img_md5=f3bf93ad2fd1e770dacb3062ba9cf05c
|
||
|
||
img_type=rootfs
|
||
img_name=rootfs.squashfs
|
||
img_size=346923008
|
||
img_md5=7d0d3742dedd7a236632e4f7439f52dd
|
||
|
||
img_type=rtos
|
||
img_name=zero.bin
|
||
img_size=424264
|
||
img_md5=8e852ae9b8f5b78f3818e4236c3e8f1d
|
||
```
|
||
|
||
### 4. Sidecar Checksum Files (`ota_md5_<NAME>.<FULL_MD5>`)
|
||
Text files containing the MD5 checksum of each 1 MiB chunk in order, separated by newlines:
|
||
```
|
||
e10adc3949ba59abbe56e057f20f883e
|
||
c33367701511b4f6020ec61ded352059
|
||
...
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Payload Slicing & Chunking Algorithm
|
||
|
||
Creality splits large image files into fixed **1 MiB** (1,048,576 bytes) chunks to manage RAM constraints on the 208MB-RAM device during download and flashing:
|
||
|
||
1. **Chunk Naming Convention**:
|
||
$$\text{chunk\_filename} = \text{base\_name} + \text{"."} + \text{index:04d} + \text{"."} + \text{prev\_md5}$$
|
||
2. **First Chunk (`index = 0000`)**:
|
||
`prev_md5` is set to the **FULL MD5** of the unchunked source file.
|
||
3. **Subsequent Chunks (`index > 0000`)**:
|
||
`prev_md5` is the MD5 checksum of the **immediately preceding chunk** (`index - 1`).
|
||
4. **Final Chunk**:
|
||
Carries the remaining bytes ($\le 1,048,576$ bytes).
|
||
|
||
---
|
||
|
||
## 6. On-Device Update Execution Flow
|
||
|
||
Understanding the device-side execution path in `/etc/ota_bin/local_ota_update.sh` explains the strict naming rules:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
actor User as User via Touchscreen
|
||
participant MS as master-server (UI)
|
||
participant LU as local_ota_update.sh
|
||
participant OF as ota_file (7z Decryptor)
|
||
participant BL as Block Devices (mmcblk0)
|
||
|
||
User->>MS: Selects USB Update
|
||
MS->>MS: Scans for 'ota_img_V' on USB
|
||
MS->>MS: Compares versions (regex [0-9]{1,3})
|
||
MS->>LU: Calls local_ota_update.sh /media/sda1/<NAME>.img
|
||
LU->>LU: Computes OTA_UNZIP_FILE_NAME=${OTA_FILE_NAME%.img*}
|
||
LU->>OF: Calls ota_file e <NAME>.img <TEMP_DIR>
|
||
OF->>OF: Derives key & extracts 7z archive
|
||
LU->>LU: Checks <TEMP_DIR>/<OTA_UNZIP_FILE_NAME>/ota_config.in
|
||
LU->>LU: Validates partition sizes against device nodes
|
||
LU->>BL: Streams xImage chunks -> mmcblk0p6 (kernel2)
|
||
LU->>BL: Streams rootfs chunks -> mmcblk0p8 (rootfs2)
|
||
LU->>BL: Streams zero.bin chunks -> mmcblk0p4 (rtos2)
|
||
LU->>BL: Writes "ota:kernel2" to /dev/mmcblk0p1
|
||
LU->>MS: Returns success
|
||
MS->>User: Reboots printer into OpenKE
|
||
```
|
||
|
||
### The Critical Folder-Name Matching Rule
|
||
|
||
Lines 312–320 of `local_ota_update.sh`:
|
||
```bash
|
||
OTA_FILE_NAME=$(basename ${OTA_FILE})
|
||
OTA_UNZIP_FILE_NAME=${OTA_FILE_NAME%.img*}
|
||
ota_site=${OTA_FILE_PATH}/${OTA_UNZIP_FILE_NAME}
|
||
/usr/bin/ota_file e ${OTA_FILE_PATH}/${OTA_FILE_NAME} ${OTA_FILE_PATH}
|
||
ota_site_config=$ota_site/ota_config.in
|
||
```
|
||
|
||
> [!IMPORTANT]
|
||
> If the archive file is named `NEBULA_ota_img_V1.1.0.34.img`, `OTA_UNZIP_FILE_NAME` evaluates to `NEBULA_ota_img_V1.1.0.34`. The root directory inside the 7z archive **must match that exact string**. If the archive has a different folder name (e.g. `OpenKE_...`), `cp -f $ota_site_config` fails and the update aborts immediately.
|