❌

Normal view

Received — 28 October 2026 ⏭ The Trail of Bits Blog

Your AI agent fixed the bug, but what else did it change?

21 October 2026 at 17:00

Today we’re publishing patching-bugs and regression-hunt, two plugins that let engineers delegate patch development and regression checks to agents. Our patching-bugs plugin reproduces reported bugs, develops fixes, and tests them. The regression-hunt plugin checks whether any changes break previously working and supported behavior. Engineers can direct the work and review their results; maintainers can supply their own security assumptions and patching requirements, so the agents work within the project’s rules.

We built patching-bugs and regression-hunt through Patch the Planet, our joint initiative with OpenAI, where we’ve co-authored hundreds of security patches for widely used open-source projects. Motivated by our major successes on projects like Scapy, our plugins draw on our analysis of merged patches and thousands of our engineers’ Codex sessions.

The plugins provide maintainers with a structured workflow, evidence, independent checks, and best practices to improve their patching hygiene. In our preliminary evaluations, regression-hunt produces 25% fewer false-positive findings than a baseline Codex agent prompted to “review the PR for regressions.”

Standardizing the patching process

Our September post reported regressions introduced by some of our Patch the Planet patches. Since then, we’ve reviewed every patch that a Trail of Bits engineer authored and maintainers merged upstream. The review confirmed 21 regressions from over 130 PRs merged: 16 functional, one performance, and four build/test. None broke a security protection.

(“Regression” refers to a new defect introduced by a change that breaks previously correct and supported behavior along a path a real user or attacker can reach.)

For each patch, an agent looked for potential regressions, confirmed each one with a proof-of-concept (PoC) run before and after the change, and then passed the results to an engineer for review.

The review was noisy: today’s bug disclosures arrive at an unprecedented speed and scale, and for patching to keep pace, the industry needs to increase its signal-to-noise ratio. The 21 confirmed regressions were identified among 58 candidates in a single run (~36% precision). But because agents take many different approaches and reach different results, each run generates more candidates to manually sort through.

That led us to two conclusions:

  1. To introduce fewer regressions, we needed a standard way to write patches, with the same checks applied every time.
  2. To catch the regressions we still introduce, we needed a standard way to check for them, with written rules for what counts as a regression.

Standardizing both steps also makes every run repeatable and comparable. No fixed checklist will catch every regression, so we collect agent trajectories from our engineers’ patching sessions, and use the tasks that an earlier version failed to benchmark for the next one (figure 1).

“Figure showing the feedback loop for generating and evaluating patches.”
Figure 1: Feedback loop for generating and evaluating patches.

How we designed the plugins

We used two primary sources to guide the design of our plugins: a review of every patch we merged upstream and the thousands of Codex sessions our engineers used to write those patches.

This led to four principles that shaped our patching-bugs and regression-hunt plugins:

  1. Opinionated guidance in as few words as possible. We give the agent wide latitude and record only Trail of Bits’ judgment of what constitutes a good patch and a real regression. The goal is to direct the agent towards the desired result with the least possible guidance.
  2. Modular steps. You don’t have to run either plugin end-to-end, as both provide multiple skills that can be used in tandem or in isolation.
  3. Strict input and output schemas. Every step takes and returns a defined schema that includes the claim, the code revision, the required check, the raw evidence, and open questions. Fixed schemas reduce variability across runs and allow each step to verify the previous step’s work rather than trusting its summary.
  4. A live proof of concept for every claim. Every conclusion depends on a PoC run against a live deployment: a built, running instance of the project, reached through the interfaces a real user would use. When reproducing a bug, proving a fix, and confirming a regression, if a step can’t produce a live PoC, its result is inconclusive.

regression-hunt reduces false positive rates

The regression-hunt plugin (Figure 2):

  1. Enumerates candidate regressions by tracing the changed code (the diff) through its callers to its real interfaces.
  2. Reproduces each candidate regression on a live deployment of the system.
  3. Replays the PoC on the live deployment on a fresh-context agent.
  4. Judges the results of all the previous steps to validate that the proof meets the requirements of a regression.

“Figure showing the regression-hunt workflow”
Figure 2: The regression-hunt workflow.

