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:
- 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:
- Release starts from a reviewed commit already merged to the protected default branch.
- A maintainer creates a reviewed release tag or starts a protected
workflow_dispatch release for an existing protected tag.
- 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.
- 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.
- 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.
- Only after that review does a maintainer approve the protected
pypi environment.
- GitHub Actions publishes to PyPI through Trusted Publisher from the previously verified artifacts only.
- A maintainer verifies the published files and hashes on PyPI and GitHub Releases.
- 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.
Package Hardening Plan
Scope and assumptions
Current known state:
pyproject.toml..pthfiles,setup.pycustom commands, or custom build backends.src/embit/util/prebuilt/, and loads them dynamically insrc/embit/util/ctypes_secp256k1.py. These binaries should be removed from the repository and from published artifacts..github/workflows/directory today.Threat model focus:
Recommended priority order:
.pth,sitecustomize.py, and any bundled native binaries.libsecp256k1binaries from Git and from published artifacts, and document the user-local install path instead.1. Codebase level
1.1 Packaging metadata and structure
What must be done:
pyproject.tomlthe single source of truth for package metadata.pyproject.toml.requires-pythonfield in[project]with the actual supported range instead of the current overly broadpython = "^3.0"in[tool.poetry.dependencies].[project.urls]forSource,Issues,Changelog,Security, andDocumentation.SECURITY.mdfile and link it in metadata and the GitHub repository.MANIFEST.inso sdist contents are deliberate, reviewed, and not inferred implicitly.src/embit/util/prebuilt/from the sdist and wheel so no native binaries are shipped by the project.docs/maintainers/releasing.mdorRELEASING.md.RELEASING.md, define the exact post-publish verification checklist maintainers must follow after a release.Why:
1.2 Package-content policy
What must be done:
.pthfiles.sitecustomize.pyorusercustomize.py.setup.pycustom commands..pthsitecustomize.pyusercustomize.py.dist-info/.egg-infometadata from third-party distributions unless explicitly approvedRequires-Dist, extras, or entry points relative to the reviewed repository metadata.Why:
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
libsecp256k1binaries at all. Users who want the ctypes-backed path must install or buildlibsecp256k1locally on their own machines. Published artifacts remain pure Python.What must be done:
src/embit/util/prebuilt/from Git history going forward and keep that path ignored for generated local files.libsecp256k1as an optional system dependency for end users, not as a packaged project asset.libsecp256k1is present,embitmay use it throughctypesembitfalls back to its pure-Python implementationlibsecp256k1are strictly for local use and must not be committed, attached to releases, or redistributed by the projectsrc/embit/util/ctypes_secp256k1.pyfocused on locating a locally installed system library, not a project-bundled binary.Why:
1.4 Dependency and toolchain hygiene
What must be done:
[build-system].requiresfor the approved build backend path.requirements-dev.txtorconstraints-dev.txt, generated from a reviewed source and refreshed intentionally.pip install --require-hashesfor CI and release-validation dependency installs..pre-commit-config.yamland pin all hooks to reviewed versions.Why:
1.5 GitHub Actions to create
Create these workflows in the canonical repository.
ci.ymlpip --require-hashes.--no-index --find-linksso the check consumes only the locally built artifacts.dependency-review.ymlcodeql.ymlpackage-content-verification.ymlsrc/embit/util/ctypes_secp256k1.py,secp256k1/**,.gitmodules, or release workflows.release.ymlworkflow_dispatchagainst a protected existing tag input.id-token: writeonly to the publish-only job.pypienvironment that requires human approval.1.6 Files and policies to add in the PR
The hardening PR should introduce at least:
SECURITY.mdCODEOWNERSRELEASING.mdMANIFEST.inlibsecp256k1install or build.github/workflows/ci.yml.github/workflows/dependency-review.yml.github/workflows/codeql.yml.github/workflows/package-content-verification.yml.github/workflows/release.yml2. GitHub repository level
2.1 Branch and tag protection
Current known rule gaps from the attached
Protect masterruleset:require_code_owner_reviewisfalserequire_last_push_approvalisfalseWhat must be done:
v*so only maintainers can create them.Recommended practical setting for this project:
required_approving_review_countat1.require_last_push_approval = trueso 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:
CODEOWNERSfile with both maintainers as code owners for:.github/workflows/**.gitmodulespyproject.tomlsetup.pyMANIFEST.insrc/embit/util/ctypes_secp256k1.pysecp256k1/**SECURITY.mdRELEASING.mdWhy:
2.3 GitHub Actions repository settings
What must be done:
GITHUB_TOKENpermissions to read-only.pypipypienvironment before the publish job can run.Why:
2.4 Security features and metadata
What must be done:
SECURITY.mdwith:Why:
2.5 Maintainer access hygiene
What must be done:
3. PyPI level
3.1 Publishing model
Preferred model:
What must be done:
Why:
3.2 Account and project ownership
What must be done:
diybitcoinhardwarerather than leaving ownership coupled to personal accounts.Why:
3.3 Maintainer account security
What must be done:
3.4 Project settings and release hygiene
What must be done:
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:
workflow_dispatchrelease for an existing protected tag.pypienvironment before any publish step. The publish-only job must download the exact artifacts from step 3 and must not rebuild anything.pypienvironment.RELEASING.mdmust 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:
Post-publish verification checklist:
Rules:
twine uploadfrom laptops.pypienvironment until the build-and-verify job has completed and its outputs have been reviewed.4.2 Credential management
DO:
DON'T:
TODO:
4.3 Developer workstation risk reduction
What must be done:
4.4 Social engineering and human-factor controls
What must be done:
4.5 Incident response
Create a written playbook covering:
Minimum incident-response rule:
PR implementation guidance
What can be implemented directly in a hardening PR:
SECURITY.mdCODEOWNERSRELEASING.mdpyproject.tomlMANIFEST.inlibsecp256k1install documentationWhat must be configured outside the PR:
Suggested rollout
SECURITY.md, release docs, package-content checks, removal of committed binaries, and documentation for locallibsecp256k1installation.