diff --git a/README.md b/README.md index 59a5921..986a218 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ tag, or opt-in to [use a full Git commit SHA] and Dependabot. ### Trusted publishing -> **NOTE**: Trusted publishing is sometimes referred to by its +> [!NOTE] +> Trusted publishing is sometimes referred to by its > underlying technology -- OpenID Connect, or OIDC for short. > If you see references to "OIDC publishing" in the context of PyPI, > this is what they're referring to. @@ -61,10 +62,11 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 ``` -> **Pro tip**: instead of using branch pointers, like `unstable/v1`, pin -versions of Actions that you use to tagged versions or sha1 commit identifiers. -This will make your workflows more secure and better reproducible, saving you -from sudden and unpleasant surprises. +> [!NOTE] +> Pro tip: instead of using branch pointers, like `unstable/v1`, pin versions of +> Actions that you use to tagged versions or sha1 commit identifiers. +> This will make your workflows more secure and better reproducible, saving you +> from sudden and unpleasant surprises. Other indices that support trusted publishing can also be used, like TestPyPI: @@ -76,7 +78,8 @@ Other indices that support trusted publishing can also be used, like TestPyPI: ``` _(don't forget to update the environment name to `testpypi` or similar!)_ -> **Pro tip**: only set the `id-token: write` permission in the job that does +> [!NOTE] +> Pro tip: only set the `id-token: write` permission in the job that does > publishing, not globally. Also, try to separate building from publishing > — this makes sure that any scripts maliciously injected into the build > or test environment won't be able to elevate privileges while flying under @@ -96,7 +99,8 @@ This GitHub Action [has nothing to do with _building package distributions_]. Users are responsible for preparing dists for upload by putting them into the `dist/` folder prior to running this Action. -> **IMPORTANT**: Since this GitHub Action is docker-based, it can only +> [!IMPORTANT] +> Since this GitHub Action is docker-based, it can only > be used from within GNU/Linux based jobs in GitHub Actions CI/CD > workflows. This is by design and is unlikely to change due to a number > of considerations we rely on. @@ -187,9 +191,10 @@ default) setting as follows: skip-existing: true ``` -> **Pro tip**: try to avoid enabling this setting where possible. If you -have steps for publishing to both PyPI and TestPyPI, consider only using -it for the latter, having the former fail loudly on duplicates. +> [!NOTE] +> Pro tip: try to avoid enabling this setting where possible. If you +> have steps for publishing to both PyPI and TestPyPI, consider only using +> it for the latter, having the former fail loudly on duplicates. ### For Debugging diff --git a/oidc-exchange.py b/oidc-exchange.py index cc625d3..671c034 100644 --- a/oidc-exchange.py +++ b/oidc-exchange.py @@ -1,3 +1,5 @@ +import base64 +import json import os import sys from http import HTTPStatus @@ -50,6 +52,25 @@ Token request failed: the server refused the request for the following reasons: {reasons} + +This generally indicates a trusted publisher configuration error, but could +also indicate an internal error on GitHub or PyPI's part. + +{rendered_claims} +""" + +_RENDERED_CLAIMS = """ +The claims rendered below are **for debugging purposes only**. You should **not** +use them to configure a trusted publisher unless they already match your expectations. + +If a claim is not present in the claim set, then it is rendered as `MISSING`. + +* `sub`: `{sub}` +* `repository`: `{repository}` +* `repository_owner`: `{repository_owner}` +* `repository_owner_id`: `{repository_owner_id}` +* `job_workflow_ref`: `{job_workflow_ref}` +* `ref`: `{ref}` """ # Rendered if the package index's token response isn't valid JSON. @@ -121,6 +142,23 @@ def assert_successful_audience_call(resp: requests.Response, domain: str): ) +def render_claims(token: str) -> str: + _, payload, _ = token.split(".", 2) + claims = json.loads(base64.urlsafe_b64decode(payload)) + + def _get(name: str) -> str: # noqa: WPS430 + return claims.get(name, "MISSING") + + return _RENDERED_CLAIMS.format( + sub=_get("sub"), + repository=_get("repository"), + repository_owner=_get("repository_owner"), + repository_owner_id=_get("repository_owner_id"), + job_workflow_ref=_get("job_workflow_ref"), + ref=_get("ref"), + ) + + repository_url = get_normalized_input("repository-url") repository_domain = urlparse(repository_url).netloc token_exchange_url = f"https://{repository_domain}/_/oidc/github/mint-token" @@ -165,7 +203,13 @@ def assert_successful_audience_call(resp: requests.Response, domain: str): for error in mint_token_payload["errors"] ) - die(_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE.format(reasons=reasons)) + rendered_claims = render_claims(oidc_token) + + die( + _SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE.format( + reasons=reasons, rendered_claims=rendered_claims, + ), + ) pypi_token = mint_token_payload.get("token") if pypi_token is None: diff --git a/requirements/runtime.txt b/requirements/runtime.txt index b3f1337..c795e84 100644 --- a/requirements/runtime.txt +++ b/requirements/runtime.txt @@ -14,7 +14,7 @@ cffi==1.15.1 # via cryptography charset-normalizer==3.2.0 # via requests -cryptography==41.0.2 +cryptography==41.0.3 # via secretstorage docutils==0.20.1 # via readme-renderer