In addition to the four principles above, there are two core aspects that are critical to the plugin:

  1. Defining a regression: The plugin has strong opinions about what a regression is and, more importantly, what a regression is NOT. The burden of proof is placed on the agent (specifically during the “adjudication agent” step) to prove that there is sufficient evidence to show that the suspected regression is not a false positive, an incomplete fix, a latent bug, a pre-existing bug, and so on. The plugin only reports a regression if all of these gates are passed.
  2. Incorporating a threat model: If a threat model is available, the orchestrator will intentionally pass this document as part of the global context for each individual agentic step. This is an attempt to require each step/agent to refer back to the threat model before reaching its conclusion.

Our review of merged PRs yielded a set of 21 confirmed regressions that we could use to evaluate regression-hunt. We used the whole set to compare the plugin with a baseline Codex agent prompted to “review the PR for regressions.”

Across those runs, regression-hunt produced 25% fewer false-positive findings than the baseline, with a 95% confidence interval of 14% to 38%. Its strict evidence requirements also left some known regressions classified as borderline: the plugin identified a possible regression but did not gather enough evidence to confirm it. Fewer false positives therefore came with a tradeoff in how many known regressions it could confidently report.

Next, we’ll investigate why these cases remained borderline and improve the plugin’s strategies for gathering evidence. Our goal is to confirm more real regressions while giving maintainers clearer information when the evidence remains inconclusive.

patching-bugs streamlines best practices from thousands of agent sessions

The patching-bugs plugin is a distillation of all the best practices we’ve identified by analyzing thousands of Codex sessions by Trail of Bits engineers (Figure 3). In addition to the more traditional capabilities (such as reproduction on live deployments and independent adversarial review), the plugin emphasizes:

  1. The three-arm proof: The proof runs the same reproduction against three specifications: the original code must fail, the patched code must pass, and a complete revert must fail again. Bringing the bug back by removing the patch provides stronger evidence that the patch—rather than an unrelated environment or test change—actually fixed the bug.
  2. Invariant-guided repair: The patch attempts to fix the underlying, violated security invariant rather than fix the specific bug. This helps generalize the patch for possible variants of the same bug class.
  3. Incorporates regression analysis: The plugin mandates that regression-hunt is run to ensure that any patch goes through the same standard pipeline to check for mistakes.

“Figure showing the patching-bugs workflow”
Figure 3: The patching-bugs workflow.

What’s next for the plugins

Both patching-bugs and regression-hunt will continue to evolve. Our engineers are using them in their Patch the Planet work now, and we’re collecting data on how they’re being used, what works, and what doesn’t. That data, along with better evaluations, will shape the next version’s capabilities:

  • Evaluate patching-bugs. We’re building an evaluation that measures whether the plugin’s patches fix the reported bug and its variants without introducing regressions.
  • Learn from agent traces. We’re analyzing the first batch of trace-level data from our engineers’ sessions. It will show whether Codex follows the rules and definitions required at each step, which steps it skips, and where its evidence is weakest.
  • Make the plugins more configurable. patching-bugs already reads a project’s own patching guidelines (more on that in a bit). Next, we want maintainers to be able to choose whether to minimize patch sets—or, when broader coverage is worth more false positives, to loosen regression-hunt’s rules to surface more candidates.

These plugins encode best practices we’ve found for an agent to patch a bug properly, and they improve when you tell them how your project works.

The plugins work best when you input two kinds of project knowledge:

  1. A THREAT_MODEL.md, which tells agents what counts as a vulnerability or a regression. The regression-hunt plugin passes it to every step, so each agent has to check its conclusions against the file. It should define the following: assets whose compromise matters, trust boundaries, access controls, supported configurations, specifications on what is or isn’t a vulnerability, and evidence required for a valid bug report (example).
  2. A PATCHING_GUIDELINES.md (or AGENTS.md), which tells agents and patching-bugs how your project wants fixes written. It should outline how to build and test the project, how big a fix should be, where a fix should go, how to present proposed changes, and what a regression test must show (example).

For our Patch the Planet work with GPAC, our engineers wrote a threat model and a project-specific patching skill that captures how GPAC’s maintainers want bugs reproduced, what evidence a fix needs, and what a patch must preserve. Liboqs and validator.js have also published threat models in accordance with our guidance.

The more comprehensive and specific these files are to your project, code conventions, and operating style, the better the output you’ll receive.

The industry needs to focus on patching

