Skip to content

Package Hardening Plan #114

@miketlk

Description

@miketlk

Package Hardening Plan

Scope and assumptions

Current known state:

  • The package has no declared runtime third-party dependencies in pyproject.toml.
  • The package does not currently appear to use install-time execution hooks such as .pth files, setup.py custom commands, or custom build backends.
  • The package currently ships prebuilt native shared libraries under src/embit/util/prebuilt/, and loads them dynamically in src/embit/util/ctypes_secp256k1.py. These binaries should be removed from the repository and from published artifacts.
  • There is no committed .github/workflows/ directory today.
  • The attached GitHub branch ruleset already blocks deletion, force-pushes, and requires PRs, but it currently does not require code owner review or last-push approval, and it does not yet require concrete CI status checks.
  • Mostly manual release process exists today. The safest target state is CI-only source artifact build and publish using GitHub Actions plus PyPI Trusted Publishing, with no maintainer-local publish path.

Threat model focus:

  • Prevent malicious package content from being introduced into a release.
  • Prevent a maintainer workstation compromise from becoming a package compromise.
  • Prevent long-lived publishing credentials from being stolen and reused.
  • Reduce social-engineering and rushed-release risk on a lightly maintained project.

Recommended priority order:

  • P0: Build and publish only from GitHub Actions via PyPI Trusted Publisher, with no manual laptop upload path.
  • P0: Protect all release-sensitive files with CODEOWNERS plus required review.
  • P0: Add CI that explicitly rejects unexpected package contents such as .pth, sitecustomize.py, and any bundled native binaries.
  • P0: Remove bundled libsecp256k1 binaries from Git and from published artifacts, and document the user-local install path instead.
  • P1: Tighten repository settings, security reporting, and action policies.
  • P1: Document a two-person release process and incident-response playbook.
  • P2: Improve reproducibility, SBOM generation, and staged release verification.

1. Codebase level

1.1 Packaging metadata and structure

What must be done:

  • Make pyproject.toml the single source of truth for package metadata.
  • Remove the duplicated legacy Poetry metadata block unless Poetry is intentionally part of the release process.
  • Keep one build path only: setuptools through PEP 621 metadata in pyproject.toml.
  • Add an explicit requires-python field in [project] with the actual supported range instead of the current overly broad python = "^3.0" in [tool.poetry.dependencies].
  • Add explicit project URLs in [project.urls] for Source, Issues, Changelog, Security, and Documentation.
  • Add a SECURITY.md file and link it in metadata and the GitHub repository.
  • Add a committed MANIFEST.in so sdist contents are deliberate, reviewed, and not inferred implicitly.
  • Exclude src/embit/util/prebuilt/ from the sdist and wheel so no native binaries are shipped by the project.
  • Add a maintainer-facing release document such as docs/maintainers/releasing.md or RELEASING.md.
  • In RELEASING.md, define the exact post-publish verification checklist maintainers must follow after a release.

Why:

  • The current mixed Poetry plus setuptools metadata is unnecessary complexity in a security-sensitive package.
  • A package should make the allowed release surface explicit. Implicit sdist contents are harder to review, and native binaries should not be part of that surface.

1.2 Package-content policy

What must be done:

  • Add a repository policy file documenting forbidden package contents:
    • No .pth files.
    • No sitecustomize.py or usercustomize.py.
    • No install-time network access.
    • No setup.py custom commands.
    • No release-time code generation from the network.
    • No bundled native shared libraries, DLLs, or other executable binaries in the package.
  • Add a CI check that builds the package and fails if the sdist or wheel contains:
    • .pth
    • sitecustomize.py
    • usercustomize.py
    • top-level executable scripts not intentionally declared
    • nested wheels, sdists, or embedded .dist-info / .egg-info metadata from third-party distributions unless explicitly approved
    • any shared libraries, DLLs, or native binaries
  • Add a CI check that parses the built sdist and wheel metadata and fails if the final artifact introduces unexpected Requires-Dist, extras, or entry points relative to the reviewed repository metadata.
  • Add a CI check that inspects built artifacts rather than just the source tree.

