Skip to content

Commit 9504c4d

Browse files
authored
Merge pull request #76 from AndrewDemsDS/feat/wipekv-revert-re-map
Break-glass recovery: :wipekv KV-reset, revert-to-stock tooling, stock-FW feature map
2 parents 5fc31e6 + c09df0a commit 9504c4d

9 files changed

Lines changed: 946 additions & 11 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ strings*.txt
1313

1414
# The entire dumps/ dir is local-only (raw dumps + captures). Track only its README.
1515
/dumps/*
16+
17+
# Raw RE analysis workspace (carved images, agent notes) — docs/13 is the deliverable
18+
/reverse-engineering/analysis/
1619
!/dumps/README.md
1720

1821
# Live captures

firmware/docs/10-firmware-ota-procedure.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,20 @@ It is deliberately **not** a `:2323` diag-console command. That console is debug
376376
unauthenticated by design, so a trigger living there would be absent from precisely the images most
377377
likely to need it.
378378

379+
**Command inventory** (token + colon suffix; plain token = the `#78` HTTPS fetch above). All ship
380+
in BOTH flavours, gated only by the token being set at build time:
381+
382+
- `<token>:slots` (>= 1.3.8): report both slots' FWHS serials + the running index (§17)
383+
- `<token>:revert` (>= 1.3.8): boot the other slot if it is strictly older (§17 Path 1)
384+
- `<token>:backup` (>= 1.3.9): stream the inactive (stock) slot's raw image (§17)
385+
- `<token>:wipekv` (>= 1.3.16): factory-reset the Matter KV (formats both Matter DCT regions,
386+
`0x3E0000`/`0x3ED000`) and reboot. The cure for the "previously cloud-paired stock unit"
387+
commissioning wedge: see [docs/12 §"SendTrustedRootCert wedge"](12-ota-convert-stock-unit.md).
388+
Wlan fast-reconnect data is untouched, but the fresh KV has no CHIP network config, so the
389+
device comes back **uncommissioned in BLE commissioning mode with Wi-Fi down**: plan to
390+
re-commission over BLE (`chip-tool pairing code-wifi ... --bypass-attestation-verifier 1`
391+
from a laptop in range), then hand off to HA via `open-commissioning-window`.
392+
379393
**Fails closed.** No `BREAKGLASS_TOKEN` in `ota-release.env` means the socket is never opened, and
380394
the boot log says so instead of staying silent. There is deliberately no default token: a default in
381395
a public repo is equivalent to no authentication. Set it in `ota-release.env` (gitignored); the
@@ -463,3 +477,112 @@ subscriptions, so treat GUI-free editing as **build-verified, not runtime-verifi
463477
clusters still require the GUI plus the `zzz_generated` edits. Cheap insurance either way: open the
464478
`.zap` in the GUI once and plain-Save before building, which forces ZAP to re-derive all metadata in
465479
one canonical pass.
480+
481+
## 17. Reverting to stock without opening the case (issue #19)
482+
483+
Two OTA-only paths back to the stock ConnectLife firmware. Path 1 was **proven on hardware
484+
2026-07-21** (office unit, stock sw 2): stock → OTA-convert to 1.3.8 → `revert --flip`
485+
stock boots and rejoins ConnectLife by itself → re-convert to 1.3.8 (full round trip).
486+
Feature-map evidence: `reverse-engineering/docs/13`.
487+
488+
### Path 1: slot-flip (no payload, preferred when it applies)
489+
490+
A stock→custom OTA conversion writes only the inactive slot, so the stock image stays intact
491+
and signature-valid in the other slot until a **second** custom OTA overwrites it. Custom
492+
firmware ≥ 1.3.8 has two break-glass commands (same listener as §13, token + colon suffix):
493+
494+
- `<token>:slots``ok: fw1_sn=<u> fw2_sn=<u> cur=<idx>` (FWHS serials of both slots)
495+
- `<token>:revert` → invalidates the **running** image's signature
496+
(`sys_update_ota_set_boot_fw_idx`) and resets; the bootloader falls back to the other slot
497+
498+
```
499+
ota-release.sh revert --flip <unit-ip> [--force]
500+
```
501+
502+
The script queries `:slots` first and refuses unless the other slot's serial is below
503+
`SERIAL_BASE` (stock carries serial 100; custom serials are `SERIAL_BASE + versionInt`), so a
504+
flip onto an older **custom** image needs `--force`. Guard inside the firmware too: revert
505+
refuses when the other slot is not older. Returning to custom afterwards = re-run the docs/12
506+
conversion (stock's dormant OTA Requestor).
507+
508+
**Why it is safe for the cloud binding:** the regions stock needs stay byte-intact under the
509+
custom firmware (constant-scanned against the deployed image): Wi-Fi profile `0x2FF000`,
510+
cloud config + dkey `0x3DB000`, device identity `0x3DD000`. A reverted unit rejoins
511+
ConnectLife as itself, no re-provisioning. The Matter DCT areas (`0x3E0000`/`0x3ED000`) are
512+
the clobbered ones, and those only cost the (anyway replaced) stock commissioning.
513+
514+
**Caveat, virgin units:** slot 2 in the factory dump is not S2292 but an Aug-2023 MP-test
515+
build (`S1798.MP_TEST_VERSION_SE`, no Matter). The running stock slot (S2292) is what a
516+
conversion preserves, so a first-generation convert flips back to S2292 as intended; just do
517+
not treat "the other slot" as interchangeable before checking `:slots`.
518+
519+
### Path 2: repackage the stock app as a Matter OTA (⚠️ BLOCKED: re-signed image fails to boot, 2026-07-21)
520+
521+
**Status: host checks all pass, but the re-signed image does NOT boot.** On the office unit a
522+
repackaged payload (stock backup + serial patch + re-HMAC + re-sum, every check green, written
523+
byte-perfect to flash, verified by a post-mortem clip dump) left the unit dark: the boot
524+
attempt visibly ran and crashed (the GD25Q32 QE bit was found CLEARED afterwards, and was
525+
cleared again after a manual re-set + power cycle, i.e. the boot flow touches SR2 and never
526+
gets far enough to re-enable quad). The SAME stock bytes with the factory signature (serial
527+
100) boot fine on the same unit. So the bootloader/ROM acceptance is stricter than the
528+
decoded HMAC+bytesum recipe (docs/13) in some way the host-side verification does not
529+
capture: possibly a second key (OTP/eFuse vs the flash partition hash_key) or an extra
530+
manifest check. Until the prebuilt `bootloader.axf` is RE'd on this point, treat
531+
`--repackage`/`--apply` as a brick risk: the only recovery is the CH341A clip.
532+
533+
Recovery recipe that worked (clip): write the unit's own dump (per-unit data preserved) with
534+
fw1 replaced by the ORIGINAL stock slot bytes (from a `revert --backup` capture, factory
535+
signature) and fw2 erased to 0xFF; `ch341flash-full.py` re-sets QE at the end. Boots stock,
536+
ConnectLife rejoins.
537+
538+
What still holds from the host work:
539+
540+
For units whose stock slot is already overwritten. Needs a stock dump of any W41H1 (per-unit
541+
data is **not** required: OTA writes only the app slot, and the `0x0` system data, Wi-Fi
542+
profile, dkey and identity live outside it). The stock image's acceptance criteria as decoded
543+
so far (docs/13): bytes `0:32` = `HMAC-SHA256(partition hash_key @ flash 0x140,
544+
image[0xE0:0x140])`, plus a 4-byte byte-sum trailer at EOF. No app-level cryptographic
545+
signature. So:
546+
547+
```
548+
ota-release.sh revert --repackage <stock-dump.bin> # carve fw1 @0x10000, patch serial @+0xF4,
549+
# re-HMAC, re-sum, wrap as rac-stock-v<N>.ota
550+
ota-release.sh revert --apply # stage on the Pi + update_node, verify sw 4
551+
```
552+
553+
`--repackage` first re-verifies the recipe byte-exact against every archived
554+
`firmware_is-v*.bin` and the dump's unpatched fw1, and dies loudly on any mismatch. The
555+
revert int is `max(version.txt, .released-version) + 1` and the patched serial follows the
556+
§11 rule (`SERIAL_BASE + int`), so the bootloader accepts the "older" stock payload. `--apply`
557+
verification is stock-aware: success = the unit reports softwareVersion **4** (vendor 5004)
558+
sustained, or drops off the fabric; `.released-version` is left alone so the next custom OTA
559+
still has to beat the last custom int.
560+
561+
**Recommended journey.** Right after the FIRST OTA conversion (docs/12), while the stock
562+
image still sits intact in the inactive slot, fetch a copy of it once and keep the file:
563+
564+
```
565+
ota-release.sh revert --backup <unit-ip> # needs custom firmware >= 1.3.9 (:backup command)
566+
```
567+
568+
`--backup` streams the inactive slot over the break-glass listener and saves it only after
569+
three checks pass (serial < `SERIAL_BASE`, HMAC, bytesum trailer). After that, any number of
570+
custom OTAs is safe: even once a second custom OTA overwrites the stock slot,
571+
`ota-release.sh revert --repackage <backup>` + `ota-release.sh revert --apply` restores
572+
stock over the air. Mind the **version-consumption rule**: each repackaged revert image
573+
carries serial `SERIAL_BASE + max(version.txt, .released-version) + 1`, burning one fleet
574+
version number, so `--repackage` bumps `version.txt` past the int it just used (commit the
575+
bump). A later custom OTA at or below that int would tie the bootloader and boot stock.
576+
577+
### What does not work (investigated, dead ends)
578+
579+
- **Downloading a public stock image.** The only firmware URL compiled into stock
580+
(`download.hismarttv.com/Content/WifiDeviceVersionFile/<id>.bin`) serves an older
581+
module generation (zero 4 KB blocks in common with the W41H1 dump). The device never polls
582+
for versions; file IDs only exist in the cloud API, harvestable by MITM-ing the phone app.
583+
- **Spoofing the cloud to push HOTA.** Gateway TLS is pinned-CA `VERIFY_REQUIRED` (the
584+
VERIFY_NONE path is unreachable dead code), and jcmd v5 is AES-256-CBC + HMAC-SHA256 keyed
585+
by the per-device dkey.
586+
- **Remote stock-dump capture.** Before 1.3.9 no flash readback path existed in either
587+
firmware; the CH341A clip dump was the only capture route. 1.3.9 adds the `:backup`
588+
break-glass command (Path 2 journey above), which reads back the inactive slot over the air.

firmware/docs/12-ota-convert-stock-unit.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,57 @@ Everything below is automated in
8686
certs), so no bypass here. Press "77", then in HA: Matter → Add device → `3497-011-2332`.
8787
(Or `commission_with_code(code, network_only=True)` via the matter-server, which is dual-homed
8888
on the IoT VLAN, see [`../../reverse-engineering/docs/02`](../../reverse-engineering/docs/02-matter-local-control.md).)
89+
9. **Back up the stock slot (do this once, right now)**: the stock image still sits intact in the
90+
inactive slot until a second custom OTA overwrites it. On custom firmware ≥ 1.3.9:
91+
```
92+
ota-release.sh revert --backup <unit-ip> # saves built-images/stock-backup-*.bin, validated
93+
```
94+
Keep that file. It is the unit's way back to ConnectLife (`revert --repackage` +
95+
`revert --apply`, docs/10 §17 Path 2) even after later OTAs overwrite the stock slot.
8996

9097
## Verified result (kitchen unit → node 14)
9198

9299
`VendorID 5004→0xFFF1 · ProductID 13825→0x8001 · SoftwareVersion 4→23`, `available=True`,
93100
climate attributes readable. Identical to a CH341A-flashed unit, but converted with zero
94101
physical access.
102+
103+
## SendTrustedRootCert wedge: previously cloud-paired units (office unit, 2026-07-21)
104+
105+
A stock unit that was ever paired to a cloud/app fabric carries that fabric's KV entries
106+
**through** the conversion (the Matter DCT regions survive an OTA by design). On the office
107+
unit (stock `SoftwareVersion 2`, older than the kitchen's 4, and once paired to Google Home,
108+
VendorID `0x6006`) that stock-era data left an **orphaned root-cert blob** the new firmware
109+
half-reads: certificate present in the cert store, no matching fabric-table entry.
110+
111+
**Symptom:** every commission attempt fails at commissioning step `SendTrustedRootCert` with
112+
`IM Error 0x00000501: General error: 0x01 (FAILURE)`. Deterministic: every controller
113+
(python-matter-server and chip-tool), every transport, survives reboot. Do not confuse it
114+
with trap #3 above (same IM error, but at `CommissioningComplete`, and transient).
115+
116+
**Why it is unfixable at the Matter level:** fabric indices allocate monotonically, so the
117+
next index permanently points at the orphaned slot; `PersistentStorageOpCertStore` then
118+
rejects `AddTrustedRootCertificate` with `INCORRECT_STATE` because a cert already exists at
119+
that index. No cluster command reaches certs whose index is not in the fabric table
120+
(`remove-fabric` of the *visible* stale fabric is necessary hygiene but not sufficient; the
121+
office unit still wedged with `CommissionedFabrics: 0`). Diagnosis without UART: PASE into
122+
the open window grants admin, so `chip-tool interactive` + `pairing code-paseonly` +
123+
`operationalcredentials read fabrics ... --fabric-filtered 0` shows the stale fabric table.
124+
125+
**Fix (firmware >= 1.3.16):** the `:wipekv` break-glass command
126+
([docs/10 §13](10-firmware-ota-procedure.md)) formats both Matter DCT regions and reboots.
127+
Recovery loop used on the office unit, no physical access beyond "77" presses:
128+
129+
1. `<token>:revert` boots the intact stock slot. Expect stock to come back **without Wi-Fi**:
130+
its Matter-provisioned credentials lived in the (custom-fw-clobbered) DCT, and the old
131+
`0x2FF000` profile points at the ConnectLife-era network.
132+
2. "77" + step 3 of the procedure above re-commissions stock over BLE (re-provisions Wi-Fi).
133+
3. Step 5-6 OTA a `:wipekv`-capable image (repackaged `5004/13825`, version > the wedged one).
134+
4. `<token>:wipekv`, then re-commission on the fresh KV. After the wipe the device is in BLE
135+
commissioning mode with Wi-Fi down, so commission from a laptop in BLE range
136+
(`chip-tool pairing code-wifi ...`), open a commissioning window, and hand off to
137+
the matter-server (`commission_with_code`, `network_only=True`); `pairing unpair` the
138+
temporary chip-tool fabric once HA is in.
139+
140+
Result: office unit = node 62, exactly one fabric (HA), `softwareVersion 10316` verified by
141+
live `read_attribute`. If converting more units with a cloud history, ship a `:wipekv`-capable
142+
image in step 5 the first time and run `:wipekv` right after step 7, before commissioning.

0 commit comments

Comments
 (0)