Skip to content

Commit fbd1e41

Browse files
Merge pull request #24 from appdevforall/tools/add-rootfs-builder
feat(tools): add build-iiab-rootfs.sh (IIAB-oA rootfs builder)
2 parents 105fc46 + 94d860e commit fbd1e41

3 files changed

Lines changed: 1115 additions & 0 deletions

File tree

‎docs/ROOTFS_MANIFEST.md‎

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# IIAB-oA rootfs manifest — contract (`schema: 1`)
2+
3+
Single source of truth for the two small members embedded in every IIAB-oA rootfs
4+
tarball. **Two producers, one schema:** the build pipeline
5+
(`tools/build-iiab-rootfs.sh`) and the in-app backup writer both emit these
6+
members; the in-app validator reads them on **import/restore** (the untrusted
7+
gates). Both sides reference THIS file instead of re-describing the recipe, so
8+
they cannot drift.
9+
10+
It exists because downloads are already covered by the sidecar `.meta4`
11+
(`aria2 --check-integrity`), but a **manual import** from external storage
12+
(USB/SD) has no sidecar — so the tarball must be able to validate **itself**.
13+
14+
Scope: **integrity** (corruption / "did this arrive intact, and is it the rootfs
15+
I think it is"). It is **not** anti-tamper — an attacker who edits the tree can
16+
recompute the manifest. Authenticity (signing) is a possible future layer, not
17+
this one.
18+
19+
---
20+
21+
## The two members
22+
23+
Both live inside the rootfs tree (the archive's top-level dir is
24+
`installed-rootfs/iiab/`):
25+
26+
| Member | Path (in-archive) | Position in tar | In treehash? |
27+
|--------|-------------------|-----------------|--------------|
28+
| Identity | `installed-rootfs/iiab/.iiab-rootfs.json` | **FIRST** | **yes** |
29+
| Integrity | `installed-rootfs/iiab/.iiab-rootfs.integrity.json` | **LAST** | **no (excluded)** |
30+
31+
- **Identity first** so the validator can read it after decompressing only a few
32+
KB — a fast arch/structure gate with no full scan.
33+
- **Integrity last** because its `treehash` cannot be known until the rest of the
34+
tree is packed (chicken-and-egg); it is the only member excluded from the hash.
35+
36+
### Identity — `.iiab-rootfs.json`
37+
```json
38+
{ "schema": 1, "kind": "iiab-rootfs",
39+
"arch": "armeabi-v7a", "deb_arch": "arm",
40+
"tier": "standard", "iiab_commit": "ab88e5d",
41+
"built": "2026.158", "base": "debian-trixie",
42+
"builder": "build-iiab-rootfs.sh" }
43+
```
44+
- `arch` uses the **Android ABI id** (`arm64-v8a` | `armeabi-v7a`); `deb_arch`
45+
(`aarch64` | `arm`) is included for reference.
46+
- The validator uses `kind` + `arch`; the rest is metadata it may surface in the
47+
UI. Unknown extra keys must be ignored (forward-compatible).
48+
49+
### Integrity — `.iiab-rootfs.integrity.json`
50+
```json
51+
{ "schema": 1, "algo": "iiab-tree-sha256-v1", "treehash": "<lowercase hex>" }
52+
```
53+
54+
---
55+
56+
## `iiab-tree-sha256-v1` — the digest (FROZEN)
57+
58+
Computed over the archive's **logical tar members** — never over an on-disk
59+
filesystem walk. (Reason: hardlinks. On disk a hardlink is a second name for one
60+
inode; in the tar, GNU tar emits the first occurrence as a regular file and the
61+
rest as hardlink entries. Hashing the members of the actual artifact makes the
62+
representation identical for whoever reads that artifact.)
63+
64+
### Per-member digest
65+
For every logical member **except** the integrity member
66+
(`installed-rootfs/iiab/.iiab-rootfs.integrity.json`), and **including** the
67+
identity member:
68+
69+
```
70+
m_i = SHA256( normalized_path + 0x00 + type + 0x00 + payload )
71+
```
72+
73+
- **`normalized_path`** (UTF-8 bytes): replace `\` -> `/`; remove one leading
74+
`./`; remove all leading `/`; remove all trailing `/`. Keep the full
75+
`installed-rootfs/...` prefix. **Do NOT trim whitespace** (a path may
76+
legitimately contain leading/trailing spaces). (D11 already guarantees no `..`.)
77+
- **`type`** (one ASCII byte) and **`payload`**, by tar typeflag:
78+
79+
| tar typeflag | `type` | `payload` |
80+
|---|---|---|
81+
| REGTYPE `0`/`\0`, CONTTYPE | `f` | file content bytes |
82+
| DIRTYPE `5` | `d` | (empty) |
83+
| SYMTYPE `2` | `l` | symlink target **as stored (raw)** + `0x00` |
84+
| LNKTYPE `1` (hardlink) | `h` | **`normalized_path` of the target** + `0x00` |
85+
| anything else (char/block/fifo/socket) | — | **build ABORTS; verifier -> `CORRUPT`/unverifiable** |
86+
87+
(The build already `--exclude '*/dev/*'` and tar drops sockets, so no other
88+
types should appear in a valid artifact.)
89+
- **pax / GNU long-name / extended-header entries are not members.** Both sides
90+
resolve them to the logical member (real name + content) and never feed the
91+
auxiliary pseudo-entry into the digest. (Python `tarfile` and a ustar/pax reader
92+
both do this transparently.)
93+
94+
### Combine (order-independent + domain-separated)
95+
```
96+
treehash = lowercase_hex( SHA256( ascii("iiab-tree-sha256-v1") + 0x00
97+
+ concat( m_i sorted ascending by raw 32-byte value ) ) )
98+
```
99+
The combine is **order-independent**: a byte-sort over fixed 32-byte digests, with
100+
no locale/collation/tar-version dependence. Cost is N x 32 bytes held in memory
101+
plus a sort (tens of MB even for a huge tree); member content is streamed, never
102+
buffered whole.
103+
104+
### Per-artifact, not cross-artifact
105+
The treehash is defined over the logical members of **the artifact as packed**,
106+
and is always verified against **that same artifact's** embedded value. Because
107+
the hardlink `f`/`h` assignment follows the archive's member order, two
108+
independent packings of the "same tree" may yield different treehashes — that is
109+
expected and is not a parity problem: a producer hashes the members it writes, and
110+
the verifier reads those same members. Producers therefore do **not** need a
111+
canonical cross-producer member order. (The build still packs the non-identity
112+
members with `tar --sort=name` for reproducible artifacts, but the recipe does not
113+
require it.)
114+
115+
### Lock points (anything below changes the hash -> bump `algo`)
116+
1. Domain prefix bytes `iiab-tree-sha256-v1` and the `0x00` after it.
117+
2. Per-member field order and the two `0x00` separators.
118+
3. The typeflag -> `type` byte mapping in the table above.
119+
4. Symlink payload is the **raw stored** target; hardlink payload is the
120+
**normalized** target; both followed by a trailing `0x00`.
121+
5. Path normalization rules exactly as stated, including **no whitespace trim**.
122+
6. Combine = SHA256 over the **ascending byte-sorted** 32-byte digests, prefixed
123+
by the domain string + `0x00`.
124+
7. The integrity member is the **only** exclusion; the identity member **is**
125+
hashed.
126+
127+
---
128+
129+
## Producing it (build pipeline & app backup)
130+
131+
1. Pack the tree with the **identity member first** and **without** the integrity
132+
member.
133+
2. Compute `treehash` over that archive's logical members (excluding the integrity
134+
member by name — it is absent at this point).
135+
3. Append the **integrity member last**, carrying `treehash`.
136+
4. **Self-verify:** re-read the finished artifact, recompute over all members
137+
except the integrity member, and assert it equals the stored `treehash`. Never
138+
ship an artifact your own verifier would reject.
139+
140+
The reference implementation is a small streaming pass (Python `tarfile` on the
141+
build side; a ustar/pax reader in the app). It streams member content (no
142+
whole-file buffering), so it scales to multi-GB / 80 GB rootfs images.
143+
144+
## Verifying it (import / restore)
145+
1. Read the **identity** member (fast, first) -> structural + `arch` gate.
146+
2. Stream the gzip->tar once, recompute the treehash over all members except the
147+
integrity member, and compare to the integrity member's `treehash`.
148+
3. On mismatch or an unverifiable member type: `Result.CORRUPT`.
149+
150+
## Rollout — soft -> strict
151+
- **Soft (now):** validate the manifest when present; fall back to the legacy
152+
ELF/structure heuristic when absent. Nothing breaks for older artifacts.
153+
- **Strict (later):** require the manifest, once both producers reliably emit it.
154+
155+
## Implementations
156+
- Build pipeline: `tools/build-iiab-rootfs.sh` (generation + build self-verify).
157+
- App: `RootfsArchiveValidator` (verifier, `Result.CORRUPT`) and the backup writer.
158+
159+
Bump `schema` on any breaking change to the members; bump the `algo` string
160+
(`iiab-tree-sha256-v2`, ...) on any change to the digest recipe.
161+
162+
---
163+
*Copyright (c) 2026 AppDevForAll.*

0 commit comments

Comments
 (0)