As AI accelerates bug discovery, we need to turn those findings into reliable fixes just as quickly. Solutions need to balance speed and quality: an agent needs to understand a report, decide what needs to change, and check that a patch fixes the issue without introducing regressions. Our patching-bugs and regression-hunt plugins bring what we’ve learned from Patch the Planet into that workflow, helping maintainers move from a bug report to a patch they can confidently review and ship.

Install patching-bugs and regression-hunt, add a THREAT_MODEL.md and a PATCHING_GUIDELINES.md to your repository, and put them to work on your next bug report. Give the tools your project’s context so they can help you focus on the right fix and validate it faster. Then share your feedback, including comments about where our defaults don’t fit your project. Those real-world cases will help us build the next version.

Package managers have an integrity problem

14 October 2026 at 13:00

I tested 25 of the most popular package managers to check whether they detect a compromised package repository injecting malicious code into legitimate packages. This code integrity problem could be solved using cryptographic hashes or signatures, but many package managers are still vulnerable to this threat. Of the 25 package managers reviewed1, fewer than half used lockfiles that checked for tampering by default, and only two verified the integrity of every package they downloaded.

Supply-chain attacks caused by developer compromise are a common story, but imagine what happens if ongoing mitigation attempts, including work by Trail of Bits2, are successful. If developers of popular packages are no longer the easiest targets, attackers will shift their focus to the next weakest link in the system: the package repositories that serve the code. The cryptographic answers to this problem are more subtle than they appear, and many implementations fall short due to deliberate design choices, resource prioritization, or the complexity of developer-friendly solutions.

One natural answer is to include a cryptographic hash of a package’s contents in a lockfile and compare hashes after downloading the package. This automatic verification was the default behavior in about half of the package managers I tested, but others required extra configuration to include or verify these hashes, used hashes in a cryptographically insecure way, or lacked hashes entirely. This is summarized in the following table.

Verifies integrity Go, vcpkg
Verifies if lockfile is present Bun, Bundler, Cargo, npm, pnpm, Poetry, SwiftPM, uv, Yarn
Verifies with lockfile + extra steps Conda, Gradle, Pip, NuGet, Pub
Does not verify with lockfile or N/A CocoaPods, Composer, Conan, cpanminus, Gem, Maven, PowerShellGet, PSResourceGet, R

Table 1: Lockfile-based security of a package manager fetching dependencies from a malicious repository, categorized by how much work it takes a developer to achieve integrity. Experimentally verified for a particular configuration. Caveats detailed below.

Another natural approach is to use signatures, avoiding the need to securely distribute a hash for every package. However, in my tests, only Go verified signatures by default; others required extra configuration, lacked verification code, or did not support signatures.

Verifies signatures Go
Verifies with configuration Bundler, Gem, Gradle, Maven, npm, NuGet
Missing or incomplete verification Bun, cpanminus, Pip, pnpm, Poetry, PowerShellGet, PSResourceGet, uv, Yarn
No signatures available for verification Cargo, CocoaPods, Composer, Conan, Conda, Pub, R, SwiftPM, vcpkg

Table 2: Signature-based security of various package managers. Also experimentally verified and subject to similar caveats.

As both tables show, I found a surprising variation in the cryptographic designs and the level of protection they provide. Despite the challenges, I also found several communities on the frontier of this integrity problem that are bringing improved designs to reality. This post explores those variations, showing where certain designs fall short, why code integrity is harder to solve than it appears, and what cryptographic designs might be adopted in the future.

Simulating malicious package repositories

Studying the cryptographic behavior of 25 package managers is quite the challenge. I wanted to minimize manual analysis, but also be 100% confident when I claim an attack from a malicious package repository is possible. To solve this problem, I built a testing harness based on Docker containers and mitmproxy that dynamically evaluates the security of package managers against a malicious server.

“Figure showing experimental design of the test harness used to simulate malicious package repositories”
Figure 1: Experimental design of the test harness used to simulate malicious package repositories

The Ubuntu-based Docker images include a package manager, sample code, and optionally a lockfile. There are conflicting opinions about whether to include lockfiles in source code repositories, so I tested both configurations. The harness instructs the package manager to fetch dependencies from the repository, and the resulting network request routes through a WireGuard network to a custom mitmproxy addon.

