spi_check: +TPL plaintext verification and optional --blob byte-match

Discovered the rkspi wrapper format during offline RC4 probing:
the RKNS-wrapper sector at 0x8000 is plaintext. Zero padding
fills 0x8200..0x85FF. Encoded metadata sits at 0x8600. The TPL
(DDR blob) starts at 0x8800 in plaintext -- not RC4 encrypted
as I first guessed.

New checks:
  - TPL entry signature (0x01 0x00 0x00 0x14 = b +4 skipping header)
    at offset 0x8800 -- catches silent TPL corruption
  - optional --blob <path>: byte-by-byte compare SPI[0x8800:+len(blob)]
    against a reference DDR blob file, reports sha256 + first-diff
    offset on mismatch

Validated against stock SPI with stock blob (PASS, sha 13c04c4f),
patched SPI with patched blob (PASS, sha 85799151), and the
cross-pair (FAIL with diagnostics).

Closes the remaining gap in phase-1 static validation -- now
we catch not just |image has no idbloader| but also |image has
the wrong DDR bytes|.
This commit is contained in:
2026-04-15 15:55:29 +02:00
parent 2bec08a868
commit f760f08300
+30 -6
View File
@@ -19,18 +19,20 @@ Exit codes:
Usage: rk3588_spi_check.py <spi-image.bin> Usage: rk3588_spi_check.py <spi-image.bin>
""" """
import struct import argparse, hashlib, struct
import sys import sys
IDBL_OFFSET = 0x8000 IDBL_OFFSET = 0x8000
RKNS_MAGIC = 0x534E4B52 # "RKNS" LE — rkspi boot header RKNS_MAGIC = 0x534E4B52 # "RKNS" LE — rkspi boot header
TPL_OFFSET = 0x8800 # plaintext DDR blob (TPL) starts here in rkspi layout
TPL_ENTRY = bytes.fromhex("01000014") # first 4 bytes of the RK3588 DDR blob entry
PAYLOAD_END = 0x60000 # stock u-boot places next section here PAYLOAD_END = 0x60000 # stock u-boot places next section here
def die(code, msg): def die(code, msg):
print(f"FAIL: {msg}", file=sys.stderr) print(f"FAIL: {msg}", file=sys.stderr)
sys.exit(code) sys.exit(code)
def main(path): def main(path, blob_path=None):
with open(path, "rb") as f: with open(path, "rb") as f:
data = f.read() data = f.read()
print(f"SPI image: {path} size=0x{len(data):x} ({len(data)} B)") print(f"SPI image: {path} size=0x{len(data):x} ({len(data)} B)")
@@ -63,11 +65,33 @@ def main(path):
flag_like = struct.unpack_from("<I", w, 12)[0] flag_like = struct.unpack_from("<I", w, 12)[0]
print(f" wrapper[+8]=0x{size_like:08x} wrapper[+12]=0x{flag_like:08x}") print(f" wrapper[+8]=0x{size_like:08x} wrapper[+12]=0x{flag_like:08x}")
# TPL content check — the DDR blob lives in plaintext at 0x8800
tpl_prefix = data[TPL_OFFSET:TPL_OFFSET + 4]
if tpl_prefix != TPL_ENTRY:
die(4, f"TPL at 0x{TPL_OFFSET:x} does not start with expected blob entry "
f"{TPL_ENTRY.hex()}: got {tpl_prefix.hex()}")
print(f"OK TPL blob entry signature at 0x{TPL_OFFSET:x}")
if blob_path:
with open(blob_path, "rb") as f:
blob = f.read()
tpl_region = data[TPL_OFFSET:TPL_OFFSET + len(blob)]
if tpl_region != blob:
# Find the first differing byte for diagnostics
diffs = [i for i in range(len(blob)) if i < len(tpl_region) and blob[i] != tpl_region[i]]
first_diff = diffs[0] if diffs else len(tpl_region)
die(5, f"TPL in SPI does NOT match blob file.\n"
f" blob sha256: {hashlib.sha256(blob).hexdigest()}\n"
f" SPI[0x{TPL_OFFSET:x}:+{len(blob)}] sha256: {hashlib.sha256(tpl_region).hexdigest()}\n"
f" first differing byte at blob offset 0x{first_diff:x}")
print(f"OK TPL bytes match {blob_path} sha256={hashlib.sha256(blob).hexdigest()[:16]}… ({len(blob)} B)")
print("\nPASS: image looks structurally sound. Safe to flash.") print("\nPASS: image looks structurally sound. Safe to flash.")
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 2: ap = argparse.ArgumentParser(description=__doc__)
print(__doc__) ap.add_argument("spi", help="u-boot-rockchip-spi-*.bin to validate")
sys.exit(1) ap.add_argument("--blob", help="optional raw DDR blob to byte-compare against the TPL region")
main(sys.argv[1]) a = ap.parse_args()
main(a.spi, a.blob)