This lesson has one job: turn the whole section's mechanisms into a signoff verdict. Filters placed but never tested, attributes carried almost end to end, keys routed past one readable flop, verification that covers only the pass path: each is a partition that looks closed and is not. The checklist below is the integrator's last walk of the design, and every item fails closed, meaning an unchecked item blocks signoff rather than passing on trust.
Six checks cover the six ways partitions break. Attribute propagation traces AxPROT from every master to every filter with no dropped sideband, the bridge-and-converter discipline. Attribute programming asks the question that propagation never does: who may set each master's attribute? Any master whose attribute is a register must have that register writable only by an access of equal or higher privilege; on NanoSoC v1 the SBR's CTRL.NS fails this check, which is the escape the first lesson showed. Firewall placement confirms each secure-only slave has its filter at the slave, so no path routes around it. Key routing proves secrets travel only from fuse shadows into the verification engine, with the connectivity check as evidence. Lifecycle gating confirms every debug and test enable is qualified by state. Negative testing proves the fail paths: each refusal returns the right code, sets the right sticky bit, and jumps nowhere, and it includes the test that a non-secure write of CTRL.NS = 0 followed by a DMA copy out of SRAM1 must fail. The script runs the checklist against a small description of the design and returns findings. It must find exactly one problem on NanoSoC v1 (the SBR attribute), none on the fixed design, and the right finding for each of four injected mistakes.
import copy
V1 = {
"secure_only": {"SRAM1": "xbar", "RSTPWR": "apb_dec"}, # slave -> where its filter sits
"routes": {"SRAM1": ["xbar"], "RSTPWR": ["xbar", "apb_dec"]}, # every path to the slave
"hops_carry_prot": {"cpu>xbar": True, "sbr>xbar": True, "xbar>bridge": True, "bridge>apb_dec": True},
"attr_registers": {"SBR.CTRL.NS": "any"}, # who may write the attribute
"key_readers": {"verify_engine"},
"debug_paths": {"jtag": "lifecycle", "scan": "lifecycle"},
"negative_tests": {"ns_read_sram1", "ns_write_rstpwr", "tampered_image", "field_debug_probe"},
}
REQUIRED_NEG = {"ns_read_sram1", "ns_write_rstpwr", "tampered_image", "field_debug_probe", "ns_clear_ns_then_copy"}
def review(d):
f = []
for slave, where in d["secure_only"].items():
if where not in d["routes"][slave] or where is None:
f.append(f"placement: no filter on a path to {slave}")
for hop, ok in d["hops_carry_prot"].items():
if not ok:
f.append(f"attributes: {hop} drops AxPROT")
for reg, who in d["attr_registers"].items():
if who != "secure":
f.append(f"attribute programming: {reg} writable from non-secure software")
if d["key_readers"] - {"verify_engine"}:
f.append("keys: readable outside the verification engine")
for path, gate in d["debug_paths"].items():
if gate != "lifecycle":
f.append(f"lifecycle: {path} not gated by state")
for t in sorted(REQUIRED_NEG - d["negative_tests"]):
f.append(f"negative test missing: {t}")
return f
findings = review(V1)
assert findings == ["attribute programming: SBR.CTRL.NS writable from non-secure software",
"negative test missing: ns_clear_ns_then_copy"], findings
fixed = copy.deepcopy(V1)
fixed["attr_registers"]["SBR.CTRL.NS"] = "secure"
fixed["negative_tests"].add("ns_clear_ns_then_copy")
assert review(fixed) == []
def mutate(change):
d = copy.deepcopy(fixed)
change(d)
return review(d)
assert mutate(lambda d: d["hops_carry_prot"].update({"bridge>apb_dec": False})) == ["attributes: bridge>apb_dec drops AxPROT"]
assert mutate(lambda d: d["key_readers"].add("apb_bus")) == ["keys: readable outside the verification engine"]
assert mutate(lambda d: d["debug_paths"].update({"scan": None})) == ["lifecycle: scan not gated by state"]
assert mutate(lambda d: d["secure_only"].update({"RSTPWR": "bridge"})) == ["placement: no filter on a path to RSTPWR"]
for line in findings:
print("NanoSoC v1 finding:", line)
print("PASS: v1 has 2 findings (the SBR escape and its missing test), the fixed design 0, each injected mistake caught")The attribute trace, the attribute-programming table (who may write each master's attribute), the firewall map, the key-routing proof, the lifecycle gating table and the negative-test log are each attached to the signoff record.
Negative tests outnumber positive ones in the security plan: refusals are the behaviour being verified.
Any waiver against a checklist item names the residual risk, the compensating control and an expiry; open-ended security waivers do not exist.
Run the negative suite last: non-secure access to each secure-only slave from every master, a non-secure write of the SBR's CTRL.NS = 0 followed by a DMA copy out of SRAM1, debug probe in each locked state, tampered image at each boot stage, and dropped attribute at each bridge. Each must refuse with the documented code and set the documented bit. A suite that passes without ever asserting a fault line has tested the happy path twice.
Suspicious: a negative-test suite that never fires its fault lines is either not running or wired to pass. Confirm the suite refuses a deliberately broken build before trusting its green on the real one.