77 mergegate-exact-binding-field the digest is read at each artifact's own path
88 mergegate-decoy-digest the right digest in a field nobody binds is not a binding
99 mergegate-noop-campaign-link a permissive link check must not satisfy the gate
10- mergegate-dead-authority an authority check that always passes must not satisfy it
10+ mergegate-dead-authority hostqual authorising anything must not satisfy it
11+ mergegate-dead-authority-eb nor must execbinding — the claim covers both readers
12+ mergegate-dead-continuity nor must a postflight that stops comparing campaign links
13+ mergegate-always-inadmissible nor must one that refuses every campaign
1114 mergegate-missing-machinery the tools must be there at all
1215 mergegate-lingering-auto the revoked automatic authority must stay revoked
1316 mergegate-unfrozen-t0 an unfrozen or unauthorised T0 is not merged as frozen
1417 mergegate-reads-t0-digest the expected digest comes from T0, not from a constant
1518 control-inventory-complete this list and the executed set are the same set
1619
17- Fixtures ship the real tools, and two controls mutate them. An earlier revision
18- shipped a stub whose check_campaign_link was a bare pass and called that world
19- compliant: the fixture demonstrated the false positive it was meant to exclude.
20- Checking for the name of a mechanism is not checking the mechanism.
20+ Fixtures ship the real tools, and four controls mutate one enforcement point
21+ each. An earlier revision shipped a stub whose check_campaign_link was a bare
22+ pass and called that world compliant: the fixture demonstrated the false
23+ positive it was meant to exclude. Checking for the name of a mechanism is not
24+ checking the mechanism, and a witness that only ever sees refusals is not
25+ checking one either — hence a positive control beside each negative one.
2126
2227Failures print FAIL[<check>]: <detail>; nothing stops at the first one.
2328
@@ -94,8 +99,11 @@ def guarded(check: str, control: Callable[[], None]) -> None:
9499 lambda d : {"bindings" : {"measurement_harness_digest" : d }},
95100}
96101
97- # Mutations appended to the real hostqual: a later definition wins, so each stub
98- # disables exactly one enforcement point and leaves the rest genuine.
102+ HOSTQUAL , EXECBINDING = mg .STEP7_TOOLS [1 ], mg .STEP7_TOOLS [2 ]
103+
104+ # Mutations of the real tools, one enforcement point each. Appending a definition
105+ # is enough where a later definition wins; the continuity check lives inside a
106+ # larger function, so that one is a targeted edit of its condition.
99107NOOP_LINK = '''
100108
101109def check_campaign_link(link, binding_path, repo):
@@ -110,6 +118,33 @@ def bind_t0(repo, path, commit):
110118 return block, check("t0", True, "stubbed: always authorised")
111119'''
112120
121+ # The second reader. The gate's message claims enforcement on both, so a control
122+ # that only breaks the first leaves half of that claim unproved.
123+ DEAD_AUTHORITY_EB = '''
124+
125+ def t0_at(repo, path, commit):
126+ return {"commit": commit, "path": path, "blob_sha": "b" * 40, "sha256": "f" * 64,
127+ "status": "FROZEN", "collection_authorized": True}
128+ '''
129+
130+ # The mutation the positive postflight control exists to catch: a tool that
131+ # refuses every campaign satisfies a negative-only witness, which would then be
132+ # reading "nothing is admissible" as "the swap was caught".
133+ ALWAYS_INADMISSIBLE = '''
134+
135+ def session_admissibility(*args, **kwargs):
136+ return {"admissible": False, "reasons": ["stubbed: nothing is ever admissible"]}
137+ '''
138+
139+ CONTINUITY_CONDITION = ('preflight.get("campaign_link_sha256") != '
140+ "sha256_file(campaign_link_path)" )
141+
142+
143+ def dead_continuity (src : str ) -> str :
144+ """Leave check_campaign_link working and remove only the comparison of the
145+ preflight's campaign link with this pass's."""
146+ return src .replace (CONTINUITY_CONDITION , "False" )
147+
113148
114149def real_tools () -> dict [str , str ]:
115150 """The tools as they actually are. A fixture that shipped a stub would prove
@@ -140,16 +175,23 @@ def run(*args: str) -> None:
140175def world (tmp : Path , * , frozen : bool = True , authorized : bool = True ,
141176 instrument : str = "print('instrument')" + NL , rebound : bool = True ,
142177 machinery : bool = True , auto_authority : bool = False , decoy : bool = False ,
143- wrong_field : bool = False , mutate : str = "" , name : str = "w" ) -> tuple [Path , str ]:
178+ wrong_field : bool = False , mutate : dict [str , object ] | None = None ,
179+ name : str = "w" ) -> tuple [Path , str ]:
144180 """A synthetic target tree, and the commit a merge into it would produce."""
145181 repo = tmp / name
146182 files = {eb .INSTRUMENT_SOURCES [0 ]: instrument ,
147183 eb .INSTRUMENT_SOURCES [1 ]: '{"decisive": []}' + NL ,
148184 mg .STEP7_NOTE : NOTE_AUTO if auto_authority else NOTE_REVOKED }
149185 if machinery :
150186 tools = real_tools ()
151- if mutate :
152- tools [mg .STEP7_TOOLS [1 ]] = tools [mg .STEP7_TOOLS [1 ]] + mutate
187+ for tool , change in (mutate or {}).items ():
188+ before = tools [tool ]
189+ tools [tool ] = change (before ) if callable (change ) else before + str (change )
190+ if tools [tool ] == before :
191+ # A mutation that lands nowhere turns an attack control into a
192+ # second positive control that nobody reads as one.
193+ raise AssertionError (f"the mutation for { tool } changed nothing; the source it "
194+ "targets has moved" )
153195 files .update (tools )
154196 else :
155197 files [mg .STEP7_TOOLS [0 ]] = "# capture only" + NL
@@ -269,7 +311,7 @@ def control_decoy_digest() -> None:
269311def control_noop_campaign_link () -> None :
270312 """The attack the previous revision of this gate could not see."""
271313 with tempfile .TemporaryDirectory () as raw :
272- repo , commit = world (Path (raw ), mutate = NOOP_LINK , name = "noop" )
314+ repo , commit = world (Path (raw ), mutate = { HOSTQUAL : NOOP_LINK } , name = "noop" )
273315 if verdicts (repo , commit ).get ("step7_machinery_enforces" ) != "fail" :
274316 fail ("mergegate-noop-campaign-link" ,
275317 "a permissive check_campaign_link satisfied the gate; the name of a mechanism "
@@ -284,20 +326,70 @@ def control_noop_campaign_link() -> None:
284326 "that got through" )
285327
286328
329+ def _dead_authority (check_name : str , tool : str , stub : str , reader : str ) -> None :
330+ """Both readers carry the authority state machine, and the gate says so.
331+ A control that breaks one of them leaves the other half of that claim
332+ standing on nothing."""
333+ with tempfile .TemporaryDirectory () as raw :
334+ repo , commit = world (Path (raw ), mutate = {tool : stub }, name = "deadauth" )
335+ if verdicts (repo , commit ).get ("step7_machinery_enforces" ) != "fail" :
336+ fail (check_name , f"a { reader } authority check that always passes satisfied the gate" )
337+ return
338+ detail = details (repo , commit )["step7_machinery_enforces" ]
339+ wrongly = [s for s in ("FROZEN+false" , "NOT_FROZEN+true" , "NOT_FROZEN+false" )
340+ if f"{ reader } accepted { s } " in detail ]
341+ if len (wrongly ) != 3 :
342+ fail (check_name , f"refused, but named { wrongly } rather than all three forbidden "
343+ f"states: { detail } " )
344+ return
345+ ok (check_name ,
346+ f"a tree whose { reader } authorises anything is refused, and the refusal names every "
347+ "forbidden state it accepted: FROZEN+false, NOT_FROZEN+true, NOT_FROZEN+false" )
348+
349+
287350def control_dead_authority () -> None :
351+ _dead_authority ("mergegate-dead-authority" , HOSTQUAL , DEAD_AUTHORITY , "hostqual" )
352+
353+
354+ def control_dead_authority_eb () -> None :
355+ _dead_authority ("mergegate-dead-authority-eb" , EXECBINDING , DEAD_AUTHORITY_EB , "execbinding" )
356+
357+
358+ def control_dead_continuity () -> None :
359+ """The campaign-swap witness needs its own sensitivity proof: a postflight
360+ that refuses everything would satisfy a negative-only control."""
288361 with tempfile .TemporaryDirectory () as raw :
289- repo , commit = world (Path (raw ), mutate = DEAD_AUTHORITY , name = "deadauth " )
362+ repo , commit = world (Path (raw ), mutate = { HOSTQUAL : dead_continuity } , name = "deadcont " )
290363 if verdicts (repo , commit ).get ("step7_machinery_enforces" ) != "fail" :
291- fail ("mergegate-dead-authority" ,
292- "a bind_t0 that always returns pass satisfied the gate" )
364+ fail ("mergegate-dead-continuity" ,
365+ "a postflight that no longer compares the preflight's campaign link with "
366+ "this pass's satisfied the gate; the swap witness proves nothing" )
293367 return
294368 detail = details (repo , commit )["step7_machinery_enforces" ]
295- if "accepted" not in detail :
296- fail ("mergegate-dead-authority" , f"refused for an unrelated reason: { detail } " )
369+ if "swapped" not in detail :
370+ fail ("mergegate-dead-continuity" , f"refused for an unrelated reason: { detail } " )
371+ return
372+ ok ("mergegate-dead-continuity" ,
373+ "removing only the preflight/postflight campaign-link comparison, and leaving "
374+ "check_campaign_link intact, is caught and named as the swap becoming admissible" )
375+
376+
377+ def control_always_inadmissible () -> None :
378+ """A refusal is only evidence if acceptance was possible."""
379+ with tempfile .TemporaryDirectory () as raw :
380+ repo , commit = world (Path (raw ), mutate = {HOSTQUAL : ALWAYS_INADMISSIBLE }, name = "noadmit" )
381+ if verdicts (repo , commit ).get ("step7_machinery_enforces" ) != "fail" :
382+ fail ("mergegate-always-inadmissible" ,
383+ "a postflight that refuses every campaign satisfied the gate; the swap "
384+ "witness was reading a blanket refusal as enforcement" )
385+ return
386+ detail = details (repo , commit )["step7_machinery_enforces" ]
387+ if "unchanged campaign was inadmissible" not in detail :
388+ fail ("mergegate-always-inadmissible" , f"refused for another reason: { detail } " )
297389 return
298- ok ("mergegate-dead-authority " ,
299- "a tree whose authority check always passes is refused, and the refusal says which "
300- "state it wrongly accepted " )
390+ ok ("mergegate-always-inadmissible " ,
391+ "the unchanged campaign must survive preflight to postflight, so a tool that refuses "
392+ "everything cannot pose as one that caught the swap " )
301393
302394
303395def control_missing_machinery () -> None :
@@ -384,6 +476,9 @@ def control_inventory_complete() -> None:
384476 ("mergegate-decoy-digest" , control_decoy_digest ),
385477 ("mergegate-noop-campaign-link" , control_noop_campaign_link ),
386478 ("mergegate-dead-authority" , control_dead_authority ),
479+ ("mergegate-dead-authority-eb" , control_dead_authority_eb ),
480+ ("mergegate-dead-continuity" , control_dead_continuity ),
481+ ("mergegate-always-inadmissible" , control_always_inadmissible ),
387482 ("mergegate-missing-machinery" , control_missing_machinery ),
388483 ("mergegate-lingering-auto" , control_lingering_auto ),
389484 ("mergegate-unfrozen-t0" , control_unfrozen_t0 ),
0 commit comments