diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..922087d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +# .github/workflows/ci.yml +name: CI +on: + push: + branches: [feature/initial-design] + pull_request: + branches: [main] + +jobs: + ci: + name: "pyCosign CI Pipeline | (Python ${{ matrix.python-version }})" + strategy: + matrix: + python-version: ["3.10"] + poetry-version: ["2.1.3"] + os: [ubuntu-22.04] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install poetry + uses: abatilo/actions-poetry@v4 + - name: Setup a local virtual environment (if no poetry.toml file) + run: | + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + - uses: actions/cache@v3 + name: Define a cache for the virtual environment based on the dependencies lock file + with: + path: ./.venv + key: venv-${{ hashFiles('poetry.lock') }} + - name: Install the project dependencies + run: poetry install + - name: Run Black in check mode + run: poetry run black --check --verbose pycosign + - name: Run Isort in check mode + run: poetry run isort --verbose pycosign \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3a919b3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3aab2d7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,149 @@ +# Contributing to pyCosign + +First off, **thank you for taking the time to contribute!** +The following guidelines help keep the project consistent and maintainable. + +--- + +## Table of Contents +1. [Code of Conduct](#code-of-conduct) +2. [Getting Started](#getting-started) +3. [Branching & Workflow](#branching--workflow) +4. [Commit Conventions](#commit-conventions) +5. [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) +6. [Coding Style](#coding-style) +7. [Testing](#testing) +8. [Pull-Request Checklist](#pull-request-checklist) +9. [Release Process](#release-process) + +--- + +## Code of Conduct +We follow the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) v2.1. +Please be respectful and inclusive in all interactions. + +--- + +### Getting Started (Poetry) + +```bash +# clone +git clone https://github.com/your-org/pycosign.git +cd pycosign + +# install Poetry (if not already) +curl -sSL https://install.python-poetry.org | python3 - + +# create & activate venv + install deps +poetry install --with dev + +# drop into project shell +poetry shell + +# install git hooks +pre-commit install + +*Poetry automatically selects Python ≥ 3.10 on your system or the one specified in pyproject.toml. All dev tools—ruff, black, mypy, pytest, invoke—are declared under [tool.poetry.group.dev].* + +### Prerequisites + +| Tool | Required Version | Notes | +|------------------|------------------|-------| +| **Poetry** | 1.8 or newer | Dependency & virtual-env manager | +| **Python** | 3.10 – 3.12 | Poetry will auto-select within this range | +| **cosign CLI** | ≥ 2.2 | `brew install sigstore/tap/cosign` or download release binary | +| **plantuml** | _optional_ | Render `.puml` diagrams locally (`brew install plantuml`) | +| **Docker** | _optional_ | Quick PlantUML preview, OCI registry tests | + +## Branching & Workflow + +* **`main`** — always releasable; tagged releases are cut from here. +* **Feature branches** — name `feature/` (e.g., `feature/registry-push`). +* **Bug-fix branches** — name `bugfix/` (e.g., `bugfix/42-null-sig-path`). +* **Documentation-only branches** — name `docs/`. + +Workflow: + +1. Create an **Issue** if one doesn’t exist. +2. Branch from `main` with the naming rules above. +3. Work locally in a Poetry shell: + ```bash + poetry shell + git checkout -b feature/my-cool-thing + ``` +4. Keep commits small, descriptive, and signed (`git commit -s`). +5. Push to your fork / to the repo: `git push -u origin feature/my-cool-thing`. +6. Open a **pull request** against `main`. GitHub Actions will run: + * lint → test → type-check → coverage → packaging checks. +7. Address review comments; squash-merge once approved. +> **Tip:** Use `git rebase -i main` to keep history clean before opening the PR. + +## Commit Conventions + +We follow **Conventional Commits**: +```bash +feature(signer): add --push flag for sign-blob +bugfix(verifier): handle missing Rekor bundle gracefully +docs: update README diagrams +``` + +## Developer Certificate of Origin (DCO) + +By submitting a PR you certify you have the right to submit the work under the project’s [MIT license](LICENSE). +Sign your commits: + +```bash +git commit -s -m "bugfix: correct typo" +``` +The `-s` flag adds the required `Signed-off-by` line. + +## Coding Style + +* **ruff** for linting (`black`, `isort`, `flake8` rules) +* Docstrings follow **Google style**. + +Run all checks: + +* **black** for formatting (PEP 8 compliant). +```bash +poetry run black --verbose pycosign +``` + +* **isort** your imports, so you don't have to. +```bash +poetry run isort --verbose pycosign +``` + +## Testing + +* **pytest** with **pytest-asyncio** for async routines. +* 90%+ coverage target. + +```bash +pytest -q +``` +CI will fail if coverage drops below the threshold. + +## Pull-Request Checklist + +Before requesting a review, make sure you can check **every** box below: + +- [ ] **Issue linked** (e.g., “Closes #123”) or marked N/A for trivial docs fixes. +- [ ] **Unit / integration tests** added or updated; `poetry run pytest` passes locally. +- [ ] **Docs updated** (`README`, `docs/`, or inline docstrings) if behavior changed. +- [ ] **Changelog entry** added under `## [Unreleased]` in `CHANGELOG.md`. +- [ ] `pre-commit run --all-files` passes (ruff, black, mypy, etc.). +- [ ] All commits are **signed** (`git commit -s`) to satisfy the DCO. +- [ ] PR title follows **Conventional Commits** (e.g., `feat(verifier): add policy flag`). +- [ ] CI status is green (GitHub Actions). + +> 🔒 PRs without a Signed-off-by line will be blocked by the DCO GitHub check. + +## Release Process + +1. Merge all features into `main`. +2. Bump version with `bumpver update --push-tag`. +3. GitHub Action builds sdist & wheels and uploads to PyPI. +4. Draft release notes in `CHANGELOG.md`. + +Happy hacking! 💙 diff --git a/LICENSE b/LICENSE index 2428e66..4412de6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 interwebshack +Copyright (c) 2025 **pyCosign contributors** Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ee1455c..cf67b9f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,100 @@ # pyCosign -Python Wrapper for Sigstor Cosign + +> A Pythonic façade around the **Sigstore `cosign` CLI** — sign, attest, and verify artifacts from pure Python or the command line. + +[![Build](https://img.shields.io/github/actions/workflow/status/interwebshack/pycosign/ci.yml?branch=main)](https://github.com/interwebshack/pycosign/actions) +[![License](https://img.shields.io/github/license/interwebshack/pycosign)](LICENSE) + +--- + +## Table of Contents +1. [Motivation](#motivation) +2. [Features](#features) +3. [Quick Start](#quick-start) +4. [CLI Examples](#cli-examples) +5. [Library Usage](#library-usage) +6. [Documentation](#documentation) +7. [Contributing](#contributing) +8. [License](#license) + +--- + +## Motivation +`cosign` is the gold-standard CLI for software signing, but orchestration in Python projects often requires boilerplate subprocess calls, error parsing, and registry plumbing. **pyCosign** wraps those concerns in a class-based API with async support—letting you focus on *why* you sign, not *how*. + +--- + +## Features +* 📜 **Sign, verify, attest** files or OCI artifacts +* 🔑 **Key sources**: PEM file, Fulcio keyless, HSM / PKCS #11 +* 🌐 **Storage targets**: Local filesystem, OCI registry, Rekor log +* ⚡ **Async orchestration** for batch pipelines +* 📝 **Rich logging** and typed return objects (`Signature`, `VerificationResult`, `Attestation`) +* 🐍 Python 3.10+ & MIT-licensed + +--- + +## Quick Start + +```bash +# install cosign (v2.2+) first +brew install sigstore/tap/cosign # macOS example + +# install pyCosign +pip install pycosign +``` + +Create a signature for hello.tgz: + +```bash +pycosign sign-blob --key cosign.key ./hello.tgz # → hello.tgz.sig +``` + +Verify it: + +```bash +pycosign verify-blob --signature hello.tgz.sig ./hello.tgz +``` + +--- + +## CLI Examples + +| Task | Command | +|------|---------| +| **Sign OCI image** | `pycosign sign registry.local/hello:1.0` | +| **Sign local file → push signature to registry** | `pycosign sign-blob --push registry.local/hello:1.0 ./hello.tgz` | +| **Attest local file with SPDX SBOM** | `pycosign attest-blob --predicate sbom.json ./hello.tgz` | +| **Verify local file with detached `.sig`** | `pycosign verify-blob --signature hello.tgz.sig ./hello.tgz` | +| **Verify attestation bundle offline** | `pycosign verify-attestation --type https://spdx.dev/Document --signature hello.tgz.att ./hello.tgz` | + +>See `pycosign --help` for the full option matrix. + +## Library Usage + +```python +from pycosign import Signer, Verifier, KeyManager + +signer = Signer(key_manager=KeyManager.from_key_file("cosign.key")) +sig_obj = signer.sign_blob_local("hello.tgz") +print(sig_obj.sig_path) # Path('hello.tgz.sig') + +verifier = Verifier() +result = verifier.verify_local("hello.tgz", sig_obj.sig_path) +assert result.verified +``` + +Async batch signing example in `/examples/async_batch.py`. + +## Documentation + +* **Design Specification** - high-level architecture & diagrams + → [pyCosign/documentation/design_specification.md](./documentation/design_specification.md) +* **Signing · Attesting · Verifying Primer** - conceptual deep-dive with sample artifacts + → [pyCosign/documentation/signing-attesting-verifying.md](./documentation/signing-attesting-verifying.md) + +## Contributing +Bug reports, feature requests, and PRs are welcome! Please see `[CONTRIBUTING.md](CONTRIBUTING.md)` for the workflow, coding style, and DCO sign-off requirements. + +## License +Released under the [MIT License](LICENSE). diff --git a/documentation/design_specification.md b/documentation/design_specification.md new file mode 100644 index 0000000..956ccc9 --- /dev/null +++ b/documentation/design_specification.md @@ -0,0 +1,174 @@ +# pyCosign Design Specification + +> **Version 0.1 – June 2025** +> This living document captures the architecture, roles, and primary use‑cases for _pyCosign_ — a Python façade around the `cosign` executable and Sigstore services. + +## Table of Contents +1. [Purpose & Scope](#purpose--scope) +2. [Reference Architecture](#reference-architecture) +3. [Role Overview](#role-overview) +4. [Signer](#signer-1) +5. [Verifier](#verifier-1) +6. [Attester](#attester-1) +7. [Glossary](#glossary) + +## Purpose & Scope +_pyCosign_ offers a **thin, class-based wrapper** that orchestrates the trusted `cosign` CLI while hiding subprocess and storage details from application developers. The project supports: + +* Detached signatures & attestations for any local file +* OCI-native signatures/attestations for container images and other OCI artifacts +* Rekor transparency-log integration (optional or offline bundle) +* Multiple key sources (file, Fulcio keyless, HSM/PKCS#11) + +> ⚠️ This document intentionally limits itself to the _foundational_ use-cases required in sprint 1. More advanced flows (HSM, batch, policy-verify) appear only as placeholders. + +## Reference Architecture +The _pyCosign_ runtime is organized around **three orthogonal roles — Signer, Verifier, and Attester** — that share a `KeyManager` for credential handling and a `RegistryClient` for OCI I/O. Each role calls the local `cosign` executable to perform cryptographic work; the Python layer concentrates on orchestration, storage decisions (local FS / OCI registry / Rekor), and observable logging. + +### Component Views +#### Signer +The **Signer** produces tamper-evident signatures for files or OCI digests. It discovers keys (or fetches a keyless Fulcio cert), invokes `cosign sign`/`sign-blob`, and persists detached `*.sig` layers to the chosen backend—filesystem, OCI registry, and/or Rekor bundle. + +![Component pyCosign Signer](./images/component_pycosign_signer.png) + +#### Verifier +The **Verifier** confirms artifact integrity by fetching signatures (local or OCI), calling `cosign verify`/`verify-blob`, applying optional policy filters (cert identity, issuer, annotations), and—if online—checking Rekor inclusion proofs. It returns a structured `VerificationResult` with verdict and log indexes. + +![Component pyCosign Verifier](./images/component_pycosign_verifier.png) + +#### Attester +The **Attester** attaches supply-chain metadata (SPDX, CycloneDX, in-toto predicates). Given a predicate file, it calls `cosign attest`/`attest-blob` to create a signed `*.att` layer, which can be saved locally, pushed to an OCI registry, and/or bundled into Rekor for transparency. + +![Component pyCosign Attester](./images/component_pycosign_attester.png) + +## Role Overview + +| Role | Responsibility | Primary Public Methods | +|------|----------------|------------------------| +| **Signer** | Produce signatures for local files or OCI digests and store them per configuration (filesystem, registry, Rekor). | `sign_blob_local` · `sign_blob_and_push` · `sign_artifact` | +| **Verifier** | Validate signatures / attestations—including optional policy checks—and, when online, confirm Rekor inclusion proofs. | `verify_local` · `verify_registry` · `verify_attestation` | +| **Attester** | Attach supply-chain metadata (SPDX, CycloneDX, in-toto predicates) to artifacts and persist detached `*.att` layers. | `attest_blob_local` · `attest_blob_and_push` · `attest_artifact` | + +## Signer + +### Role Description +The **Signer** generates tamper-evident signatures for files or OCI digests. +It discovers key material (file-based, keyless Fulcio cert, or HSM), spawns `cosign sign` / `sign-blob`, and stores detached `*.sig` layers according to runtime configuration—local filesystem, OCI registry, and/or a Rekor bundle. + +> **Component diagram:** see §Reference Architecture → Signer. + +--- + +### Use Cases + +| UC-ID | Title | Storage Target | Key Source | +|-------|-------|----------------|------------| +| **S-1** | Sign local file & save `.sig` locally | Filesystem | Key file | +| **S-2** | Sign local file & push `.sig` to OCI registry | OCI registry | Key file | +| S-3 | Sign OCI image already in registry | OCI registry | Key file | +| S-4 | Sign local file & upload **only** Rekor bundle | Rekor | Key file | +| S-5 | Keyless sign local file & save `.sig` locally | Filesystem | Fulcio cert | +| S-6 | HSM sign local file & push `.sig` to registry | OCI registry | PKCS #11 | +| S-7 | Parallel-sign multiple artifacts (async pool) | User-selected | any | + +![Signer Use Case](./images/use_case_signer.png) + +--- + +### Sequence Diagrams + +#### UC S-1 — Sign local file & save `.sig` locally + +![Signer UC S-1](./images/seq_signer_s1.png) + +#### UC S-2 — Sign local file & push `.sig` to OCI registry + +![Signer UC S-2](./images/seq_signer_s2.png) + +## Verifier + +### Role Description +The **Verifier** confirms artifact integrity and authenticity. +It retrieves detached signatures or attestations—either from the local filesystem or an OCI registry—then invokes `cosign verify` / `verify-blob` / `verify-attestation`. Optional policy filters (certificate identity, OIDC issuer, annotation key-value) and Rekor inclusion proofs can be enforced to meet stricter supply-chain requirements. Results are returned as a structured `VerificationResult` object. + +> **Component diagram:** see §Reference Architecture → Verifier. + +--- + +### Use Cases + +| UC-ID | Title | Inputs | Notes | +|-------|-------|--------|-------| +| **V-1** | Verify local file with detached `.sig` | `file`, `file.sig` | Offline verification, no registry required | +| **V-2** | Verify OCI image signature in registry | Image reference | Uses registry-native `.sig` layer | +| V-3 | Verify local bundle offline (`.sig.bundle`) | `file`, `file.sig.bundle` | No Rekor connectivity needed | +| V-4 | Verify OCI attestation in registry | Image reference | `cosign verify-attestation` | +| V-5 | Verify attestation bundle offline | `.att`, `.bundle` | Works in air-gapped mode | +| V-6 | Policy-based verify (cert/email/issuer/annotations) | Artifact ref + policy | Extends V-1/2/4 | +| V-7 | Batch / parallel verify many refs | List of refs | Async pool | + +![Verifier Use Case](./images/use_case_verifier.png) + +--- + +### Sequence Diagrams + +#### UC V-1 — Verify local file with detached `.sig` + +![Verifier UC V-1](./images/seq_verifier_v1.png) + +#### UC V-2 — Verify OCI image signature in registry + +![Verifier UC V-2](./images/seq_verifier_v2.png) + +## Attester + +### Role Description +The **Attester** enriches artifacts with cryptographically signed **supply-chain metadata**. +Given a predicate (e.g., SPDX SBOM, CycloneDX, in-toto statement), it spawns `cosign attest` / `attest-blob` to create a detached `*.att` layer. The attestation can be saved locally, pushed to an OCI registry alongside the artifact digest, and/or bundled into Rekor for transparency. Down-stream consumers can then validate provenance and SBOM data via the Verifier role. + +> **Component diagram:** see §Reference Architecture → Attester. + +--- + +### Use Cases + +| UC-ID | Title | Predicate Example | Storage Target | Key Source | +|-------|-------|-------------------|----------------|------------| +| **A-1** | Create attestation for local file, save `.att` locally | Custom JSON | Filesystem | Key file | +| **A-2** | Create attestation for local file & push `.att` to OCI registry | SPDX SBOM | OCI registry | Key file | +| A-3 | Create attestation & upload **only** Rekor bundle | Any | Rekor | Key file | +| A-4 | Attest OCI image already in registry | CycloneDX | OCI registry | Key file | +| A-5 | Keyless attestation, save `.att` locally | Any | Filesystem | Fulcio cert | +| A-6 | Batch generate attestations for many digests | Any | User-selected | any | +| A-7 | Attest with HSM & push `.att` to registry | Any | OCI registry | PKCS #11 | + +![Attester Use Case](./images/use_case_attester.png) + +--- + +### Sequence Diagrams + +#### UC A-1 — Create attestation for local file, save `.att` locally + +![Attester UC A-1](./images/seq_attester_a1.png) + +#### UC A-2 — Create attestation for local file & push `.att` to OCI registry + +![Attester UC A-2](./images/seq_attester_a2.png) + +## Glossary + +| Term | Definition | +|------|------------| +| **Artifact** | Any file (e.g., binary, SBOM) or OCI manifest digest that can be signed or attested. | +| **Detached signature (`*.sig`)** | Base-64 SLSA signature stored separately from the artifact—produced by `cosign sign`/`sign-blob`. | +| **Attestation (`*.att`)** | A signed statement—often SBOM or in-toto predicate—linking metadata to an artifact via `cosign attest`. | +| **Predicate** | JSON document that describes the attestation content (SPDX, CycloneDX, provenance, etc.). | +| **Bundle (`*.bundle`)** | Rekor inclusion proof packaged with a signature/attestation for offline verification. | +| **Fulcio** | Sigstore certificate authority that issues short-lived keyless X.509 certificates. | +| **Rekor** | Sigstore transparency log—an append-only Merkle tree that stores signature/attestation bundles. | +| **Keyless Signing** | Flow where a signer obtains an ephemeral certificate from Fulcio instead of using a local key pair. | +| **PKCS #11 / HSM** | Hardware Security Module interface allowing cosign to use hardware-protected private keys. | +| **OCI Registry** | Container registry that can store arbitrary blobs, signatures (`.sig`) and attestations (`.att`). | +| **`cosign`** | CLI tool from the Sigstore project used for signing, verifying, and attesting artifacts. | diff --git a/documentation/images/component_pycosign_attester.png b/documentation/images/component_pycosign_attester.png new file mode 100644 index 0000000..dd05e22 Binary files /dev/null and b/documentation/images/component_pycosign_attester.png differ diff --git a/documentation/images/component_pycosign_signer.png b/documentation/images/component_pycosign_signer.png new file mode 100644 index 0000000..5fa9597 Binary files /dev/null and b/documentation/images/component_pycosign_signer.png differ diff --git a/documentation/images/component_pycosign_verifier.png b/documentation/images/component_pycosign_verifier.png new file mode 100644 index 0000000..9cc1b62 Binary files /dev/null and b/documentation/images/component_pycosign_verifier.png differ diff --git a/documentation/images/seq_attester_a1.png b/documentation/images/seq_attester_a1.png new file mode 100644 index 0000000..878c35f Binary files /dev/null and b/documentation/images/seq_attester_a1.png differ diff --git a/documentation/images/seq_attester_a2.png b/documentation/images/seq_attester_a2.png new file mode 100644 index 0000000..ab667ba Binary files /dev/null and b/documentation/images/seq_attester_a2.png differ diff --git a/documentation/images/seq_signer_s1.png b/documentation/images/seq_signer_s1.png new file mode 100644 index 0000000..6ccc75f Binary files /dev/null and b/documentation/images/seq_signer_s1.png differ diff --git a/documentation/images/seq_signer_s2.png b/documentation/images/seq_signer_s2.png new file mode 100644 index 0000000..389ba8d Binary files /dev/null and b/documentation/images/seq_signer_s2.png differ diff --git a/documentation/images/seq_verifier_v1.png b/documentation/images/seq_verifier_v1.png new file mode 100644 index 0000000..ae581f3 Binary files /dev/null and b/documentation/images/seq_verifier_v1.png differ diff --git a/documentation/images/seq_verifier_v2.png b/documentation/images/seq_verifier_v2.png new file mode 100644 index 0000000..75b1cc4 Binary files /dev/null and b/documentation/images/seq_verifier_v2.png differ diff --git a/documentation/images/use_case_attester.png b/documentation/images/use_case_attester.png new file mode 100644 index 0000000..965f5c0 Binary files /dev/null and b/documentation/images/use_case_attester.png differ diff --git a/documentation/images/use_case_signer.png b/documentation/images/use_case_signer.png new file mode 100644 index 0000000..21dbd68 Binary files /dev/null and b/documentation/images/use_case_signer.png differ diff --git a/documentation/images/use_case_verifier.png b/documentation/images/use_case_verifier.png new file mode 100644 index 0000000..749d1f8 Binary files /dev/null and b/documentation/images/use_case_verifier.png differ diff --git a/documentation/puml/component_pycosign_attester.puml b/documentation/puml/component_pycosign_attester.puml new file mode 100644 index 0000000..1cf5bef --- /dev/null +++ b/documentation/puml/component_pycosign_attester.puml @@ -0,0 +1,20 @@ +@startuml Component_pyCosign_Attester +!theme plain +package "pyCosign" { + [Attester] + [KeyManager] + [RegistryClient] +} +component "cosign (executable)" as CosignCLI +package "Sigstore Services" { + [Fulcio CA] + [Rekor Transparency Log] +} +database "OCI Registry" as Registry +[Attester] ..> [KeyManager] +[Attester] --> CosignCLI : spawn\ncosign attest +[Attester] --> [RegistryClient] : push *.att +[KeyManager] ..> [Fulcio CA] : keyless cert (opt.) +[Attester] ..> [Rekor Transparency Log] : upload bundle +[RegistryClient] <--> Registry : push *.att +@enduml diff --git a/documentation/puml/component_pycosign_signer.puml b/documentation/puml/component_pycosign_signer.puml new file mode 100644 index 0000000..54016b6 --- /dev/null +++ b/documentation/puml/component_pycosign_signer.puml @@ -0,0 +1,20 @@ +@startuml Component_pyCosign_Signer +!theme plain +package "pyCosign" { + [Signer] + [KeyManager] + [RegistryClient] +} +component "cosign (executable)" as CosignCLI +package "Sigstore Services" { + [Fulcio CA] + [Rekor Transparency Log] +} +database "OCI Registry" as Registry +[Signer] ..> [KeyManager] +[Signer] --> CosignCLI : spawn\ncosign sign +[Signer] --> [RegistryClient] : push *.sig +[KeyManager] ..> [Fulcio CA] : keyless cert (opt.) +[Signer] ..> [Rekor Transparency Log] : upload bundle +[RegistryClient] <--> Registry : push *.sig +@enduml diff --git a/documentation/puml/component_pycosign_verifier.puml b/documentation/puml/component_pycosign_verifier.puml new file mode 100644 index 0000000..1e08d24 --- /dev/null +++ b/documentation/puml/component_pycosign_verifier.puml @@ -0,0 +1,18 @@ +@startuml Component_pyCosign_Verifier +!theme plain +package "pyCosign" { + [Verifier] + [KeyManager] + [RegistryClient] +} +component "cosign (executable)" as CosignCLI +package "Sigstore Services" { + [Rekor Transparency Log] +} +database "OCI Registry" as Registry +[Verifier] ..> [KeyManager] +[Verifier] --> CosignCLI : spawn\ncosign verify +[Verifier] --> [RegistryClient] : pull *.sig +[Verifier] --> [Rekor Transparency Log] : inclusion proof +[RegistryClient] <--> Registry : pull *.sig +@enduml diff --git a/documentation/puml/seq_attester_a1.puml b/documentation/puml/seq_attester_a1.puml new file mode 100644 index 0000000..89d16a2 --- /dev/null +++ b/documentation/puml/seq_attester_a1.puml @@ -0,0 +1,13 @@ +@startuml Seq_Attester_A1 +!theme plain +actor Developer +participant Attester as "pyCosign.Attester" +participant CosignCLI as "cosign attest-blob" +participant FS as "File System" + +Developer -> Attester : attest_blob_local(path,\npredicate.json,key) +Attester -> CosignCLI : cosign attest-blob --predicate predicate.json path +CosignCLI --> Attester : base64_att +Attester -> FS : write path.att +Attester --> Developer : Attestation{att_path} +@enduml diff --git a/documentation/puml/seq_attester_a2.puml b/documentation/puml/seq_attester_a2.puml new file mode 100644 index 0000000..03c97c3 --- /dev/null +++ b/documentation/puml/seq_attester_a2.puml @@ -0,0 +1,18 @@ +@startuml Seq_Attester_A2 +!theme plain +actor Developer +participant Attester as "pyCosign.Attester" +participant CosignCLI as "cosign attest-blob" +participant FS as "File System" +participant RegistryCLI as "pyCosign.RegistryClient" +database Registry as "OCI Registry" + +Developer -> Attester : attest_blob_and_push(path,\npredicate.json,key,ref) +Attester -> CosignCLI : cosign attest-blob --predicate predicate.json path +CosignCLI --> Attester : base64_att +Attester -> FS : write path.att +Attester -> RegistryCLI : push_attestation(path.att, ref) +RegistryCLI -> Registry : PUT .att +Registry --> RegistryCLI : 201 Created +Attester --> Developer : Attestation{att_ref} +@enduml diff --git a/documentation/puml/seq_signer_s1.puml b/documentation/puml/seq_signer_s1.puml new file mode 100644 index 0000000..e8ab4a4 --- /dev/null +++ b/documentation/puml/seq_signer_s1.puml @@ -0,0 +1,13 @@ +@startuml Seq_Signer_S1 +!theme plain +actor Developer +participant Signer as "pyCosign.Signer" +participant CosignCLI as "cosign sign-blob" +participant FS as "File System" + +Developer -> Signer : sign_blob_local(path, key) +Signer -> CosignCLI : cosign sign-blob --key key.pem path +CosignCLI --> Signer : base64_sig +Signer -> FS : write path.sig +Signer --> Developer : Signature{sig_path} +@enduml diff --git a/documentation/puml/seq_signer_s2.puml b/documentation/puml/seq_signer_s2.puml new file mode 100644 index 0000000..3c39dd8 --- /dev/null +++ b/documentation/puml/seq_signer_s2.puml @@ -0,0 +1,18 @@ +@startuml Seq_Signer_S2 +!theme plain +actor Developer +participant Signer as "pyCosign.Signer" +participant CosignCLI as "cosign sign-blob" +participant FS as "File System" +participant RegistryCLI as "pyCosign.RegistryClient" +database Registry as "OCI Registry" + +Developer -> Signer : sign_blob_and_push(path, key, ref) +Signer -> CosignCLI : cosign sign-blob --key key.pem path +CosignCLI --> Signer : base64_sig +Signer -> FS : write path.sig +Signer -> RegistryCLI : push(path.sig, ref) +RegistryCLI -> Registry : PUT .sig +Registry --> RegistryCLI : 201 Created +Signer --> Developer : Signature{sig_ref} +@enduml diff --git a/documentation/puml/seq_verifier_v1.puml b/documentation/puml/seq_verifier_v1.puml new file mode 100644 index 0000000..418fb83 --- /dev/null +++ b/documentation/puml/seq_verifier_v1.puml @@ -0,0 +1,13 @@ +@startuml Seq_Verifier_V1 +!theme plain +actor User +participant Verifier as "pyCosign.Verifier" +participant CosignCLI as "cosign verify-blob" +participant FS as "File System" + +User -> Verifier : verify_local(path, path.sig) +Verifier -> FS : read file + sig +Verifier -> CosignCLI : cosign verify-blob --signature path.sig path +CosignCLI --> Verifier : exit 0 / err +Verifier --> User : VerificationResult{verified} +@enduml diff --git a/documentation/puml/seq_verifier_v2.puml b/documentation/puml/seq_verifier_v2.puml new file mode 100644 index 0000000..d438727 --- /dev/null +++ b/documentation/puml/seq_verifier_v2.puml @@ -0,0 +1,17 @@ +@startuml Seq_Verifier_V2 +!theme plain +actor User +participant Verifier as "pyCosign.Verifier" +participant RegistryCLI as "pyCosign.RegistryClient" +database Registry as "OCI Registry" +participant CosignCLI as "cosign verify" + +User -> Verifier : verify_registry(ref) +Verifier -> RegistryCLI : pull_signature(ref) +RegistryCLI -> Registry : GET .sig +Registry --> RegistryCLI : *.sig +RegistryCLI --> Verifier : sig bytes +Verifier -> CosignCLI : cosign verify ref +CosignCLI --> Verifier : verified? +Verifier --> User : VerificationResult{verified} +@enduml diff --git a/documentation/puml/use_case_attester.puml b/documentation/puml/use_case_attester.puml new file mode 100644 index 0000000..837ea15 --- /dev/null +++ b/documentation/puml/use_case_attester.puml @@ -0,0 +1,32 @@ +@startuml UC_Attester +!theme plain +skinparam usecase { + BackgroundColor<> LightGray + BackgroundColor<> Wheat + BackgroundColor<> LightBlue + BackgroundColor<> Thistle + BackgroundColor<> Moccasin +} + +actor Developer +actor BuildSystem + +package "pyCosign::Attester" { + usecase "A-1 Local attestation\n📄 JSON → .att\n🗂️ FS storage, key file" as A1 <> + usecase "A-2 Attest & push registry\n🧾 SPDX SBOM\n📦 Push .att to OCI" as A2 <> + usecase "A-3 Rekor-only attestation\n📄 Any format\n📤 Upload to Rekor" as A3 <> + usecase "A-4 Attest OCI image\n📦 CycloneDX SBOM\n🖋️ Sign existing image" as A4 <> + usecase "A-5 Keyless attestation local\n📄 Any\n🔑 Fulcio cert + .att" as A5 <> + usecase "A-6 Batch attest\n🧾 List of digests\n⚙️ Parallel attest, any store" as A6 <> + usecase "A-7 HSM attest & push\n📦 OCI registry\n🔐 PKCS #11 key" as A7 <> +} + +Developer --> A1 +Developer --> A2 +Developer --> A3 +Developer --> A5 +BuildSystem --> A2 +BuildSystem --> A4 +BuildSystem --> A6 +BuildSystem --> A7 +@enduml diff --git a/documentation/puml/use_case_signer.puml b/documentation/puml/use_case_signer.puml new file mode 100644 index 0000000..b9aea65 --- /dev/null +++ b/documentation/puml/use_case_signer.puml @@ -0,0 +1,32 @@ +@startuml UC_Signer +!theme plain +skinparam usecase { + BackgroundColor<> LightGray + BackgroundColor<> Wheat + BackgroundColor<> LightBlue + BackgroundColor<> Thistle +} + +actor Developer +actor "CI/CD Runner" as CICD + +package "pyCosign::Signer" { + usecase "S-1 Sign & save locally\n📁 file + .sig\n🕵️ Offline mode" as S1 <> + usecase "S-2 Sign & push to registry\n📦 Image ref\n🔐 Push .sig layer" as S2 <> + usecase "S-3 Sign OCI image in-place\n📦 Image ref\n⚠️ Requires writable registry" as S3 <> + usecase "S-4 Sign & Rekor only\n📄 Metadata + log\n📝 No file output" as S4 <> + usecase "S-5 Keyless sign, save locally\n🔑 Sigstore OIDC\n📁 file.sig" as S5 <> + usecase "S-6 HSM sign\n📦 Registry push\n🔐 Hardware key" as S6 <> + usecase "S-7 Batch parallel sign\n🧾 List of targets\n⚙️ Async pool" as S7 <> +} + +Developer --> S1 +Developer --> S2 +Developer --> S4 +Developer --> S5 +Developer --> S6 +CICD --> S2 +CICD --> S3 +CICD --> S4 +CICD --> S7 +@enduml diff --git a/documentation/puml/use_case_verifier.puml b/documentation/puml/use_case_verifier.puml new file mode 100644 index 0000000..b2fcee3 --- /dev/null +++ b/documentation/puml/use_case_verifier.puml @@ -0,0 +1,30 @@ +@startuml UC_Verifier +!theme plain +skinparam usecase { + BackgroundColor<> LightGray + BackgroundColor<> Wheat + BackgroundColor<> LightBlue + BackgroundColor<> Thistle +} + +actor User +actor Auditor + +package "pyCosign::Verifier" { + usecase "V-1 Verify local sig\n📁 file + .sig\n🕵️ Offline mode" as V1 <> + usecase "V-2 Verify registry sig\n📦 Image ref\n🔐 .sig layer in registry" as V2 <> + usecase "V-3 Verify bundle offline\n📁 file + .sig.bundle\n🕵️ Air-gapped OK" as V3 <> + usecase "V-4 Verify registry attestation\n📦 Image ref\n📜 Attestation in registry" as V4 <> + usecase "V-5 Verify attestation bundle\n📁 .att + .bundle\n🕵️ Air-gapped OK" as V5 <> + usecase "V-6 Policy-based verify\n📋 Policy + Ref\n✅ Cert/email/issuer match" as V6 <> + usecase "V-7 Batch verify\n🧾 List of refs\n⚙️ Parallel pool" as V7 <> +} + +User --> V1 +User --> V2 +User --> V3 +Auditor --> V4 +Auditor --> V5 +Auditor --> V6 +Auditor --> V7 +@enduml diff --git a/documentation/signing-attesting-verifying.md b/documentation/signing-attesting-verifying.md new file mode 100644 index 0000000..3915298 --- /dev/null +++ b/documentation/signing-attesting-verifying.md @@ -0,0 +1,203 @@ +# Understanding Signing, Attesting, and Verifying + +This primer clarifies the three core actions in Sigstore-based supply-chain security — **signing**, **attesting**, and **verifying** — and how they relate to each other. Below we cover a Sigstore / _cosign_ workflow and shows concrete, copy-paste-ready examples for each action. + +--- + +## Table of Contents +1. [Signing](#signing) +2. [Attesting](#attesting) +3. [Verifying](#verifying) + +--- + +## Signing + +**What it is** +“Signing” produces a cryptographic **detached signature** (`*.sig`) that binds an artifact’s cryptographic digest (SHA-256) to a signer’s private key or keyless certificate. + +**Why it matters** +* Proves *integrity*: the artifact has not changed since it was signed. +* Provides *authenticity*: the signature can be traced to a specific key or X.509 identity. + +**Key points** +* `cosign sign` → images | `cosign sign-blob` → files +* Signatures can be stored on disk, in an OCI registry, and/or uploaded to Rekor for public transparency. +* Keys can live in files, cloud KMS, HSM/PKCS#11, or be issued on-demand by Fulcio (keyless). + +--- + +### Examples + +#### S-1 — Sign a local file, save `.sig` locally +```bash +cosign sign-blob --key cosign.key ./hello.tgz > hello.tgz.sig +``` +`hello.tgz.sig` (truncated): +```bash +MEUCIQDn4Qv7lF1pRc0X...lWbQIoimLxEcQIgI4lR+Q5e6b6w... +``` + +#### S-2 — Sign an OCI image already in a registry +```bash +cosign sign --key cosign.key registry.local/hello:1.0 +``` +The registry now contains: +```bash +sha256: (manifest) +└── sha256:.sig (signature layer) +``` +#### S-3 — Keyless sign a local file (OIDC + Fulcio) +```bash +# environment must have $COSIGN_EXPERIMENTAL=1 and $OIDC_TOKEN +cosign sign-blob --keyless ./hello.tgz > hello.tgz.sig +``` + +## Attesting + +**What it is** +“Attesting” creates a detached **signed statement** (`*.att`) that couples an artifact digest with **metadata** — typically supply-chain information such as an SPDX SBOM, CycloneDX BOM, or in-toto provenance. + +**Why it matters** +* Captures *how* the artifact was built (compiler flags, build environment, dependencies, source). +* Enables downstream consumers to enforce policy (e.g., “only run images with SBOM and provenance”). + +**Key points** +* `cosign attest` → images | `cosign attest-blob` → files +* The metadata payload is called a **predicate**; its schema is application-specific (SPDX, CycloneDX, in-toto, custom JSON). +* Like signatures, attestations can be stored locally, in registries, and/or logged in Rekor for public transparency. + +--- + +### Examples + +#### Predicate file (`sbom.json`) + +```json +{ + "predicateType": "https://spdx.dev/Document", + "predicate": { + "SPDXID": "SPDXRef-DOCUMENT", + "name": "hello-archive", + "packages": [ + { "SPDXID": "SPDXRef-Pkg-1", "name": "libfoo", "versionInfo": "1.2.3" } + ] + } +} +``` + +#### A-1 — Attest a local file, save `.att` locally + +```bash +cosign attest-blob \ + --key cosign.key \ + --predicate sbom.json \ + ./hello.tgz > hello.tgz.att +``` +`hello.tgz.att` (truncated DSSE envelope): +```bash +eyJhc3NlcnRpb24iOnsidHlwZSI6ImNoYXJnZSIsInJlZGF0Y... +``` + +#### A-2 — Attest an OCI image & push to registry + +```bash +cosign attest \ + --key cosign.key \ + --predicate cyclonedx.json \ + registry.local/hello:1.0 +``` +The registry now stores: +```bash +sha256:.att (attestation layer) +``` + +#### A-3 — Rekor-only attestation (no registry push) + +```bash +cosign attest-blob --key cosign.key \ + --predicate provenance.json \ + --bundle ./hello.tgz > hello.tgz.att +``` +A `.bundle` inclusion proof is embedded for offline verification. + +--- + +## Verifying + +**What it is** +“Verifying” checks that a detached signature or attestation: + +1. Cryptographically matches the artifact’s digest. +2. Chains back to a trusted root (key, Fulcio CA, or hardware-backed key). +3. Optionally appears in Rekor (proving it hasn’t been hidden or altered). +4. (Optional) Satisfies policy filters—certificate identity, issuer, annotations, etc. + +**Why it matters** +* Prevents tampered or untrusted artifacts from entering production. +* Enables automated policy gates in CI/CD pipelines and Kubernetes admission controllers. +* Provides audit evidence for compliance (SLSA, NIST SSDF). + +**Key points** +* `cosign verify(-blob)` → signatures | `cosign verify-attestation` → attestations +* Offline verification is possible using the `.bundle` file generated at signing time. +* Verification returns a machine-readable exit code plus structured JSON output when `--output json` is used. + +--- + +## How They Fit Together + +1. **Signer** generates a detached `*.sig` layer for an artifact. +2. **Attester** (often the same CI pipeline) attaches additional `*.att` metadata. +3. **Verifier** runs in downstream environments (release gates, runtime policy engines) to ensure both the signature and attestation are present, valid, and policy-compliant before the artifact is trusted. + +--- + +### Examples + +#### V-1 — Verify local file with detached `.sig` + +```bash +cosign verify-blob \ + --signature hello.tgz.sig \ + ./hello.tgz +``` +Output: Verified OK (exit 0) + +#### V-2 — Verify OCI image signature in registry + +```bash +cosign verify registry.local/hello:1.0 +``` +JSON output (truncated): +```json +{ + "critical": { "...": "..." }, + "optional": { "tlogIndex": 654321 } +} +``` + +#### V-3 — Verify attestation bundle offline + +```bash +cosign verify-attestation \ + --type https://spdx.dev/Document \ + --signature hello.tgz.att \ + ./hello.tgz +``` +Output: +```json +{"verified":true,"tlogIndex":54321,"attestation_type":"spdx"} +``` + +## Putting It Together + +| Artifact | Produced by | Purpose | +|--------------------------|--------------------|-----------------------------------------------------| +| `hello.tgz` | build step | Primary binary blob | +| `hello.tgz.sig` | Signer | Detached integrity/authenticity proof | +| `sbom.json` | build step | Human-readable SBOM | +| `hello.tgz.att` | Attester | Signed DSSE envelope of SBOM | +| `bundle.json` (optional) | Signer / Attester | Rekor inclusion proof for offline verification | + +Armed with these artifacts, any consumer (human or pipeline) can reproduce the verifying commands above to establish full trust in both the artifact and its supply-chain metadata. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..417b0f1 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,548 @@ +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "astroid" +version = "3.3.10" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb"}, + {file = "astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[[package]] +name = "black" +version = "25.1.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.8.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, + {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, + {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, + {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, + {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, + {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, + {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, + {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, + {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, + {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, + {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, + {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, + {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, + {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, + {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, + {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, + {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, + {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, + {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, + {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, + {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, + {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, + {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, + {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, + {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, + {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, + {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, + {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, + {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "dill" +version = "0.4.0" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, + {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "isort" +version = "6.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.16.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, + {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, + {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"}, + {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"}, + {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"}, + {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"}, + {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"}, + {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"}, + {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"}, + {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"}, + {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"}, + {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"}, + {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"}, + {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"}, + {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"}, + {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"}, + {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"}, + {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"}, + {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"}, + {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"}, + {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"}, + {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"}, + {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"}, + {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"}, + {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"}, + {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"}, + {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"}, + {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"}, + {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"}, + {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"}, + {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"}, + {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pylint" +version = "3.3.7" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, + {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, +] + +[package.dependencies] +astroid = ">=3.3.8,<=3.4.0.dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version == \"3.11\""}, +] +isort = ">=4.2.5,<5.13 || >5.13,<7" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2" +tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pytest" +version = "8.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, + {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, + {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, +] + +[[package]] +name = "vulture" +version = "2.14" +description = "Find dead code" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"}, + {file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10,<4.0" +content-hash = "f74ac8b23cb67d8f73eb3c0372773ef3779756255b8e57d96bc2f9dcda4223c4" diff --git a/pycosign/__init__.py b/pycosign/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ddcd090 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[project] +name = "pycosign" +version = "0.0.0" +description = "Python Wrapper for Sigstor Cosign" +authors = [{ name = "interwebshack", email = "22042209+interwebshack@users.noreply.github.com" }] +license = "MIT License" +readme = "README.md" +packages = [{ include = "pycosign" }] +exclude = ["tests/*"] + +[tool.poetry.dependencies] +python = ">=3.10,<4.0" + +[tool.poetry.group.dev.dependencies] +black = "^25.1.0" +isort = "^6.0.1" +mypy = "^1.16.0" +pylint = "^3.3.7" +pytest-cov = "^6.1.1" +vulture = "^2.14" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +pycosign = "pycosign.cli:main" + +[tool.black] +line-length = 100 +target-version = ['py310', 'py311', 'py312'] + +[tool.isort] +# https://pycqa.github.io/isort/docs/configuration/black_compatibility/ +profile = "black" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29