Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,25 @@ jobs:

- name: test
run: make test INSTALL_EXTRA=test

test-offline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false

- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
with:
python-version: 3.13
cache: "pip"
cache-dependency-path: pyproject.toml
allow-prereleases: true

- name: install firejail
run: sudo apt-get install -y firejail

- name: run tests offline
run: |
make dev INSTALL_EXTRA=test
firejail --noprofile --net=none --env=TEST_OFFLINE=1 make test-nocoverage
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- The `Attestation.verify(...)` API has been changed to accept an `offline`
parameter that, when True, disables TUF refreshes.
- The CLI `verify` commands now also accept an `--offline` flag that disables
TUF refreshes. Additionally, when used with the `verify pypi` subcommand, the
`--offline` flag enforces that the distribution and provenance file arguments
must be local file paths.

## [0.0.22]

### Changed
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ test tests: $(VENV)/pyvenv.cfg
pytest --cov=$(PY_IMPORT) $(T) $(TEST_ARGS) && \
python -m coverage report -m $(COV_ARGS)

.PHONY: test-nocoverage
test-nocoverage: $(VENV)/pyvenv.cfg
. $(VENV_BIN)/activate && \
pytest $(T) $(TEST_ARGS)

.PHONY: doc
doc: $(VENV)/pyvenv.cfg
. $(VENV_BIN)/activate && \
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ print(attestation.model_dump_json())
# Verify an attestation against a Python artifact
attestation_path = Path("test_package-0.0.1-py3-none-any.whl.attestation")
attestation = Attestation.model_validate_json(attestation_path.read_bytes())
verifier = Verifier.production()
identity = policy.Identity(identity="example@gmail.com", issuer="https://accounts.google.com")
attestation.verify(identity=identity, dist=dist)
```
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools"]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
Expand Down
26 changes: 22 additions & 4 deletions src/pypi_attestations/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ def _parser() -> argparse.ArgumentParser:
help="Use the staging environment",
)

verify_attestation_command.add_argument(
"--offline",
action="store_true",
default=False,
help="Disable TUF refresh",
)

verify_attestation_command.add_argument(
"files",
metavar="FILE",
Expand Down Expand Up @@ -157,6 +164,13 @@ def _parser() -> argparse.ArgumentParser:
help="Use the staging environment",
)

verify_pypi_command.add_argument(
"--offline",
action="store_true",
default=False,
help="Force use of local files and disable TUF refresh",
)

verify_pypi_command.add_argument(
"--provenance-file",
type=Path,
Expand Down Expand Up @@ -239,7 +253,7 @@ def _download_file(url: str, dest: Path) -> None:
_die(f"Error downloading file: {e}")


def _get_distribution_from_arg(arg: str) -> Distribution:
def _get_distribution_from_arg(arg: str, offline: bool) -> Distribution:
"""Parse the artifact argument for the `verify pypi` subcommand.

The argument can be:
Expand All @@ -248,6 +262,8 @@ def _get_distribution_from_arg(arg: str) -> Distribution:
- A path to a local file
"""
if arg.startswith("pypi:") or arg.startswith("https://"):
if offline:
_die("The '--offline' option can only be used with local files")
pypi_url = _get_direct_url_from_arg(arg)
dist_filename = pypi_url.path.split("/")[-1]
with TemporaryDirectory() as temp_dir:
Expand Down Expand Up @@ -497,7 +513,7 @@ def _verify_attestation(args: argparse.Namespace) -> None:
_die(f"Invalid Python package distribution: {e}")

try:
attestation.verify(pol, dist, staging=args.staging)
attestation.verify(pol, dist, staging=args.staging, offline=args.offline)
except VerificationError as verification_error:
_die(f"Verification failed for {file_path}: {verification_error}")

Expand All @@ -512,9 +528,11 @@ def _verify_pypi(args: argparse.Namespace) -> None:
from PyPI if not provided), and against the repository URL passed by the user
as a CLI argument.
"""
dist = _get_distribution_from_arg(args.distribution_file)
dist = _get_distribution_from_arg(args.distribution_file, offline=args.offline)

if args.provenance_file is None:
if args.offline:
_die("The '--offline' option can only be used with local files")
provenance = _get_provenance_from_pypi(dist)
else:
if not args.provenance_file.exists():
Expand All @@ -530,7 +548,7 @@ def _verify_pypi(args: argparse.Namespace) -> None:
_check_repository_identity(expected_repository_url=args.repository, publisher=publisher)
policy = publisher._as_policy() # noqa: SLF001.
for attestation in attestation_bundle.attestations:
attestation.verify(policy, dist, staging=args.staging)
attestation.verify(policy, dist, staging=args.staging, offline=args.offline)
except VerificationError as verification_error:
_die(f"Verification failed for {dist.name}: {verification_error}")

Expand Down
8 changes: 6 additions & 2 deletions src/pypi_attestations/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ def verify(
dist: Distribution,
*,
staging: bool = False,
offline: bool = False,
) -> tuple[str, Optional[dict[str, Any]]]:
"""Verify against an existing Python distribution.

Expand All @@ -241,6 +242,9 @@ def verify(
`staging` parameter can be toggled to enable the staging verifier
instead.

If `offline` is `True`, the verifier will not attempt to refresh the
TUF repository.

On failure, raises an appropriate subclass of `AttestationError`.
"""
# NOTE: Can't do `isinstance` with `Publisher` since it's
Expand All @@ -253,9 +257,9 @@ def verify(
policy = identity

if staging:
verifier = Verifier.staging()
verifier = Verifier.staging(offline=offline)
else:
verifier = Verifier.production()
verifier = Verifier.production(offline=offline)

bundle = self.to_bundle()
try:
Expand Down
48 changes: 0 additions & 48 deletions test/assets/pypi_attestations-0.0.16.tar.gz.attestation

This file was deleted.

Loading