Because the mitmproxy TLS CA is installed in the Docker image, the addon can fully manipulate the plaintext from the package repository, simulating a compromised server. I assumed any web-facing server could be compromised (e.g., Composer fetches metadata from Packagist and packages from GitHub), but backend resources, like any private signing keys, remain secure. The simulated repository embeds malicious code into the downloaded packages, and if the sample code executes the malware, the attack is successful.

I tried to keep the experimental setup reflective of real-world usage, but there are limitations. I installed the latest package manager versions, followed quickstart guides for setting up sample projects inside the container, and picked popular packages from public repositories to infect. I ran package manager commands with default flags to see if extra steps, like modifying the project manifest or running extra commands, were needed to enforce cryptographic integrity. However, I am limited to testing a single package with a single package manager version on a single platform, and the test conditions (e.g., PowerShell and CocoaPods on Ubuntu) may not represent the most common usage patterns. The setup isn’t meant to show that every developer is vulnerable to a malicious package repository, just that a developer following reasonable conventions is.

With the package managers installed and network intercept functional, one (mostly) promising observation is clear.

HTTPS is (almost) everywhere

Our harness architecture captures all network traffic, so it’s easy to test that package managers are using TLS and validating the certificates. In almost every package manager, they are.

The exception is cpanminus, the frequently recommended package manager for Perl. cpanminus uses unencrypted HTTP by default to connect to the Perl package repository, allowing any active network attacker to inject malicious code into downloaded packages. This vulnerability has been known for years, but the cpanminus developer has not switched to HTTPS, since doing so would be a breaking change to default behavior.3

Hash functions don’t always provide integrity

By using the mitmproxy addon to modify the package repository responses, I could investigate how different package managers use cryptographic hashes. Hashes are a natural way to ensure that a downloaded file has not been tampered with, but the main question is how the package manager learns the correct hash in the first place.

Microsoft’s vcpkg package manager for C and C++ libraries offers an interesting example. The package manager source code includes content hashes for thousands of dependencies. When a developer updates their library and wants to register the new content in vcpkg, they recompute the hash of their package and open a pull request against the vcpkg source code with the new hash and link to the package. When a package is downloaded by vcpkg, the hash is checked. So as long as the package manager is trusted, this approach is fully secure against a malicious package repository.4

Hashes in lockfiles

It is significantly more common that the package manager learns the correct hash via a lockfile. A lockfile is frequently created by a package manager, resides alongside a project’s source code, and references specific versions of dependencies needed by the project. If a developer checks a lockfile with cryptographic hashes into the source code repository, and the user’s package manager validates downloaded packages against the hashes, then the user’s dependencies perfectly match the developer’s.

This is how it works for the Bun package manager for JavaScript projects. Installing a package with bun add updates the bun.lock lockfile; committing the lockfile to Git and calling bun install on another copy of the repo verifies the hash in the lockfile against the new copy of the dependency. Similar behavior is found in npm, pnpm, and Yarn (also JavaScript package managers); uv and Poetry (Python package managers); Bundler (for Ruby); Cargo (for Rust); and SwiftPM (for Swift).

Other package managers support lockfiles, but with more steps than the standard add/restore process. Python’s Pip has experimental support for the pylock.toml format, and Conda has the optional library Conda lock. NuGet, the .NET package manager, generates a lockfile after adding the RestorePackagesWithLockFile property to the project.

Many package managers (Maven, R, PowerShellGet, PSResourceGet, and cpanminus) do not have dedicated lockfiles, and some have lockfiles (Gradle’s gradle.lockfile5 and Gem’s Gemfile.lock) without hashes in them.

Hashes present, but not for integrity

Surprisingly, I found a few examples where the lockfiles had a hash, but could still be compromised by a malicious server. This is not necessarily reflective of a vulnerability, since the underlying behavior is clearly documented, but it shows how there are multiple valid opinions about how lockfiles should be structured and consumed. Nevertheless, a user who is accustomed to the behavior of one package manager may be surprised by that of another.

The first example is the Pub package manager for Dart and Flutter. The following excerpt of a pubspec.lock file includes a SHA-256 hash for the uuid package.

uuid:
 dependency: "direct main"
 description:
 name: uuid
 sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
 url: "https://pub.dev"
 source: hosted
 version: "4.6.0"

