Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,19 +207,31 @@ def check_sigstore_bundle(
expected_issuer: Optional[str] = None,
) -> CheckResult:
# Resolve symlinks in the base directory up front
model_dir = Path(model_dir).resolve()
root = Path(model_dir).resolve()

signature = manifest.get("signature") or manifest.get("sigstore_bundle")
signature_path = model_dir / signature if signature else _first_existing(
model_dir, ["model.sig", "model.bundle", "sigstore.bundle"]
)

if signature:
signature_path = (root / signature).resolve()
if not signature_path.is_relative_to(root):
raise ValueError(
f"Manifest signature artifact {signature!r} resolves outside model directory {root}"
)
else:
signature_path = _first_existing(
root, ["model.sig", "model.bundle", "sigstore.bundle"]
)
if signature_path is not None:
signature_path = signature_path.resolve()
if not signature_path.is_relative_to(root):
raise ValueError(
f"Signature artifact {signature_path!r} resolves outside model directory {root}"
)

if signature_path is None or not signature_path.exists():
status = SKIP if allow_unsigned else FAIL
return CheckResult("sigstore_bundle", status, "missing Sigstore/model-signing bundle")

# Resolve symlinks for the specific signature file path
signature_path = signature_path.resolve()
model_dir = root

identity = expected_identity or manifest.get("sigstore_identity")
provider = expected_issuer or manifest.get("sigstore_identity_provider")
Expand Down
109 changes: 109 additions & 0 deletions tests/test_sigstore_bundle_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

SRC = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SRC))

from verifier import PASS, check_sigstore_bundle # noqa: E402


class SigstoreBundlePathTests(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
root = Path(self._tmp.name)
self.model_dir = root / "model"
self.model_dir.mkdir()
(self.model_dir / "model.sig").write_bytes(b"sig-bytes")
self.outside = root / "secret.sig"
self.outside.write_bytes(b"outside-sig-bytes")

def tearDown(self):
self._tmp.cleanup()

def test_normal_in_directory_signature_verifies(self):
manifest = {
"signature": "model.sig",
"sigstore_identity": "signer@example.com",
"sigstore_identity_provider": "https://accounts.example.com",
}
with patch("verifier.importlib.util.find_spec", return_value=True):
with patch("verifier.subprocess.run") as mock_run:
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = "ok"
mock_run.return_value.stderr = ""
result = check_sigstore_bundle(
self.model_dir,
manifest,
expected_identity="signer@example.com",
expected_issuer="https://accounts.example.com",
)

self.assertEqual(result.status, PASS)
command = mock_run.call_args.args[0]
sig_index = command.index("--signature")
self.assertEqual(command[sig_index + 1], str((self.model_dir / "model.sig").resolve()))

def test_parent_relative_name_is_rejected(self):
manifest = {
"signature": "../secret.sig",
"sigstore_identity": "signer@example.com",
"sigstore_identity_provider": "https://accounts.example.com",
}
with self.assertRaises(ValueError):
check_sigstore_bundle(self.model_dir, manifest)

def test_absolute_path_is_rejected(self):
manifest = {
"signature": str(self.outside),
"sigstore_identity": "signer@example.com",
"sigstore_identity_provider": "https://accounts.example.com",
}
with self.assertRaises(ValueError):
check_sigstore_bundle(self.model_dir, manifest)

def test_sigstore_bundle_key_is_checked_too(self):
manifest = {
"sigstore_bundle": "../secret.sig",
"sigstore_identity": "signer@example.com",
"sigstore_identity_provider": "https://accounts.example.com",
}
with self.assertRaises(ValueError):
check_sigstore_bundle(self.model_dir, manifest)

@unittest.skipUnless(hasattr(os, "symlink"), "symlinks unsupported")
def test_symlink_escaping_model_dir_is_rejected(self):
link = self.model_dir / "link.sig"
try:
link.symlink_to(self.outside)
except OSError:
self.skipTest("symlinks require elevated privileges or Developer Mode on Windows")
manifest = {
"signature": "link.sig",
"sigstore_identity": "signer@example.com",
"sigstore_identity_provider": "https://accounts.example.com",
}
with self.assertRaises(ValueError):
check_sigstore_bundle(self.model_dir, manifest)

@unittest.skipUnless(hasattr(os, "symlink"), "symlinks unsupported")
def test_discovered_symlink_escaping_model_dir_is_rejected(self):
(self.model_dir / "model.sig").unlink()
link = self.model_dir / "model.sig"
try:
link.symlink_to(self.outside)
except OSError:
self.skipTest("symlinks require elevated privileges or Developer Mode on Windows")
manifest = {
"sigstore_identity": "signer@example.com",
"sigstore_identity_provider": "https://accounts.example.com",
}
with self.assertRaises(ValueError):
check_sigstore_bundle(self.model_dir, manifest)


if __name__ == "__main__":
unittest.main()
Loading