Why:

  • Install-time payloads can be hidden in package contents. The only reliable defense at package level is to inspect the actual shipped artifacts in CI before publish.

1.3 Native library policy

This is the most important repo-specific hardening item.

The repository should standardize immediately on one model: the project does not ship libsecp256k1 binaries at all. Users who want the ctypes-backed path must install or build libsecp256k1 locally on their own machines. Published artifacts remain pure Python.

What must be done:

  • Remove all committed files under src/embit/util/prebuilt/ from Git history going forward and keep that path ignored for generated local files.
  • Treat libsecp256k1 as an optional system dependency for end users, not as a packaged project asset.
  • Document supported runtime behavior clearly:
    • if a compatible system libsecp256k1 is present, embit may use it through ctypes
    • if no compatible system library is present, embit falls back to its pure-Python implementation
    • user-local builds or installs of libsecp256k1 are strictly for local use and must not be committed, attached to releases, or redistributed by the project
  • Keep src/embit/util/ctypes_secp256k1.py focused on locating a locally installed system library, not a project-bundled binary.
  • If wheels are distributed, require them to remain pure-Python wheels with no bundled native code.

Why:

  • Bundled compiled artifacts are an unnecessary supply-chain risk here. Removing them entirely is more reliable than trying to verify and redistribute them safely.

1.4 Dependency and toolchain hygiene

What must be done:

  • Keep runtime dependencies at zero unless a dependency has a strong, documented justification.
  • Treat build, test, docs, pre-commit hooks, and GitHub Actions as part of the supply chain.
  • Pin exact versions in [build-system].requires for the approved build backend path.
  • Pin developer and CI dependencies used for release validation.
  • Add a locked developer constraints file for CI, for example requirements-dev.txt or constraints-dev.txt, generated from a reviewed source and refreshed intentionally.
  • Store hashes in the reviewed CI constraints set and enforce pip install --require-hashes for CI and release-validation dependency installs.
  • Ensure release builds use that same reviewed backend/toolchain set instead of resolving fresh build-backend versions in an isolated environment at publish time.
  • Update the very old pre-commit Black pin in .pre-commit-config.yaml and pin all hooks to reviewed versions.

Why:

  • The package itself is small, but the dev and release toolchain is still a dependency tree. That tree is a common compromise path.

1.5 GitHub Actions to create

Create these workflows in the canonical repository.

ci.yml

  • Trigger on PRs and pushes to the default branch.
  • Run on a Python version matrix for the supported range.
  • Install from a hashed, pinned CI constraints set using pip --require-hashes.
  • Run tests.
  • Build the sdist and pure-Python wheel in CI.
  • Inspect artifact contents for forbidden files and confirm that no native binaries are present.
  • Parse built artifact metadata and fail on unexpected dependency metadata, extras, or entry points.
  • Smoke-test install from the built artifacts in a fresh virtual environment using --no-index --find-links so the check consumes only the locally built artifacts.
  • Set minimal permissions at workflow and job level.

dependency-review.yml

  • Trigger on PRs.
  • Use GitHub Dependency Review to flag new Python dependencies and GitHub Actions dependencies.
  • Fail the PR on any new dependency unless reviewed.

codeql.yml

  • Run GitHub CodeQL for Python and GitHub Actions.
  • Treat it as a baseline code scanning signal, not as a substitute for review.