If a user calls flutter pub get to fetch dependencies, Pub downloads the package from the pub.dev server and computes the hash of the returned binary. If there is a mismatch, Pub detects the error, but installs the package anyway.

The existing content-hash from pubspec.lock doesn't match contents for:
 * uuid-4.6.0 from "https://pub.dev"

This indicates one of:
 * The content has changed on the server since you created the pubspec.lock.
 * The pubspec.lock has been corrupted.

The content-hashes in pubspec.lock has been updated.

This is documented behavior, and requires the non-default --enforce-lockfile option to block installation6.

Overwriting the lockfile after a hash mismatch was also the default behavior in pnpm until version 11.4 from May 2026, when it was changed to close “supply-chain holes.” Like Pub, pnpm also supported a non-default --frozen-lockfile flag, indicating that the previous behavior which updated the lockfile was expected.

Next, here is an excerpt of a conan.lock file, used by the Conan package manager for C and C++.

"requires": [
 "zlib/1.3.1#cac0f6daea041b0ccf42934163defb20%1765284699.337"
],

This includes the package name (zlib), the version number (1.3.1), a recipe revision cac0f6daea041b0ccf42934163defb20, and a timestamp. The 128-bit revision is an MD5 hash that includes source code. However, this hash is not intended to provide cryptographic security, but rather serves as a unique identifier. When Conan installs dependencies using this lockfile, it uses the recipe revision hash to download a particular file from the ConanCenter repository, but it does not recompute the recipe hash. Even if it did, there’s a separate package revision hash for platform-specific binaries that is not in the lockfile, and MD5 collisions could allow an attacker to create two distinct recipes with different behavior but the same hash.

The CocoaPods dependency manager for Swift and Objective-C has a similar pattern. Here is part of a Podfile.lock file for a project that uses the popular Files dependency.

SPEC CHECKSUMS:
 Files: d88e31b84d65e754935053b1c584ea8e2320698a

This 160-bit checksum is the SHA-1 hash of the Files Podspec, which contains the following link to the source code. CocoaPods uses this link to fetch the Git repository.

"source": {
 "git": "https://github.com/johnsundell/files.git",
 "tag": "4.0.2"
},

So even though the lockfile can prevent tampering with the GitHub URL in the Podspec (setting aside expensive SHA-1 collision attacks), a compromised GitHub server could just serve a malicious version of the repository with the same tag.

Finally, the Composer dependency manager for PHP includes commit hashes in the composer.lock file, as in the following excerpt.

"dist": {
 "type": "zip",
 "url": "https://api.github.com/repos/guzzle/psr7/zipball/b094ded77ee97a6027ad6cf0e8c7b9f88381814c",
 "reference": "b094ded77ee97a6027ad6cf0e8c7b9f88381814c",
 "shasum": ""
},

However, even though the b094ded... commit hash uniquely identifies the code, the ZIP archive in the URL does not include the Git tree as a way to verify the hash. Once again, GitHub could serve a malicious ZIP archive without detection by Composer.

Lockfiles have many roles besides supply-chain integrity

The previous experimental results show that lockfile hashes often can be used to enforce package integrity, but this is not their sole or even original purpose. Alternatively, they can lock dependencies to a particular functionality, enable an easy way for application developers to share a consistent environment, or facilitate reproducible builds. Analyzing package manager lockfiles through the lens of supply-chain security is a recent interpretation that can be at odds with their original design. The recent change to pnpm’s --frozen-lockfile is one example.

Another example of this tension is the question of whether the lockfile should be committed to version control. Most modern guidance recommends committing the lockfile for applications, since the final build environment is fully controlled. The question is more complicated for libraries, since package managers often ignore libraries’ lockfiles to avoid locking to conflicting transitive dependencies. Committing the lockfile makes it easier to develop the library by sharing a consistent build environment with other developers, but excluding the lockfile makes it easier to catch conflicts with different dependency versions before they are found by users. Many package managers recommend committing the lockfile for libraries (e.g., npm, pnpm, Poetry, uv, and Yarn), some recommend excluding it (e.g., Bundler), and some leave it up to the developer (e.g., Cargo).

Many of these questions are explored in a recent study by Gamage et al. The authors explore lockfile design decisions for seven package managers, analyze published projects, and perform semi-structured interviews with 15 developers. They find that the practice of committing a lockfile varies by which package manager is used, and categorize five benefits to using lockfiles, as perceived by the interview subjects: build determinism, integrity, transparency into indirect dependencies, debugging, and security scanning. Some of the developers also preferred to not use lockfiles, citing a preference for end users to resolve to the latest version of a dependency, which may have more bug fixes and vulnerability patches.

The diversity of package manager designs and views surrounding the purpose of lockfiles makes it difficult to prescribe a single recommendation. With regard to supply-chain integrity, it is common, but not universal, to view lockfiles as a source of integrity that should be committed with the source code. For package managers with lockfiles that do not provide cryptographic integrity, it is worth considering whether their users share that expectation. Even when lockfile hashes can provide cryptographic integrity, locking to specific package hashes means that users do not immediately receive subsequent updates or security fixes, since the newer package has a different hash. This leads to frequent lockfile bumping to distribute the latest hashes. Wanting immediate updates is a separate concern from wanting package integrity, but the current, common approach of using hashes in lockfiles is at odds with that goal. In conclusion, lockfile hashes are often, but not always, effective at providing cryptographic integrity, but do not work in every situation.

Signatures are common; verification is not

Signatures are a natural way to address these shortcomings, but implementations frequently fall short. In theory, developers could sign every release with their own private key, and the package manager could check the signature of the package it downloads from the package repository. In fact, signatures are relatively common in different package ecosystems, but proper signature validation is surprisingly lacking. The reality of using signatures to provide code integrity is more nuanced than it seems, and package managers have different approaches to the problem.

Many package managers have no signatures available. Arguably, vcpkg does not need them, because all package hashes are distributed with the manager, but this leaves Cargo, CocoaPods, Composer, Conan, Conda, Pub, R, and SwiftPM7. The Conan developers are aware and previously decided not to fix it, citing several implementation challenges and suggesting organizations where integrity is critical could run their own repository server. Cargo developers are actively considering different signing designs, but this effort is still in progress. Composer developers have been implementing many new protections against supply-chain attacks, with future plans for artifact integrity.

Repository signing

One straightforward approach, known as repository signing, is when the package repository signs every package once it’s uploaded, and the repository’s public key is distributed with the package manager. This doesn’t solve the problem of a totally compromised package repository, since a malicious package repository could legitimately sign a malicious package, but it would protect against untrusted mirrors or a partial compromise of a package repository’s serving infrastructure.

Repository signatures are used by the npm Registry, the NuGet Registry, and the PAUSE server for Perl module distribution. In the npm Registry, signatures are distributed within package metadata with repository-specific public keys. NuGet Registry repository signatures are found in .signature.p7s files within the distributed ZIP package, again with repository-specific public keys. The PAUSE server takes a different approach, using its GPG key to batch sign the cryptographic hashes of several packages at a time.

Developer signing

Developer signing involves the developer maintaining the private key and signing the package before uploading it to the repository for distribution. This prevents the package repository from tampering with the contents, but it also provides a way for the package repository to authenticate the uploader.

There are two common approaches for a developer asserting their public key. In the first approach, the public key is distributed out-of-band. The Maven Central repository (used by Maven and Gradle) and the RubyGems repository (used by the gem and Bundler package managers) both support this type of developer signing. Maven Central uses PGP signatures, and developer keys are distributed by public keyservers. RubyGems uses self-signed certificates, and their distribution is left to the developers (e.g., rspec). The major difference is that developer signing is mandatory for Maven Central, and optional for RubyGems. Developer signing is rare for RubyGems, due to the lack of an established chain of trust, and a previous attempt to introduce a certificate authority failed.

The second approach involves a chain of trust. The NuGet Registry and PowerShell Gallery both support developer signatures. Neither makes signatures mandatory, but the PowerShell Gallery recommends it and the NuGet Registry has strict requirements if developer signing is used. In both, the developer’s key must chain to a trusted CA, and acquiring a valid code-signing certificate can cost hundreds of dollars per year. The benefit of this approach is that trusted root certificates can be preinstalled on the platform, allowing package managers to verify public keys.

One downside of developer signing is that developers are responsible for key management, and they can lose the ability to update their code if they lose access to their key.

Identity-based signing

