Skip to content

Commit 2f39de7

Browse files
committed
fix(evidence): reject unencodable lock lane names (R30)
1 parent 3bac126 commit 2f39de7

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

‎scripts/verify_round_receipt.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"tiers": {"pr-smoke": {"lanes": [...], "quorum": {...}, "budgets": {...}},
5757
"nightly": {...}, "release": {...}}}
5858
59-
Tier objects carry a non-empty ``lanes`` list of non-empty strings plus
59+
Tier objects carry a non-empty ``lanes`` list of non-empty UTF-8-encodable strings plus
6060
``quorum``/``budgets`` objects (presence-checked only — shapes and
6161
thresholds are TBD-E5). Staged
6262
Windows/macOS lanes (``win-softhsm2``, ``win-bouncyhsm``,
@@ -176,6 +176,12 @@ def _validate_lock_tier(tier: object, spec: Any) -> list[str]:
176176
or any(not isinstance(name, str) or not name for name in lanes)
177177
):
178178
errors.append(f"{prefix}.lanes: must be a non-empty list of non-empty strings")
179+
elif not _is_utf8_encodable("".join(lanes)):
180+
# R30: an unencodable lock lane matching an unencodable receipt
181+
# lane would pass the roster (rule 5) and lane entries (rule 6)
182+
# while the bundle check (rule 7) skips -> exit 0 on a bogus
183+
# bundle. Reject it here as malformed lock schema (exit 2).
184+
errors.append(f"{prefix}.lanes: lane names must be UTF-8-encodable")
179185
for field in ("quorum", "budgets"):
180186
shape = spec.get(field)
181187
if shape is None:

‎tests/test_round_receipt_verify.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,38 @@ def test_unknown_top_level_keys_ignored(tmp_path: Path) -> None:
480480
_write(receipt, payload)
481481
result = _run(receipt, lock)
482482
assert result.returncode == EXIT_VALID, result.stderr
483+
484+
485+
# --- Fix round 2 (R30): lock lane names must be UTF-8-encodable.
486+
#
487+
# Without this, an unencodable lock lane matching an unencodable receipt
488+
# lane passes the roster (rule 5) and lane entries (rule 6) while the
489+
# bundle check (rule 7) skips -> exit 0 with a bogus bundle.
490+
491+
492+
def test_unencodable_lock_lane_name_rejects_lock(tmp_path: Path) -> None:
493+
lane = f"{chr(0xD800)}lane"
494+
bad_lock = make_lock()
495+
bad_lock["tiers"]["pr-smoke"]["lanes"] = [lane]
496+
lock = _write(tmp_path / "lock.json", bad_lock)
497+
payload = make_receipt()
498+
payload["lanes"] = {lane: {"verdict": "pass", "admitted": True, "evidence_digest": _hex64("e")}}
499+
payload["bundle_identity"] = "0" * 64
500+
receipt = _write(tmp_path / "receipt.json", payload)
501+
result = _run(receipt, lock)
502+
assert result.returncode == EXIT_USAGE, result.stderr
503+
assert "tiers.pr-smoke.lanes" in result.stderr
504+
505+
506+
def test_checked_in_lock_accepts_valid_receipt(tmp_path: Path) -> None:
507+
# Control: the committed initial lock plus a matching valid receipt
508+
# still verifies (guards R30 false-positives on normal lane names).
509+
lock = REPO_ROOT / ".github" / "provider-round.lock.json"
510+
lock_payload = json.loads(lock.read_text(encoding="utf-8"))
511+
payload = make_receipt()
512+
payload["p11lab_sha"] = lock_payload["p11lab_ref"]
513+
payload["evaluator_sha"] = lock_payload["evaluator_fw_ref"]
514+
payload["baseline_fw_sha"] = lock_payload["baseline_fw_sha"]
515+
receipt = _write(tmp_path / "receipt.json", payload)
516+
result = _run(receipt, lock)
517+
assert result.returncode == EXIT_VALID, result.stderr

0 commit comments

Comments
 (0)