CINXE.COM

Internals and Technical Details - PyPI Docs

<!doctype html> <html lang="en" class="no-js"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <link rel="prev" href="../troubleshooting/"> <link rel="next" href="../../attestations/"> <link rel="icon" href="../../assets/favicon.ico"> <meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.5.48"> <title>Internals and Technical Details - PyPI Docs</title> <link rel="stylesheet" href="../../assets/stylesheets/main.6f8fc17f.min.css"> <link rel="stylesheet" href="../../assets/stylesheets/palette.06af60db.min.css"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback"> <style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style> <link rel="stylesheet" href="../../stylesheets/extra.css"> <script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script> <script async type="text/javascript" src="/_/static/javascript/readthedocs-addons.js"></script><meta name="readthedocs-project-slug" content="docspypiorg" /><meta name="readthedocs-version-slug" content="latest" /><meta name="readthedocs-resolver-filename" content="/trusted-publishers/internals/" /><meta name="readthedocs-http-status" content="200" /></head> <body dir="ltr" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo"> <input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off"> <input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off"> <label class="md-overlay" for="__drawer"></label> <div data-md-component="skip"> <a href="#internals-and-technical-details" class="md-skip"> Skip to content </a> </div> <div data-md-component="announce"> </div> <header class="md-header md-header--shadow" data-md-component="header"> <nav class="md-header__inner md-grid" aria-label="Header"> <a href="https://pypi.org" title="PyPI Docs" class="md-header__button md-logo" aria-label="PyPI Docs" data-md-component="logo"> <img src="../../assets/logo.png" alt="logo"> </a> <label class="md-header__button md-icon" for="__drawer"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg> </label> <div class="md-header__title" data-md-component="header-title"> <div class="md-header__ellipsis"> <div class="md-header__topic"> <span class="md-ellipsis"> PyPI Docs </span> </div> <div class="md-header__topic" data-md-component="header-topic"> <span class="md-ellipsis"> Internals and Technical Details </span> </div> </div> </div> <form class="md-header__option" data-md-component="palette"> <input class="md-option" data-md-color-media="(prefers-color-scheme: light)" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to Dark mode" type="radio" name="__palette" id="__palette_0"> <label class="md-header__button md-icon" title="Switch to Dark mode" for="__palette_1" hidden> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg> </label> <input class="md-option" data-md-color-media="(prefers-color-scheme: dark)" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to Light mode" type="radio" name="__palette" id="__palette_1"> <label class="md-header__button md-icon" title="Switch to Light mode" for="__palette_0" hidden> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg> </label> </form> <script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script> <label class="md-header__button md-icon" for="__search"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg> </label> <div class="md-search" data-md-component="search" role="dialog"> <label class="md-search__overlay" for="__search"></label> <div class="md-search__inner" role="search"> <form class="md-search__form" name="search"> <input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required> <label class="md-search__icon md-icon" for="__search"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg> </label> <nav class="md-search__options" aria-label="Search"> <button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> </button> </nav> </form> <div class="md-search__output"> <div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix> <div class="md-search-result" data-md-component="search-result"> <div class="md-search-result__meta"> Initializing search </div> <ol class="md-search-result__list" role="presentation"></ol> </div> </div> </div> </div> </div> <div class="md-header__source"> <a href="https://github.com/pypi/warehouse" title="Go to repository" class="md-source" data-md-component="source"> <div class="md-source__icon md-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.--><path d="M439.55 236.05 244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81"/></svg> </div> <div class="md-source__repository"> GitHub </div> </a> </div> </nav> </header> <div class="md-container" data-md-component="container"> <main class="md-main" data-md-component="main"> <div class="md-main__inner md-grid"> <div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" > <div class="md-sidebar__scrollwrap"> <div class="md-sidebar__inner"> <nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0"> <label class="md-nav__title" for="__drawer"> <a href="https://pypi.org" title="PyPI Docs" class="md-nav__button md-logo" aria-label="PyPI Docs" data-md-component="logo"> <img src="../../assets/logo.png" alt="logo"> </a> PyPI Docs </label> <div class="md-nav__source"> <a href="https://github.com/pypi/warehouse" title="Go to repository" class="md-source" data-md-component="source"> <div class="md-source__icon md-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.--><path d="M439.55 236.05 244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81"/></svg> </div> <div class="md-source__repository"> GitHub </div> </a> </div> <ul class="md-nav__list" data-md-scrollfix> <li class="md-nav__item"> <a href="../.." class="md-nav__link"> <span class="md-ellipsis"> Welcome to PyPI User Documentation </span> </a> </li> <li class="md-nav__item md-nav__item--nested"> <input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" > <label class="md-nav__link" for="__nav_2" id="__nav_2_label" tabindex="0"> <span class="md-ellipsis"> Organization Accounts </span> <span class="md-nav__icon md-icon"></span> </label> <nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="false"> <label class="md-nav__title" for="__nav_2"> <span class="md-nav__icon md-icon"></span> Organization Accounts </label> <ul class="md-nav__list" data-md-scrollfix> <li class="md-nav__item"> <a href="../../organization-accounts/" class="md-nav__link"> <span class="md-ellipsis"> About </span> </a> </li> <li class="md-nav__item"> <a href="../../organization-accounts/org-acc-faq/" class="md-nav__link"> <span class="md-ellipsis"> FAQs </span> </a> </li> <li class="md-nav__item"> <a href="../../organization-accounts/roles-entities/" class="md-nav__link"> <span class="md-ellipsis"> Roles and Entities </span> </a> </li> <li class="md-nav__item md-nav__item--nested"> <input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_4" > <label class="md-nav__link" for="__nav_2_4" id="__nav_2_4_label" tabindex="0"> <span class="md-ellipsis"> Actions </span> <span class="md-nav__icon md-icon"></span> </label> <nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_4_label" aria-expanded="false"> <label class="md-nav__title" for="__nav_2_4"> <span class="md-nav__icon md-icon"></span> Actions </label> <ul class="md-nav__list" data-md-scrollfix> <li class="md-nav__item"> <a href="../../organization-accounts/actions/billing-actions/" class="md-nav__link"> <span class="md-ellipsis"> Billing Actions </span> </a> </li> <li class="md-nav__item"> <a href="../../organization-accounts/actions/org-actions/" class="md-nav__link"> <span class="md-ellipsis"> Organization Actions </span> </a> </li> <li class="md-nav__item"> <a href="../../organization-accounts/actions/project-actions/" class="md-nav__link"> <span class="md-ellipsis"> Project Actions </span> </a> </li> <li class="md-nav__item"> <a href="../../organization-accounts/actions/team-actions/" class="md-nav__link"> <span class="md-ellipsis"> Team Actions </span> </a> </li> </ul> </nav> </li> <li class="md-nav__item"> <a href="../../organization-accounts/pricing-and-payments/" class="md-nav__link"> <span class="md-ellipsis"> Pricing and Payments </span> </a> </li> </ul> </nav> </li> <li class="md-nav__item md-nav__item--active md-nav__item--nested"> <input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_3" checked> <label class="md-nav__link" for="__nav_3" id="__nav_3_label" tabindex="0"> <span class="md-ellipsis"> Trusted Publishers </span> <span class="md-nav__icon md-icon"></span> </label> <nav class="md-nav" data-md-level="1" aria-labelledby="__nav_3_label" aria-expanded="true"> <label class="md-nav__title" for="__nav_3"> <span class="md-nav__icon md-icon"></span> Trusted Publishers </label> <ul class="md-nav__list" data-md-scrollfix> <li class="md-nav__item"> <a href="../" class="md-nav__link"> <span class="md-ellipsis"> Getting Started </span> </a> </li> <li class="md-nav__item"> <a href="../adding-a-publisher/" class="md-nav__link"> <span class="md-ellipsis"> Adding a Trusted Publisher to an Existing PyPI Project </span> </a> </li> <li class="md-nav__item"> <a href="../creating-a-project-through-oidc/" class="md-nav__link"> <span class="md-ellipsis"> Creating a PyPI Project with a Trusted Publisher </span> </a> </li> <li class="md-nav__item"> <a href="../using-a-publisher/" class="md-nav__link"> <span class="md-ellipsis"> Publishing with a Trusted Publisher </span> </a> </li> <li class="md-nav__item"> <a href="../security-model/" class="md-nav__link"> <span class="md-ellipsis"> Security Model and Considerations </span> </a> </li> <li class="md-nav__item"> <a href="../troubleshooting/" class="md-nav__link"> <span class="md-ellipsis"> Troubleshooting </span> </a> </li> <li class="md-nav__item md-nav__item--active"> <input class="md-nav__toggle md-toggle" type="checkbox" id="__toc"> <label class="md-nav__link md-nav__link--active" for="__toc"> <span class="md-ellipsis"> Internals and Technical Details </span> <span class="md-nav__icon md-icon"></span> </label> <a href="./" class="md-nav__link md-nav__link--active"> <span class="md-ellipsis"> Internals and Technical Details </span> </a> <nav class="md-nav md-nav--secondary" aria-label="Table of contents"> <label class="md-nav__title" for="__toc"> <span class="md-nav__icon md-icon"></span> Table of contents </label> <ul class="md-nav__list" data-md-component="toc" data-md-scrollfix> <li class="md-nav__item"> <a href="#how-trusted-publishing-works" class="md-nav__link"> <span class="md-ellipsis"> How Trusted Publishing works </span> </a> </li> <li class="md-nav__item"> <a href="#qa" class="md-nav__link"> <span class="md-ellipsis"> Q&amp;A </span> </a> <nav class="md-nav" aria-label="Q&A"> <ul class="md-nav__list"> <li class="md-nav__item"> <a href="#why-does-trusted-publishing-use-a-two-phase-token-exchange" class="md-nav__link"> <span class="md-ellipsis"> Why does Trusted Publishing use a "two-phase" token exchange? </span> </a> </li> <li class="md-nav__item"> <a href="#why-is-the-pypi-project-to-publisher-relationship-many-many" class="md-nav__link"> <span class="md-ellipsis"> Why is the PyPI project to publisher relationship "many-many"? </span> </a> </li> <li class="md-nav__item"> <a href="#what-are-account-resurrection-attacks-and-how-does-pypi-protect-against-them" class="md-nav__link"> <span class="md-ellipsis"> What are account resurrection attacks, and how does PyPI protect against them? </span> </a> </li> <li class="md-nav__item"> <a href="#how-do-i-become-a-trusted-publishing-provider" class="md-nav__link"> <span class="md-ellipsis"> How do I become a Trusted Publishing provider? </span> </a> </li> </ul> </nav> </li> </ul> </nav> </li> </ul> </nav> </li> <li class="md-nav__item md-nav__item--nested"> <input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_4" > <label class="md-nav__link" for="__nav_4" id="__nav_4_label" tabindex="0"> <span class="md-ellipsis"> Digital Attestations </span> <span class="md-nav__icon md-icon"></span> </label> <nav class="md-nav" data-md-level="1" aria-labelledby="__nav_4_label" aria-expanded="false"> <label class="md-nav__title" for="__nav_4"> <span class="md-nav__icon md-icon"></span> Digital Attestations </label> <ul class="md-nav__list" data-md-scrollfix> <li class="md-nav__item"> <a href="../../attestations/" class="md-nav__link"> <span class="md-ellipsis"> Introduction </span> </a> </li> <li class="md-nav__item"> <a href="../../attestations/producing-attestations/" class="md-nav__link"> <span class="md-ellipsis"> Producing attestations </span> </a> </li> <li class="md-nav__item"> <a href="../../attestations/consuming-attestations/" class="md-nav__link"> <span class="md-ellipsis"> Consuming attestations </span> </a> </li> <li class="md-nav__item"> <a href="../../attestations/publish/v1/" class="md-nav__link"> <span class="md-ellipsis"> PyPI Publish Attestation (v1) </span> </a> </li> <li class="md-nav__item"> <a href="../../attestations/security-model/" class="md-nav__link"> <span class="md-ellipsis"> Security Model and Considerations </span> </a> </li> </ul> </nav> </li> <li class="md-nav__item"> <a href="../../project_metadata/" class="md-nav__link"> <span class="md-ellipsis"> Project Metadata </span> </a> </li> <li class="md-nav__item md-nav__item--nested"> <input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_6" > <label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0"> <span class="md-ellipsis"> APIs and Datasets </span> <span class="md-nav__icon md-icon"></span> </label> <nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false"> <label class="md-nav__title" for="__nav_6"> <span class="md-nav__icon md-icon"></span> APIs and Datasets </label> <ul class="md-nav__list" data-md-scrollfix> <li class="md-nav__item"> <a href="../../api/" class="md-nav__link"> <span class="md-ellipsis"> Introduction </span> </a> </li> <li class="md-nav__item"> <a href="../../api/index-api/" class="md-nav__link"> <span class="md-ellipsis"> Index API </span> </a> </li> <li class="md-nav__item"> <a href="../../api/json/" class="md-nav__link"> <span class="md-ellipsis"> JSON API </span> </a> </li> <li class="md-nav__item"> <a href="../../api/upload/" class="md-nav__link"> <span class="md-ellipsis"> Upload API </span> </a> </li> <li class="md-nav__item"> <a href="../../api/integrity/" class="md-nav__link"> <span class="md-ellipsis"> Integrity API </span> </a> </li> <li class="md-nav__item"> <a href="../../api/stats/" class="md-nav__link"> <span class="md-ellipsis"> Stats API </span> </a> </li> <li class="md-nav__item"> <a href="../../api/bigquery/" class="md-nav__link"> <span class="md-ellipsis"> BigQuery Datasets </span> </a> </li> <li class="md-nav__item"> <a href="../../api/feeds/" class="md-nav__link"> <span class="md-ellipsis"> RSS Feeds </span> </a> </li> <li class="md-nav__item"> <a href="../../api/secrets/" class="md-nav__link"> <span class="md-ellipsis"> Secret reporting API </span> </a> </li> </ul> </nav> </li> </ul> </nav> </div> </div> </div> <div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" > <div class="md-sidebar__scrollwrap"> <div class="md-sidebar__inner"> <nav class="md-nav md-nav--secondary" aria-label="Table of contents"> <label class="md-nav__title" for="__toc"> <span class="md-nav__icon md-icon"></span> Table of contents </label> <ul class="md-nav__list" data-md-component="toc" data-md-scrollfix> <li class="md-nav__item"> <a href="#how-trusted-publishing-works" class="md-nav__link"> <span class="md-ellipsis"> How Trusted Publishing works </span> </a> </li> <li class="md-nav__item"> <a href="#qa" class="md-nav__link"> <span class="md-ellipsis"> Q&amp;A </span> </a> <nav class="md-nav" aria-label="Q&A"> <ul class="md-nav__list"> <li class="md-nav__item"> <a href="#why-does-trusted-publishing-use-a-two-phase-token-exchange" class="md-nav__link"> <span class="md-ellipsis"> Why does Trusted Publishing use a "two-phase" token exchange? </span> </a> </li> <li class="md-nav__item"> <a href="#why-is-the-pypi-project-to-publisher-relationship-many-many" class="md-nav__link"> <span class="md-ellipsis"> Why is the PyPI project to publisher relationship "many-many"? </span> </a> </li> <li class="md-nav__item"> <a href="#what-are-account-resurrection-attacks-and-how-does-pypi-protect-against-them" class="md-nav__link"> <span class="md-ellipsis"> What are account resurrection attacks, and how does PyPI protect against them? </span> </a> </li> <li class="md-nav__item"> <a href="#how-do-i-become-a-trusted-publishing-provider" class="md-nav__link"> <span class="md-ellipsis"> How do I become a Trusted Publishing provider? </span> </a> </li> </ul> </nav> </li> </ul> </nav> </div> </div> </div> <div class="md-content" data-md-component="content"> <article class="md-content__inner md-typeset"> <a href="https://github.com/pypi/warehouse/blob/main/docs/user/trusted-publishers/internals.md" title="Edit this page" class="md-content__button md-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg> </a> <h1 id="internals-and-technical-details">Internals and Technical Details</h1> <div class="admonition note"> <p class="admonition-title">Note</p> <p>This page is <strong>not useful</strong> to <em>users</em> of trusted publishers!</p> <p>It's intended primarily for PyPI developers and developers of other package indices looking to support similar authentication models.</p> </div> <h2 id="how-trusted-publishing-works">How Trusted Publishing works</h2> <p>PyPI's Trusted Publishing functionality is built on top of <a href="https://openid.net/connect/">OpenID Connect</a>, or "OIDC" for short.</p> <p>OIDC gives <em>services</em> (like GitHub Actions) a way to <em>provably identify</em> themselves: an authorized entity (such as a GitHub user, or an automated workflow) can present an <em>OIDC token</em> to a third-party service. That party service can then verify the token and determine whether it's authorized to perform some other action.</p> <p>In the context of Trusted Publishing, the machinery is as follows:</p> <ul> <li> <p><em>OIDC identity providers</em> like GitHub ("providers" for short) generate OIDC tokens that contain scoped <em>claims</em>, which convey appropriate authorization scopes.</p> <ul> <li>For example, the <code>repo</code> claim might be bound to the value <code>octo-org/example</code>, indicating that the token should be authorized to access resources for which <code>octo-org/example</code> is a valid repository.</li> </ul> </li> <li> <p><em>Trusted Publishers</em> are pieces of configuration on PyPI that tell PyPI <em>which</em> OIDC providers to trust, and <em>when</em> (i.e., which specific set of claims to consider valid).</p> <ul> <li> <p>For example, a Trusted Publisher configuration for GitHub Actions might specify <code>repo: octo-org/example</code> with <code>workflow: release.yml</code> and <code>environment: pypi</code>, indicating that a presented OIDC token <strong>must</strong> contain exactly those claims to be considered valid.</p> </li> <li> <p>When applicable, PyPI also checks claims that prevent <a href="./#what-are-account-resurrection-attacks-and-how-does-pypi-protect-against-them">account resurrection attacks</a>. For example, with GitHub as the OIDC IdP, PyPI checks the <code>repository_owner_id</code> claim.</p> </li> </ul> </li> <li> <p><em>Token exchange</em> is how PyPI converts OIDC tokens into credentials (PyPI API tokens) that can be used to authenticate against the package upload endpoint.</p> <ul> <li>Token exchange boils down to a matching process between a presented OIDC token and every Trusted Publisher currently configured on PyPI: the token's signature is first verified (to ensure that it's actually coming from the expected provider), and then its claims are matched against zero or more projects with registered Trusted Publishers.</li> </ul> <p>If the OIDC token corresponds to one or more Trusted Publishers, then a short-lived (15 minute) PyPI API token is issued. This API token is scoped to every project with a matching Trusted Publisher, meaning that it can be used to upload to multiple projects (if so configured).</p> </li> </ul> <p>If everything goes correctly, a successful Trusted Publishing flow results in a short-lived PyPI API token <em>without any user interaction</em>, which in turn offers security and ergonomic benefits to PyPI packagers: users no longer have to worry about token provisioning or revocation.</p> <h2 id="qa">Q&amp;A</h2> <h3 id="why-does-trusted-publishing-use-a-two-phase-token-exchange">Why does Trusted Publishing use a "two-phase" token exchange?</h3> <p>As noted above, Trusted Publishing uses a "token exchange" mechanism, which happens in two phases:</p> <ol> <li> <p>The uploading client presents an OIDC token, which PyPI verifies. If valid, PyPI responds with a valid and appropriately scoped PyPI API token.</p> </li> <li> <p>The uploading client takes the valid PyPI API token that it was given and uses it as normal.</p> </li> </ol> <p>In principle, this is more complicated than necessary: PyPI could instead take the OIDC token <em>directly</em> and treat it as a special case during API token handling, skipping a network round-trip between the uploading client and the package index.</p> <p>While conceptually simpler, a "one-phase" token exchange presents problems of its own:</p> <ol> <li> <p><em>Isolation of concerns</em>: conceptually, an OIDC token is an <em>externally issued</em> token, with external concerns: it has failure modes that aren't internal to PyPI itself (e.g. a failure of the issuing identity provider to sign correctly).</p> <p>Keeping these concerns isolated from PyPI's actual business logic ensures that they remain encapsulated and do not impose design or security constraints on PyPI itself (e.g., mandating that PyPI use OIDC tokens in places where they are a poor fit).</p> </li> <li> <p><em>Complications to existing authentication and authorization logic</em>: PyPI has a large pre-existing body of AuthN and AuthZ code. Most of the existing code for API tokens is directly adapted to the PyPI API token format, which is based on <a href="https://en.wikipedia.org/wiki/Macaroons_(computer_science)">Macaroons</a>.</p> <p>Handling OIDC tokens (which are <a href="https://en.wikipedia.org/wiki/JSON_Web_Token">JSON Web Tokens</a> under the hood) would have required significant duplication of existing codepaths, which in turn means an increased testing (and vulnerability) surface. By exchanging OIDC tokens for API tokens in PyPI's existing format, our implementation could reuse our existing (and well-tested) codepaths without any significant changes.</p> </li> <li> <p><em>Automatic secret scanning and revocation challenges</em>: PyPI is a partner in <a href="https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning">GitHub's secret scanning system</a>, which allows PyPI to automatically revoke PyPI API tokens that are accidentally leaked in public repositories.</p> </li> </ol> <p>This system relies on PyPI tokens having a unique prefix: they all begin with <code>pypi-</code>. Without that prefix, GitHub would be unable to efficiently scan public repositories for tokens.</p> <p>OIDC tokens are issued by independent providers, meaning that PyPI has no ability to impose a <code>pypi-</code> prefix on them. Moreover, OIDC tokens are strictly defined as <a href="https://en.wikipedia.org/wiki/JSON_Web_Token">JSON Web Tokens</a>, meaning that they appear as mostly unstructured random characters. This makes them difficult to scan for. Finally, even an effective scanner for JWTs would need to report every compromised JWT to both its issuer (e.g., GitHub itself) <em>and</em> its consumer (e.g., PyPI), introducing complexity and additional failure modes during revocation.</p> <p>Exchanging OIDC tokens for PyPI API tokens completely sidesteps all of these problems.</p> <p>While these reasons are documented for PyPI, they are likely some of the same reasons why other "federated" consumers of OIDC (like cloud providers) do similar "two-phase" exchange mechanisms.</p> <h3 id="why-is-the-pypi-project-to-publisher-relationship-many-many">Why is the PyPI project to publisher relationship "many-many"?</h3> <p>If you play around with Trusted Publishing on PyPI, you'll notice that PyPI projects can have multiple publishers, and individual publishers can be registered to multiple projects.</p> <p>This is a "many-many" relationship between PyPI projects and their Trusted Publishers which, like "two-phase" exchange, seems more complicated in principle than necessary.</p> <p>In practice, this many-many relationship addresses publishing patterns commonly used by the Python packaging community:</p> <ol> <li><em>One publisher, many projects</em>: it's not uncommon for several related PyPI projects to share a single source repository. Moreover, it's not uncommon for several related PyPI projects to share the same release workflow, due to tandem releases (e.g., a simultaneous release of a library package and its corresponding CLI tool).</li> </ol> <p>Trusted Publishing's design accommodates this use case: maintainers can use the same <code>release.yml</code> workflow for all of their packages, rather than having to split it up by packages.</p> <ol> <li><em>One project, many publishers</em>: PyPI contains a large number of built distributions ("wheels"), some of which are "binary wheels" that contain processor, operating system, or platform-specific binaries.</li> </ol> <p>Because these binaries are specific to individual platforms, they frequently must be built on separate platforms, often on dedicated builder configurations for each platform.</p> <p>From there, it is common to have each individual platform builder also perform releases for that platform: Linux-specific wheels are uploaded by the Linux builder, etc.</p> <p>This is arguably <strong>not best practice</strong>, in terms of reliability and isolation of concerns: the best practice would be to <em>collect</em> all platform-specific builds in a final platform-agnostic publishing step, which could then be a single publisher.</p> <p>However, in the interest of getting Trusted Publishers into users' hands without requiring them to make significant unrelated changes to the builds, the Trusted Publishing feature allows users to register multiple publishers against a single project. Consequently, <code>sampleproject</code> can be published from both <code>release-linux.yml</code> and <code>release-macos.yml</code> without needing to be refactored into a single <code>release.yml</code>.</p> <h3 id="what-are-account-resurrection-attacks-and-how-does-pypi-protect-against-them">What are account resurrection attacks, and how does PyPI protect against them?</h3> <p>Some OIDC providers support username changes, so a claim of <code>repository_owner: octo-org</code> might not necessarily refer to the same <code>octo-org</code> that a user initially authorized in a Trusted Publisher configuration.</p> <p>If a repository owner changes their username or deletes their account, a malicious actor may be able to take the freed username and create their own repositories under the original trusted name. This is known as an <em>account resurrection attack</em>.</p> <p>To solve this issue for GitHub-based publishers, PyPI always checks the <code>repository_owner_id</code> claim. This claim attests to the ID of the repository owner, which is stable and permanent unlike usernames. When a Trusted Publisher is configured, PyPI looks up the configured username's ID and stores it. During API token minting, PyPI checks the <code>repository_owner_id</code> claim against the stored ID and fails if they don't match. Through this process, only the original GitHub user remains authorized to publish to their PyPI projects, even if they change their username or delete their account.</p> <h3 id="how-do-i-become-a-trusted-publishing-provider">How do I become a Trusted Publishing provider?</h3> <p>If you are an operator of a hosted compute service or are a CI provider, you may want PyPI to support your platform or service as a Trusted Publisher.</p> <p>There are three primary requirements for adding a new Trusted Publisher platform to PyPI:</p> <ol> <li> <p><strong>OIDC Identity Provider</strong>: Trusted Publishing relies on a given platform operating an identity provider using the <a href="https://openid.net/connect/">OpenID Connect</a> specification. Other forms of identity providers are not eligible.</p> </li> <li> <p><strong>OIDC Discovery</strong>: Your OIDC IdP <strong>must</strong> support <a href="https://openid.net/specs/openid-connect-discovery-1_0.html">OpenID Connect Discovery</a>, i.e. serve a <code>https://{iss}/.well-known/openid-configuration</code> endpoint that contains, at minimum:</p> <ul> <li><code>jwks_uri</code>: a URL to the JSON Web Key (JWK) set used by the IdP for signing;</li> <li><code>claims_supported</code>: an array of claim names that PyPI should expect to see inside OIDC credentials issued by the IdP</li> </ul> <p>(where <code>iss</code> is the value of the <code>iss</code> claim in a provided OIDC token)</p> <p>IdPs that cannot provide discovery or these fields within the discovery response are not eligible.</p> </li> <li> <p><strong>Reasonable OIDC claim set</strong>: Your OIDC claims must sufficiently identify a unique workload that may be scoped to a PyPI project or set of projects. These claims must support the prevention of resurrection attacks, meaning that reusable or mutatable claims (such as a repository or project name) must be backed by an immutable and guaranteed unique identifier (such as a numeric ID). Additionally, the claimset must support a customizable <code>aud</code> claim that can be set to the value <code>pypi</code>. Identity providers that don't meet this standard for claims are not eligible.</p> </li> <li> <p><strong>Reliability &amp; notability</strong>: The effort necessary to integrate with a new Trusted Publisher is not exceptional, but not trivial either. In the interest of making the best use of PyPI's finite resources, we only plan to support platforms that have a reasonable level of usage among PyPI users for publishing. Additionally, we have high standards for overall reliability and security in the operation of a supported Identity Provider: in practice, this means that a home-grown or personal use IdP will not be eligible.</p> </li> </ol> <p>If you feel as if your platform sufficiently meets these requirements, we encourage you to <a href="https://github.com/pypi/warehouse/issues/new?template=feature-request.md">file an issue</a> requesting Trusted Publisher support for your platform or service.</p> </article> </div> <script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script> </div> </main> <footer class="md-footer"> <div class="md-footer-meta md-typeset"> <div class="md-footer-meta__inner md-grid"> <div class="md-copyright"> Made with <a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener"> Material for MkDocs </a> </div> <div class="md-social"> <a href="https://github.com/pypi" target="_blank" rel="noopener" title="github.com" class="md-social__link"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!--! Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.--><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg> </a> <a href="https://twitter.com/pypi" target="_blank" rel="noopener" title="twitter.com" class="md-social__link"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.--><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253"/></svg> </a> </div> </div> </div> </footer> </div> <div class="md-dialog" data-md-component="dialog"> <div class="md-dialog__inner md-typeset"></div> </div> <script id="__config" type="application/json">{"base": "../..", "features": ["content.action.edit"], "search": "../../assets/javascripts/workers/search.6ce7567c.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}}</script> <script src="../../assets/javascripts/bundle.83f73b43.min.js"></script> </body> </html>

Pages: 1 2 3 4 5 6 7 8 9 10