An increasingly common middle ground is “identity-based” or “keyless” signing from Sigstore. Rather than require developers to manage long-term private keys, a developer can use Sigstore’s Fulcio CA to issue a fresh signing certificate on demand, for free. To prevent impersonation, the developer proves their identity with an OIDC token: a cryptographically signed statement from a third party like Google, Microsoft, or GitHub that attests to the email address, username, or potentially other claims about the developer’s identity. The verifier can check that the username is correct and the certificate chain links back to Sigstore Fulcio’s root certificate, proving that someone with access to that account created the signature.

Keyless signing is particularly useful because it can be integrated into a developer’s existing workflow-based deployment pipeline. This is frequently called provenance attestation or trusted publishing8. GitHub and GitLab provide OIDC tokens to Actions and CI/CD workflows that attest to the build workflow, the repository, and the commit hash. A workflow can use this token to obtain a signing certificate from Sigstore, sign the built package, and upload the signed package to the package repository. Since the signing certificate includes the commit hash and workflow details, this cryptographically links the built binary artifact to its source code.

Provenance attestation via Sigstore is supported by Python’s PyPI, the npm Registry, RubyGems repository, and Maven Central. While Sigstore attestations are not mandatory, there is already better adoption than RubyGems’ optional support for developer-managed signing keys.

Verification code is skipped, unconfigured, or nonexistent

Unfortunately, just because these signatures exist does not mean they are being verified by package managers.

In the npm package manager, the repository signatures are not checked when running npm install. Checking the signatures requires running npm audit signatures, which uses Sigstore to validate the registry public keys and then validates the signatures of the installed packages. pnpm recently added the pnpm audit signatures command, but that implementation fetches the registry public keys from the potentially malicious registry rather than verifying with Sigstore, so an adversary could simply add their own key and signature. Bun and Yarn currently lack an equivalent audit signatures command.

In the NuGet package manager, the default signatureValidationMode is accept, which installs invalidly signed packages. The user must set this to require and configure the list of trusted signers, such as with dotnet nuget trust source. These configurations (which include the repository public keys) can be securely distributed within a nuget.config configuration file, functioning as a lockfile that locks the trusted signers.

Ruby’s Gem and Bundler, and Java’s Maven and Gradle, which all use repositories with developer signatures, do not verify signatures by default. They require the user to manually add developer public keys (gem cert --add) or list the allowed public keys that should be trusted. In Maven, one can use the third-party pgpverify-maven-plugin with pgp-keys-map.list to specify the trusted PGP fingerprints. Gradle offers dependency verification, which works similarly.

Finally, several package managers seemingly lack any sort of verification code at all. Pip, uv, and Poetry do not currently have a way to verify identity-based attestations described in PEP740. cpanminus does not have a way to verify the integrity of the repository-signed CHECKSUMS file. It once did, but in response to a verification bypass vulnerability, this verification code was removed. Even though PowerShell is supported on non-Windows platforms, Authenticode signature verification is not9. At one point, the PowerShell repository noted that this issue “should be fixed in a future release.”

Go package management and transparency logs

Go’s built-in package manager is the only one I tested that verifies signatures by default, and it also introduces one final concept for cryptographic package integrity. When go get or go mod download fetches a package10, it hashes the package contents and compares the hash, package name, and version against the checksum database. The contents of the database are registry-signed (analogous to PGP signatures in Perl’s PAUSE server), and the default checksum database public key is hardcoded in Go’s source code. As long as the checksum database’s private key is used honestly, this design ensures that the package name and version are enough to uniquely identify the binary contents of a package. As a result, the go.mod lockfile achieves package integrity without needing to store cryptographic hashes.11

Go’s checksum database is an example of a transparency log, or an immutable, global, tamper-resistant log of metadata. The same capability is provided by Sigstore’s Rekor and certificate transparency logs. In package managers, it cryptographically ensures that the package manager only ever installs the same binaries recorded by the transparency log. If everyone agrees on the transparency log, then everyone agrees on the package contents. This makes it more difficult for an adversary to selectively and undetectably target a victim with a malicious package. Transparency logs can still fail if there are other vulnerabilities, the log’s private key is compromised, or no one checks the transparency log for malicious behavior.

Of all the package managers I tested, Go performed the best at maintaining cryptographic integrity in the face of a compromised package repository or proxy. However, the registry-signature-like design means that much of the system’s trust is concentrated in a single private key.

A proposed cryptographic design for package managers