package-content-verification.yml

  • Trigger on PRs that touch packaging metadata, src/embit/util/ctypes_secp256k1.py, secp256k1/**, .gitmodules, or release workflows.
  • Build the sdist and wheel and verify that neither artifact contains bundled native binaries.
  • Verify that the submodule URL matches the expected upstream location if the submodule remains vendored for developer reference or local build documentation.
  • Verify that package metadata, entry points, and dependency metadata match the reviewed source tree.
  • Publish artifact inspection logs as CI artifacts.

release.yml

  • Trigger only from reviewed tags created by maintainers, or from workflow_dispatch against a protected existing tag input.
  • Verify in the workflow that the release tag points to a commit already merged into the protected default branch before any publish step or protected-environment approval.
  • Split release handling into an unprivileged build-and-verify job and a separate publish-only job.
  • Build artifacts in CI only, in the unprivileged job.
  • Do not restore reusable caches in the release build job. If caching is unavoidable, scope it to the exact release commit with no restore fallback from PR or branch workflows.
  • Re-run tests and package-content checks.
  • Generate SHA256 checksums.
  • Generate GitHub artifact attestations for release artifacts.
  • Generate an SBOM if practical.
  • Have the publish-only job download the previously validated artifacts, verify their hashes, and upload them without rebuilding artifacts or running project code.
  • Grant id-token: write only to the publish-only job.
  • Publish to PyPI using Trusted Publisher only, through a protected pypi environment that requires human approval.

1.6 Files and policies to add in the PR

The hardening PR should introduce at least:

  • SECURITY.md
  • CODEOWNERS
  • RELEASING.md
  • MANIFEST.in
  • documentation for local libsecp256k1 install or build
  • .github/workflows/ci.yml
  • .github/workflows/dependency-review.yml
  • .github/workflows/codeql.yml
  • .github/workflows/package-content-verification.yml
  • .github/workflows/release.yml

2. GitHub repository level

2.1 Branch and tag protection

Current known rule gaps from the attached Protect master ruleset:

  • require_code_owner_review is false
  • require_last_push_approval is false
  • no concrete required CI checks are defined yet

What must be done:

  • Keep the existing no-deletion, no-force-push, PR-only, and linear-history protections.
  • Turn on required code owner review.
  • Turn on last-push approval.
  • Require the concrete CI checks from the new workflows before merge.
  • Keep bypass actors empty.
  • Require pull request conversation resolution.
  • Keep squash or rebase only. Do not allow merge commits.
  • Protect release tags such as v* so only maintainers can create them.

Recommended practical setting for this project:

  • Keep required_approving_review_count at 1.
  • Add CODEOWNERS so that the second maintainer must review changes to release-sensitive files.
  • Use require_last_push_approval = true so the pusher cannot self-approve the final state.

This is stricter than the current rules without making a two-maintainer project unworkable.

2.2 CODEOWNERS

What must be done:

  • Add a CODEOWNERS file with both maintainers as code owners for:
    • .github/workflows/**
    • .gitmodules
    • pyproject.toml
    • setup.py
    • MANIFEST.in
    • src/embit/util/ctypes_secp256k1.py
    • secp256k1/**
    • SECURITY.md
    • RELEASING.md

Why:

  • Release-surface changes should never merge without a maintainer consciously reviewing them.

2.3 GitHub Actions repository settings

What must be done:

  • Set the repository default GITHUB_TOKEN permissions to read-only.
  • Elevate permissions per job only where needed.
  • Restrict Actions to GitHub-authored actions and explicitly approved actions, or maintain a tight allowlist.
  • Require all third-party actions to be pinned to full-length commit SHAs.
  • Disable or tightly control self-hosted runners for this repository.
  • Require approval for workflows from forks before they can access sensitive paths or environments.
  • Set shorter artifact and log retention for routine CI, and longer retention only for release artifacts.
  • Use protected environments:
    • pypi
  • Require reviewer approval for the pypi environment before the publish job can run.

Why:

  • If GitHub is the publisher, GitHub Actions itself becomes part of the trusted computing base. Its permissions and action sources must be constrained.

2.4 Security features and metadata

What must be done:

  • Enable private vulnerability reporting.
  • Enable dependency graph.
  • Enable Dependabot alerts.
  • Enable Dependabot security updates where applicable.
  • Enable code scanning and keep CodeQL results visible.
  • Add SECURITY.md with:
    • reporting address or method
    • supported versions
    • response expectations
    • disclosure expectations
  • Use GitHub Releases for every published PyPI version.
  • Enable immutable releases if available for the repository.

Why:

  • These settings make compromise detection, coordinated disclosure, and release verification materially easier.

2.5 Maintainer access hygiene

What must be done:

  • Review repository admins and collaborators.
  • Remove any stale admin or write access.
  • Avoid granting admin when maintain or triage permissions are sufficient.
  • Verify all maintainers have hardware-backed 2FA configured on GitHub.
  • Do not add new maintainers based only on GitHub messages or email. Require out-of-band verification.

3. PyPI level

3.1 Publishing model

Preferred model:

  • Publish only from GitHub Actions in the canonical repository via PyPI Trusted Publisher.
  • Do not publish from maintainers' laptops.
  • Do not store PyPI API tokens in GitHub secrets.

What must be done:

  • Configure a PyPI Trusted Publisher for the canonical repository and the specific release workflow file.
  • Restrict publishing to the release workflow only.
  • Keep manual browser uploads disabled as an operational norm.
  • After Trusted Publishing is verified end to end, revoke all existing PyPI API tokens for this project.
  • Do not keep any break-glass publish token. If Trusted Publishing is unavailable, restore the trusted CI publish path rather than using a direct upload fallback.

Why:

  • Trusted Publishing removes the main credential theft target in CI: reusable PyPI API tokens.

3.2 Account and project ownership

What must be done:

  • Review all owners and maintainers on the PyPI project.
  • If possible, move the project under a PyPI organization account representing diybitcoinhardware rather than leaving ownership coupled to personal accounts.

Why:

  • Personal account coupling is unnecessary supply-chain risk.

3.3 Maintainer account security

What must be done:

  • Confirm both owners use at least two PyPI 2FA methods:
    • WebAuthn security key
    • TOTP
  • Confirm both owners have recovery codes stored offline in separate secure locations.
  • Confirm both owners use unique passwords not reused anywhere else.
  • Review PyPI security history periodically for suspicious events.

3.4 Project settings and release hygiene

What must be done:

  • Ensure verified email addresses are present for maintainers.
  • Keep project URLs current so users can find source, security policy, and release notes.
  • Publish the sdist and pure-Python wheel only if both are intentionally supported and verified from the protected CI release workflow.
  • If a bad release is ever published:
    • yank it immediately
    • revoke any exposed credentials
    • review PyPI security history
    • communicate publicly and precisely which versions are affected

4. Operational measures

This section is limited to operational controls that directly support release integrity, least privilege, and compromise containment.

4.1 Safe deployment process

Required release process:

  1. Release starts from a reviewed commit already merged to the protected default branch.
  2. A maintainer creates a reviewed release tag or starts a protected workflow_dispatch release for an existing protected tag.
  3. GitHub Actions runs an unprivileged build-and-verify job that builds the artifacts, runs tests, runs package-content checks, and generates checksums, artifact attestations, and inspection logs.
  4. The workflow stops at the protected pypi environment before any publish step. The publish-only job must download the exact artifacts from step 3 and must not rebuild anything.
  5. The maintainer performs a deliberate release review after step 3 completes and before approving publish. If a second maintainer is available, they should do this review or approve the environment, but the process must not depend on that.
  6. Only after that review does a maintainer approve the protected pypi environment.
  7. GitHub Actions publishes to PyPI through Trusted Publisher from the previously verified artifacts only.
  8. A maintainer verifies the published files and hashes on PyPI and GitHub Releases.
  9. If any verification fails, yank the release immediately, investigate, and document the incident.

RELEASING.md must define the pre-publish review in step 5 and the post-publish verification in step 8. Keep both checklists short and mechanical.

Pre-publish review checklist:

  • Confirm the tag resolves to the intended commit already merged on the protected default branch.
  • Confirm CI passed tests, package-content checks, and artifact inspection for that exact commit.
  • Compare the artifact filenames and SHA256 values against the CI-generated checksums.
  • Confirm the publish-only job is configured to download the previously built artifacts and cannot rebuild them.
  • Confirm the release notes match the tagged changes and version.

Post-publish verification checklist:

  • Compare the PyPI and GitHub Release artifact SHA256 values against the CI-generated checksums.
  • Confirm the published artifacts correspond to the intended release commit and tag.
  • Confirm the published artifact set matches what the release workflow produced, with no unexpected extra files.
  • Confirm the release job did not restore reusable caches from other workflows.

Rules:

  • No twine upload from laptops.
  • No local build-push-distribute flow for releases.
  • No emergency direct uploads.
  • No release from a dirty local checkout.
  • No publishing from forks.
  • Do not approve the pypi environment until the build-and-verify job has completed and its outputs have been reviewed.
  • When practical, perform the approval and verification from a fresh browser session or separate device from the one used for day-to-day development.

4.2 Credential management

DO:

  • Use hardware-backed 2FA on GitHub and PyPI.
  • Keep recovery codes offline and separately from the primary device.
  • Use a password manager with a strong unique password for each service.
  • Revoke all legacy PyPI tokens after Trusted Publishing is verified.

DON'T:

  • Do not store PyPI tokens in repository secrets if Trusted Publisher is configured.
  • Do not use direct-upload PyPI tokens for this project.
  • Do not store credentials in shell history, dotfiles, plaintext notes, screenshots, or shared chat tools.
  • Do not paste secrets into terminal commands that will be written to history.
  • Do not authenticate to GitHub or PyPI on untrusted or borrowed machines.

TODO:

  • Audit for existing PyPI tokens and revoke all project publish tokens after Trusted Publishing is verified.
  • Document where recovery codes are stored and who can access them in an emergency.

4.3 Developer workstation risk reduction

What must be done:

  • Use a separate low-privilege environment for reviewing untrusted packages, dependency changes, and PR reproductions.
  • Do not inspect untrusted packages on the same machine profile that holds release credentials or other sensitive secrets.
  • Separate the release browser profile or OS account from day-to-day browsing and experimentation.

4.4 Social engineering and human-factor controls

What must be done:

  • Verify any request to add a maintainer, rotate credentials, or publish urgently through a second channel.
  • Do not grant maintainer access based only on GitHub issues, email, or chat.

4.5 Incident response

Create a written playbook covering:

  • How to revoke PyPI Trusted Publisher and lock down GitHub repository access.
  • How to yank a release, rotate credentials, and inspect GitHub audit logs, workflow runs, and PyPI security history.
  • How to communicate affected versions quickly and precisely.

Minimum incident-response rule:

  • If package compromise is even plausible, assume maintainer and CI credentials may also be compromised until proven otherwise.

PR implementation guidance

What can be implemented directly in a hardening PR:

  • SECURITY.md
  • CODEOWNERS
  • RELEASING.md
  • packaging cleanup in pyproject.toml
  • MANIFEST.in
  • local libsecp256k1 install documentation
  • GitHub workflow files
  • artifact content checks

What must be configured outside the PR:

  • GitHub Actions repository settings
  • branch protection and tag protection updates
  • protected environments and reviewers
  • enabling GitHub security features
  • PyPI Trusted Publisher
  • PyPI owner and token cleanup
  • maintainer account 2FA verification

Suggested rollout

  • Land one hardening PR with workflows, CODEOWNERS, SECURITY.md, release docs, package-content checks, removal of committed binaries, and documentation for local libsecp256k1 installation.
  • In the same rollout, enable required status checks, code owner review, last-push approval, private vulnerability reporting, CodeQL, and dependency review.
  • Configure PyPI Trusted Publisher and stop manual uploads.
  • Audit repository and PyPI collaborators.
  • Move PyPI ownership to an organization account if feasible.
  • Add release attestations as part of the same hardening track rather than deferring them to a later phase.
  • Keep SBOM generation and incident drills in the next phase if they are not ready for the initial rollout.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions