Skip to content

Commit aa72d60

Browse files
committed
Close release guard bypasses
1 parent bb195e6 commit aa72d60

2 files changed

Lines changed: 770 additions & 42 deletions

File tree

tests/test_release_guard.py

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from __future__ import annotations
22

3+
import base64
34
import hashlib
45
import importlib.util
56
import io
67
import json
78
import os
89
import subprocess
9-
import sys
1010
import tarfile
1111
import tempfile
1212
import unittest
@@ -126,7 +126,7 @@ def probe(
126126
}
127127
return {
128128
"python": os.path.abspath(runtime_python),
129-
"prefix": str(Path(sys.prefix).resolve()),
129+
"prefix": str(runtime_dirs[0].parent.resolve()),
130130
"imports": imports,
131131
"versions": {name: versions[name] for name in distributions},
132132
}
@@ -154,7 +154,7 @@ def _attest_args(
154154
distribution="demo_distribution",
155155
runtime_package_dir=runtime_dirs,
156156
runtime_module=modules,
157-
runtime_python=Path(sys.executable),
157+
runtime_python=runtime_dirs[0].parent / "python",
158158
dependency=["demo-dependency"],
159159
runtime_label="test-runtime",
160160
signing_key_file=signing_key_file,
@@ -203,6 +203,7 @@ def _verify_signature(
203203
signature: Path,
204204
allowed_signers: Path,
205205
signer: str,
206+
expected_fingerprint: object,
206207
) -> None:
207208
if not allowed_signers.is_file() or signer != "test-runtime":
208209
raise release_guard.GuardError("deployment manifest signature is invalid")
@@ -258,7 +259,7 @@ def test_multi_package_signed_attestation_rechecks_current_runtime(self) -> None
258259
):
259260
attested = release_guard.command_attest(attest_args)
260261
verified = release_guard.command_verify(verify_args)
261-
self.assertEqual(attested["schema_version"], 2)
262+
self.assertEqual(attested["schema_version"], 3)
262263
self.assertEqual(
263264
verified["packages"], ["demo_pkg", "demo_server"]
264265
)
@@ -310,6 +311,84 @@ def test_manifest_edit_is_rejected_and_remote_mode_requires_fresh_signature(self
310311
), self.assertRaisesRegex(release_guard.GuardError, "signature is invalid"):
311312
release_guard.command_verify(verify_args)
312313

