The same path that lets bring-up read every register lets an attacker do the same, and the SRAM1 contents that the secure filter protects on the mission path are wide open on an unlocked debug path. Yet locking debug completely bricks failure analysis on returned parts. NanoSoC v1 shows how thin the protection already is: the secure filter protects SRAM1 only against masters whose security attribute software cannot choose, and the stream bridge's attribute can be rewritten from a non-secure register slot. A debug master must not add a second hole of the same kind. The compromise has to be designed once and enforced in hardware: what each lifecycle state allows, how bring-up authenticates, and which power keeps the path alive when the chip sleeps.
Debug permission follows the lifecycle state of the extension, which a lifecycle register and fuse-backed shadow registers would hold: in development everything is open, in production the secure regions are closed unless an authenticated unlock succeeds, and for a returned part a limited window reopens under a challenge-response owned by the product team. The enforcement point matters. The debug master drives AxPROT from the lifecycle and unlock state, and the interconnect and APB decoder filters deny it exactly as for a mission master. Duplicating the filter inside the access port would create two filters that can disagree, so there is one rule set and the debug master is simply another master whose attribute the software cannot choose. Power follows the same always-on rule as the access path: the debug logic, the unlock state and the wake-capable triggers live in the always-on domain on the peripheral clock, so sleep never locks out the debugger and the debugger never blocks sleep without an explicit debug power request the software can see.
# not-run: secured-debug permission table for the debug extension, illustrative
development: AxPROT[1] = 0 (secure): all four regions allowed, trace all sources
production: AxPROT[1] = 1 (non-secure) until an authenticated unlock; SRAM1 and RSTPWR denied
returned part: as production; a challenge-response unlock drives AxPROT[1] = 0
enforcement: the existing interconnect and APB decoder filters; no second filter in the port
power: debug logic and unlock state in PD_AON on clk_per; survives sleep# Lifecycle x region matrix, derived from the real filter rule and checked against the table.
REGIONS = ("rom", "sram0", "sram1", "rstpwr")
SECURE_ONLY = {"sram1", "rstpwr"} # NanoSoC: AxPROT[1] = 0 required
def debug_axprot1(state, unlocked):
"""The debug master drives the attribute from lifecycle and unlock state."""
return 0 if (state == "development" or unlocked) else 1
def filter_allows(axprot1, region): # the existing interconnect/APB rule
return not (region in SECURE_ONLY and axprot1 == 1)
def matrix(state, unlocked):
return {r: filter_allows(debug_axprot1(state, unlocked), r) for r in REGIONS}
EXPECT = { # written independently, from the permission table
("development", False): dict(rom=True, sram0=True, sram1=True, rstpwr=True),
("production", False): dict(rom=True, sram0=True, sram1=False, rstpwr=False),
("production", True): dict(rom=True, sram0=True, sram1=True, rstpwr=True),
("returned", False): dict(rom=True, sram0=True, sram1=False, rstpwr=False),
}
for key, want in EXPECT.items():
assert matrix(*key) == want, key
denied = [r for r, ok in matrix("production", False).items() if not ok]
assert len(denied) == 2 # two denied cells in production
# a second, disagreeing filter inside the access port would be caught by the same table
def port_filter_allows(axprot1, region): # forgets RSTPWR
return not (region == "sram1" and axprot1 == 1)
mismatch = [r for r in REGIONS
if port_filter_allows(1, r) != filter_allows(1, r)]
assert mismatch == ["rstpwr"]
print("PASS: 4 states match the table; production denies 2 cells; a disagreeing second filter is caught")The permission table above with each cell traced to a hardware enforcement point, not a software promise, and the enforcement shared with the mission filters.
The unlock mechanism with its authentication strength stated and its failure mode closed, failed unlocks deny, never retry forever.
Debug power in the always-on domain confirmed, with the debug power request visible to the power controller when debug holds the chip awake.
Test the locks from the debug side, not the mission side: attempt SRAM1 and reset-controller accesses in each lifecycle state with and without unlock, and confirm denial is clean with an error response rather than a hang. Enter sleep with the debugger attached and confirm the path survives and the wake triggers fire; then confirm the chip can still reach its deepest sleep with debug detached. A debug path that blocks sleep is a power-budget bug wearing a debug badge.
Correct, provided the state table says so: production denies secure regions without unlock. The remedy is the authenticated unlock or the returned-part flow, not a wider-open default. Convenience that bypasses the table is the vulnerability.