2025年5月: GitHub Actionsでデジタル証明書付きでパッケージをリリース(takanory)¶
鈴木たかのり(@takanory)です。 今月の「Python Monthly Topics」では、Pythonのパッケージを公開するときに、デジタル証明書(Digital attestations)を用いてより安全に公開する方法について紹介します。
PEP 740の提案とその背景¶
この機能はPEP 740によって2024年1月に提案され、2024年7月に採択されました。
多くのPythonのパッケージはPyPI(the Python Package Index)で配布されています。 このPEPではパッケージにデジタル署名された証明書と、その証明書を検証するためのメタデータをアップロード、ダウンロードできるようにPyPIのAPIを変更するというものです。
なお、このPEPの採択によってPyPIへのアップロード時のデジタル証明書の添付や、pip
コマンドでの証明書の検証が必須になるわけではないことに注意してください。
このPEPの提案の動機(Motivation)としては以下が述べられています。
Pythonパッケージには配布元などの情報がメタデータとして含まれているが、認証がされていない。 暗号化されたデジタル証明書によって、Pythonパッケージが認証されたリポジトリから作成されたことを確認できるようにする。
Pythonパッケージを乗っ取ろうとする攻撃者にとっては、プライベートな署名情報にアクセスする必要があるため難易度が上がる
現在はPythonパッケージの検証手段はリリースファイルごとのPGP署名しかなく、インデックスページを証明する手段が存在しない。メタデータによってインデックスの証明書の有効性を確認できる
なお、インデックス証明書の仕様は現在PyPA(Python Packaging Authority)によってメンテナンスしています。 詳細な仕様については以下のドキュメントを参照してください。
デジタル証明書付きパッケージの例¶
実際にデジタル証明書が付いているパッケージと付いていないパッケージの例を示します。 以下は、筆者が開発しているsphinx-nekochanというライブラリで、Sphinxにネコチャン絵文字を挿入する機能を追加します。このライブラリでは0.1.5のパッケージでは証明書が付いておらず、0.2.0以降は証明書が付いています。具体的に見てみましょう。
sphinx-nekochan 0.1.5のファイル情報は以下のような内容です。 ファイルに関する情報はハッシュなどがありますが、配布元などの情報はありません。

デジタル証明書がないsphinx-nekochan 0.1.5のファイル情報¶
sphinx-nekochan 0.3.4のファイル情報ではファイルのメタデータ、ハッシュなどは変わらないですが、その下にProvenance(物の起源)という情報が増えています。 Provenanceには以下のような情報が表示されています。
Publisher: どこからリリースされたか(ここではGitHub Action)
Attestations: 証明書情報
Source repository: どのリポジトリのどのバージョンか、またリポジトリの所有者情報
Publication detail: リリース用のトークンはどこで生成された物か、実行されたワークフローのコード
このような情報を出力することによって、正規のリポジトリからリリースされたパッケージであるということを証明しています。 また、画面の左側を見てみると、Verified Details(確認済みの詳細情報)のところに0.3.4ではRepositoryが追加されています。これはリポジトリがこのパッケージの提供としてPyPIによって確認済みであるということを示しています。

デジタル証明書付きのsphinx-nekochan 0.3.4のファイル情報¶
本題とはそれますが、Sphinxについて本連載で以前紹介しているので興味のある方は参照してください。
PEP 740に対応したパッケージの一覧¶
「Are we PEP 740 yet? 🔏」というWebサイトで、著名なパッケージ(最もダウンロード数が多い360のパッケージ)がPEP 740に対応済みかがまとめられています。
Are we PEP 740 yet? 🔏: https://trailofbits.github.io/are-we-pep740-yet/
現在68/360のパッケージがPEP 740に対応済みのようで、まだまだ対応していないパッケージが多いことがわかります。 各パッケージの色の意味は以下の通りです。
緑(19%):PEP 740に対応済み
無色(31%):最新のパッケージは証明書が利用可能になる前にアップロードされた
黄色(44%):証明書がアップロードされていない
マゼンタ(6%):証明書に対応していないリポジトリでホストされているもの(後述)
一覧を見てみると、著名なライブラリでも対応していないものがたくさんあることがわかります。 これからPEP 740に対応したパッケージが増えることを期待します。

Are we PEP 740 yet? 🔏¶
デジタル証明書付きでパッケージをリリース¶
では実際にパッケージをデジタル証明書付きでリリースする方法について説明します。 ソースコードのリポジトリにGitHubを使用している場合は、GitHub Actionsでリリースを行います。
1. PyPIのプロジェクトにTrusted Publisherを設定する¶
まずPyPI上のプロジェクトでTrusted Publisher(信頼できる配布元)としてGitHubのリポジトリを登録します。 PyPIにログインし、プロジェクト一覧画面で設定対象となるプロジェクトの「Manage」ボタンをクリックします。

