New OIDC providers for Trusted Publishing

Yeah, totally fair. But as Paul says, passing through each time can also be done - the package URLs don’t have to be on the same host as the index is.

Another client-side solution that I really like is simpleindex, which can solve the problem of mapping specific packages to specific feeds, or you can implement as much logic as you like (e.g. I have one for $work that does transparent authentication).

I wonder if in light of this: A GitHub Issue Title Compromised 4,000 Developer Machines (grith.ai) [LWN.net] (and of several other cases like this) we will eventually decide to re-evaluate the security of software built on github.

1 Like

This is probably already off-topic and I guess I’m about to make it more off-topic but here goes…

Developers make mistakes, it’s not really GitHub’s fault - the ability to run things in a privileged context when certain events occur is very useful and the developers really just need to take proper precautions when running things in those contexts. I guess you could say GitHub should take the more nuclear approach and enable the read-only token by default on all repos (yes, it’s a setting that you can switch on repo or user/org-wide - I highly recommend it) but that would presumably break a lot of people’s existing workflows since we didn’t use to have this setting.

I guess the truth is that a lot of people don’t enjoy writing CI workflows and so just take the path of least resistance without first reading about how Actions and its security model work before using them :person_shrugging:

Did you read about the issue?

In my opinion unexpected data ending up in a build because a separate build filled up the cache with corrupt data is very much a github issue. I think it’s unexpected to most people that regardless if you accept a PR or not, its binary output can end up in the main branch.

1 Like

github has bad default behaviors, but I think that’s somewhat out of neccessary scope for this discussion.

I don’t think github (or any other CI platform for that matter) should be in a position of trust from pypi’s perspective (though not my decision).

It would be correct to say that pypi administrators have chosen to trust that github is doing the right thing here, but have no way of verifying this.

Making that more clear opens the door to arbitrary providers here by just labeling where the relevent attestations came from and allowing users to decide if they choose to trust those attestations rather than relying on pypi’s determination of trust and then still needing to decide if they trust the provider themselves.

2 Likes

actually, perhaps not out of scope, as it’s demonstrative of why it’s important to be clearer about what such attestations actually indicate, then when looking at that, allowing arbitrary providers makes more sense without requiring any additional extension of trust.

All that pypi knows when receiving such attestations, is that the uploader has the ability to claim a certain build system was used.

That doesn’t imply that the build system properly protected it’s secrets or that the user’s build was not compromised and can be trusted, only that if the secrets haven’t been compromised, that the build system is where that build came from.

The example above shows that the builds of specific packages can be compromised due to bad platform defaults without requiring that the build system itself or it’s secrets be compromised.

2 Likes

This description feels oversimplified. This is not what happens for the pull_request event as it runs using the pull request’s workflow file:

When a cache is created by a workflow run triggered on a pull request, the cache is created for the merge ref (refs/pull/.../merge). Because of this, the cache will have a limited scope and can only be restored by re-runs of the pull request. It cannot be restored by the base branch or other pull requests targeting that base branch.

Dependency caching reference - GitHub Docs

The cache would be affected when running with pull_request_target event (and other events such as the issue event) since those run in the context of the default branch (or PR’s base branch in case of pull_request_target) and yes, I think it’s expected for those runs to be able to read and write to the cache of that branch. You need to take proper precautions when running things in such a privileged context. You need to treat user-modifiable fields in the event data just as you would any other untrusted input. GitHub’s documentation does mention this in many places (especially in guides that are meant to introduce you to writing workflows), though there are definitely some often-visited reference pages where that’s not reiterated, even though it probably should.

And one thing I didn’t include in the nuclear approach GitHub could take earlier - it would make sense to also completely prevent using ${{ … }} in the run field and have people put them in env vars since interpolation there does not try to perform any escaping at all. Of course, people would still have to use those env vars safely by, e.g. quoting them in bash, but perhaps more people are already aware how to write safe bash scripts. GitHub’s security guide for actions does mention this, but a design that makes it harder to do things wrong sure would be appreciated. Non-nuclear approach, with just using warnings, probably wouldn’t cut it since I don’t think the majority of people look at CI warnings that are hidden in successful job summaries.

This is the biggest part of what puts me off the whole attestations thing to begin with. Everyone thinks they mean more than they do. Especially when it comes to boolean things like the Verified details :white_check_mark: section on PyPI – it’s a desirable status even though no-one who tells me I should have it can tell me what possible attack vector it’s blocking.

4 Likes

Are we talking Attestations or Trusted Publishing here?

For attestations the hard part isn’t really for PyPI, the attestations are coming from somewhere else, and the end user has to verify them, so the end user has to determine whether to trust a given key or not. PyPI limiting arbitrary providers is mostly about limiting the impact of that. One of the big problems with the GPG signing was that end users had to (in theory) manage trust mappings on a per project basis with no mechanism for communicating expected vs unexpected changes.

For Trusted Publishing end users don’t have to trust the provider, it’s just PyPI using the provider’s OIDC token to authenticate the upload “instead” of a PyPI API token [1], so limiting providers there is mostly about wanting to manage the risk of a provider going away or proving to be untrusthy and having to block them and dealing with the fallout of that.


  1. Technically there’s a little dance here where the upload is still authenticated by a PyPI API token, there’s a separate endpoint that authenticates the OIDC token and then gives them a time boxed API Token. ↩︎

1 Like

I’ve not seen one used without the other, but I probably should have written out the whole thing to be clear as to which arguments apply to which parts, and why I don’t think pypi limiting OIDC providers actually accomplishes anything in terms of project safety: projects still have to place their trust in the right place.

The little bit in the footnote discourse doesn’t like to quote is extremely relevant here:

Technically there’s a little dance here where the upload is still authenticated by a PyPI API token, there’s a separate endpoint that authenticates the OIDC token and then gives them a time boxed API Token.

