From 9f3c0f4749bc893f009f75324799c31e0b93637b Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 18 Sep 2025 13:29:34 -0400 Subject: [PATCH 01/18] PEP: Index support for Trusted Publishing Signed-off-by: William Woodruff --- peps/pep-9999.rst | 400 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 peps/pep-9999.rst diff --git a/peps/pep-9999.rst b/peps/pep-9999.rst new file mode 100644 index 00000000000..a8f4a9cb2cc --- /dev/null +++ b/peps/pep-9999.rst @@ -0,0 +1,400 @@ +PEP: 9999 +Title: Index support for Trusted Publishing +Author: William Woodruff +Sponsor: Donald Stufft +PEP-Delegate: Donald Stufft +Status: Draft +Type: Standards Track +Topic: Packaging +Created: 2025-09-15 +Post-History: `08-Aug-2025 `__ +Resolution: TODO + +Abstract +======== + +This PEP proposes a standard mechanism through which arbitrary +Python package indices can support "Trusted Publishing," a misuse-resistant +credential exchange scheme already implemented by the Python Package Index +(PyPI). + +The mechanism proposed in this PEP is designed to encapsulate PyPI's +`existing implementation `_ +of Trusted Publishing, while allowing other indices to implement the same +scheme in a manner that is discoverable by and interoperable with existing +Python package uploading clients. + +Rationale and Motivation +======================== + +"Trusted Publishing" is PyPI's term of art for using the +`OpenID Connect (OIDC) standard `_ +to exchange a short-lived *identity credential* from a trusted +third-party service (like a CI/CD or cloud provider) for a short-lived, +minimally-scoped *upload credential* that can be used to publish +to the index. + +Trusted Publishing was originally designed and enabled on PyPI in 2023 as +a non-standard (PyPI-specific) feature, much like the existing +`upload API `_. It has seen +widespread adoption in that capacity: over 640,000 files have been published +to PyPI using a Trusted Publisher (as of September 2025), and PyPI's +design has inspired similar designs in the +`Rust (crates.io) `_, +`Ruby (RubyGems) `_, and +`JavaScript (npm) `_ ecosystems. + +The absence of a standard for Trusted Publishing presents a long-term +impediment for adoption: third-party indices (i.e. those other than +PyPI and TestPyPI) cannot easily implement Trusted Publishing without +referencing PyPI's unstandardized design. This in turn poses a long-term +maturity risk similar to that of the unstandardized upload API: package upload +clients (like `twine `_ and +`uv `_) must either accept behavioral differences +between indices (leading to an accretion of hacks) or continue to reject +non-PyPI implementations of Trusted Publishing. + +Specification +============= + +This PEP's specification contains two parts: + +* A *discovery* mechanism that package upload clients can use to determine + whether an arbitrary Python package index host supports Trusted Publishing. +* A *token exchange* mechanism that package upload clients can use to + exchange an identity credential for an upload credential. + +.. _constraints: + +Constraints +----------- + +Unless explicitly stated otherwise, the following constraints +apply to all parts of this PEP's specification: + +* All URLs **MUST** have + `potentially trustworthy origins `_. + In practice, this means that all URLs **MUST** either use the ``https`` + scheme, be some variant of a local loopback (``localhost``, + ``127.0.0.1``, etc.), or otherwise be considered *a priori* trustworthy + in the context of the interaction (e.g. an internal network). + + Uploading clients **MUST** reject any URLs that do not meet this constraint. + +* All server-supplied URLs (i.e. those in discovery responses) **MUST** + have the same host subcomponent as the user-provided upload URL. Uploading + clients **MUST** reject any URLs that do not meet this constraint. + + In practice, this means that a discovery request to + ``https://upload.example.com/.well-known/pytp/{key}`` can only + return URLs with the ``upload.example.com`` host. + +* All client-made HTTP requests **SHOULD** have an + ``Accept: application/vnd.pypi.pytp.v1+json`` header. In the absence of + an ``Accept`` header, the receiving server **MUST** behave as if this header + were present. + + Receiving servers **SHOULD** respond with a ``406 Not Acceptable`` + status code if any other ``Accept`` header is present. + +.. _discovery: + +Trusted Publishing Discovery +---------------------------- + +All Python package uploading is currently "endpoint driven," in the sense +uploading clients (like twine and uv) are given an upload URL (and +**not** merely a domain name). + +For example, to upload to PyPI, uploading clients are expected to connect +to ``https://upload.pypi.org/legacy/``. + +The discovery mechanism proposed below takes advantage of this fact to +allow single domains to advertise support for multiple indices +(and their corresponding upload endpoints). + +The discovery mechanism is as follows: + +1. The uploading client is given an upload URL, e.g. + ``https://upload.example.com/legacy/``. + +2. The uploading client extracts the *path component* of the URL, + as defined in :rfc:`3986`. If the path component is empty, + the empty string should be used. + + For the above example, the path component is + ``/legacy/``. + +3. The uploading client takes the SHA2-256 hash of the path component, + producing the *discovery key*. + + For the above example, the discovery key is + ``af030c06750716b1b35852298fe852b90def13dcbd012a5fe5148470f1206bfc``. [#fn-hash]_ + +4. The uploading client constructs a *discovery URL* by taking the + scheme and authority components (as defined in :rfc:`3986`) + of the upload URL and appending ``/.well-known/pytp/`` + and the discovery key. + + For the above example, the discovery URL is + ``https://upload.example.com/.well-known/pytp/af030c06750716b1b35852298fe852b90def13dcbd012a5fe5148470f1206bfc``. + +5. The uploading client performs an HTTP GET request to the discovery URL. + +6. The server responds with a ``200 OK`` status code and a body + containing a JSON object if the index supports Trusted Publishing + for the given upload URL. The JSON object **MUST** contain the following + fields: + + - ``audience-endpoint``: a string containing the URL of the OIDC + audience endpoint to be used during token exchange. + - ``mint-token-endpoint``: a string containing the URL of the + token minting endpoint to be used during token exchange. + + For the above example, a valid response body would be: + + .. code-block:: json + + { + "audience-endpoint": "https://upload.example.com/_/oidc/audience", + "mint-token-endpoint": "https://upload.example.com/_/oidc/mint-token" + } + +If the server does not support Trusted Publishing for the given +upload URL, it **MUST** respond with a ``404 Not Found`` status code. +When responding with a ``404 Not Found``, the server **SHOULD NOT** +include a response body. If a response body is included, it **MUST** +be ignored by the client. + +Servers **MAY** additionally respond with any other standard HTTP +error code in the 400 or 500 range to indicate an error condition. + +Non-``200 OK``, non-``404 Not Found`` responses **MAY** include a body which, +if present, **MUST** be a JSON object containing an +:ref:`Error Response `. + +.. _token-exchange: + +Trusted Publishing Token Exchange +--------------------------------- + +Once an uploading client has performed a successful :ref:`discovery ` +flow, it can proceed to perform the actual Trusted Publishing token exchange. + +Token exchange occurs in three steps: + +1. The uploading client uses the *audience endpoint* obtained + during discovery to ask the index for its expected OIDC audience. +2. The uploading client uses the expected audience to obtain an + appropriately bound *identity credential* from the Trusted Publishing + provider being used (i.e. the CI/CD or cloud provider that the upload + is being performed from). The details of this step are provider-specific, + and are out of scope for this PEP. [#fn-oidc]_ +3. The uploading client uses the *mint token endpoint* obtained + during discovery to exchange the obtained identity credential + for a short-lived *upload credential* that can be used to upload + to the index. + +.. _audience-retrieval: + +Audience Retrieval +~~~~~~~~~~~~~~~~~~ + +To retrieve the expected OIDC audience, the uploading client performs +an HTTP GET request to the *audience endpoint* obtained during +:ref:`discovery `. + +On success, the server responds with a ``200 OK`` status code and a body +containing a JSON object with the following field: + +- ``audience``: a string containing the expected OIDC audience. + +On failure, the server **MUST** respond with any standard HTTP +error code in the 400 or 500 range to indicate an error condition. +Failure responses **MAY** include a body which, if present, +**MUST** be a JSON object containing an +:ref:`Error Response `. + +.. _token-minting: + +Token Minting +~~~~~~~~~~~~~ + +After the uploading client has performed +:ref:`audience retrieval ` and obtained an +identity credential from the Trusted Publishing provider, it can +proceed to mint an upload credential. + +To mint an upload credential, the uploading client performs +an HTTP POST request to the *mint token endpoint* obtained during +:ref:`discovery `. + +On success, the server responds with a ``200 OK`` status code and a body +containing a JSON object with the following field: + +- ``token``: a string containing the upload credential. The format + of the upload credential is implementation-defined and index-specific. +- ``expires``: an **optional** integer containing a Unix timestamp + indicating when the upload credential expires. If this field is not + present, the uploading client **MAY** assume an expiration point + of not more than 15 minutes (900 seconds) after the time of + their request. + + The server **MUST NOT** issue temporary upload credentials + that expire in less than 15 minutes (900 seconds) or more than + 6 hours (21,600 seconds) from the time of the request. [#fn-expires]_ + + The uploading client **MAY** use this time (or the minimum specified + above) to determine when to refresh the upload credential, if needed. + +On failure, the server **MUST** respond with any standard HTTP +error code in the 400 or 500 range to indicate an error condition. +Failure responses **MUST** include a body which, if present, +**MUST** be a JSON object containing an :ref:`Error Response `. + +.. _error-responses: + +Error Responses +--------------- + +When an error response body is included, it **MUST** be a JSON object +containing the following fields: + +- ``message``: a string containing a short, high-level + human-readable summary of the error. + +- ``errors``: an array of one or more objects, each containing + the following fields: + + - ``code``: a string containing a machine-readable error code. + - ``description``: a string containing a human-readable + description of the error. + +This PEP does not specify any particular error codes. Clients **SHOULD NOT** +assume that error codes are consistent across different indices, and instead +**MUST** treat error codes as opaque strings. + +Security Implications +===================== + +This PEP does not identify any positive or negative security implications +associated with the Trusted Publishing discovery or exchange flows themselves. + +Separately from the flows, Trusted Publishing *itself* has a +`security model on PyPI `_ +and is considered to be a more secure alternative to long-lived +API tokens or passwords. The primary positive security implications of +Trusted Publishing are: + +- All issued upload credentials are short-lived and can be minimally scoped, + limiting the "blast radius" of a compromised credential. In particular, + automatic expiry means that attackers cannot mount "harvest now, use later" + campaigns against packages that use Trusted Publishing. +- Trusted Publishing conceptually links an uploaded package to the identity + of the CI/CD or cloud provider that's authorized to upload it. This linkage + is implicit from the perspective of downstream consumers, but can be made + explicit through :pep:`740` attestations or (less formally) + `URL verification `_. + +Backwards Compatibility +======================= + +This PEP does not change any existing behavior and is fully backwards compatible +with existing upload clients and indices. + +Existing clients that perform PyPI's non-standard Trusted Publishing +upload flow will continue to work as before, as will existing uploads +to all indices that do not implement Trusted Publishing. + +How To Teach This +================= + +This PEP is a *formalization* of Trusted Publishing, which has already +seen widespread adoption in the Python packaging ecosystem. That adoption +has been accompanied by a variety of educational resources on +adopting Trusted Publishing as an end user, including: + +* Python Packaging User Guide: :ref:`packaging:trusted-publishing` +* PyPI: `Publishing to PyPI with a Trusted Publisher `_ +* pyOpenSci: `Setup Trusted Publishing for secure and automated publishing via GitHub Actions `_ + +Rejected Ideas +============== + +"Lateral" Discovery +------------------- + +This PEP's discovery mechanism uses the `.well-known` location scheme +defined in :rfc:`8615`. This scheme is widely adopted by machine-to-machine +protocols, including OpenID Connect itself +(for `OpenID Connect Discovery `_). + +An alternative idea considered was to use a "lateral" discovery mechanism, +in which the uploading client would attempt discovery by constructing a +adjacent path relative to the upload URL. For example, for +``https://upload.example.com/legacy/``, the uploading client would +attempt to discover Trusted Publishing support at +``https://upload.example.com/legacy/pytp`` (or some equivalent). + +The advantage of this approach is that it doesn't require index operators +to have control over their (sub-)domain, which the ``.well-known`` scheme +expects (as well-known URIs can only be served from the root of a domain). + +However, this approach also has downsides: + +* It assumes that arbitrary indices can provide an adjacent path without + interfering with existing functionality, which isn't necessarily true. + For example, a given third-party implementation may already use + all routes under `/legacy/{*}` for other purposes. +* It's less consistent with existing machine-to-machine protocol + conventions, which overwhelmingly use the `.well-known` scheme. Developing + a custom location scheme here would require additional informational + materials for server administrators and operators who are accustomed + to the `.well-known` scheme. + +"Implicit" Discovery +-------------------- + +Another alternative idea considered was the perform "implicit" discovery, +similar to what PyPI currently does for Trusted Publishing: instead of an +explicit :ref:`discovery ` step, the uploading client could jump +straight to attempting the audience and token minting steps, and +handle any errors that arise. + +The advantage of this approach is simplicity: it eliminates the network +round-trip needed for the discovery step, and eliminates the indirection +of obtaining the audience and mint token endpoints from the discovery +response. + +This approach too has downsides: + +* It implicitly limits a given domain to a single index/upload implementation, + since the implicit "discovery" step on PyPI is to construct the audience + and mint token endpoints against the base domain of the upload URL. + This limitation is acceptable in the context of a single index host + like PyPI, but does not generalize to other index topologies (like + index hosts that provide isolated private indices). +* It relies on entirely static endpoint construction rules for + the audience and mint token endpoints, which means significant disruption + to existing clients if those endpoints ever need to change. + +.. rubric:: Footnotes + +.. [#fn-hash] ``shasum -a 256 <<< '/legacy/'`` + +.. [#fn-oidc] Widely used CI/CD and cloud providers various implement "ambient" + OIDC token retrieval mechanisms that aren't standardized. + These various mechanisms are currently abstracted over by + existing components of the Python packaging ecosystem, + such as the `id package `_. + +.. [#fn-expires] The maximum expiry time of 6 hours is chosen to match + common runtime limits on popular CI/CD providers like + GitHub Actions. + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. + From bbce881937dcfeb29d68f7cbd8b9207d323c5793 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 18 Sep 2025 13:31:44 -0400 Subject: [PATCH 02/18] remove TODO Signed-off-by: William Woodruff --- peps/pep-9999.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/peps/pep-9999.rst b/peps/pep-9999.rst index a8f4a9cb2cc..51d2934f390 100644 --- a/peps/pep-9999.rst +++ b/peps/pep-9999.rst @@ -8,7 +8,6 @@ Type: Standards Track Topic: Packaging Created: 2025-09-15 Post-History: `08-Aug-2025 `__ -Resolution: TODO Abstract ======== From 834074fc082b1bc6ea3f79240579448cbce9a838 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 18 Sep 2025 13:36:54 -0400 Subject: [PATCH 03/18] fix date format Signed-off-by: William Woodruff --- peps/pep-9999.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-9999.rst b/peps/pep-9999.rst index 51d2934f390..08cb303b828 100644 --- a/peps/pep-9999.rst +++ b/peps/pep-9999.rst @@ -6,7 +6,7 @@ PEP-Delegate: Donald Stufft Status: Draft Type: Standards Track Topic: Packaging -Created: 2025-09-15 +Created: 15-Sep-2025 Post-History: `08-Aug-2025 `__ Abstract From 6a88fbb7f672b55ba0d3b75e208c6b20147aef07 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 18 Sep 2025 13:37:37 -0400 Subject: [PATCH 04/18] fix backticks Signed-off-by: William Woodruff --- peps/pep-9999.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/peps/pep-9999.rst b/peps/pep-9999.rst index 08cb303b828..82bdf251a47 100644 --- a/peps/pep-9999.rst +++ b/peps/pep-9999.rst @@ -323,7 +323,7 @@ Rejected Ideas "Lateral" Discovery ------------------- -This PEP's discovery mechanism uses the `.well-known` location scheme +This PEP's discovery mechanism uses the ``.well-known`` location scheme defined in :rfc:`8615`. This scheme is widely adopted by machine-to-machine protocols, including OpenID Connect itself (for `OpenID Connect Discovery `_). @@ -344,12 +344,12 @@ However, this approach also has downsides: * It assumes that arbitrary indices can provide an adjacent path without interfering with existing functionality, which isn't necessarily true. For example, a given third-party implementation may already use - all routes under `/legacy/{*}` for other purposes. + all routes under ``/legacy/{*}`` for other purposes. * It's less consistent with existing machine-to-machine protocol - conventions, which overwhelmingly use the `.well-known` scheme. Developing + conventions, which overwhelmingly use the ``.well-known`` scheme. Developing a custom location scheme here would require additional informational materials for server administrators and operators who are accustomed - to the `.well-known` scheme. + to the ``.well-known`` scheme. "Implicit" Discovery -------------------- @@ -396,4 +396,3 @@ Copyright This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive. - From 7f27678698e99089cad500731976c1ddbdfb317c Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 18 Sep 2025 13:50:47 -0400 Subject: [PATCH 05/18] fix code block Signed-off-by: William Woodruff --- peps/pep-9999.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/peps/pep-9999.rst b/peps/pep-9999.rst index 82bdf251a47..3854c53983b 100644 --- a/peps/pep-9999.rst +++ b/peps/pep-9999.rst @@ -152,12 +152,12 @@ The discovery mechanism is as follows: For the above example, a valid response body would be: - .. code-block:: json + .. code-block:: json - { - "audience-endpoint": "https://upload.example.com/_/oidc/audience", - "mint-token-endpoint": "https://upload.example.com/_/oidc/mint-token" - } + { + "audience-endpoint": "https://upload.example.com/_/oidc/audience", + "mint-token-endpoint": "https://upload.example.com/_/oidc/mint-token" + } If the server does not support Trusted Publishing for the given upload URL, it **MUST** respond with a ``404 Not Found`` status code. From 7a12aaed9ed97216b9f7cdc86f75781e4f2a1a5f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 19 Sep 2025 10:53:02 -0400 Subject: [PATCH 06/18] PEP 807: assign number Signed-off-by: William Woodruff --- .github/CODEOWNERS | 1 + peps/{pep-9999.rst => pep-0807.rst} | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename peps/{pep-9999.rst => pep-0807.rst} (99%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 42c10c3dcb3..ecd9c0a787f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -679,6 +679,7 @@ peps/pep-0800.rst @JelleZijlstra peps/pep-0801.rst @warsaw peps/pep-0802.rst @AA-Turner peps/pep-0803.rst @encukou +peps/pep-0807.rst @dstufft # ... peps/pep-2026.rst @hugovk # ... diff --git a/peps/pep-9999.rst b/peps/pep-0807.rst similarity index 99% rename from peps/pep-9999.rst rename to peps/pep-0807.rst index 3854c53983b..c7f3b2fc8fd 100644 --- a/peps/pep-9999.rst +++ b/peps/pep-0807.rst @@ -1,4 +1,4 @@ -PEP: 9999 +PEP: 807 Title: Index support for Trusted Publishing Author: William Woodruff Sponsor: Donald Stufft From 0387609fcd21ec6420f094654c086ed05cdfaa02 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 19 Sep 2025 17:32:01 -0400 Subject: [PATCH 07/18] Update peps/pep-0807.rst Co-authored-by: Carol Willing --- peps/pep-0807.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index c7f3b2fc8fd..1ff905d6cc9 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -73,7 +73,7 @@ apply to all parts of this PEP's specification: * All URLs **MUST** have `potentially trustworthy origins `_. - In practice, this means that all URLs **MUST** either use the ``https`` + In practice, this means that all URLs **MUST** use the ``https`` scheme, be some variant of a local loopback (``localhost``, ``127.0.0.1``, etc.), or otherwise be considered *a priori* trustworthy in the context of the interaction (e.g. an internal network). From 60ba4b5d741c4345dbef35774392c0d6160bef4a Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 19 Sep 2025 17:36:52 -0400 Subject: [PATCH 08/18] PEP 807: address feedback Signed-off-by: William Woodruff --- peps/pep-0807.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 1ff905d6cc9..56b087d86d8 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -88,7 +88,7 @@ apply to all parts of this PEP's specification: ``https://upload.example.com/.well-known/pytp/{key}`` can only return URLs with the ``upload.example.com`` host. -* All client-made HTTP requests **SHOULD** have an +* All client requests **SHOULD** have an ``Accept: application/vnd.pypi.pytp.v1+json`` header. In the absence of an ``Accept`` header, the receiving server **MUST** behave as if this header were present. @@ -147,7 +147,7 @@ The discovery mechanism is as follows: - ``audience-endpoint``: a string containing the URL of the OIDC audience endpoint to be used during token exchange. - - ``mint-token-endpoint``: a string containing the URL of the + - ``token-mint-endpoint``: a string containing the URL of the token minting endpoint to be used during token exchange. For the above example, a valid response body would be: @@ -156,7 +156,7 @@ The discovery mechanism is as follows: { "audience-endpoint": "https://upload.example.com/_/oidc/audience", - "mint-token-endpoint": "https://upload.example.com/_/oidc/mint-token" + "token-mint-endpoint": "https://upload.example.com/_/oidc/mint-token" } If the server does not support Trusted Publishing for the given From d4f3cfccf549d59933a4690adbdbc91fb72b8c60 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 24 Sep 2025 10:10:28 -0400 Subject: [PATCH 09/18] Apply suggestions from code review Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- peps/pep-0807.rst | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 56b087d86d8..a3657484b7d 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -7,7 +7,7 @@ Status: Draft Type: Standards Track Topic: Packaging Created: 15-Sep-2025 -Post-History: `08-Aug-2025 `__ +Post-History: `08-Aug-2025 `__, Abstract ======== @@ -35,7 +35,7 @@ to the index. Trusted Publishing was originally designed and enabled on PyPI in 2023 as a non-standard (PyPI-specific) feature, much like the existing -`upload API `_. It has seen +`upload API `__. It has seen widespread adoption in that capacity: over 640,000 files have been published to PyPI using a Trusted Publisher (as of September 2025), and PyPI's design has inspired similar designs in the @@ -63,7 +63,6 @@ This PEP's specification contains two parts: * A *token exchange* mechanism that package upload clients can use to exchange an identity credential for an upload credential. -.. _constraints: Constraints ----------- @@ -71,8 +70,8 @@ Constraints Unless explicitly stated otherwise, the following constraints apply to all parts of this PEP's specification: -* All URLs **MUST** have - `potentially trustworthy origins `_. +* All URLs **MUST** have `potentially trustworthy origins + `__. In practice, this means that all URLs **MUST** use the ``https`` scheme, be some variant of a local loopback (``localhost``, ``127.0.0.1``, etc.), or otherwise be considered *a priori* trustworthy @@ -96,13 +95,12 @@ apply to all parts of this PEP's specification: Receiving servers **SHOULD** respond with a ``406 Not Acceptable`` status code if any other ``Accept`` header is present. -.. _discovery: Trusted Publishing Discovery ---------------------------- All Python package uploading is currently "endpoint driven," in the sense -uploading clients (like twine and uv) are given an upload URL (and +uploading clients (like *twine* and *uv*) are given an upload URL (and **not** merely a domain name). For example, to upload to PyPI, uploading clients are expected to connect @@ -154,10 +152,10 @@ The discovery mechanism is as follows: .. code-block:: json - { - "audience-endpoint": "https://upload.example.com/_/oidc/audience", - "token-mint-endpoint": "https://upload.example.com/_/oidc/mint-token" - } + { + "audience-endpoint": "https://upload.example.com/_/oidc/audience", + "token-mint-endpoint": "https://upload.example.com/_/oidc/mint-token" + } If the server does not support Trusted Publishing for the given upload URL, it **MUST** respond with a ``404 Not Found`` status code. @@ -170,7 +168,7 @@ error code in the 400 or 500 range to indicate an error condition. Non-``200 OK``, non-``404 Not Found`` responses **MAY** include a body which, if present, **MUST** be a JSON object containing an -:ref:`Error Response `. +`Error Response`_. .. _token-exchange: @@ -229,7 +227,7 @@ an HTTP POST request to the *mint token endpoint* obtained during :ref:`discovery `. On success, the server responds with a ``200 OK`` status code and a body -containing a JSON object with the following field: +containing a JSON object with the following fields: - ``token``: a string containing the upload credential. The format of the upload credential is implementation-defined and index-specific. @@ -314,8 +312,10 @@ has been accompanied by a variety of educational resources on adopting Trusted Publishing as an end user, including: * Python Packaging User Guide: :ref:`packaging:trusted-publishing` -* PyPI: `Publishing to PyPI with a Trusted Publisher `_ -* pyOpenSci: `Setup Trusted Publishing for secure and automated publishing via GitHub Actions `_ +* PyPI: `Publishing to PyPI with a Trusted Publisher + `__ +* pyOpenSci: `Setup Trusted Publishing for secure and automated publishing via GitHub Actions + `__ Rejected Ideas ============== @@ -325,8 +325,8 @@ Rejected Ideas This PEP's discovery mechanism uses the ``.well-known`` location scheme defined in :rfc:`8615`. This scheme is widely adopted by machine-to-machine -protocols, including OpenID Connect itself -(for `OpenID Connect Discovery `_). +protocols, including OpenID Connect itself (for `OpenID Connect Discovery +`__). An alternative idea considered was to use a "lateral" discovery mechanism, in which the uploading client would attempt discovery by constructing a @@ -377,7 +377,9 @@ This approach too has downsides: the audience and mint token endpoints, which means significant disruption to existing clients if those endpoints ever need to change. -.. rubric:: Footnotes + +Footnotes +========= .. [#fn-hash] ``shasum -a 256 <<< '/legacy/'`` From 7a0c198c211ff5b7fde1490669b2e6a7f55788f3 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 24 Sep 2025 10:27:09 -0400 Subject: [PATCH 10/18] PEP 807: split rationale/motivation Signed-off-by: William Woodruff --- peps/pep-0807.rst | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index a3657484b7d..5091c7bbef0 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -23,8 +23,8 @@ of Trusted Publishing, while allowing other indices to implement the same scheme in a manner that is discoverable by and interoperable with existing Python package uploading clients. -Rationale and Motivation -======================== +Motivation +========== "Trusted Publishing" is PyPI's term of art for using the `OpenID Connect (OIDC) standard `_ @@ -53,6 +53,33 @@ clients (like `twine `_ and between indices (leading to an accretion of hacks) or continue to reject non-PyPI implementations of Trusted Publishing. +Rationale +========= + +The lack of an existing standard for Trusted Publishing is the primary +rationale for this PEP. + +The design proposed in this PEP closely follows PyPI's existing implementation, +with an added layer of `discovery `__ +that enables uploading clients to determine whether an arbitrary index +supports Trusted Publishing without making PyPI-specific assumptions. + +The rationale for this design is as follows: + +1. The existing (unstandardized) implementation of Trusted Publishign on PyPI + has a proven track record, and is already widely adopted in uploading tools. + A significant deviation from the existing design would introduce + unnecessary compatibility risks. +2. The discovery mechanism proposed in this PEP is designed to be + consistent with existing standards for machine-to-machine protocols, + namely :rfc:`8615` (Well-Known URIs). Additionally, this discovery mechanism + is designed to allow multiple indices to be hosted under a single + domain, which is a common topology for third-party index hosts. + +In sum, the rationale for this PEP is to standardize PyPI's existing +interfaces *and* make them discoverable while allowing index hosts +that don't match PyPI's topology to implement Trusted Publishing. + Specification ============= @@ -168,15 +195,16 @@ error code in the 400 or 500 range to indicate an error condition. Non-``200 OK``, non-``404 Not Found`` responses **MAY** include a body which, if present, **MUST** be a JSON object containing an -`Error Response`_. +`Error Response `_. .. _token-exchange: Trusted Publishing Token Exchange --------------------------------- -Once an uploading client has performed a successful :ref:`discovery ` -flow, it can proceed to perform the actual Trusted Publishing token exchange. +Once an uploading client has performed a successful +`discovery `__ flow, it can proceed to perform +the actual Trusted Publishing token exchange. Token exchange occurs in three steps: @@ -199,7 +227,7 @@ Audience Retrieval To retrieve the expected OIDC audience, the uploading client performs an HTTP GET request to the *audience endpoint* obtained during -:ref:`discovery `. +`discovery `__. On success, the server responds with a ``200 OK`` status code and a body containing a JSON object with the following field: @@ -224,7 +252,7 @@ proceed to mint an upload credential. To mint an upload credential, the uploading client performs an HTTP POST request to the *mint token endpoint* obtained during -:ref:`discovery `. +`discovery `__. On success, the server responds with a ``200 OK`` status code and a body containing a JSON object with the following fields: @@ -356,7 +384,7 @@ However, this approach also has downsides: Another alternative idea considered was the perform "implicit" discovery, similar to what PyPI currently does for Trusted Publishing: instead of an -explicit :ref:`discovery ` step, the uploading client could jump +explicit `discovery `__ step, the uploading client could jump straight to attempting the audience and token minting steps, and handle any errors that arise. From 16c0a17aa93ddf37cef3e14cc6118dac348ce6ca Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 24 Sep 2025 10:33:54 -0400 Subject: [PATCH 11/18] PEP 807: feedback Signed-off-by: William Woodruff --- peps/pep-0807.rst | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 5091c7bbef0..5a6c5ee56a0 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -153,7 +153,7 @@ The discovery mechanism is as follows: producing the *discovery key*. For the above example, the discovery key is - ``af030c06750716b1b35852298fe852b90def13dcbd012a5fe5148470f1206bfc``. [#fn-hash]_ + ``0cace9579789849db6e16d48df183951c8f17582200d84bc93c7678d6c8f78a7``. [#fn-hash]_ 4. The uploading client constructs a *discovery URL* by taking the scheme and authority components (as defined in :rfc:`3986`) @@ -267,7 +267,10 @@ containing a JSON object with the following fields: The server **MUST NOT** issue temporary upload credentials that expire in less than 15 minutes (900 seconds) or more than - 6 hours (21,600 seconds) from the time of the request. [#fn-expires]_ + 6 hours (21,600 seconds) from the time of the request. + + The maximum expiry time of 6 hours is chosen to match common runtime limits + on popular CI/CD providers like GitHub Actions. The uploading client **MAY** use this time (or the minimum specified above) to determine when to refresh the upload credential, if needed. @@ -409,7 +412,15 @@ This approach too has downsides: Footnotes ========= -.. [#fn-hash] ``shasum -a 256 <<< '/legacy/'`` +.. [#fn-hash] + .. code-block:: python + + >>> import hashlib + ... + ... path = "/legacy/" + ... key = hashlib.sha256(path.encode("utf-8")).hexdigest() + ... print(key) + 0cace9579789849db6e16d48df183951c8f17582200d84bc93c7678d6c8f78a7 .. [#fn-oidc] Widely used CI/CD and cloud providers various implement "ambient" OIDC token retrieval mechanisms that aren't standardized. @@ -417,10 +428,6 @@ Footnotes existing components of the Python packaging ecosystem, such as the `id package `_. -.. [#fn-expires] The maximum expiry time of 6 hours is chosen to match - common runtime limits on popular CI/CD providers like - GitHub Actions. - Copyright ========= From fe15971cda572032c755468690d7624f9607e916 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 24 Sep 2025 10:39:47 -0400 Subject: [PATCH 12/18] PEP 807: address more feedback Signed-off-by: William Woodruff --- peps/pep-0807.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 5a6c5ee56a0..1adde751192 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -215,7 +215,7 @@ Token exchange occurs in three steps: provider being used (i.e. the CI/CD or cloud provider that the upload is being performed from). The details of this step are provider-specific, and are out of scope for this PEP. [#fn-oidc]_ -3. The uploading client uses the *mint token endpoint* obtained +3. The uploading client uses the *token minting endpoint* obtained during discovery to exchange the obtained identity credential for a short-lived *upload credential* that can be used to upload to the index. @@ -251,7 +251,7 @@ identity credential from the Trusted Publishing provider, it can proceed to mint an upload credential. To mint an upload credential, the uploading client performs -an HTTP POST request to the *mint token endpoint* obtained during +an HTTP POST request to the *token minting endpoint* obtained during `discovery `__. On success, the server responds with a ``200 OK`` status code and a body @@ -305,6 +305,10 @@ assume that error codes are consistent across different indices, and instead Security Implications ===================== +This PEP seeks to improve the security and transparency of the Python packaging +ecosystem by formally standardizing the Trusted Publishing flow already +used by PyPI. + This PEP does not identify any positive or negative security implications associated with the Trusted Publishing discovery or exchange flows themselves. @@ -393,19 +397,19 @@ handle any errors that arise. The advantage of this approach is simplicity: it eliminates the network round-trip needed for the discovery step, and eliminates the indirection -of obtaining the audience and mint token endpoints from the discovery +of obtaining the audience and token minting endpoints from the discovery response. This approach too has downsides: * It implicitly limits a given domain to a single index/upload implementation, since the implicit "discovery" step on PyPI is to construct the audience - and mint token endpoints against the base domain of the upload URL. + and token minting endpoints against the base domain of the upload URL. This limitation is acceptable in the context of a single index host like PyPI, but does not generalize to other index topologies (like index hosts that provide isolated private indices). * It relies on entirely static endpoint construction rules for - the audience and mint token endpoints, which means significant disruption + the audience and token minting endpoints, which means significant disruption to existing clients if those endpoints ever need to change. From 290582a2890045aeb4b5fd1296e27d49bf36005a Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 24 Sep 2025 10:42:06 -0400 Subject: [PATCH 13/18] PEP 807: prefix refs Signed-off-by: William Woodruff --- peps/pep-0807.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 1adde751192..7734aaf8b67 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -197,7 +197,7 @@ Non-``200 OK``, non-``404 Not Found`` responses **MAY** include a body which, if present, **MUST** be a JSON object containing an `Error Response `_. -.. _token-exchange: +.. _pep807-token-exchange: Trusted Publishing Token Exchange --------------------------------- @@ -220,7 +220,7 @@ Token exchange occurs in three steps: for a short-lived *upload credential* that can be used to upload to the index. -.. _audience-retrieval: +.. _pep807-audience-retrieval: Audience Retrieval ~~~~~~~~~~~~~~~~~~ @@ -238,7 +238,7 @@ On failure, the server **MUST** respond with any standard HTTP error code in the 400 or 500 range to indicate an error condition. Failure responses **MAY** include a body which, if present, **MUST** be a JSON object containing an -:ref:`Error Response `. +:ref:`Error Response `. .. _token-minting: @@ -246,7 +246,7 @@ Token Minting ~~~~~~~~~~~~~ After the uploading client has performed -:ref:`audience retrieval ` and obtained an +:ref:`audience retrieval ` and obtained an identity credential from the Trusted Publishing provider, it can proceed to mint an upload credential. @@ -278,9 +278,9 @@ containing a JSON object with the following fields: On failure, the server **MUST** respond with any standard HTTP error code in the 400 or 500 range to indicate an error condition. Failure responses **MUST** include a body which, if present, -**MUST** be a JSON object containing an :ref:`Error Response `. +**MUST** be a JSON object containing an :ref:`Error Response `. -.. _error-responses: +.. _pep807-error-responses: Error Responses --------------- From f75ed044f39ed2b012212a74eef7bcbe8cde410b Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 24 Sep 2025 10:56:40 -0400 Subject: [PATCH 14/18] PEP 807: update TP usage stats/qualify with total file count Signed-off-by: William Woodruff --- peps/pep-0807.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 7734aaf8b67..0e3cd897a56 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -36,9 +36,10 @@ to the index. Trusted Publishing was originally designed and enabled on PyPI in 2023 as a non-standard (PyPI-specific) feature, much like the existing `upload API `__. It has seen -widespread adoption in that capacity: over 640,000 files have been published -to PyPI using a Trusted Publisher (as of September 2025), and PyPI's -design has inspired similar designs in the +widespread adoption in that capacity: over one million files have been published +to PyPI using a Trusted Publisher (as of September 2025), representing +approximately one every eight files uploaded to PyPI since becoming +available. Additionally, PyPI's design has inspired similar designs in the `Rust (crates.io) `_, `Ruby (RubyGems) `_, and `JavaScript (npm) `_ ecosystems. From 808e64d3442356f18799272991a3f0bed3f7ce0f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 29 Sep 2025 10:01:58 -0400 Subject: [PATCH 15/18] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- peps/pep-0807.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 0e3cd897a56..d4509735604 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -6,7 +6,7 @@ PEP-Delegate: Donald Stufft Status: Draft Type: Standards Track Topic: Packaging -Created: 15-Sep-2025 +Created: 19-Sep-2025 Post-History: `08-Aug-2025 `__, Abstract @@ -38,7 +38,7 @@ a non-standard (PyPI-specific) feature, much like the existing `upload API `__. It has seen widespread adoption in that capacity: over one million files have been published to PyPI using a Trusted Publisher (as of September 2025), representing -approximately one every eight files uploaded to PyPI since becoming +approximately one in every eight files uploaded to PyPI since becoming available. Additionally, PyPI's design has inspired similar designs in the `Rust (crates.io) `_, `Ruby (RubyGems) `_, and @@ -49,7 +49,7 @@ impediment for adoption: third-party indices (i.e. those other than PyPI and TestPyPI) cannot easily implement Trusted Publishing without referencing PyPI's unstandardized design. This in turn poses a long-term maturity risk similar to that of the unstandardized upload API: package upload -clients (like `twine `_ and +clients (like `Twine `_ and `uv `_) must either accept behavioral differences between indices (leading to an accretion of hacks) or continue to reject non-PyPI implementations of Trusted Publishing. @@ -427,11 +427,11 @@ Footnotes ... print(key) 0cace9579789849db6e16d48df183951c8f17582200d84bc93c7678d6c8f78a7 -.. [#fn-oidc] Widely used CI/CD and cloud providers various implement "ambient" +.. [#fn-oidc] Widely used CI/CD and cloud providers variously implement "ambient" OIDC token retrieval mechanisms that aren't standardized. These various mechanisms are currently abstracted over by existing components of the Python packaging ecosystem, - such as the `id package `_. + such as the :pypi:`id` package. Copyright ========= From 44eb1bc7ca3d7704cbb58c80514b0b203cb78d0e Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 29 Sep 2025 10:02:30 -0400 Subject: [PATCH 16/18] fix link Signed-off-by: William Woodruff --- peps/pep-0807.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index d4509735604..3a13d158bd1 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -99,7 +99,7 @@ Unless explicitly stated otherwise, the following constraints apply to all parts of this PEP's specification: * All URLs **MUST** have `potentially trustworthy origins - `__. + `__. In practice, this means that all URLs **MUST** use the ``https`` scheme, be some variant of a local loopback (``localhost``, ``127.0.0.1``, etc.), or otherwise be considered *a priori* trustworthy From f9840d70edcbd9cfcb7233f4daec004e66a2295b Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:59:49 +0100 Subject: [PATCH 17/18] Fix internal references --- peps/pep-0807.rst | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/peps/pep-0807.rst b/peps/pep-0807.rst index 3a13d158bd1..881dc1914bd 100644 --- a/peps/pep-0807.rst +++ b/peps/pep-0807.rst @@ -61,7 +61,7 @@ The lack of an existing standard for Trusted Publishing is the primary rationale for this PEP. The design proposed in this PEP closely follows PyPI's existing implementation, -with an added layer of `discovery `__ +with an added layer of `discovery `__ that enables uploading clients to determine whether an arbitrary index supports Trusted Publishing without making PyPI-specific assumptions. @@ -196,15 +196,13 @@ error code in the 400 or 500 range to indicate an error condition. Non-``200 OK``, non-``404 Not Found`` responses **MAY** include a body which, if present, **MUST** be a JSON object containing an -`Error Response `_. - -.. _pep807-token-exchange: +`Error Response `__. Trusted Publishing Token Exchange --------------------------------- Once an uploading client has performed a successful -`discovery `__ flow, it can proceed to perform +`discovery `__ flow, it can proceed to perform the actual Trusted Publishing token exchange. Token exchange occurs in three steps: @@ -221,14 +219,12 @@ Token exchange occurs in three steps: for a short-lived *upload credential* that can be used to upload to the index. -.. _pep807-audience-retrieval: - Audience Retrieval ~~~~~~~~~~~~~~~~~~ To retrieve the expected OIDC audience, the uploading client performs an HTTP GET request to the *audience endpoint* obtained during -`discovery `__. +`discovery `__. On success, the server responds with a ``200 OK`` status code and a body containing a JSON object with the following field: @@ -239,21 +235,19 @@ On failure, the server **MUST** respond with any standard HTTP error code in the 400 or 500 range to indicate an error condition. Failure responses **MAY** include a body which, if present, **MUST** be a JSON object containing an -:ref:`Error Response `. - -.. _token-minting: +`Error Response `__. Token Minting ~~~~~~~~~~~~~ After the uploading client has performed -:ref:`audience retrieval ` and obtained an +`audience retrieval`_ and obtained an identity credential from the Trusted Publishing provider, it can proceed to mint an upload credential. To mint an upload credential, the uploading client performs an HTTP POST request to the *token minting endpoint* obtained during -`discovery `__. +`discovery `__. On success, the server responds with a ``200 OK`` status code and a body containing a JSON object with the following fields: @@ -279,9 +273,7 @@ containing a JSON object with the following fields: On failure, the server **MUST** respond with any standard HTTP error code in the 400 or 500 range to indicate an error condition. Failure responses **MUST** include a body which, if present, -**MUST** be a JSON object containing an :ref:`Error Response `. - -.. _pep807-error-responses: +**MUST** be a JSON object containing an `Error Response `__. Error Responses --------------- @@ -392,7 +384,7 @@ However, this approach also has downsides: Another alternative idea considered was the perform "implicit" discovery, similar to what PyPI currently does for Trusted Publishing: instead of an -explicit `discovery `__ step, the uploading client could jump +explicit `discovery `__ step, the uploading client could jump straight to attempting the audience and token minting steps, and handle any errors that arise. @@ -418,14 +410,16 @@ Footnotes ========= .. [#fn-hash] - .. code-block:: python - - >>> import hashlib - ... - ... path = "/legacy/" - ... key = hashlib.sha256(path.encode("utf-8")).hexdigest() - ... print(key) - 0cace9579789849db6e16d48df183951c8f17582200d84bc93c7678d6c8f78a7 + + The discovery key may be computed thus: + + .. code-block:: pycon + + >>> import hashlib + >>> path = "/legacy/" + >>> key = hashlib.sha256(path.encode("utf-8")).hexdigest() + >>> print(key) + 0cace9579789849db6e16d48df183951c8f17582200d84bc93c7678d6c8f78a7 .. [#fn-oidc] Widely used CI/CD and cloud providers variously implement "ambient" OIDC token retrieval mechanisms that aren't standardized. From 3c694ab5aeeba4b0e67a34922100a70aacf95a1e Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 29 Sep 2025 11:12:23 -0400 Subject: [PATCH 18/18] Update .github/CODEOWNERS Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 30aa233c01c..44886418e3e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -680,6 +680,7 @@ peps/pep-0801.rst @warsaw peps/pep-0802.rst @AA-Turner peps/pep-0803.rst @encukou peps/pep-0804.rst @pradyunsg +# ... peps/pep-0807.rst @dstufft # ... peps/pep-2026.rst @hugovk