diff --git a/ToDo.md b/ToDo.md index a45f6f3..2edd6d9 100644 --- a/ToDo.md +++ b/ToDo.md @@ -1,5 +1,4 @@ # New features -- firmware download from git repository where releases are generated -- define how firmware should be downloaded, identified, etc. -- add option to download firmware the legacy way but with ability to specify server, password, login and define the structure with device id +- firmware download from git repository where releases are generated (define how firmware should be downloaded, identified, etc.) + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 65ec03b..3499108 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -74,14 +74,13 @@ Five modules encapsulating all business logic. Key objects: - `HEADER_SIZE = 48` — total header size on disk. -- `DEVICE_HEADER_SIZE = 44` — header size sent to the device - (without the `prevAppVersion` field). +- `DEVICE_HEADER_SIZE = 48` — header size sent to the device (identical to the file header). - `FirmwareHeader` — dataclass with fields and helpers (`format_product_id`, `license_id`, `unique_id`, `payload_size`). - `parse_header(bytes) -> FirmwareHeader` — parses the first 48 bytes. - `load_firmware(path) -> (FirmwareHeader, bytes)` — parses from file. -- `build_device_header(bytes) -> bytes` — strips `prevAppVersion` before - transmission. +- `build_device_header(bytes) -> bytes` — returns the 48-byte wire header + (identical to the file header). - `split_pages(payload, page_size) -> list[bytes]` — splits payload into equal pages; an incomplete final page is dropped (matching C++ behaviour). @@ -407,7 +406,7 @@ offset size field 4 4 productId (MSB) 8 4 productId (LSB) 12 4 appVersion - 16 4 prevAppVersion ← stripped before sending to device + 16 4 prevAppVersion 20 4 pageCount 24 4 flashPageSize 28 16 IV @@ -415,7 +414,7 @@ offset size field 48 … encrypted pages ``` -Wire header = bytes `[0:16] + [20:48]` = 44 B (sent with the `START` command). +Wire header = bytes `[0:48]` = 48 B (sent with the `START` command, identical to the file header). ### Product ID Convention @@ -459,7 +458,7 @@ operations (compatibility check, device matching) the full 64-bit value is used. After ACK for `GET_VERSION` the device sends 16 bytes of device info (`u32 bootloaderVersion`, `u64 productId`, `u32 flashPageSize`). -The host sends `START` (`0x02`) immediately followed by the 44 B wire header +The host sends `START` (`0x02`) immediately followed by the 48 B wire header as a single transmission. The device responds ACK/NAK. Then for each page the host sends `NEXT_PAGE` (`0x03`) + `flashPageSize` bytes, and the device ACKs. diff --git a/docs/FIRMWARE_FORMAT.md b/docs/FIRMWARE_FORMAT.md index 25d0be7..c0d6212 100644 --- a/docs/FIRMWARE_FORMAT.md +++ b/docs/FIRMWARE_FORMAT.md @@ -14,7 +14,7 @@ Host implementation: [`src/secure_loader/core/firmware.py`](https://github.com/n 2. [Header Layout (48 B)](#-header-layout-48-b) 3. [Field Semantics](#-field-semantics) 4. [productId Derived Fields](#-productid-derived-fields) -5. [Wire Header (44 B)](#-wire-header-44-b) +5. [Wire Header (48 B)](#-wire-header-48-b) 6. [Payload](#-payload) 7. [Host Parsing API](#-host-parsing-api) @@ -89,7 +89,7 @@ informational purposes. Not validated by the bootloader protocol. Version of the previous application image. Used by the host to request an older firmware from the HTTP source (`fetch_previous`). GitHub Releases support is planned — see [Roadmap](GITHUB_SOURCE_MIGRATION.md). -This field is **not** sent to the device — see [Wire Header](#-wire-header-44-b). +This field is transmitted to the device as part of the 48-byte wire header — see [Wire Header](#-wire-header-48-b). ### `pageCount` (u32) @@ -148,38 +148,36 @@ for the full fetch workflow. --- -## 📡 Wire Header (44 B) +## 📡 Wire Header (48 B) -The header that the device receives during a firmware update is **not** the -full 48-byte disk header. The `prevAppVersion` field (bytes 16–19) is -stripped before transmission. +The header transmitted to the device during a firmware update is identical to +the full 48-byte file header. All fields — including `prevAppVersion` — are +sent as-is. ```mermaid flowchart LR - subgraph disk["Disk header — 48 B"] + subgraph disk["File header — 48 B"] A["bytes 0–15
protocolVersion
productId MSB + LSB
appVersion"] B["bytes 16–19
prevAppVersion"] C["bytes 20–47
pageCount · flashPageSize
IV · crc32"] end - subgraph wire["Wire header — 44 B (sent to device)"] + subgraph wire["Wire header — 48 B (sent to device)"] W["START command payload"] end A -- "copied" --> W + B -- "copied" --> W C -- "copied" --> W - B -. "❌ stripped" .- W - - style B fill:#ffe0e0,stroke:#cc6666 ``` Expressed as a slice operation: ``` -wire_header = disk_header[0:16] + disk_header[20:48] +wire_header = disk_header[0:48] = protocolVersion + productId (MSB+LSB) + appVersion - + pageCount + flashPageSize + IV + crc32 + + prevAppVersion + pageCount + flashPageSize + IV + crc32 ``` -This 44-byte block is the payload of the `START` command. +This 48-byte block is the payload of the `START` command. --- @@ -205,7 +203,7 @@ flowchart LR file["📄 .bin file"] --> lf["load_firmware(path)"] lf --> hdr["FirmwareHeader\n(frozen dataclass)"] lf --> raw["raw bytes"] - raw --> bdh["build_device_header(raw)\n→ 44 B wire header"] + raw --> bdh["build_device_header(raw)\n→ 48 B wire header"] raw --> sp["split_pages(payload, page_size)\n→ list[bytes]"] hdr --> fields["license_id · unique_id\npayload_size\nformat_product_id() · …"] ``` @@ -214,7 +212,7 @@ flowchart LR from secure_loader.core.firmware import ( parse_header, # parse_header(data: bytes) -> FirmwareHeader load_firmware, # load_firmware(path) -> (FirmwareHeader, bytes) - build_device_header, # build_device_header(raw: bytes) -> bytes (44 B) + build_device_header, # build_device_header(raw: bytes) -> bytes (48 B) split_pages, # split_pages(payload, page_size) -> list[bytes] ) ``` diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 8352713..dde58bf 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -51,7 +51,7 @@ delimiter. | Name | Code | Payload | Meaning | |------|------|---------|---------| | `GET_VERSION` | `0x01` | — | Request device info | -| `START` | `0x02` | 44 B wire header | Begin firmware transfer | +| `START` | `0x02` | 48 B wire header | Begin firmware transfer | | `NEXT_PAGE` | `0x03` | `flashPageSize` B | One payload page | | `RESET` | `0x04` | — | Soft-reset the device | | `NONE` | `0x00` | — | Reserved, unused | @@ -100,8 +100,8 @@ One rule covers all commands — no translation table needed on either side. ### `START` (`0x02`) -**Host sends:** `0x02`, then the **44-byte wire header** -(see [Firmware Format — Wire Header](FIRMWARE_FORMAT.md#-wire-header-44-b)). +**Host sends:** `0x02`, then the **48-byte wire header** +(see [Firmware Format — Wire Header](FIRMWARE_FORMAT.md#-wire-header-48-b)). **Device responds:** @@ -157,7 +157,7 @@ sequenceDiagram Note over H: validates compatibility
(bootloaderVersion == protocolVersion, productId match) - H->>D: 0x02 START + 44 B wire header + H->>D: 0x02 START + 48 B wire header D->>H: 0x42 ACK (flash erased, decryption ready) loop pageCount pages @@ -272,7 +272,7 @@ stateDiagram-v2 [*] --> WAIT_CMD : power on / reset WAIT_CMD --> WAIT_CMD : 0x01 GET_VERSION → reply 0x41 + 16 B info - WAIT_CMD --> READ_HEADER : 0x02 START → read 44 B wire header + WAIT_CMD --> READ_HEADER : 0x02 START → read 48 B wire header WAIT_CMD --> [*] : 0x04 RESET → reply 0x44 · reset READ_HEADER --> WAIT_CMD : header invalid → 0x82 NAK diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index b701edc..cfa53bf 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -318,7 +318,7 @@ The command sequence: 1. Opens the serial port and polls `GET_VERSION` every 500 ms until the bootloader responds. 2. Reads device info (bootloader version, product ID, page size). 3. Checks compatibility — product ID and protocol version must match the firmware header (unless `--force`). -4. Sends `START` with the 44-byte wire header. +4. Sends `START` with the 48-byte wire header. 5. Streams all `pageCount` pages, reporting progress to stdout. 6. Reports success or failure. diff --git a/src/secure_loader/core/firmware.py b/src/secure_loader/core/firmware.py index 0bb2868..bc25318 100644 --- a/src/secure_loader/core/firmware.py +++ b/src/secure_loader/core/firmware.py @@ -19,10 +19,9 @@ The 64-bit productId is reconstructed as ``(MSB << 32) | LSB``. -The header that the device actually receives during a firmware update does -**not** contain ``prevAppVersion`` — that field is stripped before transmission -(see :class:`DeviceHeader`). ``prevAppVersion`` is only used by the host-side -downloader to request the previous version from a remote source. +The wire header sent to the device during a firmware update is identical to +the full 48-byte file header. ``prevAppVersion`` is transmitted and the +bootloader struct now includes it (bootloader v1.2+). """ from __future__ import annotations @@ -38,8 +37,8 @@ HEADER_SIZE: int = 48 """Total size of the firmware header in bytes.""" -DEVICE_HEADER_SIZE: int = 44 -"""Size of the header sent to the device (header without ``prevAppVersion``).""" +DEVICE_HEADER_SIZE: int = HEADER_SIZE +"""Size of the header sent to the device (identical to the file header, 48 bytes).""" IV_SIZE: int = 16 """Size of the initialization vector in bytes.""" @@ -214,24 +213,16 @@ def load_firmware(path: str | Path) -> tuple[FirmwareHeader, bytes]: def build_device_header(raw: bytes | bytearray) -> bytes: - """Build the header that is actually transmitted to the device. + """Return the 48-byte wire header transmitted to the device during CMD_START. - Wire header = bytes ``[0:16]`` (protocol + productId + appVersion) - concatenated with bytes ``[20:48]`` (pageCount + pageLen + IV + CRC), - dropping the 4-byte ``prevAppVersion`` field. + The wire header is identical to the file header — all fields including + ``prevAppVersion`` are transmitted (bootloader struct is 48 bytes). """ if len(raw) < HEADER_SIZE: raise FirmwareFormatError( f"firmware too short: need at least {HEADER_SIZE} bytes, got {len(raw)}" ) - first = bytes(raw[0:16]) - second = bytes(raw[20:HEADER_SIZE]) - wire = first + second - if len(wire) != DEVICE_HEADER_SIZE: - raise FirmwareFormatError( - f"built device header is {len(wire)} bytes, expected {DEVICE_HEADER_SIZE}" - ) - return wire + return bytes(raw[0:HEADER_SIZE]) def split_pages(payload: bytes, page_size: int) -> list[bytes]: diff --git a/tests/test_firmware.py b/tests/test_firmware.py index a2f250b..2e5bbf1 100644 --- a/tests/test_firmware.py +++ b/tests/test_firmware.py @@ -68,13 +68,19 @@ def test_accepts_extra_trailing_bytes(self, sample_header_bytes: bytes) -> None: class TestBuildDeviceHeader: - def test_strips_prev_app_version_field(self, sample_firmware: bytes) -> None: + def test_wire_header_is_full_48_byte_file_header(self, sample_firmware: bytes) -> None: wire = build_device_header(sample_firmware) + assert len(wire) == 48 assert len(wire) == DEVICE_HEADER_SIZE - # First 16 bytes of the file are transmitted verbatim. - assert wire[:16] == sample_firmware[:16] - # Then comes [20:48] — the 4 bytes at [16:20] (prevAppVersion) are skipped. - assert wire[16:] == sample_firmware[20:HEADER_SIZE] + # Wire header is identical to the first HEADER_SIZE bytes of the file. + assert wire == sample_firmware[:HEADER_SIZE] + + def test_prev_app_version_present_at_offset_16(self, sample_firmware: bytes) -> None: + wire = build_device_header(sample_firmware) + # prevAppVersion is at bytes [16:20] in both the file and the wire header. + assert wire[16:20] == sample_firmware[16:20] + # Confirm the value is non-zero (matches the fixture's 0x01020300). + assert wire[16:20] != b"\x00\x00\x00\x00" def test_rejects_short_blobs(self) -> None: with pytest.raises(FirmwareFormatError):