One potential approach that combines the ideas in this post is to include signature information in the lockfile. Rather than commit to the hash of a particular package, the lockfile could commit to a developer’s identity or public key. This reuses much of the infrastructure in place for lockfile hashes, but it separates the question of immediate updates from the question of package integrity.

With this design, the lockfile could specify a single dependency version (for build reproducibility) or a lower bound (for immediate updates), and the included identity ensures that a malicious repository or proxy cannot easily tamper with whatever package version the manager resolves to. In addition, a transparency log can protect against selective targeting, building on Sigstore’s identity-based signing and transparency log service. Sigstore avoids long-lived keys by default, but it also supports self-managed keys, KMS providers, and hardware tokens. This gives an easily adopted and secure default while still giving flexibility to developers who desire more control over their keys.

This idea is not new and not my own. Attestation identities are part of the pylock.toml Python lockfile specification, the trusted-keys in Gradle’s verification metadata, and the pgp-keys-map.list file in the pgpverify-maven-plugin for Maven. This is the direction that many package managers are already moving towards, as evidenced by the increasing adoption of Sigstore into publishing workflows, attention on supply-chain security, and bugfixes that specifically cite potential tampering by a malicious adversary. There is increasing recognition that this is a problem, and increasing appetite to fix it.

It won’t be easy. There are many subtle nuances that have to be considered and addressed, signing verification code needs to be developed and audited, and developers need to adopt the resulting design. In many of the package managers I reviewed as part of this project, I found communities of people who have been thinking about and pushing for cryptographic integrity and package signing for years. They know best what those package managers need, and they deserve the resources to make this a reality.

One of the greatest cryptographic success stories in the past decade has been the nearly universal adoption of HTTPS, driven in part by industry and community recognition of the security risks of unencrypted traffic, and the nonprofit Let’s Encrypt providing free and easy-to-use TLS certificates as a public good. Package signing is in a similar state now to where HTTPS was a decade ago: persistent adversaries are a huge concern, and cryptographic security is nearly nonexistent. Universal package signing is not here yet, but it is on the horizon. With the right attention and investment, we can make it a reality.

Acknowledgments

Thank you to William Woodruff for valuable discussions about lockfiles, identity signing, and the challenges of package manager security.


  1. This post focuses on dependency managers for source code rather than system package managers like APT, Chocolatey, or Homebrew. The line between the two is blurred, but concentrating on one helps highlight particular usage and deployment patterns in that group. ↩︎

  2. Trail of Bits has several ongoing and completed projects to improve supply-chain security in some of these package ecosystems, including helping address both account takeover threats and the cryptographic integrity problem. ↩︎

  3. cpanminus is included in the default Docker container for Perl, but the container build scripts patch cpanminus to use HTTPS by default. ↩︎

  4. A subtle distinction here is that vcpkg must be updated using git pull in order to fetch the latest package hashes, shifting the question from securely installing packages to securely updating the package manager. For consistency with other package managers, we do not consider this question, and we note this as a limitation of our threat model. ↩︎

  5. Gradle can have a verification-metadata.xml file with content hashes, even though gradle.lockfile does not. ↩︎

  6. Note that some flutter subcommands, including flutter run and flutter test, call flutter pub get by default, so these commands can also overwrite the pubspec.lock file unless called with --no-pub. ↩︎

  7. The SwiftPM client has two main ways to download packages: via Git and via the custom Swift Package Registry Service Specification. The latter supports signatures, and the server protocol is implemented by private services such as JFrog Artifactory and AWS CodeArtifact. As far as I can find, there is no repository for the general public that implements this protocol, so I tested the signatureless Git format. ↩︎

  8. Trusted publishing can also refer to using OIDC tokens to authenticate to a package repository, and it does not necessarily involve identity-based signing. ↩︎

  9. Although I did not test PowerShell on Windows, the documented default ExecutionPolicy behavior is to block scripts on Windows client machines, and to require a valid signature for Windows servers. This achieves package integrity. ↩︎

  10. Go calls these downloadable artifacts “modules,” which consist of “packages.” For the sake of consistent terminology with the rest of the post, I use “package” in the general sense, not the Go-specific one. ↩︎

  11. The go.sum file stores hashes, but it is not a lockfile. Instead, it is a local cache for entries from the checksum database. ↩︎

❌