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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package with dev extras
run: pip install -e ".[dev]"

- name: Run test suite (CPU-only)
run: python -m pytest tests/ -q
29 changes: 29 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Docs

on:
push:
branches: [main]

permissions:
contents: write

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install mkdocs
run: pip install mkdocs-material

- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Deploy to gh-pages
run: mkdocs gh-deploy --force
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 A2R Lab

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A pytest plugin that lets you run GPU equivalence tests locally, sign the result

The trust model is simple: if you can push to GitHub, you can sign a receipt. Verification fetches your public keys from `github.com/{username}.keys`, exactly as SSH does.

> **Requirements:** Python 3.8+ · pytest 7.0+ · cryptography 41.0+
> **Requirements:** Python 3.11+ · pytest 7.0+ · cryptography 41.0+
> The package is not yet on PyPI — install from source with `pip install -e .` (see [Installation](#installation)).

---
Expand Down Expand Up @@ -180,11 +180,25 @@ gpu_proof_check(
| `--gpu-proof-mode` | `local` | `local` or `ci-gpu` |
| `--gpu-proof-out` | `gpu-proof.json` | Receipt output path |
| `--gpu-proof-key` | auto | SSH private key path |
| `--gpu-proof-signing-backend` | `ed25519` | `ed25519` or `none` |
| `--gpu-proof-signing-backend` | `ed25519` | `ed25519` or `none` (writes an **unsigned** receipt with `"signature": null`; the verifier rejects it unless `--allow-unsigned` is passed) |
| `--gpu-proof-required-marker` | `gpu_proof` | Marker name that flags a test for the receipt |
| `--gpu-proof-fingerprint-paths` | `src,tests` | Comma-separated paths to fingerprint |
| `--gpu-proof-github-user` | auto | GitHub username (auto-detected from git remote) |
| `--gpu-proof-policy` | — | Path to policy YAML |
| `--gpu-proof-fail-on-skip` | off | Fail if any `gpu_required` test is skipped |
| `--gpu-proof-fail-on-skip` | off | Exit non-zero and write no receipt if any marked or `gpu_required` test is skipped |

Defaults for most of these can also be set in your project's `pyproject.toml`
under `[tool.gpu_proof]` (CLI flags take precedence):

```toml
[tool.gpu_proof]
mode = "local"
output = "gpu-proof.json"
fingerprint_paths = ["src", "tests"]
required_marker = "gpu_proof"
max_age_days = 30 # used by the verifier
require_gpu = false # used by the verifier (see below)
```

---

Expand All @@ -211,9 +225,16 @@ python -m pytest_gpu_proof verify --receipt gpu-proof.json
1. **Signature** — fetches `github.com/{signer}.keys`, verifies Ed25519/ECDSA/RSA signature
2. **Fingerprint** — recomputes SHA-256 digest of `src/` and `tests/`, compares to receipt
3. **Commit SHA** — compares receipt commit SHA to current HEAD
4. **Test outcomes** — all tests recorded in the receipt must have passed
4. **Test outcomes** — all tests recorded in the receipt must have passed; skipped marked tests fail verification unless `--allow-skipped` is passed
5. **Freshness** — receipt must be younger than `max_age_days` (default: 30)
6. **Dirty policy** — configurable via policy file
7. **GPU info** (optional) — with `--require-gpu` (or `require_gpu = true` in `[tool.gpu_proof]`), the receipt's `environment.gpu_info` must be present

Additional flags:

- `--allow-unsigned` — accept receipts with `"signature": null` (produced by `--gpu-proof-signing-backend=none`). This disables the entire trust story; the verifier prints a loud warning.
- `--allow-skipped` — accept receipts that contain skipped marked tests.
- `--require-gpu` — reject receipts whose `environment.gpu_info` is null/absent. This is **modest hardening, not proof**: `gpu_info` is self-reported by the recording machine, so it only guards against accidentally signing on a GPU-less box, not against a dishonest signer.

---

Expand Down Expand Up @@ -291,6 +312,13 @@ A signed receipt proves that **an accepted signer attested to a specific test ru

This is appropriate for **team workflows where the signer is a trusted team member** and the goal is to avoid paying for GPU CI on every merge, not to provide adversarial security guarantees.

The optional `--require-gpu` verifier flag adds a modest extra check — the receipt must contain self-reported `environment.gpu_info` — but this is hardening against mistakes (signing on a GPU-less machine), not proof of GPU execution.

**SSH key support caveats:**

- The plugin signs the raw receipt bytes with the key loaded from disk — it does **not** produce SSHSIG-format signatures, so `ssh-keygen -Y verify` cannot validate receipts. Use `gpu-proof verify` instead.
- Keys that live only in an SSH agent, and FIDO/hardware-backed `sk-ssh-ed25519`/`sk-ecdsa` keys, are **not** supported: signing needs direct access to a private key file readable by the `cryptography` library.

See [docs/security_model.md](docs/security_model.md) for a full discussion.

---
Expand Down Expand Up @@ -330,7 +358,7 @@ Tests are CPU-only. No GPU or network access required.

## Compatibility

- Python 3.8+
- Python 3.11+
- pytest 7.0+
- `cryptography` 41.0+
- `pytest-xdist` is **not** supported in v1 (parallel workers would write conflicting receipts)
8 changes: 8 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Roadmap

- **PyPI publishing (own session):** register the `pytest-gpu-proof` name, trusted
publishing via GitHub Actions (OIDC), README as long_description, versioning +
release discipline, CHANGELOG. Prereqs (CI, LICENSE, docs site, 3.11 floor)
landed on `fixes-and-ci` 2026-07-02.
- SSHSIG-compatible signing (`ssh-keygen -Y verify` interop; agent-only + FIDO keys).
- CI-issued nonce / challenge mode for stronger replay protection.
33 changes: 33 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# pytest-gpu-proof

A pytest plugin that lets you run GPU equivalence tests locally, sign the results with your existing SSH key, and have GitHub Actions verify the receipt — **without re-running the GPU tests in CI**.

The trust model is simple: if you can push to GitHub, you can sign a receipt. Verification fetches your public keys from `github.com/{username}.keys`, exactly as SSH does.

> **Requirements:** Python 3.11+ · pytest 7.0+ · cryptography 41.0+
> The package is not yet on PyPI — install from source with `pip install -e .` or `pip install "git+https://github.com/A2R-Lab/pytest-gpu-proof.git"`.

## How it works

```
Local machine (GPU) GitHub Actions (CPU only)
───────────────────────────── ──────────────────────────────────────
pytest --gpu-proof-enable → → gpu-proof verify --receipt gpu-proof.json
runs your GPU tests fetches your public keys from
computes code fingerprint github.com/{you}.keys
signs receipt with SSH key verifies signature + fingerprint
writes gpu-proof.json exits 0 (pass) or 1 (fail)
```

**Zero new key management.** The plugin uses the SSH key you already have in `~/.ssh/` (the same one you use to push to GitHub). Your public key is already on GitHub. The verifier reads it from there.

## Documentation

- [Quickstart](quickstart.md) — local CUDA proof, GitHub verification, end to end
- [Local Mode](local_mode.md) — the default workflow: sign locally, verify in CI
- [CI-GPU Mode](ci_gpu_mode.md) — run the tests on a GitHub-hosted GPU runner instead
- [Architecture](architecture.md) — package layout and data flow
- [Security Model](security_model.md) — what a receipt does and does not prove
- [Landscape](landscape.md) — why this tool exists rather than an existing one

See the [README on GitHub](https://github.com/A2R-Lab/pytest-gpu-proof#readme) for the full CLI reference, receipt format, and examples.
2 changes: 1 addition & 1 deletion examples/github_gpu_runner/.github/workflows/gpu-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:

- name: Install dependencies
run: |
pip install pytest-gpu-proof
pip install "git+https://github.com/A2R-Lab/pytest-gpu-proof.git"
# Install your project's GPU dependencies here, e.g.:
# pip install torch --index-url https://download.pytorch.org/whl/cu121

Expand Down
2 changes: 1 addition & 1 deletion examples/local_receipt_verify/.github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
python-version: "3.11"

- name: Install pytest-gpu-proof
run: pip install pytest-gpu-proof
run: pip install "git+https://github.com/A2R-Lab/pytest-gpu-proof.git"

- name: Verify receipt
# The receipt (gpu-proof.json) is committed to the repo.
Expand Down
8 changes: 4 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
set -e

# Check Python version — requires 3.8+
PY_TOO_OLD=$(python3 -c "import sys; print(sys.version_info < (3, 8))" 2>/dev/null || echo "True")
# Check Python version — requires 3.11+
PY_TOO_OLD=$(python3 -c "import sys; print(sys.version_info < (3, 11))" 2>/dev/null || echo "True")
if [ "$PY_TOO_OLD" = "True" ]; then
echo "ERROR: pytest-gpu-proof requires Python 3.8 or later."
echo "ERROR: pytest-gpu-proof requires Python 3.11 or later."
echo " You are running: $(python3 --version 2>&1)"
echo " Please switch to Python 3.8+ before installing."
echo " Please switch to Python 3.11+ before installing."
exit 1
fi

Expand Down
38 changes: 38 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
site_name: pytest-gpu-proof
site_description: >-
pytest plugin for GPU equivalence testing with signed receipts verified via
GitHub SSH keys
repo_url: https://github.com/A2R-Lab/pytest-gpu-proof
repo_name: A2R-Lab/pytest-gpu-proof

theme:
name: material
palette:
- scheme: default
primary: indigo
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: indigo
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- navigation.sections
- content.code.copy

markdown_extensions:
- admonition
- pymdownx.superfences
- pymdownx.highlight
- tables

nav:
- Home: index.md
- Quickstart: quickstart.md
- Local Mode: local_mode.md
- CI-GPU Mode: ci_gpu_mode.md
- Architecture: architecture.md
- Security Model: security_model.md
- Landscape: landscape.md
13 changes: 9 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "pytest-gpu-proof"
version = "0.1.0"
description = "pytest plugin for GPU equivalence testing with signed receipts verified via GitHub SSH keys"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.11"
license = { text = "MIT" }
keywords = ["pytest", "gpu", "cuda", "testing", "signing", "attestation"]
classifiers = [
Expand Down Expand Up @@ -42,8 +42,13 @@ packages = ["src/pytest_gpu_proof"]
testpaths = ["tests"]

[tool.gpu_proof]
# Default plugin configuration — can override here or in pytest.ini
# Default plugin/verifier configuration, read from the rootdir pyproject.toml.
# CLI flags take precedence over values set here.
# mode = "local"
# fingerprint_paths = ["src", "tests"]
# output = "gpu-proof.json"
# max_age_days = 30
# signing_backend = "ed25519"
# required_marker = "gpu_proof"
# fail_on_skip = false
# fingerprint_paths = ["src", "tests"]
# max_age_days = 30 # verifier freshness default
# require_gpu = false # verifier: require environment.gpu_info in the receipt
24 changes: 24 additions & 0 deletions src/pytest_gpu_proof/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,27 @@ def main():
metavar="N",
help="Override max receipt age in days",
)
vp.add_argument(
"--allow-unsigned",
action="store_true",
default=False,
help="Accept UNSIGNED receipts (signature: null). Unsigned receipts prove "
"nothing about who ran the tests — use only if you accept that.",
)
vp.add_argument(
"--allow-skipped",
action="store_true",
default=False,
help="Accept receipts that contain skipped marked tests (default: reject)",
)
vp.add_argument(
"--require-gpu",
action="store_true",
default=None,
help="Fail verification if the receipt's environment.gpu_info is null/absent "
"(modest hardening, not proof of GPU execution). Can also be set via "
"require_gpu = true in [tool.gpu_proof].",
)

args = parser.parse_args()

Expand All @@ -52,6 +73,9 @@ def main():
repo_root=args.repo,
github_user_override=args.github_user,
max_age_days=args.max_age_days,
allow_unsigned=args.allow_unsigned,
allow_skipped=args.allow_skipped,
require_gpu=args.require_gpu,
)
sys.exit(0 if ok else 1)

Expand Down
Loading
Loading