プロジェクトの「Manage」をクリック¶
サイドメニューで「Publishing」を選択し、「Add a new publisher」に配布元の情報を登録します。 ここでは以下の項目を設定します。
Owner:リポジトリの所有者であるGitHub OrganizationまたはGitHub username
Repository name:リポジトリの名前
Workflow name:リリースワークフローのファイル名(このあと作成します)
Environment name:GitHub Actionsの環境名(オプション)

新規Publisherを追加¶
ここで「Add」ボタンをクリックしてTrusted Publisherの登録は完了です。 「Trusted Publisher Management」に先ほど入力したPublisherが追加されています。

Trusted Publisher ManagementにPublisherが追加された¶
なお、執筆時点ではPyPIではTrusted PublisherとしてGitHub以外にGoogle Cloud、ActiveState、GitLab CI/CDに対応しています。 GitHub Actions以外を使用する場合の設定方法については、以下のドキュメントを参照してください。
2. GitHub Actionsにワークフローを設定する¶
次に先ほどTrusted Publisherとして設定したGitHub Actionsを作成します。 先ほどの設定画面では以下の値を入力していました。
Owner:
takanory
Repository name:
sphinx-nekochan
Workflow name:
workflow.yml
この場合はhttps://github.com/takanory/sphinx-nekochanリポジトリの.github/workflows/
ディレクトリにworkflow.yml
という名前で、ワークフロー用のファイルを作成します。
コードの内容は以下で参照できます。
上記のコードは、以下のページで提供されているコードを元にしています。 コメントを追加して解説します。
パッケージのビルド¶
GitHubにpushすると、GitHub Actionが実行されます。
最初のbuild
ジョブでbuildライブラリをインストールし、このライブラリを使用してパッケージをビルドします。
name: build
on: push # pushすると実行される
jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install pypa/build # build用のライブラリをインストール
run: >-
python3 -m
pip install
build
--user
- name: Build a binary wheel and a source tarball # パッケージをビルド
run: python3 -m build
- name: Store the distribution packages # ビルドしたパッケージを保存
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
パッケージのリリース¶
次のジョブpublish-to-pypi
では、先ほど作成したパッケージをPyPIにリリースします。
最後に実行しているpypa/gh-action-pypi-publish
のGithub Actionによって、デジタル証明書付きでパッケージがリリースされます。
PyPA(Python Packaging Authority)という、Pythonのパッケージ作成に関するワーキンググループが作成したGitHub Actionを使用することにより、簡単にデジタル証明書付きでのパッケージリリースが可能となっています。
publish-to-pypi:
name: Publish Python 🐍 distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/') # タグがプッシュされたときのみリリースを行う
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi # PyPIで設定したEnvironment nameと合わせる
url: https://pypi.org/p/sphinx-nekochan # 自分のパッケージのURLを設定
permissions:
id-token: write # OpenID Connectトークンを取得し、信頼された公開処理とするために必要な設定
steps:
- name: Download all the dists # さきほど保存したパッケージをダウンロード
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI # パッケージをPyPIにリリース
uses: pypa/gh-action-pypi-publish@release/v1
GitHubリリースの作成¶
最後のジョブgithub-release
はGitHubのリリースを作成します。
リリースにはSigstoreの署名が含まれます。
github-release:
name: Sign the Python 🐍 distribution 📦 with Sigstore and upload them to GitHub Release
needs:
- publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write # GitHubリリースの作成に必要
id-token: write # sigstoreに必要
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore # Sigstoreでパッケージに署名をする
uses: sigstore/gh-action-sigstore-python@v3.0.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release # GitHubリリースを作成
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
"$GITHUB_REF_NAME"
--repo "$GITHUB_REPOSITORY"
--notes ""
- name: Upload artifact signatures to GitHub Release # パッケージと署名をアップロード
env:
GITHUB_TOKEN: ${{ github.token }}
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: >-
gh release upload
"$GITHUB_REF_NAME" dist/**
--repo "$GITHUB_REPOSITORY"
GitHub以外でのリリース方法¶
GitHub Actions以外を使用したデジタル証明書付きのパッケージリリース方法については、以下のドキュメントを参照してください。 Google Cloud、ActiveState、GitLab CI/CDでのリリース方法が記述してあります。
gh-action-pypi-publishの解説¶
先述のとおり、デジタル証明書付きのパッケージのリリースはpypa/gh-action-pypi-publish
というGitHub Actionによって行われます。
ここでは、このGitHub Actionの中でどういった処理が行われているかを解説します。
ソースコードは以下のリポジトリで管理されています。
以下で主要なファイルについて説明します。
Dockerfile:Dockerの設定ファイル¶
設定に従ってDockerイメージが作成されます。
Pythonの環境を構築したあとに各種スクリプトがDocker環境にコピーされ、twine-upload.sh
が実行されます。
twine-upload.sh:処理全体のスクリプト¶
メインの処理となるスクリプトで、デジタル証明書の作成、PyPIへのアップロードなどを行います。
このコードの中でoidc-exchange.py
スクリプトでPyPIにアップロードするためのトークンを取得し、INPUT_PASSWORD
環境変数に保存します。
次にattestations.py
スクリプトでデジタル証明書を作成します。
最後に、twine upload
コマンドの引数に--attestations
を追加することで、PyPIにデジタル証明書付きでパッケージをアップロードしています。
このときにINPUT_PASSWORD
環境変数に保存された一時的なトークンを使用することで、安全にパッケージのアップロードが行われます。
twine-upload.sh
の一部コードに日本語コメントを付加¶if "${TRUSTED_PUBLISHING}" ; then
# PyPIのトークンを取得してINPUT_PASSWORDに設定する
INPUT_PASSWORD="$(python /app/oidc-exchange.py)"
if [[ ${INPUT_ATTESTATIONS,,} != "false" ]] ; then
# デジタル証明書を生成してアップロード
python /app/attestations.py "${INPUT_PACKAGES_DIR%%/}"
# --attestations引数を追加
TWINE_EXTRA_ARGS="--attestations $TWINE_EXTRA_ARGS"
# twine uploadコマンドでPythonパッケージとデジタル証明書をアップロード
exec twine upload ${TWINE_EXTRA_ARGS} ${INPUT_PACKAGES_DIR%%/}/*
oidc-exchange.py:OpenID ConnectでPyPIからトークンを取得¶
このPythonスクリプトではOIDC(OpenID Connect)を使用して、PyPIにアップロードするためのトークンを取得します。 このトークンは毎回生成され、使用後に自動的に期限切れになります。
最初に、idモジュールのdetect_credential()
関数により、PyPIとやりとりするためのトークンを取得してoidc_token
に格納します。
内部ではdetect_github()関数が呼ばれており、GitHubのOIDCプロバイダーである$ACTIONS_ID_TOKEN_REQUEST_URL
にアクセスしてトークンを取得しています。
次に、PyPIのトークン交換用URL(token_exchange_url
)にトークン(oidc_token
)を渡します。
PyPIでは受け取ったトークンを検証し、結果を返却します。その返却された値をmint_token_resp
に代入します。
mint_token_resp
はJSON形式なのでPythonオブジェクトに変換し、その中にあるtoken
キーの値を取得してpypi_token
に代入します。
この値がPyPIにアクセスするための一時的なトークンとなります。
oidc-exchange.py
の一部コードに日本語コメントを付加¶try:
# GitHubのOIDCトークンを取得
oidc_token = id.detect_credential(audience=oidc_audience)
...
# PyPIのトークン交換用URLにアクセス
mint_token_resp = requests.post(
token_exchange_url,
json={'token': oidc_token},
timeout=5, # S113 wants a timeout
)
try:
# レスポンスのJSONを変換
mint_token_payload = mint_token_resp.json()
...
# PyPIにアップロードするためのトークンを取得
pypi_token = mint_token_payload.get('token')
# PyPIトークンを出力してtwine-upload.shに渡す
print(pypi_token)
attestations.py:デジタル証明書を作成¶
デジタル証明書ファイルを作成するPythonスクリプトです。
main()
関数の中でattest_dist()
関数を呼び出して、デジタル証明書ファイルを作成しています。
attest_dist()
関数では、pypi-attestationsモジュールのDistribution.from_file()
メソッドでファイルからPythonパッケージのディストリビューションを表すインスタンスを生成して、dist
に代入します。
そして、Attestation.sign()
メソッドでデジタル証明書を作成し、attestation_path
にJSON形式で書き出します。
このattestation_path
に保存されたJSON形式のファイルがtwine upload
コマンドでパッケージと一緒にアップロードされることで、デジタル証明書付きのリリースとなります。
attestations.py
の一部コードに日本語コメントを付加¶from pypi_attestations import Attestation, Distribution
def attest_dist(
dist_path: Path,
attestation_path: Path,
signer: Signer,
) -> None:
# Pythonディストリビューションファイルのインスタンスを生成
dist = Distribution.from_file(dist_path)
# デジタル証明書を作成
attestation = Attestation.sign(signer, dist)
attestation_path.write_text(attestation.model_dump_json(), encoding='utf-8')
def main() -> None:
# attest_dict()関数でデジタル証明書を作成
with SigningContext.production().signer(identity, cache=True) as signer:
for dist_path, attestation_path in dist_to_attestation_map.items():
attest_dist(dist_path, attestation_path, signer)
まとめ¶
今回はPEP 740により、デジタル証明書付きでPyPIにパッケージをリリースするという提案と、GitHub Actionsを使用してリリースする方法について紹介しました。 デジタル証明書付きのリリースが一般的になれば、より安全にPyPIにアップロードされているパッケージが利用できるようになると思われます。
後半ではgh-action-pypi-publish
の中で具体的にどのような処理が行われているかを解説し、トークンを交換する手順やデジタル証明書の作成手順について説明しました。
自身でPythonパッケージをメンテナンスしている方は、ぜひデジタル証明書付きでのリリースに挑戦してみてください。