In reality, if a CI provider is untrustworthy and doesn’t provide (or isn’t supported by pypi to allow them to provide) trusted publishing, the fallout will be larger, because people will be giving those CI providers access to long lived tokens capable of publishing.

The only real argument against arbitrary trusted publishers I can make if the section here: Security Model and Considerations - PyPI Docs is accurate is that not supporting it simplifies the UI needed for pypi.

2 Likes

Projects never need to place any trust anywhere when uploading packages. Only users of the package need to place trust. Optionally, projects can trust things on behalf users, but honestly, I trust PyPI and the organisations it (partially) trusts a lot more than the hundreds of projects I use.

I like GitHub’s attestations because it provides the exact commit of the definition of the workflow used to build and publish a file (for most projects: some use manual triggers or separate projects which are usually useless to me). I have to trust GitHub is being honest, but otherwise I can verify the security state of the project easily.

This is a pretty myopic view. When either using trusted packaging or entrusting a token to a CI platform without access to trusted packaging, you are placing trust in that platform to secure the relevent secrets, and to protect the build system from attackers. Failure at either of these can lead to malicious packages being uploaded on your behalf.

Besides that, even if you were to only manually upload, if you’re using a third party CI platform to build the packages for any reason, you have to trust that the system isn’t compromised.

Who bears the cost when a project and it’s maintainers place their trust in the wrong place may be their users, but that’s ultimately still a decision of trust that belongs to the project.

Attestations then give users to the knowledge that allows them to make further determinations of trust, though I get the feeling that very few people correctly understand this to do so.

2 Likes

@dstufft Am I missing something, or is it not possible to create a project-scoped token for a project you have “Maintainer” rights on via team membership, or “Owner”-equivalent rights on via organization membership, via the PyPI web interface?

Uh, it’s entirely possible the UI might not allow that. That UI predates organizations and it wouldn’t surprise me if that got missed accidentally when adding organizations.

That shouldn’t affect caveats or the API token itself though.

The permission system is able to answer the question “is X able to do Y to Z”, but it’s not setup to enumerate all of the Z’s that X can do Y (or do anything) to, so implementing that UI, if I remember correctly, had to query for a list of projects independently of the permissions system, and if I had to guess, it’s only looking at pre-org permission tables.

OK so for those just reading on the sidelines and wanting a quick way to construct a PyPI token for publishing several packages, here it is:

import pypitoken
token = pypitoken.Token.load("pypi-YourAccountScopedTokenHere")
token.restrict(project_names=["project1", "project2"])
print(token.dump()) # => pypi-NewTokenRestrictedToThoseProjects

Just like @dstufft said, it was quite easy (after I remembered to re-read his post and not try to do it myself with pymacaroons…).

I wish pypitoken had a CLI so it could be installed and used with pipx install pypitoken but I’m way too exhausted to do this myself.

(Is there a way to pin this reply to the start of the thread somehow? Maybe a bored mod could do it…)

10 Likes

“Trust” in the context of Trusted Publishing just means OIDC federation; it’s a way to make explicit the otherwise implicit trust relationship between PyPI and any CI/CD system (specifically, the relationship that happens when a user manually issues an API token and stores it in their CI/CD system’s secrets, effectively trusting the CI/CD to maintain the secrecy of their token.

To your points below: you can make an argument that supporting arbitrary OIDC IdPs would be a harm reduction step, insofar as TP is generally more misuse-resistant than manually issued credentials. But the underlying problems around this have been addressed before: supporting additional OIDC IdPs in PyPI is a nontrivial engineering undertaking (since OIDCs exhibit significant claim diversity), and doing so has limited value if only a handful of packages actually use a given OIDC IdP (since the IdP itself needs to maintain PKI, which at small scales is morally equivalent to maintaining the secrecy of the API token(s) themselves).

In other words the juice is not worth the squeeze; PyPI can better serve the Python community by performing maintenance and working on other features, rather than chasing down all the ways in which FooCorp’s OIDC IdP is slightly different just so 15 packages can be published from it.

(This isn’t to say that PyPI shouldn’t support OIDC IdPs that do demonstrate significant uptake, like Forgejo. I think it should.)

In the context of attestations (which as @dstufft notes is distinct from TP), PyPI does have a mechanism for verifying the honesty of the federated party – attestations are backed by signing keys bound to certificates, and those certificates are committed to an append-only transparency log. The attestations themselves are also committed to a separate log.

In the event of a compromise of GitHub’s OIDC PKI, PyPI (or any other interested party) could audit the transparency log for indicators of malicious activity.

I think I (and others) have been clear that an attestation is not, on its own, any particular signal for trust or quality. What it is from PyPI’s perspective is strong proof that a particular distribution came from a particular place (GitHub, GitLab, Google, or whatever else), which serves two purposes at the moment:

  1. It addresses a minor form of malicious activity, i.e. putting another repository’s links in the metadata to make the project appear more legitimate or popular than it actually is.
  2. It makes a handful of incident response tasks (namely around precise timelines) much easier, since the transparency log provides an exact record of the temporality and order of malicious activity in a compromised package.

Apart from these two purposes, attestations could provide direct end-user value if clients were to verify them (upon locking an initial identity set, similar to how Go TOFUs its go.sum on first resolution). But even then, an attestation itself would not be a signal of anything; what it would become is a marker by which to measure changes (e.g. a new version being published _without_an attestation or with a different machine identity).

5 Likes

OK, I’m now less exhausted so I did this myself. (Also, I realized that I’m dreading maintenance work on some of my packages when it involves rotating tokens to add privileges, and that’s never a good thing.) Meet pypitoken-cli, a tool which adds restrictions to PyPI tokens.

7 Likes