Release artifact authenticity
Overview
PyHanko uses several mechanisms to provide assurances regarding the authenticity of its release artifacts, with the goal of mitigating its exposure to downstream software supply chain issues.
Note
For the purposes of all security checks described here, GitHub effectively acts as the trust root. In case of doubt, cross-reference the content of this page with its source on GitHub.
Warning
PGP signing of releases has been discontinued. If the methods of release validation described here do not meet your needs, please start a thread on the discussion forum.
Build provenance attestations
Scope
PyHanko’s release pipeline in GitHub Actions uses the GitHub Actions OIDC token to certify release artifacts as originating from a specific repository, ref and build workflow. It does so using GitHub artifact attestations, which produce SLSA build provenance backed by the public Sigstore instance.
The following are important to keep in mind:
The machine identity used to obtain the attestation is also the one used to authenticate to PyPI.
While Sigstore’s OIDC-based keyless signing procedure does not rely on any maintainer-controlled secrets, deploying cannot be done without manual maintainer review, and only repository admins can push
v*tags.The provenance attestation covers the pyHanko release artifacts themselves. It does not make any guarantees about transitive dependencies that your package manager may or may not pull in.
The
gh attestation verifyflow below checks the repository, ref and workflow, but not the GitHub Actions deployment environment behind which the maintainer approval sits. That claim is signed too; see Verifying the deployment environment for how to check it.
Long story short, as long as you trust GitHub’s security controls, these checks are appropriate.
Verifying build provenance
Install the GitHub CLI (
gh, version 2.49 or later)Download the release artifacts you are interested in through whichever channel you prefer (e.g. using
pip download, or manual download from GitHub/PyPI)Run the snippet below.
Note that gh attestation verify takes a single artifact per invocation, so we
loop over the files we want to check.
#!/bin/bash
EXPECTED_VERSION=<version number goes here>
REPO=MatthiasValvekens/pyHanko
for artifact in pyhanko-$EXPECTED_VERSION-*.whl pyhanko-$EXPECTED_VERSION.tar.gz; do
gh attestation verify "$artifact" \
--repo "$REPO" \
--signer-workflow "$REPO/.github/workflows/package.yml" \
--source-ref "refs/tags/v$EXPECTED_VERSION"
done
The two identity-pinning flags are what make this check meaningful, and both are worth understanding:
--signer-workflowRequires the attestation to have been signed by pyHanko’s packaging workflow. Without this flag, any workflow in the repository would be an acceptable signer.
--source-refRequires the build to have run from the release tag you expect, rather than from some other branch or tag in the repository.
For pyhanko-certvalidator (and subprojects other than pyhanko in general) you need to
make sure the tag is prefixed correctly.
#!/bin/bash
EXPECTED_VERSION=<version number goes here>
REPO=MatthiasValvekens/pyHanko
TAG="pyhanko-certvalidator/v$EXPECTED_VERSION"
for artifact in pyhanko_certvalidator-$EXPECTED_VERSION-*.whl \
pyhanko_certvalidator-$EXPECTED_VERSION.tar.gz; do
gh attestation verify "$artifact" \
--repo "$REPO" \
--signer-workflow "$REPO/.github/workflows/package.yml" \
--source-ref "refs/tags/$TAG"
done
You can of course inspect the validated provenance data for any other authenticated metadata
that you think might be useful; --format json will print the full verification
result, including the provenance predicate.
Attestations on PyPI
PyHanko is published to PyPI using PyPI’s trusted publisher model, and the publishing step also uploads PEP 740 attestations for the distributions it uploads. These are served by PyPI itself, and are visible through PyPI’s integrity API.
These attestations share the same trust root (Sigstore) and the same family of GitHub Actions OIDC identity as the build provenance described above.
They can also be used to audit PyPI’s trusted publisher mechanism, as described in Verifying the deployment environment.
Verifying the deployment environment
The pyHanko release pipeline signs its provenance from inside a GitHub Actions
deployment environment. Deploying into that environment requires a manual
maintainer approval, so the name of the environment is a meaningful part of the
release identity: it distinguishes a genuine production upload from, say, a build
that a fork or a stray workflow managed to sign with the same repository’s
identity. This deployment environment is also part of the trusted publisher
data that PyPI checks when releases are uploaded: only artifacts from the release
environment are accepted.
Note
PyHanko’s build pipeline actually uses two types of approval-gated envs.
There’s a release build environment, which is there to ensure that all attested production artifacts produced by the packaging workflow are gated behind maintainer approval.
There’s a separate release publishing environment for publishing these artifacts to PyPI. The reason why they are kept separate is to minimise the amount of code that runs with PyPI publish privileges.
For production releases, the build-release environment is used
to build the artifacts, and the release environment is used
to publish to PyPI.
The environment name is encoded in the signing certificate itself, as a Fulcio
X.509 extension with OID 1.3.6.1.4.1.57264.1.23 (“Deployment Environment”).
Because it is part of the signed certificate, it can be verified with the same
degree of confidence as the repository, ref and workflow.
Note
At the time of writing (July 2026), none of gh attestation verify,
slsa-verifier, or GitHub’s attestation web UI surface the deployment
environment, even though it is present in the certificate. Verifying it
therefore requires a small amount of scripting, shown below. This may change
as the tooling matures.
The attest_provenance.py
script verifies the environment (together with the repository, ref and
workflow(s)) against the signature, using the sigstore Python
package directly.
It works against either provenance source: a downloaded
provenance.sigstore.json bundle, or a response from PyPI’s integrity API.
Note
The examples below show how to validate the provenance of a source tarball
of the main library (pyhanko*.tar.gz), but the process
is the same for wheels, also those built for other pyHanko components
(pyhanko-certvalidator, pyhanko-cli)
in this repository.
Against a downloaded provenance.sigstore.json bundle:
EXPECTED_VERSION=<version number goes here>
REPO=MatthiasValvekens/pyHanko
uv sync --group provenance-check
uv run dev/attest_provenance.py pyhanko-$EXPECTED_VERSION.tar.gz \
--source-ref "refs/tags/v$EXPECTED_VERSION" \
--bundle provenance.sigstore.json
Against PyPI’s integrity API (download the provenance response first):
EXPECTED_VERSION=<version number goes here>
REPO=MatthiasValvekens/pyHanko
curl -sL \
"https://pypi.org/integrity/pyhanko/v$EXPECTED_VERSION/pyhanko-$EXPECTED_VERSION.tar.gz/provenance" \
-o provenance.json
uv sync --group provenance-check
uv run dev/attest_provenance.py pyhanko-$EXPECTED_VERSION.tar.gz \
--source-ref "refs/tags/v$EXPECTED_VERSION" \
--pypi-provenance provenance.json
For subprojects other than pyhanko, adjust the filenames and use a prefixed
tag as the --source-ref (e.g. refs/tags/pyhanko-certvalidator/v$EXPECTED_VERSION).
Legacy releases
Releases up to and including pyHanko 0.36.0, and the contemporaneous releases
of the other packages in the pyHanko suite, predate the move to GitHub artifact
attestations. Their release pages carry two different sets of artifacts, which
remain valid and are documented here for archival purposes:
*.sigstore.jsonbundles, one per artifact, verifiable with thesigstorePython package;a
multiple.intoto.jsonlSLSA provenance file, verifiable withslsa-verifier.
Both mechanisms were backed by Sigstore and by the same GitHub Actions OIDC identity, and their security guarantees largely coincided with each other and with those of the current attestations.
To verify the Sigstore bundles on such a release:
#!/bin/bash
EXPECTED_VERSION=<version number goes here>
REPO=MatthiasValvekens/pyHanko
sigstore verify github \
--cert-identity "https://github.com/$REPO/.github/workflows/release.yml@refs/tags/v$EXPECTED_VERSION" \
--ref "refs/tags/v$EXPECTED_VERSION" \
--repo "$REPO" \
pyhanko-$EXPECTED_VERSION-*.whl pyhanko-$EXPECTED_VERSION.tar.gz
To verify the SLSA provenance on such a release:
EXPECTED_VERSION=<version number goes here>
REPO=MatthiasValvekens/pyHanko
slsa-verifier verify-artifact \
--source-tag "v$EXPECTED_VERSION" \
--provenance-path ./multiple.intoto.jsonl \
--source-uri "github.com/$REPO" \
pyhanko-$EXPECTED_VERSION-*.whl pyhanko-$EXPECTED_VERSION.tar.gz
In both cases, subprojects other than pyhanko use a prefixed tag, e.g.
pyhanko-certvalidator/v$EXPECTED_VERSION.