This lesson has one job: say what image authentication guarantees without leaning on algorithm names. The ROM must confirm the image came from its owner, has not been altered, is allowed to run on this device, and is not an older version being replayed. Each guarantee maps to a check on the manifest plus the image bytes; the mathematics underneath is deliberately out of scope, and nothing here names parameters, key sizes or algorithms.
Authentication checks four things in order. Origin asks whether the manifest was issued by the owner, answered by the fused key. Integrity asks whether the loaded bytes match the manifest, answered over the 8 KB image the SPI loader placed in SRAM0. Freshness asks whether the manifest version is at least the minimum held in the fuse version field of the previous topic (a thermometer-coded counter that can only rise), which defeats replay of an older signed image. Binding asks whether the manifest names this device class and these memory ranges, so an image for another product or another load address is refused. Only all four passing sets the pass flag; the checksum lesson's 192-microsecond read is the mechanical analogue, proving the bytes match, while these checks prove the bytes are authorised.
The script models the policy logic, which is the integrator-owned part: version comparison against a fused minimum, containment of the manifest's declared load address and device class, and the all-four conjunction. The load window is the SRAM0 image area, 0x2000_0000 to 0x2000_FFFF; SRAM1 is secure-only and is not an image target. Each case carries the manifest's own load base and device class, so binding is computed from manifest fields and can refuse. The cryptography that produces the pass inputs is a black box here by design.
fused_min_version = 7
this_device_class = "NANO-A"
window_lo, window_hi = 0x2000_0000, 0x2000_FFFF
image_len = 8 * 1024
# (name, origin ok, integrity ok, version, load_base, device_class, expected verdict)
cases = [
("good", True, True, 7, 0x2000_0000, "NANO-A", True),
("bad origin", False, True, 9, 0x2000_0000, "NANO-A", False),
("bad integrity", True, False, 9, 0x2000_0000, "NANO-A", False),
("rollback", True, True, 6, 0x2000_0000, "NANO-A", False),
("wrong product", True, True, 9, 0x2000_0000, "NANO-B", False),
("wrong load address", True, True, 9, 0x2000_F000, "NANO-A", False),
("targets SRAM1", True, True, 9, 0x2001_0000, "NANO-A", False),
]
refused_by = {}
for name, origin, integrity, version, load_base, device_class, expect in cases:
fresh = version >= fused_min_version
bound = (device_class == this_device_class
and window_lo <= load_base and load_base + image_len - 1 <= window_hi)
verdict = origin and integrity and fresh and bound
print(f"{name}: version {version}, fresh {fresh}, bound {bound} -> {verdict}")
assert verdict == expect, name
if not verdict:
refused_by[name] = [c for c, ok in (("origin", origin), ("integrity", integrity), ("freshness", fresh), ("binding", bound)) if not ok]
assert refused_by["wrong product"] == ["binding"]
assert refused_by["wrong load address"] == ["binding"]
assert refused_by["rollback"] == ["freshness"]
# inject a bug: a verifier that never checks binding must be caught by the new cases
def buggy(origin, integrity, version):
return origin and integrity and version >= fused_min_version
escapes = [name for name, o, i, v, base, cls, expect in cases if buggy(o, i, v) and not expect]
assert escapes == ["wrong product", "wrong load address", "targets SRAM1"], escapes
print("PASS: only the all-checks-pass image is authorised; a verifier without binding admits", escapes)The manifest format lists all four checks with the store each draws on: keys, image bytes, version minimum, device binding.
Rollback has a monotonic minimum that software cannot lower: the thermometer-coded version field in the fuse array, with its owner named.
The load address is part of the authorised manifest, not an independent loader decision.
Present four images: correct, wrong signer, one flipped bit, and an older version. Confirm exactly one boots. Then present a correct, authorised image built for a different product and another built for a different load address, and confirm both are refused: binding is the check most often skipped, which is why the script above carries a case for each. If all four boot, the policy logic defaults to pass and the black box was never consulted.
Binding refuses it: the manifest names a device class and load window the ROM enforces. Without binding, a valid image for one product becomes a valid attack on another.