314+
def test_remote_mode_still_compares_signed_runtime_hashes_to_wheel(self) -> None:
315+
with tempfile.TemporaryDirectory() as temporary:
316+
root = Path(temporary)
317+
repo = self._init_repo(root)
318+
wheel, sdist = self._write_artifacts(root)
319+
runtime_dirs, modules = self._runtime(root)
320+
key_file = root / "signing.key"
321+
key_file.write_text("private test key", encoding="ascii")
322+
manifest = root / "deployment.json"
323+
with mock.patch.object(
324+
release_guard,
325+
"_runtime_probe",
326+
side_effect=self._probe(runtime_dirs, modules),
327+
), mock.patch.object(
328+
release_guard, "_sign_manifest_file", side_effect=self._sign
329+
):
330+
release_guard.command_attest(
331+
self._attest_args(
332+
repo, wheel, sdist, runtime_dirs, modules, key_file, manifest
333+
)
334+
)
335+
336+
payload = json.loads(manifest.read_text(encoding="utf-8"))
337+
package_files = payload["runtime"]["package_files"]["demo_server"]
338+
package_files[next(iter(package_files))] = "0" * 64
339+
manifest.write_text(json.dumps(payload), encoding="utf-8")
340+
self._sign(manifest, key_file)
341+
verify_args = self._verify_args(
342+
repo,
343+
wheel,
344+
sdist,
345+
key_file,
346+
manifest,
347+
allow_remote_attestation=True,
348+
)
349+
with mock.patch.object(
350+
release_guard,
351+
"_verify_manifest_signature",
352+
side_effect=self._verify_signature,
353+
), self.assertRaisesRegex(
354+
release_guard.GuardError, "deployed runtime package demo_server and wheel"
355+
):
356+
release_guard.command_verify(verify_args)
357+
358+
def test_component_profile_and_source_containment_are_mandatory(self) -> None:
359+
profile = release_guard.COMPONENT_PROFILES["RealtimeSTT"]
360+
self.assertEqual(
361+
tuple(item[0] for item in profile["packages"]),
362+
("RealtimeSTT", "RealtimeSTT_server", "example_fastapi_server"),
363+
)
364+
with tempfile.TemporaryDirectory() as temporary:
365+
repo = Path(temporary).resolve()
366+
with self.assertRaisesRegex(release_guard.GuardError, "stay inside"):
367+
release_guard._repo_member(repo, "../outside", "source package")
368+
369+
def test_component_signer_key_fingerprint_is_pinned(self) -> None:
370+
with tempfile.TemporaryDirectory() as temporary:
371+
root = Path(temporary)
372+
manifest = root / "deployment.json"
373+
signature = root / "deployment.json.sig"
374+
allowed = root / "allowed_signers"
375+
manifest.write_text("{}", encoding="utf-8")
376+
signature.write_text("invalid", encoding="ascii")
377+
encoded = base64.b64encode(b"different-key").decode("ascii")
378+
allowed.write_text(
379+
f"linux-services ssh-ed25519 {encoded} test\n", encoding="utf-8"
380+
)
381+
with self.assertRaisesRegex(release_guard.GuardError, "trust profile"):
382+
release_guard._verify_manifest_signature(
383+
manifest,
384+
signature,
385+
allowed,
386+
"linux-services",
387+
release_guard.COMPONENT_PROFILES["RealtimeTTS"][
388+
"signer_fingerprint"
389+
],
390+
)
391+
313392
def test_remote_branch_and_tag_must_both_equal_release_head(self) -> None:
314393
with tempfile.TemporaryDirectory() as temporary:
315394
root = Path(temporary)
@@ -337,13 +416,26 @@ def test_remote_branch_and_tag_must_both_equal_release_head(self) -> None:
337416
text=True,
338417
check=True,
339418
).stdout.strip()
340-
result = release_guard._assert_remote_release_refs(
341-
repo, "origin", "main", "v1.0", head, "1.0"
342-
)
419+
profile = {
420+
"remote_repository": "example.invalid/demo",
421+
"remote_branch": "main",
422+
}
423+
with mock.patch.object(
424+
release_guard,
425+
"_canonical_remote_url",
426+
return_value="example.invalid/demo",
427+
):
428+
result = release_guard._assert_remote_release_refs(
429+
repo, "origin", "main", "v1.0", head, "1.0", profile
430+
)
343431
self.assertEqual(result["branch"], "refs/heads/main")
344-
with self.assertRaisesRegex(release_guard.GuardError, "does not match"):
432+
with mock.patch.object(
433+
release_guard,
434+
"_canonical_remote_url",
435+
return_value="example.invalid/demo",
436+
), self.assertRaisesRegex(release_guard.GuardError, "does not match"):
345437
release_guard._assert_remote_release_refs(
346-
repo, "origin", "main", "v2.0", head, "1.0"
438+
repo, "origin", "main", "v2.0", head, "1.0", profile
347439
)
348440

349441
def test_package_index_confirmation_compares_exact_hashes(self) -> None:
@@ -376,6 +468,33 @@ def test_package_index_confirmation_compares_exact_hashes(self) -> None:
376468
)
377469
self.assertEqual(confirmed, expected)
378470

471+
response["urls"].append(
472+
{
473+
"filename": "unexpected.zip",
474+
"digests": {"sha256": "123"},
475+
"size": 9,
476+
}
477+
)
478+
encoded_with_extra = json.dumps(response).encode()
479+
with mock.patch.object(
480+
release_guard.urllib.request,
481+
"urlopen",
482+
return_value=io.BytesIO(encoded_with_extra),
483+
), mock.patch.object(
484+
release_guard.time, "monotonic", side_effect=[0.0, 2.0]
485+
), mock.patch.object(
486+
release_guard.time, "sleep"
487+
), self.assertRaisesRegex(
488+
release_guard.GuardError, "artifact set is not exact"
489+
):
490+
release_guard._confirm_published(
491+
"pypi",
492+
{"name": "demo", "version": "1.0"},
493+
expected,
494+
"",
495+
1,
496+
)
497+
379498

380499
if __name__ == "__main__":
381500
unittest.main()

0 commit comments

Comments
 (0)